@@ -106,6 +106,7 @@ const SEND_BUTTON_ACTIVE =
106106const SEND_BUTTON_DISABLED = 'bg-[var(--c-808080)] dark:bg-[var(--c-808080)]'
107107
108108const MAX_CHAT_TEXTAREA_HEIGHT = 200
109+ const SPEECH_RECOGNITION_LANG = 'en-US'
109110
110111const DROP_OVERLAY_ICONS = [
111112 PdfIcon ,
@@ -267,13 +268,18 @@ export function UserInput({
267268 const [ isListening , setIsListening ] = useState ( false )
268269 const recognitionRef = useRef < SpeechRecognitionInstance | null > ( null )
269270 const prefixRef = useRef ( '' )
271+ const valueRef = useRef ( value )
270272
271273 useEffect ( ( ) => {
272274 return ( ) => {
273275 recognitionRef . current ?. abort ( )
274276 }
275277 } , [ ] )
276278
279+ useEffect ( ( ) => {
280+ valueRef . current = value
281+ } , [ value ] )
282+
277283 const textareaRef = mentionMenu . textareaRef
278284 const wasSendingRef = useRef ( false )
279285 const atInsertPosRef = useRef < number | null > ( null )
@@ -488,6 +494,80 @@ export function UserInput({
488494 [ handleSubmit , mentionTokensWithContext , value , textareaRef ]
489495 )
490496
497+ const startRecognition = useCallback ( ( ) => {
498+ const w = window as WindowWithSpeech
499+ const SpeechRecognitionAPI = w . SpeechRecognition || w . webkitSpeechRecognition
500+ if ( ! SpeechRecognitionAPI ) return
501+
502+ const recognition = new SpeechRecognitionAPI ( )
503+ recognition . continuous = true
504+ recognition . interimResults = true
505+ recognition . lang = SPEECH_RECOGNITION_LANG
506+
507+ recognition . onresult = ( event : SpeechRecognitionEvent ) => {
508+ let transcript = ''
509+ for ( let i = 0 ; i < event . results . length ; i ++ ) {
510+ transcript += event . results [ i ] [ 0 ] . transcript
511+ }
512+ const prefix = prefixRef . current
513+ const newVal = prefix ? `${ prefix } ${ transcript } ` : transcript
514+ setValue ( newVal )
515+ valueRef . current = newVal
516+ }
517+
518+ recognition . onend = ( ) => {
519+ if ( recognitionRef . current === recognition ) {
520+ prefixRef . current = valueRef . current
521+ try {
522+ recognition . start ( )
523+ } catch {
524+ recognitionRef . current = null
525+ setIsListening ( false )
526+ }
527+ }
528+ }
529+
530+ recognition . onerror = ( e : SpeechRecognitionErrorEvent ) => {
531+ if ( recognitionRef . current !== recognition ) return
532+ if ( e . error === 'aborted' || e . error === 'not-allowed' ) {
533+ recognitionRef . current = null
534+ setIsListening ( false )
535+ }
536+ }
537+
538+ recognitionRef . current = recognition
539+ try {
540+ recognition . start ( )
541+ } catch {
542+ recognitionRef . current = null
543+ setIsListening ( false )
544+ }
545+ } , [ ] )
546+
547+ const restartRecognition = useCallback (
548+ ( newPrefix : string ) => {
549+ if ( ! recognitionRef . current ) return
550+ prefixRef . current = newPrefix
551+ recognitionRef . current . abort ( )
552+ recognitionRef . current = null
553+ startRecognition ( )
554+ } ,
555+ [ startRecognition ]
556+ )
557+
558+ const toggleListening = useCallback ( ( ) => {
559+ if ( isListening ) {
560+ recognitionRef . current ?. stop ( )
561+ recognitionRef . current = null
562+ setIsListening ( false )
563+ return
564+ }
565+
566+ prefixRef . current = value
567+ startRecognition ( )
568+ setIsListening ( true )
569+ } , [ isListening , value , startRecognition ] )
570+
491571 const handleInputChange = useCallback ( ( e : React . ChangeEvent < HTMLTextAreaElement > ) => {
492572 const newValue = e . target . value
493573 const caret = e . target . selectionStart ?? newValue . length
@@ -499,16 +579,19 @@ export function UserInput({
499579 ) {
500580 const before = newValue . slice ( 0 , caret - 1 )
501581 const after = newValue . slice ( caret )
502- setValue ( `${ before } ${ after } ` )
582+ const adjusted = `${ before } ${ after } `
583+ setValue ( adjusted )
503584 atInsertPosRef . current = caret - 1
504585 setPlusMenuOpen ( true )
505586 setPlusMenuSearch ( '' )
506587 setPlusMenuActiveIndex ( 0 )
588+ restartRecognition ( adjusted )
507589 return
508590 }
509591
510592 setValue ( newValue )
511- } , [ ] )
593+ restartRecognition ( newValue )
594+ } , [ restartRecognition ] )
512595
513596 const handleSelectAdjust = useCallback ( ( ) => {
514597 const textarea = textareaRef . current
@@ -536,56 +619,6 @@ export function UserInput({
536619 [ isInitialView ]
537620 )
538621
539- const toggleListening = useCallback ( ( ) => {
540- if ( isListening ) {
541- recognitionRef . current ?. stop ( )
542- recognitionRef . current = null
543- setIsListening ( false )
544- return
545- }
546-
547- const w = window as WindowWithSpeech
548- const SpeechRecognitionAPI = w . SpeechRecognition || w . webkitSpeechRecognition
549- if ( ! SpeechRecognitionAPI ) return
550-
551- prefixRef . current = value
552-
553- const recognition = new SpeechRecognitionAPI ( )
554- recognition . continuous = true
555- recognition . interimResults = true
556- recognition . lang = 'en-US'
557-
558- recognition . onresult = ( event : SpeechRecognitionEvent ) => {
559- let transcript = ''
560- for ( let i = 0 ; i < event . results . length ; i ++ ) {
561- transcript += event . results [ i ] [ 0 ] . transcript
562- }
563- const prefix = prefixRef . current
564- setValue ( prefix ? `${ prefix } ${ transcript } ` : transcript )
565- }
566-
567- recognition . onend = ( ) => {
568- if ( recognitionRef . current === recognition ) {
569- try {
570- recognition . start ( )
571- } catch {
572- recognitionRef . current = null
573- setIsListening ( false )
574- }
575- }
576- }
577- recognition . onerror = ( e : SpeechRecognitionErrorEvent ) => {
578- if ( e . error === 'aborted' || e . error === 'not-allowed' ) {
579- recognitionRef . current = null
580- setIsListening ( false )
581- }
582- }
583-
584- recognitionRef . current = recognition
585- recognition . start ( )
586- setIsListening ( true )
587- } , [ isListening , value ] )
588-
589622 const renderOverlayContent = useCallback ( ( ) => {
590623 const contexts = contextManagement . selectedContexts
591624
0 commit comments