| | import { useCallback, useRef } from 'react'; |
| | import { useToastContext, TooltipAnchor, ListeningIcon, Spinner } from '@librechat/client'; |
| | import { useLocalize, useSpeechToText, useGetAudioSettings } from '~/hooks'; |
| | import { useChatFormContext } from '~/Providers'; |
| | import { globalAudioId } from '~/common'; |
| | import { cn } from '~/utils'; |
| |
|
| | const isExternalSTT = (speechToTextEndpoint: string) => speechToTextEndpoint === 'external'; |
| | export default function AudioRecorder({ |
| | disabled, |
| | ask, |
| | methods, |
| | textAreaRef, |
| | isSubmitting, |
| | }: { |
| | disabled: boolean; |
| | ask: (data: { text: string }) => void; |
| | methods: ReturnType<typeof useChatFormContext>; |
| | textAreaRef: React.RefObject<HTMLTextAreaElement>; |
| | isSubmitting: boolean; |
| | }) { |
| | const { setValue, reset, getValues } = methods; |
| | const localize = useLocalize(); |
| | const { showToast } = useToastContext(); |
| | const { speechToTextEndpoint } = useGetAudioSettings(); |
| |
|
| | const existingTextRef = useRef<string>(''); |
| |
|
| | const onTranscriptionComplete = useCallback( |
| | (text: string) => { |
| | if (isSubmitting) { |
| | showToast({ |
| | message: localize('com_ui_speech_while_submitting'), |
| | status: 'error', |
| | }); |
| | return; |
| | } |
| | if (text) { |
| | const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement | null; |
| | if (globalAudio) { |
| | console.log('Unmuting global audio'); |
| | globalAudio.muted = false; |
| | } |
| | |
| | const finalText = |
| | isExternalSTT(speechToTextEndpoint) && existingTextRef.current |
| | ? `${existingTextRef.current} ${text}` |
| | : text; |
| | ask({ text: finalText }); |
| | reset({ text: '' }); |
| | existingTextRef.current = ''; |
| | } |
| | }, |
| | [ask, reset, showToast, localize, isSubmitting, speechToTextEndpoint], |
| | ); |
| |
|
| | const setText = useCallback( |
| | (text: string) => { |
| | let newText = text; |
| | if (isExternalSTT(speechToTextEndpoint)) { |
| | |
| | newText = existingTextRef.current ? `${existingTextRef.current} ${text}` : text; |
| | } else { |
| | |
| | newText = existingTextRef.current ? `${existingTextRef.current} ${text}` : text; |
| | } |
| | setValue('text', newText, { |
| | shouldValidate: true, |
| | }); |
| | }, |
| | [setValue, speechToTextEndpoint], |
| | ); |
| |
|
| | const { isListening, isLoading, startRecording, stopRecording } = useSpeechToText( |
| | setText, |
| | onTranscriptionComplete, |
| | ); |
| |
|
| | if (!textAreaRef.current) { |
| | return null; |
| | } |
| |
|
| | const handleStartRecording = async () => { |
| | existingTextRef.current = getValues('text') || ''; |
| | startRecording(); |
| | }; |
| |
|
| | const handleStopRecording = async () => { |
| | stopRecording(); |
| | |
| | if (!isExternalSTT(speechToTextEndpoint)) { |
| | existingTextRef.current = ''; |
| | } |
| | }; |
| |
|
| | const renderIcon = () => { |
| | if (isListening === true) { |
| | return <ListeningIcon className="stroke-red-500" />; |
| | } |
| | if (isLoading === true) { |
| | return <Spinner className="stroke-text-secondary" />; |
| | } |
| | return <ListeningIcon className="stroke-text-secondary" />; |
| | }; |
| |
|
| | return ( |
| | <TooltipAnchor |
| | description={localize('com_ui_use_micrphone')} |
| | render={ |
| | <button |
| | id="audio-recorder" |
| | type="button" |
| | aria-label={localize('com_ui_use_micrphone')} |
| | onClick={isListening === true ? handleStopRecording : handleStartRecording} |
| | disabled={disabled} |
| | className={cn( |
| | 'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover', |
| | )} |
| | title={localize('com_ui_use_micrphone')} |
| | aria-pressed={isListening} |
| | > |
| | {renderIcon()} |
| | </button> |
| | } |
| | /> |
| | ); |
| | } |
| |
|