mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
🎤 feat: add custom speech config, browser TTS/STT features, and dynamic speech tab settings (#2921)
* feat: update useTextToSpeech and useSpeechToText hooks to support external audio endpoints This commit updates the useTextToSpeech and useSpeechToText hooks in the Input directory to support external audio endpoints. It introduces the useGetExternalTextToSpeech and useGetExternalSpeechToText hooks, which determine whether the audio endpoints should be set to 'browser' or 'external' based on the value of the endpointTTS and endpointSTT Recoil states. The useTextToSpeech and useSpeechToText hooks now use these new hooks to determine whether to use external audio endpoints * feat: add userSelect style to ConversationModeSwitch label * fix: remove unused updateTokenWebsocket function and import The updateTokenWebsocket function and its import are no longer used in the OpenAIClient module. This commit removes the function and import to clean up the codebase * feat: support external audio endpoints in useTextToSpeech and useSpeechToText hooks This commit updates the useTextToSpeech and useSpeechToText hooks in the Input directory to support external audio endpoints. It introduces the useGetExternalTextToSpeech and useGetExternalSpeechToText hooks, which determine whether the audio endpoints should be set to 'browser' or 'external' based on the value of the endpointTTS and endpointSTT Recoil states. The useTextToSpeech and useSpeechToText hooks now use these new hooks to determine whether to use external audio endpoints * feat: update AutomaticPlayback component to AutomaticPlaybackSwitch; tests: added AutomaticPlaybackSwitch.spec > > This commit renames the AutomaticPlayback component to AutomaticPlaybackSwitch in the Speech directory. The new name better reflects the purpose of the component and aligns with the naming convention used in the codebase. * feat: update useSpeechToText hook to include interimTranscript This commit updates the useSpeechToText hook in the client/src/components/Chat/Input/AudioRecorder.tsx file to include the interimTranscript state. This allows for real-time display of the speech-to-text transcription while the user is still speaking. The interimTranscript is now used to update the text area value during recording. * feat: Add customConfigSpeech API endpoint for retrieving custom speech configuration This commit adds a new API endpoint in the file under the directory. This endpoint is responsible for retrieving the custom speech configuration using the function from the module * feat: update store var and ; fix: getCustomConfigSpeech * fix: client tests, removed unused import * feat: Update useCustomConfigSpeechQuery to return an array of custom speech configurations This commit modifies the useCustomConfigSpeechQuery function in the client/src/data-provider/queries.ts file to return an array of custom speech configurations instead of a single object. This change allows for better handling and manipulation of the data in the application * feat: Update useCustomConfigSpeechQuery to return an array of custom speech configurations * refactor: Update variable name in speechTab schema * refactor: removed unused and nested code * fix: using recoilState * refactor: Update Speech component to use useCallback for setting settings * fix: test * fix: tests * feature: ensure that the settings don't change after modifying then through the UI * remove comment * fix: Handle error gracefully in getCustomConfigSpeech and getVoices endpoints * fix: Handle error * fix: backend tests * fix: invalid custom config logging * chore: add back custom config info logging * chore: revert loadCustomConfig spec --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
5d985746cb
commit
1aad315de6
50 changed files with 598 additions and 179 deletions
|
|
@ -31,15 +31,26 @@ export default function AudioRecorder({
|
|||
}
|
||||
};
|
||||
|
||||
const { isListening, isLoading, startRecording, stopRecording, speechText, clearText } =
|
||||
useSpeechToText(handleTranscriptionComplete);
|
||||
const {
|
||||
isListening,
|
||||
isLoading,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
interimTranscript,
|
||||
speechText,
|
||||
clearText,
|
||||
} = useSpeechToText(handleTranscriptionComplete);
|
||||
|
||||
useEffect(() => {
|
||||
if (textAreaRef.current) {
|
||||
if (isListening && textAreaRef.current) {
|
||||
methods.setValue('text', interimTranscript, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
} else if (textAreaRef.current) {
|
||||
textAreaRef.current.value = speechText;
|
||||
methods.setValue('text', speechText, { shouldValidate: true });
|
||||
}
|
||||
}, [speechText, methods, textAreaRef]);
|
||||
}, [interimTranscript, speechText, methods, textAreaRef]);
|
||||
|
||||
const handleStartRecording = async () => {
|
||||
await startRecording();
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ const ChatForm = ({ index = 0 }) => {
|
|||
const submitButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const SpeechToText = useRecoilValue(store.SpeechToText);
|
||||
const TextToSpeech = useRecoilValue(store.TextToSpeech);
|
||||
const SpeechToText = useRecoilValue(store.speechToText);
|
||||
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||
|
||||
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
deleteFiles({ files: filesToDelete as TFile[] });
|
||||
setRowSelection({});
|
||||
}}
|
||||
className="dark:hover:bg-gray-850/25 ml-1 gap-2 sm:ml-0"
|
||||
className="ml-1 gap-2 dark:hover:bg-gray-850/25 sm:ml-0"
|
||||
disabled={!table.getFilteredSelectedRowModel().rows.length || isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export default function HoverButtons({
|
|||
const { endpoint: _endpoint, endpointType } = conversation ?? {};
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [TextToSpeech] = useRecoilState<boolean>(store.TextToSpeech);
|
||||
const [TextToSpeech] = useRecoilState<boolean>(store.textToSpeech);
|
||||
|
||||
const {
|
||||
hideEditButton,
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export default function DataTableFile<TData, TValue>({
|
|||
deleteFiles({ files: filesToDelete as TFile[] });
|
||||
setRowSelection({});
|
||||
}}
|
||||
className="dark:hover:bg-gray-850/25 ml-1 gap-2 sm:ml-0"
|
||||
className="ml-1 gap-2 dark:hover:bg-gray-850/25 sm:ml-0"
|
||||
disabled={!table.getFilteredSelectedRowModel().rows.length || isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
|
|
|
|||
|
|
@ -75,18 +75,21 @@ export const fileTableColumns: ColumnDef<TFile>[] = [
|
|||
return (
|
||||
<>
|
||||
{attachedVectorStores.map((vectorStore, index) => {
|
||||
if (index === 4)
|
||||
{return (
|
||||
<span
|
||||
key={index}
|
||||
className="ml-2 mt-2 flex w-fit flex-row items-center rounded-full bg-[#f5f5f5] px-2 text-gray-500"
|
||||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
if (index === 4) {
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className="ml-2 mt-2 flex w-fit flex-row items-center rounded-full bg-[#f5f5f5] px-2 text-gray-500"
|
||||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
|
||||
{attachedVectorStores.length - index} more
|
||||
</span>
|
||||
);}
|
||||
if (index > 4) {return null;}
|
||||
{attachedVectorStores.length - index} more
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (index > 4) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span key={index} className="ml-2 mt-2 rounded-full bg-[#f2f8ec] px-2 text-[#91c561]">
|
||||
{vectorStore.name}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default function ScrollToBottom({ scrollHandler }: Props) {
|
|||
return (
|
||||
<button
|
||||
onClick={scrollHandler}
|
||||
className="dark:bg-gray-850/90 absolute bottom-5 right-1/2 cursor-pointer rounded-full border border-gray-200 bg-white bg-clip-padding text-gray-600 dark:border-white/10 dark:text-gray-200"
|
||||
className="absolute bottom-5 right-1/2 cursor-pointer rounded-full border border-gray-200 bg-white bg-clip-padding text-gray-600 dark:border-white/10 dark:bg-gray-850/90 dark:text-gray-200"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
|
|
|
|||
|
|
@ -10,18 +10,16 @@ export default function ConversationModeSwitch({
|
|||
}) {
|
||||
const localize = useLocalize();
|
||||
const [conversationMode, setConversationMode] = useRecoilState<boolean>(store.conversationMode);
|
||||
const [advancedMode] = useRecoilState<boolean>(store.advancedMode);
|
||||
const [textToSpeech] = useRecoilState<boolean>(store.TextToSpeech);
|
||||
const [speechToText] = useRecoilState<boolean>(store.speechToText);
|
||||
const [textToSpeech] = useRecoilState<boolean>(store.textToSpeech);
|
||||
const [, setAutoSendText] = useRecoilState<boolean>(store.autoSendText);
|
||||
const [, setDecibelValue] = useRecoilState(store.decibelValue);
|
||||
const [, setAutoTranscribeAudio] = useRecoilState<boolean>(store.autoTranscribeAudio);
|
||||
|
||||
const handleCheckedChange = (value: boolean) => {
|
||||
if (!advancedMode) {
|
||||
setAutoTranscribeAudio(value);
|
||||
setAutoSendText(value);
|
||||
setDecibelValue(-45);
|
||||
}
|
||||
setAutoTranscribeAudio(value);
|
||||
setAutoSendText(value);
|
||||
setDecibelValue(-45);
|
||||
setConversationMode(value);
|
||||
if (onCheckedChange) {
|
||||
onCheckedChange(value);
|
||||
|
|
@ -40,7 +38,7 @@ export default function ConversationModeSwitch({
|
|||
onCheckedChange={handleCheckedChange}
|
||||
className="ml-4"
|
||||
data-testid="ConversationMode"
|
||||
disabled={!textToSpeech}
|
||||
disabled={!textToSpeech || !speechToText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default function AutoSendTextSwitch({
|
|||
}) {
|
||||
const localize = useLocalize();
|
||||
const [autoSendText, setAutoSendText] = useRecoilState<boolean>(store.autoSendText);
|
||||
const [SpeechToText] = useRecoilState<boolean>(store.SpeechToText);
|
||||
const [SpeechToText] = useRecoilState<boolean>(store.speechToText);
|
||||
|
||||
const handleCheckedChange = (value: boolean) => {
|
||||
setAutoSendText(value);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default function AutoTranscribeAudioSwitch({
|
|||
const [autoTranscribeAudio, setAutoTranscribeAudio] = useRecoilState<boolean>(
|
||||
store.autoTranscribeAudio,
|
||||
);
|
||||
const [speechToText] = useRecoilState<boolean>(store.SpeechToText);
|
||||
const [speechToText] = useRecoilState<boolean>(store.speechToText);
|
||||
|
||||
const handleCheckedChange = (value: boolean) => {
|
||||
setAutoTranscribeAudio(value);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { cn, defaultTextProps, optionText } from '~/utils/';
|
|||
|
||||
export default function DecibelSelector() {
|
||||
const localize = useLocalize();
|
||||
const speechToText = useRecoilValue(store.SpeechToText);
|
||||
const speechToText = useRecoilValue(store.speechToText);
|
||||
const [decibelValue, setDecibelValue] = useRecoilState(store.decibelValue);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,21 +5,21 @@ import store from '~/store';
|
|||
|
||||
export default function EngineSTTDropdown() {
|
||||
const localize = useLocalize();
|
||||
const [endpointSTT, setEndpointSTT] = useRecoilState<string>(store.endpointSTT);
|
||||
const [engineSTT, setEngineSTT] = useRecoilState<string>(store.engineSTT);
|
||||
const endpointOptions = [
|
||||
{ value: 'browser', display: localize('com_nav_browser') },
|
||||
{ value: 'external', display: localize('com_nav_external') },
|
||||
];
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
setEndpointSTT(value);
|
||||
setEngineSTT(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{localize('com_nav_engine')}</div>
|
||||
<Dropdown
|
||||
value={endpointSTT}
|
||||
value={engineSTT}
|
||||
onChange={handleSelect}
|
||||
options={endpointOptions}
|
||||
width={180}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import { Dropdown } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export default function LanguageSTTDropdown() {
|
||||
const localize = useLocalize();
|
||||
const [languageSTT, setLanguageSTT] = useRecoilState<string>(store.languageSTT);
|
||||
|
||||
const languageOptions = [
|
||||
{ value: 'af', display: 'Afrikaans' },
|
||||
{ value: 'eu', display: 'Basque' },
|
||||
{ value: 'bg', display: 'Bulgarian' },
|
||||
{ value: 'ca', display: 'Catalan' },
|
||||
{ value: 'ar-EG', display: 'Arabic (Egypt)' },
|
||||
{ value: 'ar-JO', display: 'Arabic (Jordan)' },
|
||||
{ value: 'ar-KW', display: 'Arabic (Kuwait)' },
|
||||
{ value: 'ar-LB', display: 'Arabic (Lebanon)' },
|
||||
{ value: 'ar-QA', display: 'Arabic (Qatar)' },
|
||||
{ value: 'ar-AE', display: 'Arabic (UAE)' },
|
||||
{ value: 'ar-MA', display: 'Arabic (Morocco)' },
|
||||
{ value: 'ar-IQ', display: 'Arabic (Iraq)' },
|
||||
{ value: 'ar-DZ', display: 'Arabic (Algeria)' },
|
||||
{ value: 'ar-BH', display: 'Arabic (Bahrain)' },
|
||||
{ value: 'ar-LY', display: 'Arabic (Libya)' },
|
||||
{ value: 'ar-OM', display: 'Arabic (Oman)' },
|
||||
{ value: 'ar-SA', display: 'Arabic (Saudi Arabia)' },
|
||||
{ value: 'ar-TN', display: 'Arabic (Tunisia)' },
|
||||
{ value: 'ar-YE', display: 'Arabic (Yemen)' },
|
||||
{ value: 'cs', display: 'Czech' },
|
||||
{ value: 'nl-NL', display: 'Dutch' },
|
||||
{ value: 'en-AU', display: 'English (Australia)' },
|
||||
{ value: 'en-CA', display: 'English (Canada)' },
|
||||
{ value: 'en-IN', display: 'English (India)' },
|
||||
{ value: 'en-NZ', display: 'English (New Zealand)' },
|
||||
{ value: 'en-ZA', display: 'English (South Africa)' },
|
||||
{ value: 'en-GB', display: 'English (UK)' },
|
||||
{ value: 'en-US', display: 'English (US)' },
|
||||
{ value: 'fi', display: 'Finnish' },
|
||||
{ value: 'fr-FR', display: 'French' },
|
||||
{ value: 'gl', display: 'Galician' },
|
||||
{ value: 'de-DE', display: 'German' },
|
||||
{ value: 'el-GR', display: 'Greek' },
|
||||
{ value: 'he', display: 'Hebrew' },
|
||||
{ value: 'hu', display: 'Hungarian' },
|
||||
{ value: 'is', display: 'Icelandic' },
|
||||
{ value: 'it-IT', display: 'Italian' },
|
||||
{ value: 'id', display: 'Indonesian' },
|
||||
{ value: 'ja', display: 'Japanese' },
|
||||
{ value: 'ko', display: 'Korean' },
|
||||
{ value: 'la', display: 'Latin' },
|
||||
{ value: 'zh-CN', display: 'Mandarin Chinese' },
|
||||
{ value: 'zh-TW', display: 'Taiwanese' },
|
||||
{ value: 'zh-HK', display: 'Cantonese' },
|
||||
{ value: 'ms-MY', display: 'Malaysian' },
|
||||
{ value: 'no-NO', display: 'Norwegian' },
|
||||
{ value: 'pl', display: 'Polish' },
|
||||
{ value: 'xx-piglatin', display: 'Pig Latin' },
|
||||
{ value: 'pt-PT', display: 'Portuguese' },
|
||||
{ value: 'pt-br', display: 'Portuguese (Brasil)' },
|
||||
{ value: 'ro-RO', display: 'Romanian' },
|
||||
{ value: 'ru', display: 'Russian' },
|
||||
{ value: 'sr-SP', display: 'Serbian' },
|
||||
{ value: 'sk', display: 'Slovak' },
|
||||
{ value: 'es-AR', display: 'Spanish (Argentina)' },
|
||||
{ value: 'es-BO', display: 'Spanish (Bolivia)' },
|
||||
{ value: 'es-CL', display: 'Spanish (Chile)' },
|
||||
{ value: 'es-CO', display: 'Spanish (Colombia)' },
|
||||
{ value: 'es-CR', display: 'Spanish (Costa Rica)' },
|
||||
{ value: 'es-DO', display: 'Spanish (Dominican Republic)' },
|
||||
{ value: 'es-EC', display: 'Spanish (Ecuador)' },
|
||||
{ value: 'es-SV', display: 'Spanish (El Salvador)' },
|
||||
{ value: 'es-GT', display: 'Spanish (Guatemala)' },
|
||||
{ value: 'es-HN', display: 'Spanish (Honduras)' },
|
||||
{ value: 'es-MX', display: 'Spanish (Mexico)' },
|
||||
{ value: 'es-NI', display: 'Spanish (Nicaragua)' },
|
||||
{ value: 'es-PA', display: 'Spanish (Panama)' },
|
||||
{ value: 'es-PY', display: 'Spanish (Paraguay)' },
|
||||
{ value: 'es-PE', display: 'Spanish (Peru)' },
|
||||
{ value: 'es-PR', display: 'Spanish (Puerto Rico)' },
|
||||
{ value: 'es-ES', display: 'Spanish (Spain)' },
|
||||
{ value: 'es-US', display: 'Spanish (US)' },
|
||||
{ value: 'es-UY', display: 'Spanish (Uruguay)' },
|
||||
{ value: 'es-VE', display: 'Spanish (Venezuela)' },
|
||||
{ value: 'sv-SE', display: 'Swedish' },
|
||||
{ value: 'tr', display: 'Turkish' },
|
||||
{ value: 'zu', display: 'Zulu' },
|
||||
];
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
setLanguageSTT(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{localize('com_nav_language')}</div>
|
||||
<Dropdown
|
||||
value={languageSTT}
|
||||
onChange={handleSelect}
|
||||
options={languageOptions}
|
||||
width={220}
|
||||
position={'left'}
|
||||
testId="LanguageSTTDropdown"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ export default function SpeechToTextSwitch({
|
|||
onCheckedChange?: (value: boolean) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const [speechToText, setSpeechToText] = useRecoilState<boolean>(store.SpeechToText);
|
||||
const [speechToText, setSpeechToText] = useRecoilState<boolean>(store.speechToText);
|
||||
|
||||
const handleCheckedChange = (value: boolean) => {
|
||||
setSpeechToText(value);
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export { default as SpeechToTextSwitch } from './SpeechToTextSwitch';
|
|||
export { default as EngineSTTDropdown } from './EngineSTTDropdown';
|
||||
export { default as DecibelSelector } from './DecibelSelector';
|
||||
export { default as AutoTranscribeAudioSwitch } from './AutoTranscribeAudioSwitch';
|
||||
export { default as LanguageSTTDropdown } from './LanguageSTTDropdown';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Lightbulb, Cog } from 'lucide-react';
|
||||
import { useOnClickOutside, useMediaQuery } from '~/hooks';
|
||||
|
|
@ -10,7 +10,7 @@ import ConversationModeSwitch from './ConversationModeSwitch';
|
|||
import {
|
||||
TextToSpeechSwitch,
|
||||
EngineTTSDropdown,
|
||||
AutomaticPlayback,
|
||||
AutomaticPlaybackSwitch,
|
||||
CacheTTSSwitch,
|
||||
VoiceDropdown,
|
||||
PlaybackRate,
|
||||
|
|
@ -18,16 +18,100 @@ import {
|
|||
import {
|
||||
DecibelSelector,
|
||||
EngineSTTDropdown,
|
||||
LanguageSTTDropdown,
|
||||
SpeechToTextSwitch,
|
||||
AutoSendTextSwitch,
|
||||
AutoTranscribeAudioSwitch,
|
||||
} from './STT';
|
||||
import { useCustomConfigSpeechQuery } from '~/data-provider';
|
||||
|
||||
function Speech() {
|
||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||
const [advancedMode, setAdvancedMode] = useRecoilState<boolean>(store.advancedMode);
|
||||
const [autoTranscribeAudio] = useRecoilState<boolean>(store.autoTranscribeAudio);
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const { data } = useCustomConfigSpeechQuery();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||
|
||||
const [advancedMode, setAdvancedMode] = useRecoilState(store.advancedMode);
|
||||
const [autoTranscribeAudio, setAutoTranscribeAudio] = useRecoilState(store.autoTranscribeAudio);
|
||||
const [conversationMode, setConversationMode] = useRecoilState(store.conversationMode);
|
||||
const [speechToText, setSpeechToText] = useRecoilState(store.speechToText);
|
||||
const [textToSpeech, setTextToSpeech] = useRecoilState(store.textToSpeech);
|
||||
const [cacheTTS, setCacheTTS] = useRecoilState(store.cacheTTS);
|
||||
const [engineSTT, setEngineSTT] = useRecoilState<string>(store.engineSTT);
|
||||
const [languageSTT, setLanguageSTT] = useRecoilState<string>(store.languageSTT);
|
||||
const [decibelValue, setDecibelValue] = useRecoilState(store.decibelValue);
|
||||
const [autoSendText, setAutoSendText] = useRecoilState(store.autoSendText);
|
||||
const [engineTTS, setEngineTTS] = useRecoilState<string>(store.engineTTS);
|
||||
const [voice, setVoice] = useRecoilState<string>(store.voice);
|
||||
const [languageTTS, setLanguageTTS] = useRecoilState<string>(store.languageTTS);
|
||||
const [automaticPlayback, setAutomaticPlayback] = useRecoilState(store.automaticPlayback);
|
||||
const [playbackRate, setPlaybackRate] = useRecoilState(store.playbackRate);
|
||||
|
||||
const updateSetting = useCallback(
|
||||
(key, newValue) => {
|
||||
const settings = {
|
||||
conversationMode: { value: conversationMode, setFunc: setConversationMode },
|
||||
advancedMode: { value: advancedMode, setFunc: setAdvancedMode },
|
||||
speechToText: { value: speechToText, setFunc: setSpeechToText },
|
||||
textToSpeech: { value: textToSpeech, setFunc: setTextToSpeech },
|
||||
cacheTTS: { value: cacheTTS, setFunc: setCacheTTS },
|
||||
engineSTT: { value: engineSTT, setFunc: setEngineSTT },
|
||||
languageSTT: { value: languageSTT, setFunc: setLanguageSTT },
|
||||
autoTranscribeAudio: { value: autoTranscribeAudio, setFunc: setAutoTranscribeAudio },
|
||||
decibelValue: { value: decibelValue, setFunc: setDecibelValue },
|
||||
autoSendText: { value: autoSendText, setFunc: setAutoSendText },
|
||||
engineTTS: { value: engineTTS, setFunc: setEngineTTS },
|
||||
voice: { value: voice, setFunc: setVoice },
|
||||
languageTTS: { value: languageTTS, setFunc: setLanguageTTS },
|
||||
automaticPlayback: { value: automaticPlayback, setFunc: setAutomaticPlayback },
|
||||
playbackRate: { value: playbackRate, setFunc: setPlaybackRate },
|
||||
};
|
||||
|
||||
if (settings[key]) {
|
||||
const setting = settings[key];
|
||||
setting.setFunc(newValue);
|
||||
}
|
||||
},
|
||||
[
|
||||
conversationMode,
|
||||
advancedMode,
|
||||
speechToText,
|
||||
textToSpeech,
|
||||
cacheTTS,
|
||||
engineSTT,
|
||||
languageSTT,
|
||||
autoTranscribeAudio,
|
||||
decibelValue,
|
||||
autoSendText,
|
||||
engineTTS,
|
||||
voice,
|
||||
languageTTS,
|
||||
automaticPlayback,
|
||||
playbackRate,
|
||||
setConversationMode,
|
||||
setAdvancedMode,
|
||||
setSpeechToText,
|
||||
setTextToSpeech,
|
||||
setCacheTTS,
|
||||
setEngineSTT,
|
||||
setLanguageSTT,
|
||||
setAutoTranscribeAudio,
|
||||
setDecibelValue,
|
||||
setAutoSendText,
|
||||
setEngineTTS,
|
||||
setVoice,
|
||||
setLanguageTTS,
|
||||
setAutomaticPlayback,
|
||||
setPlaybackRate,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
updateSetting(key, value);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const contentRef = useRef(null);
|
||||
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
|
||||
|
|
@ -91,13 +175,13 @@ function Speech() {
|
|||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<EngineSTTDropdown />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<LanguageSTTDropdown />
|
||||
</div>
|
||||
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<TextToSpeechSwitch />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<AutomaticPlayback />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<EngineTTSDropdown />
|
||||
</div>
|
||||
|
|
@ -119,6 +203,9 @@ function Speech() {
|
|||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<EngineSTTDropdown />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<LanguageSTTDropdown />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<AutoTranscribeAudioSwitch />
|
||||
</div>
|
||||
|
|
@ -135,7 +222,7 @@ function Speech() {
|
|||
<TextToSpeechSwitch />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<AutomaticPlayback />
|
||||
<AutomaticPlaybackSwitch />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<EngineTTSDropdown />
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Switch } from '~/components/ui';
|
|||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export default function AutomaticPlayback({
|
||||
export default function AutomaticPlaybackSwitch({
|
||||
onCheckedChange,
|
||||
}: {
|
||||
onCheckedChange?: (value: boolean) => void;
|
||||
|
|
@ -10,7 +10,7 @@ export default function CacheTTSSwitch({
|
|||
}) {
|
||||
const localize = useLocalize();
|
||||
const [cacheTTS, setCacheTTS] = useRecoilState<boolean>(store.cacheTTS);
|
||||
const [textToSpeech] = useRecoilState<boolean>(store.TextToSpeech);
|
||||
const [textToSpeech] = useRecoilState<boolean>(store.textToSpeech);
|
||||
|
||||
const handleCheckedChange = (value: boolean) => {
|
||||
setCacheTTS(value);
|
||||
|
|
|
|||
|
|
@ -5,21 +5,21 @@ import store from '~/store';
|
|||
|
||||
export default function EngineTTSDropdown() {
|
||||
const localize = useLocalize();
|
||||
const [endpointTTS, setEndpointTTS] = useRecoilState<string>(store.endpointTTS);
|
||||
const [engineTTS, setEngineTTS] = useRecoilState<string>(store.engineTTS);
|
||||
const endpointOptions = [
|
||||
{ value: 'browser', display: localize('com_nav_browser') },
|
||||
{ value: 'external', display: localize('com_nav_external') },
|
||||
];
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
setEndpointTTS(value);
|
||||
setEngineTTS(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{localize('com_nav_engine')}</div>
|
||||
<Dropdown
|
||||
value={endpointTTS}
|
||||
value={engineTTS}
|
||||
onChange={handleSelect}
|
||||
options={endpointOptions}
|
||||
width={180}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { cn, defaultTextProps, optionText } from '~/utils/';
|
|||
|
||||
export default function DecibelSelector() {
|
||||
const localize = useLocalize();
|
||||
const textToSpeech = useRecoilValue(store.TextToSpeech);
|
||||
const textToSpeech = useRecoilValue(store.textToSpeech);
|
||||
const [playbackRate, setPlaybackRate] = useRecoilState(store.playbackRate);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default function TextToSpeechSwitch({
|
|||
onCheckedChange?: (value: boolean) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const [TextToSpeech, setTextToSpeech] = useRecoilState<boolean>(store.TextToSpeech);
|
||||
const [TextToSpeech, setTextToSpeech] = useRecoilState<boolean>(store.textToSpeech);
|
||||
|
||||
const handleCheckedChange = (value: boolean) => {
|
||||
setTextToSpeech(value);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { render, fireEvent } from 'test/layout-test-utils';
|
||||
import AutomaticPlaybackSwitch from '../AutomaticPlaybackSwitch';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
describe('AutomaticPlaybackSwitch', () => {
|
||||
/**
|
||||
* Mock function to set the text-to-speech state.
|
||||
*/
|
||||
let mockSetAutomaticPlayback: jest.Mock<void, [boolean]> | ((value: boolean) => void) | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSetAutomaticPlayback = jest.fn();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const { getByTestId } = render(
|
||||
<RecoilRoot>
|
||||
<AutomaticPlaybackSwitch />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
expect(getByTestId('AutomaticPlayback')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onCheckedChange when the switch is toggled', () => {
|
||||
const { getByTestId } = render(
|
||||
<RecoilRoot>
|
||||
<AutomaticPlaybackSwitch onCheckedChange={mockSetAutomaticPlayback} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
const switchElement = getByTestId('AutomaticPlayback');
|
||||
fireEvent.click(switchElement);
|
||||
|
||||
expect(mockSetAutomaticPlayback).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export { default as AutomaticPlayback } from './AutomaticPlayback';
|
||||
export { default as AutomaticPlaybackSwitch } from './AutomaticPlaybackSwitch';
|
||||
export { default as CacheTTSSwitch } from './CacheTTSSwitch';
|
||||
export { default as EngineTTSDropdown } from './EngineTTSDropdown';
|
||||
export { default as PlaybackRate } from './PlaybackRate';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue