Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -488,84 +494,30 @@ export function UserInput({
[handleSubmit, mentionTokensWithContext, value, textareaRef]
)

const handleInputChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
Comment thread
waleedlatif1 marked this conversation as resolved.
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
}

setValue(newValue)
}, [])

const handleSelectAdjust = useCallback(() => {
const textarea = textareaRef.current
if (!textarea) return
const pos = textarea.selectionStart ?? 0
const r = mentionTokensWithContext.findRangeContaining(pos)
if (r) {
const snapPos = pos - r.start < r.end - pos ? r.start : r.end
setTimeout(() => {
textarea.setSelectionRange(snapPos, snapPos)
}, 0)
}
}, [textareaRef, mentionTokensWithContext])

const handleInput = useCallback(
(e: React.FormEvent<HTMLTextAreaElement>) => {
const maxHeight = isInitialView ? window.innerHeight * 0.3 : MAX_CHAT_TEXTAREA_HEIGHT
autoResizeTextarea(e, maxHeight)

// Sync overlay scroll
if (overlayRef.current) {
overlayRef.current.scrollTop = (e.target as HTMLTextAreaElement).scrollTop
}
},
[isInitialView]
)

const toggleListening = useCallback(() => {
if (isListening) {
recognitionRef.current?.stop()
recognitionRef.current = null
setIsListening(false)
return
}

const startRecognition = useCallback((): boolean => {
const w = window as WindowWithSpeech
const SpeechRecognitionAPI = w.SpeechRecognition || w.webkitSpeechRecognition
if (!SpeechRecognitionAPI) return

prefixRef.current = value
if (!SpeechRecognitionAPI) return false

const recognition = new SpeechRecognitionAPI()
recognition.continuous = true
recognition.interimResults = true
recognition.lang = 'en-US'
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
setValue(prefix ? `${prefix} ${transcript}` : transcript)
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 {
Expand All @@ -574,17 +526,103 @@ export function UserInput({
}
}
}

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
recognition.start()
setIsListening(true)
}, [isListening, value])
try {
recognition.start()
return true
} catch {
recognitionRef.current = null
return false
}
}, [])

const restartRecognition = useCallback(
(newPrefix: string) => {
if (!recognitionRef.current) return
prefixRef.current = newPrefix
recognitionRef.current.abort()
recognitionRef.current = null
startRecognition()
},
[startRecognition]
)
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

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 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)
restartRecognition(newValue)
},
[restartRecognition]
)

const handleSelectAdjust = useCallback(() => {
const textarea = textareaRef.current
if (!textarea) return
const pos = textarea.selectionStart ?? 0
const r = mentionTokensWithContext.findRangeContaining(pos)
if (r) {
const snapPos = pos - r.start < r.end - pos ? r.start : r.end
setTimeout(() => {
textarea.setSelectionRange(snapPos, snapPos)
}, 0)
}
}, [textareaRef, mentionTokensWithContext])

const handleInput = useCallback(
(e: React.FormEvent<HTMLTextAreaElement>) => {
const maxHeight = isInitialView ? window.innerHeight * 0.3 : MAX_CHAT_TEXTAREA_HEIGHT
autoResizeTextarea(e, maxHeight)

// Sync overlay scroll
if (overlayRef.current) {
overlayRef.current.scrollTop = (e.target as HTMLTextAreaElement).scrollTop
}
},
[isInitialView]
)

const renderOverlayContent = useCallback(() => {
const contexts = contextManagement.selectedContexts
Expand Down
Loading