youssefreda9 commited on
Commit
4bde1ea
·
1 Parent(s): a9630ec

fix: Grammar retry on rate-limit + cursor position after correction

Browse files

- Grammar service: transient errors (rate limiting, timeout) no longer cached permanently — next request will retry loading instead of failing forever
- Editor: cursor now placed right after corrected text instead of jumping to start — applied to all 3 code paths (UUID span, text-match fallback, offset fallback)

src/js/editor.js CHANGED
@@ -451,6 +451,15 @@ function applySuggestionAtOffsets(suggestion) {
451
  parent.insertBefore(correctedNode, errorSpan);
452
  parent.removeChild(errorSpan);
453
  parent.normalize();
 
 
 
 
 
 
 
 
 
454
  } else {
455
  // Fallback: find span by matching original text
456
  const allErrorSpans = document.querySelectorAll('.spelling-error, .grammar-error, .punctuation-suggestion');
@@ -462,6 +471,15 @@ function applySuggestionAtOffsets(suggestion) {
462
  p.insertBefore(correctedNode, span);
463
  p.removeChild(span);
464
  p.normalize();
 
 
 
 
 
 
 
 
 
465
  found = true;
466
  }
467
  });
@@ -472,6 +490,8 @@ function applySuggestionAtOffsets(suggestion) {
472
  const after = text.substring(suggestion.end);
473
  const newText = before + suggestion.correction + after;
474
  setEditorHTML(escapeHtml(newText));
 
 
475
  }
476
  }
477
  hideTooltip();
@@ -517,6 +537,15 @@ function applyAlternativeCorrection(suggestion, correctionText) {
517
  parent.insertBefore(correctedNode, errorSpan);
518
  parent.removeChild(errorSpan);
519
  parent.normalize();
 
 
 
 
 
 
 
 
 
520
  } else {
521
  const allErrorSpans = document.querySelectorAll('.spelling-error, .grammar-error, .punctuation-suggestion');
522
  let found = false;
@@ -527,6 +556,15 @@ function applyAlternativeCorrection(suggestion, correctionText) {
527
  p.insertBefore(correctedNode, span);
528
  p.removeChild(span);
529
  p.normalize();
 
 
 
 
 
 
 
 
 
530
  found = true;
531
  }
532
  });
@@ -536,6 +574,8 @@ function applyAlternativeCorrection(suggestion, correctionText) {
536
  const after = text.substring(suggestion.end);
537
  const newText = before + correctionText + after;
538
  setEditorHTML(escapeHtml(newText));
 
 
539
  }
540
  }
541
  hideTooltip();
 
451
  parent.insertBefore(correctedNode, errorSpan);
452
  parent.removeChild(errorSpan);
453
  parent.normalize();
454
+ // Place cursor right after the corrected text
455
+ try {
456
+ const sel = window.getSelection();
457
+ const r = document.createRange();
458
+ r.setStartAfter(correctedNode);
459
+ r.collapse(true);
460
+ sel.removeAllRanges();
461
+ sel.addRange(r);
462
+ } catch(e) {}
463
  } else {
464
  // Fallback: find span by matching original text
465
  const allErrorSpans = document.querySelectorAll('.spelling-error, .grammar-error, .punctuation-suggestion');
 
471
  p.insertBefore(correctedNode, span);
472
  p.removeChild(span);
473
  p.normalize();
474
+ // Place cursor right after the corrected text
475
+ try {
476
+ const sel = window.getSelection();
477
+ const r = document.createRange();
478
+ r.setStartAfter(correctedNode);
479
+ r.collapse(true);
480
+ sel.removeAllRanges();
481
+ sel.addRange(r);
482
+ } catch(e) {}
483
  found = true;
484
  }
485
  });
 
490
  const after = text.substring(suggestion.end);
491
  const newText = before + suggestion.correction + after;
492
  setEditorHTML(escapeHtml(newText));
493
+ // Place cursor after the inserted correction
494
+ setCaretOffset(suggestion.start + suggestion.correction.length);
495
  }
496
  }
497
  hideTooltip();
 
537
  parent.insertBefore(correctedNode, errorSpan);
538
  parent.removeChild(errorSpan);
539
  parent.normalize();
540
+ // Place cursor right after the corrected text
541
+ try {
542
+ const sel = window.getSelection();
543
+ const r = document.createRange();
544
+ r.setStartAfter(correctedNode);
545
+ r.collapse(true);
546
+ sel.removeAllRanges();
547
+ sel.addRange(r);
548
+ } catch(e) {}
549
  } else {
550
  const allErrorSpans = document.querySelectorAll('.spelling-error, .grammar-error, .punctuation-suggestion');
551
  let found = false;
 
556
  p.insertBefore(correctedNode, span);
557
  p.removeChild(span);
558
  p.normalize();
559
+ // Place cursor right after the corrected text
560
+ try {
561
+ const sel = window.getSelection();
562
+ const r = document.createRange();
563
+ r.setStartAfter(correctedNode);
564
+ r.collapse(true);
565
+ sel.removeAllRanges();
566
+ sel.addRange(r);
567
+ } catch(e) {}
568
  found = true;
569
  }
570
  });
 
574
  const after = text.substring(suggestion.end);
575
  const newText = before + correctionText + after;
576
  setEditorHTML(escapeHtml(newText));
577
+ // Place cursor after the inserted correction
578
+ setCaretOffset(suggestion.start + correctionText.length);
579
  }
580
  }
581
  hideTooltip();
src/nlp/grammar/grammar_service.py CHANGED
@@ -67,6 +67,9 @@ def get_grammar_model():
67
  """
68
  Lazy-load the grammar model on first call.
69
  Returns the GrammarChecker instance, or raises RuntimeError if loading fails.
 
 
 
70
  """
71
  global _grammar_checker, _load_error
72
 
@@ -101,9 +104,22 @@ def get_grammar_model():
101
 
102
  except Exception as e:
103
  import traceback
104
- _load_error = str(e)
105
  logger.error(f"Failed to load grammar model: {e}")
106
  logger.error(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  raise RuntimeError(f"Grammar model load failed: {e}")
108
 
109
 
 
67
  """
68
  Lazy-load the grammar model on first call.
69
  Returns the GrammarChecker instance, or raises RuntimeError if loading fails.
70
+
71
+ Transient errors (rate limiting, network timeouts) are NOT cached —
72
+ the next request will retry loading. Only permanent failures are cached.
73
  """
74
  global _grammar_checker, _load_error
75
 
 
104
 
105
  except Exception as e:
106
  import traceback
107
+ error_msg = str(e)
108
  logger.error(f"Failed to load grammar model: {e}")
109
  logger.error(traceback.format_exc())
110
+
111
+ # Transient errors (rate limiting, network) should NOT be cached —
112
+ # allow retry on next request
113
+ transient_keywords = ['Too many requests', 'rate limit', 'timeout',
114
+ 'ConnectionError', 'ConnectTimeout', 'ReadTimeout']
115
+ is_transient = any(kw.lower() in error_msg.lower() for kw in transient_keywords)
116
+
117
+ if is_transient:
118
+ logger.warning(f"Grammar load error is TRANSIENT — will retry on next request: {error_msg}")
119
+ # Do NOT set _load_error — next call will retry
120
+ else:
121
+ _load_error = error_msg # Cache permanent failures only
122
+
123
  raise RuntimeError(f"Grammar model load failed: {e}")
124
 
125