// Note Editor: manages review notes with auto-save and three-level scoping // Scopes: overall (page-level), file (per-file), suggestion (per-suggestion) const NoteEditor = { repoId: null, analysisId: null, _saveTimers: {}, // debounce timers by key _notes: {}, // loaded notes keyed by "scope:scopeKey" /** * Initialize the note editor for a review page. * @param {number} repoId - Repository ID * @param {number} analysisId - Analysis ID (from the most recent review) */ init(repoId, analysisId) { this.repoId = repoId; this.analysisId = analysisId; this._notes = {}; this._saveTimers = {}; // Bind existing note editor placeholders (from diff-viewer suggestion cards) this._bindPlaceholders(); // Watch for new placeholders added dynamically (from SSE streaming) this._observeNewPlaceholders(); }, /** * Load all notes for the current analysis and populate editors. */ async loadNotes() { if (!this.analysisId) return; try { const resp = await fetch(`/api/repos/${this.repoId}/review/notes?analysis_id=${this.analysisId}`, { credentials: 'same-origin' }); if (!resp.ok) return; const notes = await resp.json(); // Index notes notes.forEach(n => { const key = n.scope + ':' + (n.scope_key || ''); this._notes[key] = n; }); // Populate all existing editors this._populateAllEditors(); } catch (err) { console.error('Failed to load notes:', err); } }, /** * Save a note (upsert). Debounced to avoid rapid-fire requests. */ saveNote(scope, scopeKey, content) { const key = scope + ':' + (scopeKey || ''); // Clear existing timer for this key if (this._saveTimers[key]) { clearTimeout(this._saveTimers[key]); } // Debounce: wait 800ms after last keystroke this._saveTimers[key] = setTimeout(async () => { delete this._saveTimers[key]; try { const resp = await fetch(`/api/repos/${this.repoId}/review/notes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ analysis_id: this.analysisId, scope: scope, scope_key: scopeKey || '', content: content, }), }); if (resp.ok) { const note = await resp.json(); this._notes[key] = note; // Update save indicator const indicator = document.querySelector(`[data-note-key="${key}"] .note-save-indicator`); if (indicator) { indicator.textContent = '已保存'; indicator.classList.remove('text-gray-400'); indicator.classList.add('text-green-500'); setTimeout(() => { indicator.textContent = ''; indicator.classList.remove('text-green-500'); }, 2000); } } } catch (err) { console.error('Failed to save note:', err); } }, 800); }, /** * Create a note editor HTML for a given scope. * @param {string} scope - 'overall', 'file', or 'suggestion' * @param {string} scopeKey - empty for overall, filename for file, suggestionId for suggestion * @returns {string} HTML string */ createEditorHTML(scope, scopeKey) { const key = scope + ':' + (scopeKey || ''); const existing = this._notes[key]; const content = existing ? existing.content : ''; const placeholder = this._getPlaceholder(scope); return `