// src/js/editor.js // Editor management and state let analyzeTimeout; let analyzeAbortController = null; let _lastInputTime = 0; const ANALYZE_DEBOUNCE_MS = 1000; const MAX_ANALYZE_LENGTH = 5000; // Pipeline Hardening v3.3: Guard to prevent input events during suggestion apply let _isApplyingSuggestion = false; // ── Custom Undo/Redo Stack ── const _undoStack = []; const _redoStack = []; const _MAX_UNDO = 50; function pushUndoState() { const editor = getEditorElement(); if (!editor) return; const html = editor.innerHTML; // Avoid duplicate consecutive entries if (_undoStack.length > 0 && _undoStack[_undoStack.length - 1] === html) return; _undoStack.push(html); if (_undoStack.length > _MAX_UNDO) _undoStack.shift(); _redoStack.length = 0; // Clear redo on new action } function editorUndo() { const editor = getEditorElement(); if (!editor || _undoStack.length === 0) return false; _redoStack.push(editor.innerHTML); editor.innerHTML = _undoStack.pop(); updateEditorStats(); updatePlaceholder(); analyzeTextDelayed(); return true; } function editorRedo() { const editor = getEditorElement(); if (!editor || _redoStack.length === 0) return false; _undoStack.push(editor.innerHTML); editor.innerHTML = _redoStack.pop(); updateEditorStats(); updatePlaceholder(); analyzeTextDelayed(); return true; } // Dismissed words whitelist — words the user chose to keep as-is const _dismissedWords = new Set( JSON.parse(localStorage.getItem('bayan_dismissed_words') || '[]') ); function _saveDismissedWords() { try { localStorage.setItem('bayan_dismissed_words', JSON.stringify([..._dismissedWords])); } catch (e) {} } /** * Initialize the editor */ function initEditor() { const editor = getEditorElement(); if (!editor) { console.warn('Editor element not found'); return; } // Restore draft if no document was explicitly loaded yet try { const draft = localStorage.getItem('bayan_editor_draft'); if (draft && !editor.innerHTML.trim()) { editor.innerHTML = draft; // Trigger analysis on load setTimeout(analyzeTextDelayed, 500); } } catch (e) {} // Debounced undo push — saves state after 500ms of no typing let _undoInputTimer = null; editor.addEventListener('input', () => { // Pipeline Hardening v3.3: Skip re-analysis when programmatically applying suggestions if (_isApplyingSuggestion) return; _lastInputTime = Date.now(); updateEditorStats(); updatePlaceholder(); analyzeTextDelayed(); // Push undo state after typing pauses clearTimeout(_undoInputTimer); _undoInputTimer = setTimeout(pushUndoState, 500); try { localStorage.setItem('bayan_editor_draft', editor.innerHTML); } catch (e) {} }); // Strip formatting on paste — prevent rich HTML (colors, opacity, fonts) // from being carried over when pasting from chat, web pages, etc. editor.addEventListener('paste', (e) => { e.preventDefault(); const text = (e.clipboardData || window.clipboardData).getData('text/plain'); if (!text) return; // Insert plain text at cursor position const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); range.deleteContents(); // Split by newlines to preserve paragraph structure const lines = text.split(/\r?\n/); const fragment = document.createDocumentFragment(); lines.forEach((line, i) => { if (i > 0) fragment.appendChild(document.createElement('br')); fragment.appendChild(document.createTextNode(line)); }); range.insertNode(fragment); // Move cursor to end of pasted content range.collapse(false); selection.removeAllRanges(); selection.addRange(range); // Trigger editor update updateEditorStats(); updatePlaceholder(); analyzeTextDelayed(); try { localStorage.setItem('bayan_editor_draft', editor.innerHTML); } catch (e) {} }); editor.addEventListener('click', (e) => { handleEditorClick(e); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideTooltip(); }); // Custom Undo/Redo — on editor only, capture phase to beat browser native undo // Uses e.code instead of e.key so shortcuts work with any keyboard language editor.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.code === 'KeyZ' && !e.shiftKey) { if (_undoStack.length > 0) { e.preventDefault(); e.stopImmediatePropagation(); editorUndo(); return; } } if ((e.ctrlKey || e.metaKey) && (e.code === 'KeyY' || (e.code === 'KeyZ' && e.shiftKey))) { if (_redoStack.length > 0) { e.preventDefault(); e.stopImmediatePropagation(); editorRedo(); return; } } }, true); document.addEventListener('click', (e) => { const popover = document.getElementById('editor-tooltip'); if (popover && popover.classList.contains('show') && !popover.contains(e.target) && !e.target.classList.contains('spelling-error') && !e.target.classList.contains('grammar-error') && !e.target.classList.contains('punctuation-suggestion')) { hideTooltip(); } }); const applyAllBtn = document.getElementById('apply-all-btn'); const applyAllSheet = document.getElementById('apply-all-sheet'); if (applyAllBtn) applyAllBtn.addEventListener('click', applyAllSuggestions); if (applyAllSheet) applyAllSheet.addEventListener('click', applyAllSuggestions); updatePlaceholder(); } function updateEditorStats() { const text = getEditorText(); const words = text.trim() ? text.trim().split(/\s+/).length : 0; const wordCountEl = document.getElementById('word-count'); if (wordCountEl) { wordCountEl.textContent = words.toLocaleString('ar-EG'); } // Word count goal const goalEl = document.getElementById('word-goal-indicator'); if (goalEl) { try { const goal = parseInt(localStorage.getItem('bayan_word_goal') || '0', 10); if (goal > 0) { const pct = Math.min(Math.round((words / goal) * 100), 100); goalEl.style.display = 'inline-block'; goalEl.textContent = `${pct.toLocaleString('ar-EG')}% من ${goal.toLocaleString('ar-EG')}`; goalEl.classList.toggle('goal-reached', pct >= 100); } else { goalEl.style.display = 'none'; } } catch(e) { goalEl.style.display = 'none'; } } // Item 4: Enhanced stats if (typeof updateEnhancedStats === 'function') { updateEnhancedStats(); } } function updatePlaceholder() { const editor = getEditorElement(); if (!editor) return; const text = (editor.textContent || '').trim(); if (!text || text.length === 0) { editor.setAttribute('data-empty', 'true'); } else { editor.removeAttribute('data-empty'); } } function analyzeTextDelayed() { clearTimeout(analyzeTimeout); // Abort any in-flight request so it doesn't overwrite while user types if (analyzeAbortController) { analyzeAbortController.abort(); } analyzeTimeout = setTimeout(() => { // Double-check user hasn't typed in the last DEBOUNCE period const timeSinceLastInput = Date.now() - _lastInputTime; if (timeSinceLastInput >= ANALYZE_DEBOUNCE_MS - 100) { analyzeText(); } }, ANALYZE_DEBOUNCE_MS); } function findSuggestionById(id) { // Pipeline Hardening v3.3: UUID-based lookup instead of index const suggestions = window.currentSuggestions || []; return suggestions.find(s => s.id === id) || null; } function findSuggestionElement(id) { return document.querySelector(`[data-suggestion-id="${id}"]`); } async function analyzeText() { const text = getEditorText(); updateEditorStats(); updatePlaceholder(); if (!text || text.trim().length === 0) { renderWithoutSuggestions(text); updateSuggestionCounts(0, 0, 0); updateWritingScore(0, 0, 0); updateSuggestionsList([]); window.currentSuggestions = []; updateAnalysisLimitBanner(false); return; } const isTruncated = text.length > MAX_ANALYZE_LENGTH; const textForApi = isTruncated ? text.substring(0, MAX_ANALYZE_LENGTH) : text; updateAnalysisLimitBanner(isTruncated); if (analyzeAbortController) { analyzeAbortController.abort(); } analyzeAbortController = new AbortController(); setAnalyzingState(true); try { const savedSelection = saveSelection(); // Network delay indicator: show message if API takes > 10s const longerTimer = setTimeout(() => { if (typeof showToast === 'function') showToast('\u0627\u0644\u062a\u062d\u0644\u064a\u0644 \u064a\u0623\u062e\u0630 \u0648\u0642\u062a\u064b\u0627 \u0623\u0637\u0648\u0644...', 'warning'); }, 10000); const response = await fetch('/api/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: textForApi }), signal: analyzeAbortController.signal }); clearTimeout(longerTimer); if (!response.ok) { console.error('Analyze API error:', response.status); renderWithoutSuggestions(text); return; } const data = await response.json(); if (data.status !== 'success' || !data.suggestions) { renderWithoutSuggestions(text); return; } // Filter out dismissed (whitelisted) words const rawSuggestions = sortSuggestions(data.suggestions || []); window.currentSuggestions = rawSuggestions.filter( s => !_dismissedWords.has(s.original) ); // Use DOM overlay instead of innerHTML replacement to preserve formatting const editor = getEditorElement(); overlaySuggestions(editor, window.currentSuggestions); if (savedSelection) { restoreSelection(savedSelection); } const spellingCount = window.currentSuggestions.filter((s) => s.type === 'spelling').length; const grammarCount = window.currentSuggestions.filter((s) => s.type === 'grammar').length; const punctuationCount = window.currentSuggestions.filter((s) => s.type === 'punctuation').length; updateSuggestionCounts(spellingCount, grammarCount, punctuationCount); updateWritingScore(spellingCount, grammarCount, punctuationCount); updateSuggestionsList(window.currentSuggestions); } catch (error) { if (error.name === 'AbortError') return; console.error('Analysis error:', error); renderWithoutSuggestions(text); if (typeof showToast === 'function') showToast('\u062a\u0639\u0630\u0651\u0631 \u0627\u0644\u062a\u062d\u0644\u064a\u0644 \u2014 \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0627\u062a\u0635\u0627\u0644', 'error'); } finally { setAnalyzingState(false); } } function renderWithoutSuggestions(text) { const editor = getEditorElement(); if (!editor) return; // Just clear overlays, don't replace content (preserves formatting) clearOverlays(editor); updatePlaceholder(); } function updateSuggestionCounts(spelling, grammar, punctuation) { const spellingEl = document.getElementById('spelling-count'); const grammarEl = document.getElementById('grammar-count'); const punctuationEl = document.getElementById('punctuation-count'); if (spellingEl) spellingEl.textContent = spelling.toLocaleString('ar-EG'); if (grammarEl) grammarEl.textContent = grammar.toLocaleString('ar-EG'); if (punctuationEl) punctuationEl.textContent = punctuation.toLocaleString('ar-EG'); } function handleEditorClick(e) { const target = e.target; if (target.classList.contains('spelling-error') || target.classList.contains('grammar-error') || target.classList.contains('punctuation-suggestion')) { showTooltip(target); } } function showTooltip(element) { const id = element.dataset.suggestionId; const suggestion = findSuggestionById(id); if (!suggestion) return; const tooltip = document.getElementById('editor-tooltip'); if (!tooltip) return; const typeEl = document.getElementById('tooltip-type'); const originalEl = document.getElementById('tooltip-original'); const alternativesEl = document.getElementById('tooltip-alternatives'); const typeMap = { spelling: { label: 'خطأ إملائي' }, grammar: { label: 'خطأ نحوي' }, punctuation: { label: 'علامات ترقيم' } }; if (typeEl) { const typeInfo = typeMap[suggestion.type] || { label: suggestion.type }; typeEl.innerHTML = typeInfo.label; typeEl.className = `popover-type popover-type--${suggestion.type}`; } if (originalEl) { originalEl.innerHTML = `${escapeHtml(suggestion.original)} ${escapeHtml(suggestion.correction)}`; } // Render alternatives if (alternativesEl) { // Use shared helper (defined in ui.js, loaded before editor.js) const alts = (typeof resolveAlternatives === 'function') ? resolveAlternatives(suggestion) : (suggestion.alternatives && suggestion.alternatives.length > 0) ? suggestion.alternatives : [suggestion.correction, suggestion.original]; let html = ''; // Render corrections first (non-keep) alts.forEach((alt, i) => { const isKeep = alt === suggestion.original; if (isKeep) return; // render keep button last const isMain = i === 0; const btnClass = isMain ? 'popover-alt-btn popover-alt-main' : 'popover-alt-btn'; html += ``; }); // Render keep button at end html += ``; alternativesEl.innerHTML = html; // Bind click events for alternatives alternativesEl.querySelectorAll('.popover-alt-btn').forEach(btn => { btn.addEventListener('click', () => { const correctionText = btn.dataset.altCorrection; if (correctionText === suggestion.original) { dismissSuggestion(suggestion); } else { applyAlternativeCorrection(suggestion, correctionText); } }); }); } const rect = element.getBoundingClientRect(); let top = rect.bottom + 10; let left = rect.left; if (left + 320 > window.innerWidth) { left = window.innerWidth - 330; } if (top + 150 > window.innerHeight) { top = rect.top - 150; } tooltip.style.top = `${top}px`; tooltip.style.left = `${Math.max(8, left)}px`; tooltip.classList.add('show'); window.currentApplySuggestion = suggestion; window.currentSuggestionElement = element; window.currentSuggestionId = id; } function hideTooltip() { const tooltip = document.getElementById('editor-tooltip'); if (tooltip) { tooltip.classList.remove('show'); } window.currentApplySuggestion = null; window.currentSuggestionElement = null; } function applySuggestionAtOffsets(suggestion) { _isApplyingSuggestion = true; try { pushUndoState(); // Save state before correction // Pipeline Hardening v3.3: UUID-based span lookup const suggestionId = suggestion.id; const errorSpan = suggestionId ? document.querySelector(`[data-suggestion-id="${suggestionId}"]`) : null; if (errorSpan) { const parent = errorSpan.parentNode; const correctedNode = document.createTextNode(suggestion.correction); parent.insertBefore(correctedNode, errorSpan); parent.removeChild(errorSpan); parent.normalize(); // Place cursor right after the corrected text try { const sel = window.getSelection(); const r = document.createRange(); r.setStartAfter(correctedNode); r.collapse(true); sel.removeAllRanges(); sel.addRange(r); } catch(e) {} } else { // Fallback: find span by matching original text const allErrorSpans = document.querySelectorAll('.spelling-error, .grammar-error, .punctuation-suggestion'); let found = false; allErrorSpans.forEach(span => { if (!found && span.textContent === suggestion.original) { const p = span.parentNode; const correctedNode = document.createTextNode(suggestion.correction); p.insertBefore(correctedNode, span); p.removeChild(span); p.normalize(); // Place cursor right after the corrected text try { const sel = window.getSelection(); const r = document.createRange(); r.setStartAfter(correctedNode); r.collapse(true); sel.removeAllRanges(); sel.addRange(r); } catch(e) {} found = true; } }); if (!found) { // Last resort: offset-based replacement const text = getEditorText(); const before = text.substring(0, suggestion.start); const after = text.substring(suggestion.end); const newText = before + suggestion.correction + after; setEditorHTML(escapeHtml(newText)); // Place cursor after the inserted correction setCaretOffset(suggestion.start + suggestion.correction.length); } } hideTooltip(); // Re-focus editor so Ctrl+Z works immediately after tooltip correction const _ed = getEditorElement(); if (_ed) _ed.focus(); // Remove applied suggestion from list (UUID-based, no re-indexing needed) if (window.currentSuggestions) { window.currentSuggestions = window.currentSuggestions.filter( s => s.id !== suggestion.id ); const spellingCount = window.currentSuggestions.filter(s => s.type === 'spelling').length; const grammarCount = window.currentSuggestions.filter(s => s.type === 'grammar').length; const punctuationCount = window.currentSuggestions.filter(s => s.type === 'punctuation').length; updateSuggestionCounts(spellingCount, grammarCount, punctuationCount); updateWritingScore(spellingCount, grammarCount, punctuationCount); updateSuggestionsList(window.currentSuggestions); } // Pipeline Hardening v3.3: Do NOT call analyzeTextDelayed() — prevents recursive re-analysis } finally { _isApplyingSuggestion = false; } // P2/User Request: Auto re-analyze after applying suggestion // Calls analyzeText() DIRECTLY (not delayed) for instant re-analysis. setTimeout(() => { analyzeText(); }, 300); } function applyCorrection() { if (!window.currentApplySuggestion) return; applySuggestionAtOffsets(window.currentApplySuggestion); if (typeof showToast === 'function') showToast('✓ تم التصحيح'); } function applyAlternativeCorrection(suggestion, correctionText) { _isApplyingSuggestion = true; try { pushUndoState(); // Save state before correction // Pipeline Hardening v3.3: UUID-based span lookup const suggestionId = suggestion.id; const errorSpan = suggestionId ? document.querySelector(`[data-suggestion-id="${suggestionId}"]`) : null; if (errorSpan) { const parent = errorSpan.parentNode; const correctedNode = document.createTextNode(correctionText); parent.insertBefore(correctedNode, errorSpan); parent.removeChild(errorSpan); parent.normalize(); // Place cursor right after the corrected text try { const sel = window.getSelection(); const r = document.createRange(); r.setStartAfter(correctedNode); r.collapse(true); sel.removeAllRanges(); sel.addRange(r); } catch(e) {} } else { const allErrorSpans = document.querySelectorAll('.spelling-error, .grammar-error, .punctuation-suggestion'); let found = false; allErrorSpans.forEach(span => { if (!found && span.textContent === suggestion.original) { const p = span.parentNode; const correctedNode = document.createTextNode(correctionText); p.insertBefore(correctedNode, span); p.removeChild(span); p.normalize(); // Place cursor right after the corrected text try { const sel = window.getSelection(); const r = document.createRange(); r.setStartAfter(correctedNode); r.collapse(true); sel.removeAllRanges(); sel.addRange(r); } catch(e) {} found = true; } }); if (!found) { const text = getEditorText(); const before = text.substring(0, suggestion.start); const after = text.substring(suggestion.end); const newText = before + correctionText + after; setEditorHTML(escapeHtml(newText)); // Place cursor after the inserted correction setCaretOffset(suggestion.start + correctionText.length); } } hideTooltip(); // Re-focus editor so Ctrl+Z works immediately after tooltip correction const _ed2 = getEditorElement(); if (_ed2) _ed2.focus(); // Remove applied suggestion (UUID-based, no re-indexing needed) if (window.currentSuggestions) { window.currentSuggestions = window.currentSuggestions.filter( s => s.id !== suggestion.id ); const spellingCount = window.currentSuggestions.filter(s => s.type === 'spelling').length; const grammarCount = window.currentSuggestions.filter(s => s.type === 'grammar').length; const punctuationCount = window.currentSuggestions.filter(s => s.type === 'punctuation').length; updateSuggestionCounts(spellingCount, grammarCount, punctuationCount); updateWritingScore(spellingCount, grammarCount, punctuationCount); updateSuggestionsList(window.currentSuggestions); } // Pipeline Hardening v3.3: Do NOT call analyzeTextDelayed() — prevents recursive re-analysis } finally { _isApplyingSuggestion = false; } // P2/User Request: Auto re-analyze after applying alternative correction setTimeout(() => { analyzeText(); }, 300); } function dismissSuggestion(suggestion) { pushUndoState(); // Save state before dismiss // Add the word to dismissed whitelist so it's never flagged again if (suggestion.original) { _dismissedWords.add(suggestion.original); _saveDismissedWords(); } // Remove the error highlight but keep the text as-is // Pipeline Hardening v3.3: UUID-based span lookup const suggestionId = suggestion.id; const errorSpan = suggestionId ? document.querySelector(`[data-suggestion-id="${suggestionId}"]`) : null; if (errorSpan) { // Unwrap: replace span with its text content const parent = errorSpan.parentNode; while (errorSpan.firstChild) { parent.insertBefore(errorSpan.firstChild, errorSpan); } parent.removeChild(errorSpan); parent.normalize(); } if (window.currentSuggestions) { window.currentSuggestions = window.currentSuggestions.filter( s => s.id !== suggestion.id ); // Pipeline Hardening v3.3: No re-indexing needed — UUID-based const spellingCount = window.currentSuggestions.filter(s => s.type === 'spelling').length; const grammarCount = window.currentSuggestions.filter(s => s.type === 'grammar').length; const punctuationCount = window.currentSuggestions.filter(s => s.type === 'punctuation').length; updateSuggestionCounts(spellingCount, grammarCount, punctuationCount); updateWritingScore(spellingCount, grammarCount, punctuationCount); updateSuggestionsList(window.currentSuggestions); } hideTooltip(); // Re-focus editor so Ctrl+Z works immediately const _ed3 = getEditorElement(); if (_ed3) _ed3.focus(); } function applySuggestionById(id) { // Pipeline Hardening v3.3: UUID-based lookup const suggestion = findSuggestionById(id); if (!suggestion) return; applySuggestionAtOffsets(suggestion); } function applyAllSuggestions() { // CRITICAL: Sort in REVERSE order (highest start offset first). const suggestions = [...(window.currentSuggestions || [])].sort((a, b) => b.start - a.start); if (suggestions.length === 0) return; _isApplyingSuggestion = true; try { pushUndoState(); // Save state before applying all // DOM-based approach: replace each error span individually to preserve formatting let appliedCount = 0; suggestions.forEach((s) => { const sid = s.id; const errorSpan = sid ? document.querySelector(`[data-suggestion-id="${sid}"]`) : null; if (errorSpan) { const parent = errorSpan.parentNode; const correctedNode = document.createTextNode(s.correction); parent.insertBefore(correctedNode, errorSpan); parent.removeChild(errorSpan); parent.normalize(); appliedCount++; } }); // Fallback: if DOM approach missed some, do text-based replacement if (appliedCount < suggestions.length) { let text = getEditorText(); // Re-sort remaining by offset (they weren't found as spans) const remaining = suggestions.filter(s => { const sid = s.id; return !sid || !document.querySelector(`[data-suggestion-id="${sid}"]`); }).sort((a, b) => b.start - a.start); remaining.forEach((s) => { text = text.substring(0, s.start) + s.correction + text.substring(s.end); }); if (remaining.length > 0) { setEditorHTML(escapeHtml(text)); } } // Place cursor at end of editor content const editor = getEditorElement(); if (editor) { try { const sel = window.getSelection(); const range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } catch(e) {} editor.focus(); } hideTooltip(); // Clear suggestions window.currentSuggestions = []; updateSuggestionCounts(0, 0, 0); updateWritingScore(0, 0, 0); updateSuggestionsList([]); if (typeof showToast === 'function') showToast('✓ تم تطبيق ' + suggestions.length + ' تصحيح'); } finally { _isApplyingSuggestion = false; } // P2/User Request: Auto re-analyze after applying all suggestions setTimeout(() => { analyzeText(); }, 300); } function clearEditor() { // Don't prompt if editor is already empty const text = getEditorText(); if (text.trim().length > 0) { if (!confirm('هل أنت متأكد من مسح كل المحتوى؟')) return; } setEditorHTML(''); window.currentSuggestions = []; updateSuggestionCounts(0, 0, 0); updateWritingScore(0, 0, 0); updateSuggestionsList([]); updateEditorStats(); updatePlaceholder(); updateAnalysisLimitBanner(false); if (typeof updateExportButtonStates === 'function') updateExportButtonStates(); } /** * Load plain text into editor — sole entry point for document import * @param {string} text - UTF-8 plain text * @param {object} options - { analyze: true, filename: string } */ function loadDocumentText(text, options = {}) { const normalized = typeof normalizeImportedText === 'function' ? normalizeImportedText(text) : String(text || '').replace(/^\uFEFF/, ''); setEditorHTML(escapeHtml(normalized)); window.currentSuggestions = []; hideTooltip(); updatePlaceholder(); updateEditorStats(); updateSuggestionCounts(0, 0, 0); updateWritingScore(0, 0, 0); updateSuggestionsList([]); updateAnalysisLimitBanner(normalized.length > MAX_ANALYZE_LENGTH); if (typeof updateExportButtonStates === 'function') { updateExportButtonStates(); } if (options.analyze !== false) { analyzeTextDelayed(); } } function copyText() { const text = getEditorText(); navigator.clipboard.writeText(text).then(() => { if (typeof showToast === 'function') showToast('✓ تم نسخ النص'); }).catch(() => { const temp = document.createElement('textarea'); temp.value = text; document.body.appendChild(temp); temp.select(); document.execCommand('copy'); document.body.removeChild(temp); if (typeof showToast === 'function') showToast('✓ تم نسخ النص'); }); } // ── Feedback API (P2) ── function _sendFeedback(suggestion, helpful) { const apiBase = window.BAYAN_API_BASE || ''; fetch(`${apiBase}/api/feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ suggestion_id: suggestion.id || '', helpful: helpful, original: suggestion.original || '', correction: suggestion.correction || '', text: (document.getElementById('editor-container')?.textContent || '').substring(0, 200), }) }).catch(err => console.warn('[Feedback] Failed:', err)); } if (typeof module !== 'undefined' && module.exports) { module.exports = { initEditor, analyzeText, analyzeTextDelayed, clearEditor, copyText, loadDocumentText, updateEditorStats, showTooltip, hideTooltip, applyCorrection, applySuggestionById, applyAllSuggestions }; }