-
Notifications
You must be signed in to change notification settings - Fork 3.5k
v0.6.9: general ux improvements for tables, mothership #3747
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a783b9d
83eb3ed
8d93c85
b09a073
7b6149d
a7f344b
34ea99e
77eafab
b9926df
59182d5
f6975fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
* fix(home): voice input text persistence bugs * fix(home): gate setIsListening on startRecognition success * fix(home): handle startRecognition failure in restartRecognition * fix(home): reset speech prefix on submit while mic is active
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,6 +106,7 @@ const SEND_BUTTON_ACTIVE = | |
| const SEND_BUTTON_DISABLED = 'bg-[var(--c-808080)] dark:bg-[var(--c-808080)]' | ||
|
|
||
| const MAX_CHAT_TEXTAREA_HEIGHT = 200 | ||
| const SPEECH_RECOGNITION_LANG = 'en-US' | ||
|
|
||
| const DROP_OVERLAY_ICONS = [ | ||
| PdfIcon, | ||
|
|
@@ -267,13 +268,18 @@ export function UserInput({ | |
| const [isListening, setIsListening] = useState(false) | ||
| const recognitionRef = useRef<SpeechRecognitionInstance | null>(null) | ||
| const prefixRef = useRef('') | ||
| const valueRef = useRef(value) | ||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| recognitionRef.current?.abort() | ||
| } | ||
| }, []) | ||
|
|
||
| useEffect(() => { | ||
| valueRef.current = value | ||
| }, [value]) | ||
|
|
||
| const textareaRef = mentionMenu.textareaRef | ||
| const wasSendingRef = useRef(false) | ||
| const atInsertPosRef = useRef<number | null>(null) | ||
|
|
@@ -390,6 +396,84 @@ export function UserInput({ | |
| [textareaRef] | ||
| ) | ||
|
|
||
| const startRecognition = useCallback((): boolean => { | ||
| const w = window as WindowWithSpeech | ||
| const SpeechRecognitionAPI = w.SpeechRecognition || w.webkitSpeechRecognition | ||
| if (!SpeechRecognitionAPI) return false | ||
|
|
||
| const recognition = new SpeechRecognitionAPI() | ||
| recognition.continuous = true | ||
| recognition.interimResults = true | ||
| recognition.lang = SPEECH_RECOGNITION_LANG | ||
|
|
||
| recognition.onresult = (event: SpeechRecognitionEvent) => { | ||
| let transcript = '' | ||
| for (let i = 0; i < event.results.length; i++) { | ||
| transcript += event.results[i][0].transcript | ||
| } | ||
| const prefix = prefixRef.current | ||
| const newVal = prefix ? `${prefix} ${transcript}` : transcript | ||
| setValue(newVal) | ||
| valueRef.current = newVal | ||
| } | ||
|
|
||
| recognition.onend = () => { | ||
| if (recognitionRef.current === recognition) { | ||
| prefixRef.current = valueRef.current | ||
| try { | ||
| recognition.start() | ||
| } catch { | ||
| recognitionRef.current = null | ||
| setIsListening(false) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| recognition.onerror = (e: SpeechRecognitionErrorEvent) => { | ||
| if (recognitionRef.current !== recognition) return | ||
| if (e.error === 'aborted' || e.error === 'not-allowed') { | ||
| recognitionRef.current = null | ||
| setIsListening(false) | ||
| } | ||
| } | ||
|
|
||
| recognitionRef.current = recognition | ||
| try { | ||
| recognition.start() | ||
| return true | ||
| } catch { | ||
| recognitionRef.current = null | ||
| return false | ||
| } | ||
| }, []) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. startRecognition captures stale setValue closure referenceLow Severity The |
||
|
|
||
| const restartRecognition = useCallback( | ||
| (newPrefix: string) => { | ||
| if (!recognitionRef.current) return | ||
| prefixRef.current = newPrefix | ||
| recognitionRef.current.abort() | ||
| recognitionRef.current = null | ||
| if (!startRecognition()) { | ||
| setIsListening(false) | ||
| } | ||
| }, | ||
| [startRecognition] | ||
| ) | ||
|
|
||
| const toggleListening = useCallback(() => { | ||
| if (isListening) { | ||
| recognitionRef.current?.stop() | ||
| recognitionRef.current = null | ||
| setIsListening(false) | ||
| return | ||
| } | ||
|
|
||
| prefixRef.current = value | ||
| if (startRecognition()) { | ||
| setIsListening(true) | ||
| } | ||
| }, [isListening, value, startRecognition]) | ||
|
|
||
| const handleSubmit = useCallback(() => { | ||
| const fileAttachmentsForApi: FileAttachmentForApi[] = files.attachedFiles | ||
| .filter((f) => !f.uploading && f.key) | ||
|
|
@@ -407,13 +491,14 @@ export function UserInput({ | |
| contextManagement.selectedContexts.length > 0 ? contextManagement.selectedContexts : undefined | ||
| ) | ||
| setValue('') | ||
| restartRecognition('') | ||
| files.clearAttachedFiles() | ||
| contextManagement.clearContexts() | ||
|
|
||
| if (textareaRef.current) { | ||
| textareaRef.current.style.height = 'auto' | ||
| } | ||
| }, [onSubmit, files, value, contextManagement, textareaRef]) | ||
| }, [onSubmit, files, value, contextManagement, textareaRef, restartRecognition]) | ||
|
|
||
| const handleKeyDown = useCallback( | ||
| (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||
|
|
@@ -488,27 +573,33 @@ export function UserInput({ | |
| [handleSubmit, mentionTokensWithContext, value, textareaRef] | ||
| ) | ||
|
|
||
| const handleInputChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||
| const newValue = e.target.value | ||
| const caret = e.target.selectionStart ?? newValue.length | ||
|
|
||
| if ( | ||
| caret > 0 && | ||
| newValue.charAt(caret - 1) === '@' && | ||
| (caret === 1 || /\s/.test(newValue.charAt(caret - 2))) | ||
| ) { | ||
| const before = newValue.slice(0, caret - 1) | ||
| const after = newValue.slice(caret) | ||
| setValue(`${before}${after}`) | ||
| atInsertPosRef.current = caret - 1 | ||
| setPlusMenuOpen(true) | ||
| setPlusMenuSearch('') | ||
| setPlusMenuActiveIndex(0) | ||
| return | ||
| } | ||
| const handleInputChange = useCallback( | ||
| (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||
| const newValue = e.target.value | ||
| const caret = e.target.selectionStart ?? newValue.length | ||
|
|
||
| if ( | ||
| caret > 0 && | ||
| newValue.charAt(caret - 1) === '@' && | ||
| (caret === 1 || /\s/.test(newValue.charAt(caret - 2))) | ||
| ) { | ||
| const before = newValue.slice(0, caret - 1) | ||
| const after = newValue.slice(caret) | ||
| const adjusted = `${before}${after}` | ||
| setValue(adjusted) | ||
| atInsertPosRef.current = caret - 1 | ||
| setPlusMenuOpen(true) | ||
| setPlusMenuSearch('') | ||
| setPlusMenuActiveIndex(0) | ||
| restartRecognition(adjusted) | ||
| return | ||
| } | ||
|
|
||
| setValue(newValue) | ||
| }, []) | ||
| setValue(newValue) | ||
| restartRecognition(newValue) | ||
| }, | ||
| [restartRecognition] | ||
| ) | ||
|
|
||
| const handleSelectAdjust = useCallback(() => { | ||
| const textarea = textareaRef.current | ||
|
|
@@ -536,56 +627,6 @@ export function UserInput({ | |
| [isInitialView] | ||
| ) | ||
|
|
||
| const toggleListening = useCallback(() => { | ||
| if (isListening) { | ||
| recognitionRef.current?.stop() | ||
| recognitionRef.current = null | ||
| setIsListening(false) | ||
| return | ||
| } | ||
|
|
||
| const w = window as WindowWithSpeech | ||
| const SpeechRecognitionAPI = w.SpeechRecognition || w.webkitSpeechRecognition | ||
| if (!SpeechRecognitionAPI) return | ||
|
|
||
| prefixRef.current = value | ||
|
|
||
| const recognition = new SpeechRecognitionAPI() | ||
| recognition.continuous = true | ||
| recognition.interimResults = true | ||
| recognition.lang = 'en-US' | ||
|
|
||
| recognition.onresult = (event: SpeechRecognitionEvent) => { | ||
| let transcript = '' | ||
| for (let i = 0; i < event.results.length; i++) { | ||
| transcript += event.results[i][0].transcript | ||
| } | ||
| const prefix = prefixRef.current | ||
| setValue(prefix ? `${prefix} ${transcript}` : transcript) | ||
| } | ||
|
|
||
| recognition.onend = () => { | ||
| if (recognitionRef.current === recognition) { | ||
| try { | ||
| recognition.start() | ||
| } catch { | ||
| recognitionRef.current = null | ||
| setIsListening(false) | ||
| } | ||
| } | ||
| } | ||
| recognition.onerror = (e: SpeechRecognitionErrorEvent) => { | ||
| if (e.error === 'aborted' || e.error === 'not-allowed') { | ||
| recognitionRef.current = null | ||
| setIsListening(false) | ||
| } | ||
| } | ||
|
|
||
| recognitionRef.current = recognition | ||
| recognition.start() | ||
| setIsListening(true) | ||
| }, [isListening, value]) | ||
|
|
||
| const renderOverlayContent = useCallback(() => { | ||
| const contexts = contextManagement.selectedContexts | ||
|
|
||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.