@@ -33,6 +33,10 @@ class WritingAssistant {
3333 this . undoDelay = 1000 ; // 1 second delay before saving undo state
3434 this . isUndoRedoOperation = false ;
3535
36+ // Debounce timer for parseSections (performance optimization)
37+ this . parseSectionsTimer = null ;
38+ this . parseSectionsDelay = 150 ; // 150ms debounce delay
39+
3640 this . init ( ) ;
3741 }
3842
@@ -125,17 +129,27 @@ class WritingAssistant {
125129 // Document text editing and cursor tracking
126130 documentTextarea . addEventListener ( 'input' , ( ) => this . handleDocumentTextChange ( ) ) ;
127131 documentTextarea . addEventListener ( 'click' , ( ) => this . handleCursorChange ( ) ) ;
128- documentTextarea . addEventListener ( 'keyup' , ( ) => this . handleCursorChange ( ) ) ;
132+ // Only handle cursor change for navigation keys (not regular typing)
133+ documentTextarea . addEventListener ( 'keyup' , ( e ) => {
134+ const navigationKeys = [ 'ArrowUp' , 'ArrowDown' , 'ArrowLeft' , 'ArrowRight' ,
135+ 'Home' , 'End' , 'PageUp' , 'PageDown' ] ;
136+ if ( navigationKeys . includes ( e . key ) ) {
137+ this . handleCursorChange ( ) ;
138+ }
139+ } ) ;
129140
130141 // Mode button controls (Ideas, Rewrite, Improve, Proofread)
142+ // Prevent buttons from stealing focus from the editor
131143 document . querySelectorAll ( '.mode-btn' ) . forEach ( btn => {
144+ btn . addEventListener ( 'mousedown' , ( e ) => e . preventDefault ( ) ) ;
132145 btn . addEventListener ( 'click' , ( ) => {
133146 const mode = btn . getAttribute ( 'data-mode' ) ;
134147 this . generateSuggestionForCurrentSection ( mode ) ;
135148 } ) ;
136149 } ) ;
137150
138- // Use suggestion button
151+ // Use suggestion button - also prevent focus steal
152+ useSuggestionBtn . addEventListener ( 'mousedown' , ( e ) => e . preventDefault ( ) ) ;
139153 useSuggestionBtn . addEventListener ( 'click' , ( ) => this . useSuggestion ( ) ) ;
140154
141155 // Initialize undo/redo system
@@ -443,19 +457,30 @@ class WritingAssistant {
443457 }
444458
445459 handleDocumentTextChange ( ) {
446- console . log ( 'handleDocumentTextChange called' ) ;
447460 const textarea = document . getElementById ( 'document-text' ) ;
448461 this . documentText = textarea . value ;
449- console . log ( 'Current document text:' , this . documentText ) ;
450462
451463 // Save undo state if this wasn't an undo/redo operation
452464 if ( ! this . isUndoRedoOperation ) {
453465 this . scheduleUndoStateSave ( ) ;
454466 }
455467
456- console . log ( 'About to call parseSections' ) ;
457- this . parseSections ( ) ;
458- this . handleCursorChange ( ) ;
468+ // Debounce parseSections to avoid expensive operations on every keystroke
469+ // Note: handleCursorChange is called after parseSections completes, not here,
470+ // to avoid UI flickering due to stale section positions
471+ this . scheduleParseSections ( ) ;
472+ }
473+
474+ scheduleParseSections ( ) {
475+ // Clear existing timer
476+ if ( this . parseSectionsTimer ) {
477+ clearTimeout ( this . parseSectionsTimer ) ;
478+ }
479+
480+ // Schedule parseSections after delay
481+ this . parseSectionsTimer = setTimeout ( ( ) => {
482+ this . parseSections ( ) ;
483+ } , this . parseSectionsDelay ) ;
459484 }
460485
461486 handleTitleChange ( ) {
@@ -477,8 +502,15 @@ class WritingAssistant {
477502 if ( str1 . length === 0 && str2 . length === 0 ) return 1 ;
478503 if ( str1 . length === 0 || str2 . length === 0 ) return 0 ;
479504
480- // Use Levenshtein distance for accurate similarity calculation
505+ // Cheap length pre-filter: if one string is more than 2x the length of
506+ // the other, similarity can't exceed 0.5 (our threshold), so skip Levenshtein
507+ const minLen = Math . min ( str1 . length , str2 . length ) ;
481508 const maxLen = Math . max ( str1 . length , str2 . length ) ;
509+ if ( maxLen > 2 * minLen ) {
510+ return minLen / maxLen ; // Return upper bound (actual similarity is lower)
511+ }
512+
513+ // Use Levenshtein distance for accurate similarity calculation
482514 const distance = this . levenshteinDistance ( str1 , str2 ) ;
483515 const similarity = 1 - ( distance / maxLen ) ;
484516
@@ -513,11 +545,8 @@ class WritingAssistant {
513545 }
514546
515547 parseSections ( ) {
516- console . log ( 'parseSections called' ) ;
517548 const text = this . documentText ;
518549 const oldSections = [ ...this . sections ] ; // Save previous sections
519- console . log ( 'Old sections count:' , oldSections . length ) ;
520- console . log ( 'Old sections:' , oldSections ) ;
521550 this . sections = [ ] ;
522551
523552 if ( ! text . trim ( ) ) {
@@ -530,7 +559,6 @@ class WritingAssistant {
530559
531560 // Split by double newlines (blank lines)
532561 const rawSections = text . split ( / \n \s * \n / ) ;
533- console . log ( 'Raw sections count:' , rawSections . length ) ;
534562 let currentPos = 0 ;
535563
536564 rawSections . forEach ( ( sectionText , index ) => {
@@ -551,52 +579,59 @@ class WritingAssistant {
551579 } ;
552580
553581 // Try to preserve generated_text from previous sections
554- // Look for a section with the same or similar text content
555- // Use findIndex to get the index so we can mark it as matched
556- const matchingOldSectionIndex = oldSections . findIndex ( ( oldSection , oldIndex ) => {
557- // Skip sections that have already been matched to another new section
558- if ( matchedOldSectionIndices . has ( oldIndex ) ) {
559- return false ;
560- }
561-
562- // Skip sections without generated text
563- if ( ! oldSection . generated_text ) {
564- return false ;
565- }
566-
567- // Calculate similarity between ORIGINAL text (when suggestion was generated) and new text
568- // This prevents the "moving target" bug where each keystroke is compared to the previous keystroke
582+ // Optimization: Try index-based matching first (most common case during typing)
583+ let matchingOldSectionIndex = - 1 ;
584+
585+ // Step 1: Try matching at the same index first (fast path for normal editing)
586+ if ( index < oldSections . length &&
587+ ! matchedOldSectionIndices . has ( index ) &&
588+ oldSections [ index ] . generated_text ) {
589+ const oldSection = oldSections [ index ] ;
569590 const originalText = oldSection . original_text || oldSection . text ;
570591 const similarity = this . calculateTextSimilarity ( originalText , trimmed ) ;
592+ if ( similarity > 0.5 ) {
593+ matchingOldSectionIndex = index ;
594+ }
595+ }
571596
572- // Debug logging
573- console . log ( 'Similarity check:' ) ;
574- console . log ( ' Original text (when generated):' , originalText ) ;
575- console . log ( ' New text:' , trimmed ) ;
576- console . log ( ' Similarity:' , similarity ) ;
577- console . log ( ' Keep suggestion?' , similarity > 0.5 ) ;
578-
579- // Keep suggestion if more than 50% of the text is the same
580- return similarity > 0.5 ;
581- } ) ;
597+ // Step 2: Fall back to searching all old sections only if index-based match failed
598+ if ( matchingOldSectionIndex === - 1 ) {
599+ matchingOldSectionIndex = oldSections . findIndex ( ( oldSection , oldIndex ) => {
600+ // Skip the index we already checked
601+ if ( oldIndex === index ) {
602+ return false ;
603+ }
604+ // Skip sections that have already been matched to another new section
605+ if ( matchedOldSectionIndices . has ( oldIndex ) ) {
606+ return false ;
607+ }
608+ // Skip sections without generated text
609+ if ( ! oldSection . generated_text ) {
610+ return false ;
611+ }
612+ // Calculate similarity between ORIGINAL text and new text
613+ const originalText = oldSection . original_text || oldSection . text ;
614+ const similarity = this . calculateTextSimilarity ( originalText , trimmed ) ;
615+ return similarity > 0.5 ;
616+ } ) ;
617+ }
582618
583619 if ( matchingOldSectionIndex !== - 1 ) {
584- console . log ( 'Preserving suggestion for section' ) ;
585620 const matchingOldSection = oldSections [ matchingOldSectionIndex ] ;
586621 newSection . generated_text = matchingOldSection . generated_text ;
587622 // Also preserve the original text so we continue comparing against it
588623 newSection . original_text = matchingOldSection . original_text || matchingOldSection . text ;
589624 // Mark this old section as matched so it won't be reused for other new sections
590625 matchedOldSectionIndices . add ( matchingOldSectionIndex ) ;
591- } else {
592- console . log ( 'Clearing suggestion for section' ) ;
593626 }
594627
595628 this . sections . push ( newSection ) ;
596629 }
597630 } ) ;
598631
599632 this . updateSectionInfo ( ) ;
633+ // Update cursor/suggestion panel after sections are rebuilt
634+ this . handleCursorChange ( ) ;
600635 }
601636
602637 handleCursorChange ( ) {
@@ -769,19 +804,9 @@ class WritingAssistant {
769804 // Save undo state before making changes
770805 this . saveUndoState ( ) ;
771806
772- // Debug section boundaries
773- console . log ( 'Section replacement debug:' ) ;
774- console . log ( 'Current section text:' , JSON . stringify ( currentSection . text ) ) ;
775- console . log ( 'Section start pos:' , currentSection . startPos ) ;
776- console . log ( 'Section end pos:' , currentSection . endPos ) ;
777- console . log ( 'Generated text:' , JSON . stringify ( currentSection . generated_text ) ) ;
778-
779807 const beforeSection = this . documentText . substring ( 0 , currentSection . startPos ) ;
780808 const afterSection = this . documentText . substring ( currentSection . endPos ) ;
781809
782- console . log ( 'Before section:' , JSON . stringify ( beforeSection . slice ( - 10 ) ) ) ; // Last 10 chars
783- console . log ( 'After section:' , JSON . stringify ( afterSection . slice ( 0 , 10 ) ) ) ; // First 10 chars
784-
785810 // Clean the generated text to prevent extra blank lines
786811 const cleanGeneratedText = currentSection . generated_text . trim ( ) ;
787812
@@ -1270,7 +1295,6 @@ class WritingAssistant {
12701295 applySavedAISettings ( ) {
12711296 // No server call needed - metadata is sent with each generation request
12721297 // This method kept for compatibility but does nothing
1273- console . log ( 'applySavedAISettings: No action needed - using stateless approach' ) ;
12741298 }
12751299
12761300 loadCurrentDocumentToSettingsForm ( ) {
@@ -1325,8 +1349,6 @@ class WritingAssistant {
13251349 model : documentData . metadata . model || ''
13261350 } ;
13271351
1328- console . log ( 'restoreDocumentMetadata: Setting documentMetadata to:' , this . documentMetadata ) ;
1329-
13301352 // Update the UI form fields AND the settings modal if it's open
13311353 // Use setTimeout to ensure DOM updates are processed
13321354 setTimeout ( ( ) => {
@@ -1358,7 +1380,6 @@ class WritingAssistant {
13581380 // Keep suggestion if more than 50% of the text is the same
13591381 if ( similarity > 0.5 ) {
13601382 currentSection . generated_text = savedSection . generated_text ;
1361- console . log ( `Restored generated_text for section ${ i } (similarity: ${ ( similarity * 100 ) . toFixed ( 1 ) } %):` , savedSection . generated_text . substring ( 0 , 50 ) + '...' ) ;
13621383 }
13631384 }
13641385 }
@@ -1426,10 +1447,8 @@ class WritingAssistant {
14261447
14271448 // Clear redo stack when new state is saved
14281449 this . redoStack = [ ] ;
1429-
1450+
14301451 this . lastUndoState = currentState ;
1431-
1432- console . log ( `Undo state saved. Stack size: ${ this . undoStack . length } ` ) ;
14331452 }
14341453
14351454undo ( ) {
@@ -1523,7 +1542,6 @@ undo() {
15231542 }
15241543
15251544 this . showMessage ( 'Undo applied' , 'success' ) ;
1526- console . log ( `Undo applied. Undo stack: ${ this . undoStack . length } , Redo stack: ${ this . redoStack . length } ` ) ;
15271545 }
15281546
15291547 redo ( ) {
@@ -1579,15 +1597,13 @@ undo() {
15791597 }
15801598
15811599 this . showMessage ( 'Redo applied' , 'success' ) ;
1582- console . log ( `Redo applied. Undo stack: ${ this . undoStack . length } , Redo stack: ${ this . redoStack . length } ` ) ;
15831600 }
1584-
1601+
15851602 clearUndoHistory ( ) {
15861603 this . undoStack = [ ] ;
15871604 this . redoStack = [ ] ;
15881605 this . lastUndoState = null ;
15891606 this . saveUndoState ( ) ; // Save current state as first undo point
1590- console . log ( 'Undo history cleared' ) ;
15911607 }
15921608
15931609 // Hotkey Management System
@@ -1717,8 +1733,6 @@ undo() {
17171733 // Update the system
17181734 this . updateButtonLabels ( ) ;
17191735 this . setupHotkeyListener ( ) ;
1720-
1721- console . log ( 'Hotkeys updated:' , this . hotkeys ) ;
17221736 }
17231737
17241738 async createNewDocument ( ) {
@@ -2345,7 +2359,6 @@ undo() {
23452359 }
23462360
23472361 async loadDocumentFromServer ( filename ) {
2348- console . log ( 'loadDocumentFromServer: called with filename:' , filename ) ;
23492362 try {
23502363 // Get document data directly from the load endpoint
23512364 const response = await this . authFetch ( `/documents/load/${ filename } ` ) ;
@@ -2591,17 +2604,14 @@ undo() {
25912604 async performAutoSave ( ) {
25922605 // Only auto-save if document has been saved at least once
25932606 if ( ! this . currentFilename ) {
2594- console . log ( 'Auto-save skipped: Document has not been saved yet' ) ;
25952607 this . startAutoSaveTimer ( ) ; // Restart timer
25962608 return ;
25972609 }
25982610
25992611 try {
2600- console . log ( 'Performing auto-save...' ) ;
26012612 await this . saveWithFilename ( this . currentFilename , false , true ) ;
26022613 this . lastSaveTime = new Date ( ) ;
26032614 this . showMessage ( 'Document auto-saved' , 'success' ) ;
2604- console . log ( 'Auto-save completed successfully' ) ;
26052615 } catch ( error ) {
26062616 console . error ( 'Auto-save failed:' , error ) ;
26072617 this . showMessage ( 'Auto-save failed' , 'error' ) ;
@@ -2664,8 +2674,6 @@ undo() {
26642674 body : formData ,
26652675 keepalive : true
26662676 } ) ) ;
2667-
2668- console . log ( 'Sync save performed on page exit' ) ;
26692677 } catch ( error ) {
26702678 console . error ( 'Sync save failed:' , error ) ;
26712679 } finally {
0 commit comments