55
66import { predictiveAutocomplete } from '@/lib/contextual-engine/predictive-autocomplete' ;
77import type { ContextualNiche } from '@/lib/contextual-engine/types' ;
8+ import { asSuggestionSuffix } from '@/lib/agentic/suggestion-suffix' ;
89
910const AI_ALLOW_RATIO = 0.4 ; // at most ~40% of fallbacks may hit AI
1011const MIN_AI_GAP_MS = 14_000 ;
@@ -36,15 +37,17 @@ export function completeOfflineSuffix(
3637 const text = String ( draft || '' ) ;
3738 if ( text . trim ( ) . length < 2 ) return '' ;
3839
40+ const finish = ( raw : string ) => asSuggestionSuffix ( text , raw ) ;
41+
3942 try {
4043 const pred = predictiveAutocomplete . predict ( text , text . length , {
4144 niche : options . niche || 'productivity' ,
4245 minConfidence : options . minConfidence ?? 0.55 ,
4346 } ) ;
44- const inline = String ( pred . inlineSuffix || '' ) . trim ( ) ;
47+ const inline = finish ( String ( pred . inlineSuffix || '' ) ) ;
4548 if ( inline . length >= 2 ) return inline . slice ( 0 , 120 ) ;
46- const top = pred . suggestions ?. [ 0 ] ?. text ;
47- if ( top && String ( top ) . trim ( ) . length >= 2 ) return String ( top ) . trim ( ) . slice ( 0 , 120 ) ;
49+ const top = finish ( String ( pred . suggestions ?. [ 0 ] ?. text || '' ) ) ;
50+ if ( top . length >= 2 ) return top . slice ( 0 , 120 ) ;
4851 } catch { }
4952
5053 const words = text . trim ( ) . split ( / \s + / ) ;
@@ -54,23 +57,22 @@ export function completeOfflineSuffix(
5457 const body = String ( s ?. text || '' ) ;
5558 const idx = body . toLowerCase ( ) . indexOf ( tail ) ;
5659 if ( idx >= 0 ) {
57- const rest = body . slice ( idx + tail . length ) . replace ( / ^ \s + / , '' ) ;
60+ const rest = finish ( body . slice ( idx + tail . length ) ) ;
5861 if ( rest . length >= 4 ) return rest . slice ( 0 , 100 ) ;
5962 }
6063 }
6164 }
6265
63- // Weak cold-start: borrow a short fragment from a sample that shares a word
64- const lastWord = ( words [ words . length - 1 ] || '' ) . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ] / gi, '' ) ;
66+ // Weak cold-start: only remainder after shared opening with draft (never restate draft)
67+ const lastWord = ( words [ words . length - 1 ] || '' ) . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ' ] / gi, '' ) ;
6568 if ( lastWord . length >= 4 ) {
6669 for ( const s of samples ) {
67- const body = String ( s ?. text || '' ) ;
68- if ( body . toLowerCase ( ) . includes ( lastWord ) ) {
69- const parts = body . split ( / [ . ! ? \n ] / ) . map ( ( p ) => p . trim ( ) ) . filter ( ( p ) => p . length > 12 ) ;
70- const pick = parts [ 0 ] ;
71- if ( pick && ! text . toLowerCase ( ) . includes ( pick . toLowerCase ( ) . slice ( 0 , 20 ) ) ) {
72- return pick . slice ( 0 , 80 ) ;
73- }
70+ const body = String ( s ?. text || '' ) . trim ( ) ;
71+ if ( ! body . toLowerCase ( ) . includes ( lastWord ) ) continue ;
72+ const parts = body . split ( / [ . ! ? \n ] / ) . map ( ( p ) => p . trim ( ) ) . filter ( ( p ) => p . length > 12 ) ;
73+ for ( const pick of parts ) {
74+ const suffix = finish ( pick ) ;
75+ if ( suffix . length >= 4 ) return suffix . slice ( 0 , 80 ) ;
7476 }
7577 }
7678 }
@@ -100,7 +102,6 @@ export function suggestOfflineReply(
100102 . map ( ( s ) => String ( s ?. text || '' ) . trim ( ) )
101103 . filter ( ( s ) => s . length >= 8 && s . length <= 160 ) ;
102104
103- // Prefer short sample replies that do not echo the parent verbatim
104105 for ( const s of pool ) {
105106 if ( parent && parent . slice ( 0 , 40 ) . toLowerCase ( ) === s . slice ( 0 , 40 ) . toLowerCase ( ) ) continue ;
106107 if ( / ^ ( j u s t s h a r e d | s h a r e d a n u p d a t e ) / i. test ( s ) ) continue ;
@@ -109,7 +110,6 @@ export function suggestOfflineReply(
109110
110111 if ( ! parent ) return '' ;
111112
112- // Soft topic hook from parent first words — restrained, not fake praise
113113 const topic = parent
114114 . replace ( / \s + / g, ' ' )
115115 . slice ( 0 , 48 )
@@ -160,9 +160,7 @@ export async function pickCompletionSource(opts: {
160160 allowAi : boolean ;
161161} ) : Promise < 'offline' | 'ai' | 'none' > {
162162 if ( opts . offlineSuffix . trim ( ) . length >= 2 ) {
163- // Even with offline hit, rarely skip to AI only if offline is tiny AND budget — prefer offline
164163 if ( opts . offlineSuffix . trim ( ) . length >= 6 ) return 'offline' ;
165- // Short offline: still prefer offline 70%
166164 if ( Math . random ( ) < 0.7 ) return 'offline' ;
167165 }
168166 if ( ! opts . allowAi ) return opts . offlineSuffix . trim ( ) ? 'offline' : 'none' ;
0 commit comments