import React, { useState, useRef, useEffect, useCallback } from 'react'; import { useRecoilState } from 'recoil'; import * as Tabs from '@radix-ui/react-tabs'; import { Lightbulb, Cog } from 'lucide-react'; import { useOnClickOutside, useMediaQuery } from '@librechat/client'; import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query'; import { CloudBrowserVoicesSwitch, AutomaticPlaybackSwitch, TextToSpeechSwitch, EngineTTSDropdown, CacheTTSSwitch, VoiceDropdown, PlaybackRate, } from './TTS'; import { AutoTranscribeAudioSwitch, LanguageSTTDropdown, SpeechToTextSwitch, AutoSendTextSelector, EngineSTTDropdown, DecibelSelector, } from './STT'; import ConversationModeSwitch from './ConversationModeSwitch'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; import store from '~/store'; function Speech() { const localize = useLocalize(); const [confirmClear, setConfirmClear] = useState(false); const { data } = useGetCustomConfigSpeechQuery(); const isSmallScreen = useMediaQuery('(max-width: 767px)'); const [sttExternal, setSttExternal] = useState(false); const [ttsExternal, setTtsExternal] = useState(false); 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(store.engineSTT); const [languageSTT, setLanguageSTT] = useRecoilState(store.languageSTT); const [decibelValue, setDecibelValue] = useRecoilState(store.decibelValue); const [autoSendText, setAutoSendText] = useRecoilState(store.autoSendText); const [engineTTS, setEngineTTS] = useRecoilState(store.engineTTS); const [voice, setVoice] = useRecoilState(store.voice); const [cloudBrowserVoices, setCloudBrowserVoices] = useRecoilState( store.cloudBrowserVoices, ); const [languageTTS, setLanguageTTS] = useRecoilState(store.languageTTS); const [automaticPlayback, setAutomaticPlayback] = useRecoilState(store.automaticPlayback); const [playbackRate, setPlaybackRate] = useRecoilState(store.playbackRate); const updateSetting = useCallback( (key: string, newValue: string | number) => { const settings = { sttExternal: { value: sttExternal, setFunc: setSttExternal }, ttsExternal: { value: ttsExternal, setFunc: setTtsExternal }, 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 }, cloudBrowserVoices: { value: cloudBrowserVoices, setFunc: setCloudBrowserVoices }, languageTTS: { value: languageTTS, setFunc: setLanguageTTS }, automaticPlayback: { value: automaticPlayback, setFunc: setAutomaticPlayback }, playbackRate: { value: playbackRate, setFunc: setPlaybackRate }, }; const setting = settings[key]; if (setting) { setting.setFunc(newValue); } }, [ sttExternal, ttsExternal, conversationMode, advancedMode, speechToText, textToSpeech, cacheTTS, engineSTT, languageSTT, autoTranscribeAudio, decibelValue, autoSendText, engineTTS, voice, cloudBrowserVoices, languageTTS, automaticPlayback, playbackRate, setSttExternal, setTtsExternal, setConversationMode, setAdvancedMode, setSpeechToText, setTextToSpeech, setCacheTTS, setEngineSTT, setLanguageSTT, setAutoTranscribeAudio, setDecibelValue, setAutoSendText, setEngineTTS, setVoice, setCloudBrowserVoices, setLanguageTTS, setAutomaticPlayback, setPlaybackRate, ], ); useEffect(() => { if (data && data.message !== 'not_found') { Object.entries(data).forEach(([key, value]) => { // Only apply config values as defaults if no user preference exists in localStorage const existingValue = localStorage.getItem(key); if (existingValue === null && key !== 'sttExternal' && key !== 'ttsExternal') { updateSetting(key, value); } else if (key === 'sttExternal' || key === 'ttsExternal') { updateSetting(key, value); } }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); // Reset engineTTS if it is set to a removed/invalid value (e.g., 'edge') // TODO: remove this once the 'edge' engine is fully deprecated useEffect(() => { const validEngines = ['browser', 'external']; if (!validEngines.includes(engineTTS)) { setEngineTTS('browser'); } }, [engineTTS, setEngineTTS]); const contentRef = useRef(null); useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []); return (
setAdvancedMode(false)} className={cn( 'group m-1 flex items-center justify-center gap-2 bg-transparent px-4 py-2 text-sm text-text-secondary transition-all duration-200 ease-in-out radix-state-active:bg-secondary radix-state-active:text-foreground radix-state-active:shadow-lg', isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl', 'w-full', )} value="simple" style={{ userSelect: 'none' }} > setAdvancedMode(true)} className={cn( 'group m-1 flex items-center justify-center gap-2 bg-transparent px-4 py-2 text-sm text-text-secondary transition-all duration-200 ease-in-out radix-state-active:bg-secondary radix-state-active:text-foreground radix-state-active:shadow-lg', isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl', 'w-full', )} value="advanced" style={{ userSelect: 'none' }} >
{autoTranscribeAudio && (
)}
{engineTTS === 'browser' && (
)}
); } export default React.memo(Speech);