mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 11:20:15 +01:00
🎯 fix: Prevent UI De-sync By Removing Redundant States (#5333)
* fix: remove local state from Dropdown causing de-sync * refactor: cleanup STT code, avoid redundant states to prevent de-sync and side effects * fix: reset transcript after sending final text to prevent data loss * fix: clear timeout on component unmount to prevent memory leaks
This commit is contained in:
parent
b55e695541
commit
e309c6abef
8 changed files with 149 additions and 145 deletions
|
|
@ -1,73 +1,79 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useChatFormContext, useToastContext } from '~/Providers';
|
||||
import { ListeningIcon, Spinner } from '~/components/svg';
|
||||
import { useLocalize, useSpeechToText } from '~/hooks';
|
||||
import { useChatFormContext } from '~/Providers';
|
||||
import { TooltipAnchor } from '~/components/ui';
|
||||
import { globalAudioId } from '~/common';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function AudioRecorder({
|
||||
textAreaRef,
|
||||
methods,
|
||||
ask,
|
||||
isRTL,
|
||||
disabled,
|
||||
ask,
|
||||
methods,
|
||||
textAreaRef,
|
||||
isSubmitting,
|
||||
}: {
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
methods: ReturnType<typeof useChatFormContext>;
|
||||
ask: (data: { text: string }) => void;
|
||||
isRTL: boolean;
|
||||
disabled: boolean;
|
||||
ask: (data: { text: string }) => void;
|
||||
methods: ReturnType<typeof useChatFormContext>;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
isSubmitting: boolean;
|
||||
}) {
|
||||
const { setValue, reset } = methods;
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const handleTranscriptionComplete = (text: string) => {
|
||||
if (text) {
|
||||
const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement;
|
||||
if (globalAudio) {
|
||||
console.log('Unmuting global audio');
|
||||
globalAudio.muted = false;
|
||||
const onTranscriptionComplete = useCallback(
|
||||
(text: string) => {
|
||||
if (isSubmitting) {
|
||||
showToast({
|
||||
message: localize('com_ui_speech_while_submitting'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
ask({ text });
|
||||
methods.reset({ text: '' });
|
||||
clearText();
|
||||
}
|
||||
};
|
||||
if (text) {
|
||||
const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement | null;
|
||||
if (globalAudio) {
|
||||
console.log('Unmuting global audio');
|
||||
globalAudio.muted = false;
|
||||
}
|
||||
ask({ text });
|
||||
reset({ text: '' });
|
||||
}
|
||||
},
|
||||
[ask, reset, showToast, localize, isSubmitting],
|
||||
);
|
||||
|
||||
const {
|
||||
isListening,
|
||||
isLoading,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
interimTranscript,
|
||||
speechText,
|
||||
clearText,
|
||||
} = useSpeechToText(handleTranscriptionComplete);
|
||||
|
||||
useEffect(() => {
|
||||
if (isListening && textAreaRef.current) {
|
||||
methods.setValue('text', interimTranscript, {
|
||||
const setText = useCallback(
|
||||
(text: string) => {
|
||||
setValue('text', text, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
} else if (textAreaRef.current) {
|
||||
textAreaRef.current.value = speechText;
|
||||
methods.setValue('text', speechText, { shouldValidate: true });
|
||||
}
|
||||
}, [interimTranscript, speechText, methods, textAreaRef]);
|
||||
},
|
||||
[setValue],
|
||||
);
|
||||
|
||||
const handleStartRecording = async () => {
|
||||
await startRecording();
|
||||
};
|
||||
const { isListening, isLoading, startRecording, stopRecording } = useSpeechToText(
|
||||
setText,
|
||||
onTranscriptionComplete,
|
||||
);
|
||||
|
||||
const handleStopRecording = async () => {
|
||||
await stopRecording();
|
||||
};
|
||||
if (!textAreaRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleStartRecording = async () => startRecording();
|
||||
|
||||
const handleStopRecording = async () => stopRecording();
|
||||
|
||||
const renderIcon = () => {
|
||||
if (isListening) {
|
||||
if (isListening === true) {
|
||||
return <ListeningIcon className="stroke-red-500" />;
|
||||
}
|
||||
if (isLoading) {
|
||||
if (isLoading === true) {
|
||||
return <Spinner className="stroke-gray-700 dark:stroke-gray-300" />;
|
||||
}
|
||||
return <ListeningIcon className="stroke-gray-700 dark:stroke-gray-300" />;
|
||||
|
|
@ -77,7 +83,7 @@ export default function AudioRecorder({
|
|||
<TooltipAnchor
|
||||
id="audio-recorder"
|
||||
aria-label={localize('com_ui_use_micrphone')}
|
||||
onClick={isListening ? handleStopRecording : handleStartRecording}
|
||||
onClick={isListening === true ? handleStopRecording : handleStartRecording}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'absolute flex size-[35px] items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue