Skip to content

Commit 604f2be

Browse files
committed
fix(home): voice input text persistence bugs
1 parent 0c80438 commit 604f2be

File tree

1 file changed

+85
-52
lines changed
  • apps/sim/app/workspace/[workspaceId]/home/components/user-input

1 file changed

+85
-52
lines changed

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const SEND_BUTTON_ACTIVE =
106106
const SEND_BUTTON_DISABLED = 'bg-[var(--c-808080)] dark:bg-[var(--c-808080)]'
107107

108108
const MAX_CHAT_TEXTAREA_HEIGHT = 200
109+
const SPEECH_RECOGNITION_LANG = 'en-US'
109110

110111
const 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

Comments
 (0)