youssefreda9 commited on
Commit
f1671d1
·
1 Parent(s): 09be182

FIX-32: Fix word duplication bug in website frontend

Browse files

Root cause: applyAllSuggestions used a mixed DOM+text fallback approach.
After DOM-based fixes shifted the editor text, the text-based fallback
used stale offsets from the original API response, inserting corrections
at wrong positions. This caused 'المشروعالمشروع.' (word duplication).

Fix:
1. applyAllSuggestions now uses PURE text-based reverse-order replacement.
Since patches are sorted highest-offset-first, each offset is valid
because nothing before it has been modified yet.
2. _isApplyingSuggestion guard now stays active 400ms (past the 300ms
re-analysis timeout) to prevent normalize()/input events from
triggering duplicate analysis calls.

Files changed (1) hide show
  1. src/js/editor.js +18 -30
src/js/editor.js CHANGED
@@ -531,7 +531,9 @@ function applySuggestionAtOffsets(suggestion) {
531
  }
532
  // Pipeline Hardening v3.3: Do NOT call analyzeTextDelayed() — prevents recursive re-analysis
533
  } finally {
534
- _isApplyingSuggestion = false;
 
 
535
  }
536
  // P2/User Request: Auto re-analyze after applying suggestion
537
  // Calls analyzeText() DIRECTLY (not delayed) for instant re-analysis.
@@ -617,7 +619,8 @@ function applyAlternativeCorrection(suggestion, correctionText) {
617
  }
618
  // Pipeline Hardening v3.3: Do NOT call analyzeTextDelayed() — prevents recursive re-analysis
619
  } finally {
620
- _isApplyingSuggestion = false;
 
621
  }
622
  // P2/User Request: Auto re-analyze after applying alternative correction
623
  setTimeout(() => { analyzeText(); }, 300);
@@ -679,36 +682,19 @@ function applyAllSuggestions() {
679
  try {
680
  pushUndoState(); // Save state before applying all
681
 
682
- // DOM-based approach: replace each error span individually to preserve formatting
683
- let appliedCount = 0;
 
 
 
 
 
684
  suggestions.forEach((s) => {
685
- const sid = s.id;
686
- const errorSpan = sid ? document.querySelector(`[data-suggestion-id="${sid}"]`) : null;
687
- if (errorSpan) {
688
- const parent = errorSpan.parentNode;
689
- const correctedNode = document.createTextNode(s.correction);
690
- parent.insertBefore(correctedNode, errorSpan);
691
- parent.removeChild(errorSpan);
692
- parent.normalize();
693
- appliedCount++;
694
- }
695
- });
696
-
697
- // Fallback: if DOM approach missed some, do text-based replacement
698
- if (appliedCount < suggestions.length) {
699
- let text = getEditorText();
700
- // Re-sort remaining by offset (they weren't found as spans)
701
- const remaining = suggestions.filter(s => {
702
- const sid = s.id;
703
- return !sid || !document.querySelector(`[data-suggestion-id="${sid}"]`);
704
- }).sort((a, b) => b.start - a.start);
705
- remaining.forEach((s) => {
706
  text = text.substring(0, s.start) + s.correction + text.substring(s.end);
707
- });
708
- if (remaining.length > 0) {
709
- setEditorHTML(escapeHtml(text));
710
  }
711
- }
 
712
 
713
  // Place cursor at end of editor content
714
  const editor = getEditorElement();
@@ -733,7 +719,9 @@ function applyAllSuggestions() {
733
  updateSuggestionsList([]);
734
  if (typeof showToast === 'function') showToast('✓ تم تطبيق ' + suggestions.length + ' تصحيح');
735
  } finally {
736
- _isApplyingSuggestion = false;
 
 
737
  }
738
  // P2/User Request: Auto re-analyze after applying all suggestions
739
  setTimeout(() => { analyzeText(); }, 300);
 
531
  }
532
  // Pipeline Hardening v3.3: Do NOT call analyzeTextDelayed() — prevents recursive re-analysis
533
  } finally {
534
+ // FIX-32: Delay guard reset until AFTER re-analysis fires,
535
+ // preventing normalize()/input events from triggering double analysis.
536
+ setTimeout(() => { _isApplyingSuggestion = false; }, 400);
537
  }
538
  // P2/User Request: Auto re-analyze after applying suggestion
539
  // Calls analyzeText() DIRECTLY (not delayed) for instant re-analysis.
 
619
  }
620
  // Pipeline Hardening v3.3: Do NOT call analyzeTextDelayed() — prevents recursive re-analysis
621
  } finally {
622
+ // FIX-32: Delay guard reset until AFTER re-analysis fires.
623
+ setTimeout(() => { _isApplyingSuggestion = false; }, 400);
624
  }
625
  // P2/User Request: Auto re-analyze after applying alternative correction
626
  setTimeout(() => { analyzeText(); }, 300);
 
682
  try {
683
  pushUndoState(); // Save state before applying all
684
 
685
+ // FIX-32: Use PURE TEXT-BASED approach to avoid DOM/offset mismatch.
686
+ // The old mixed DOM+fallback approach caused word duplication because
687
+ // after DOM-based fixes shifted the text, the fallback's stale offsets
688
+ // would insert corrections at wrong positions.
689
+ // Reverse-order text replacement is safe — each patch's offsets are
690
+ // valid because we haven't modified anything before them yet.
691
+ let text = getEditorText();
692
  suggestions.forEach((s) => {
693
+ if (s.start >= 0 && s.end <= text.length && s.start <= s.end) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  text = text.substring(0, s.start) + s.correction + text.substring(s.end);
 
 
 
695
  }
696
+ });
697
+ setEditorHTML(escapeHtml(text));
698
 
699
  // Place cursor at end of editor content
700
  const editor = getEditorElement();
 
719
  updateSuggestionsList([]);
720
  if (typeof showToast === 'function') showToast('✓ تم تطبيق ' + suggestions.length + ' تصحيح');
721
  } finally {
722
+ // FIX-32: Delay guard reset until AFTER re-analysis fires,
723
+ // preventing input events from triggering double analysis.
724
+ setTimeout(() => { _isApplyingSuggestion = false; }, 400);
725
  }
726
  // P2/User Request: Auto re-analyze after applying all suggestions
727
  setTimeout(() => { analyzeText(); }, 300);