diff --git a/client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx b/client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx index 601bbb96bb8..b5ecb3b7c7e 100644 --- a/client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx +++ b/client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx @@ -35,13 +35,13 @@ const formSet: Set = new Set([ ]); const EXPIRY = { - THIRTY_MINUTES: { display: 'in 30 minutes', value: 30 * 60 * 1000 }, - TWO_HOURS: { display: 'in 2 hours', value: 2 * 60 * 60 * 1000 }, - TWELVE_HOURS: { display: 'in 12 hours', value: 12 * 60 * 60 * 1000 }, - ONE_DAY: { display: 'in 1 day', value: 24 * 60 * 60 * 1000 }, - ONE_WEEK: { display: 'in 7 days', value: 7 * 24 * 60 * 60 * 1000 }, - ONE_MONTH: { display: 'in 30 days', value: 30 * 24 * 60 * 60 * 1000 }, - NEVER: { display: 'never', value: 0 }, + THIRTY_MINUTES: { label: 'in 30 minutes', value: 30 * 60 * 1000 }, + TWO_HOURS: { label: 'in 2 hours', value: 2 * 60 * 60 * 1000 }, + TWELVE_HOURS: { label: 'in 12 hours', value: 12 * 60 * 60 * 1000 }, + ONE_DAY: { label: 'in 1 day', value: 24 * 60 * 60 * 1000 }, + ONE_WEEK: { label: 'in 7 days', value: 7 * 24 * 60 * 60 * 1000 }, + ONE_MONTH: { label: 'in 30 days', value: 30 * 24 * 60 * 60 * 1000 }, + NEVER: { label: 'never', value: 0 }, }; const SetKeyDialog = ({ @@ -72,7 +72,7 @@ const SetKeyDialog = ({ const [userKey, setUserKey] = useState(''); const { data: endpointsConfig } = useGetEndpointsQuery(); - const [expiresAtLabel, setExpiresAtLabel] = useState(EXPIRY.TWELVE_HOURS.display); + const [expiresAtLabel, setExpiresAtLabel] = useState(EXPIRY.TWELVE_HOURS.label); const { getExpiry, saveUserKey } = useUserKey(endpoint); const { showToast } = useToastContext(); const localize = useLocalize(); @@ -84,7 +84,7 @@ const SetKeyDialog = ({ }; const submit = () => { - const selectedOption = expirationOptions.find((option) => option.display === expiresAtLabel); + const selectedOption = expirationOptions.find((option) => option.label === expiresAtLabel); let expiresAt; if (selectedOption?.value === 0) { @@ -170,14 +170,14 @@ const SetKeyDialog = ({ {expiryTime === 'never' ? localize('com_endpoint_config_key_never_expires') : `${localize('com_endpoint_config_key_encryption')} ${new Date( - expiryTime, + expiryTime ?? 0, ).toLocaleString()}`} {' '} option.display)} + options={expirationOptions.map((option) => option.label)} sizeClasses="w-[185px]" /> @@ -185,7 +185,7 @@ const SetKeyDialog = ({ userKey={userKey} setUserKey={setUserKey} endpoint={ - endpoint === EModelEndpoint.gptPlugins && config?.azure + endpoint === EModelEndpoint.gptPlugins && (config?.azure ?? false) ? EModelEndpoint.azureOpenAI : endpoint } diff --git a/client/src/components/Nav/ExportConversation/ExportModal.tsx b/client/src/components/Nav/ExportConversation/ExportModal.tsx index f63a39c3cab..7cb7c28aba8 100644 --- a/client/src/components/Nav/ExportConversation/ExportModal.tsx +++ b/client/src/components/Nav/ExportConversation/ExportModal.tsx @@ -25,11 +25,11 @@ export default function ExportModal({ const [recursive, setRecursive] = useState(true); const typeOptions = [ - { value: 'screenshot', display: 'screenshot (.png)' }, - { value: 'text', display: 'text (.txt)' }, - { value: 'markdown', display: 'markdown (.md)' }, - { value: 'json', display: 'json (.json)' }, - { value: 'csv', display: 'csv (.csv)' }, + { value: 'screenshot', label: 'screenshot (.png)' }, + { value: 'text', label: 'text (.txt)' }, + { value: 'markdown', label: 'markdown (.md)' }, + { value: 'json', label: 'json (.json)' }, + { value: 'csv', label: 'csv (.csv)' }, ]; useEffect(() => { diff --git a/client/src/components/Nav/SettingsTabs/Chat/FontSizeSelector.tsx b/client/src/components/Nav/SettingsTabs/Chat/FontSizeSelector.tsx index 6bc3aaf4aa6..e140c8a4d77 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/FontSizeSelector.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/FontSizeSelector.tsx @@ -14,11 +14,11 @@ export default function FontSizeSelector() { }; const options = [ - { value: 'text-xs', display: localize('com_nav_font_size_xs') }, - { value: 'text-sm', display: localize('com_nav_font_size_sm') }, - { value: 'text-base', display: localize('com_nav_font_size_base') }, - { value: 'text-lg', display: localize('com_nav_font_size_lg') }, - { value: 'text-xl', display: localize('com_nav_font_size_xl') }, + { value: 'text-xs', label: localize('com_nav_font_size_xs') }, + { value: 'text-sm', label: localize('com_nav_font_size_sm') }, + { value: 'text-base', label: localize('com_nav_font_size_base') }, + { value: 'text-lg', label: localize('com_nav_font_size_lg') }, + { value: 'text-xl', label: localize('com_nav_font_size_xl') }, ]; return ( diff --git a/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx b/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx index 538a844cab9..26ce56fcd76 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx @@ -12,9 +12,9 @@ export const ForkSettings = () => { const [remember, setRemember] = useRecoilState(store.rememberForkOption); const forkOptions = [ - { value: ForkOptions.DIRECT_PATH, display: localize('com_ui_fork_visible') }, - { value: ForkOptions.INCLUDE_BRANCHES, display: localize('com_ui_fork_branches') }, - { value: ForkOptions.TARGET_LEVEL, display: localize('com_ui_fork_all_target') }, + { value: ForkOptions.DIRECT_PATH, label: localize('com_ui_fork_visible') }, + { value: ForkOptions.INCLUDE_BRANCHES, label: localize('com_ui_fork_branches') }, + { value: ForkOptions.TARGET_LEVEL, label: localize('com_ui_fork_all_target') }, ]; return ( diff --git a/client/src/components/Nav/SettingsTabs/General/General.tsx b/client/src/components/Nav/SettingsTabs/General/General.tsx index 66d6d80106a..c416238e2b7 100644 --- a/client/src/components/Nav/SettingsTabs/General/General.tsx +++ b/client/src/components/Nav/SettingsTabs/General/General.tsx @@ -21,9 +21,9 @@ export const ThemeSelector = ({ const localize = useLocalize(); const themeOptions = [ - { value: 'system', display: localize('com_nav_theme_system') }, - { value: 'dark', display: localize('com_nav_theme_dark') }, - { value: 'light', display: localize('com_nav_theme_light') }, + { value: 'system', label: localize('com_nav_theme_system') }, + { value: 'dark', label: localize('com_nav_theme_dark') }, + { value: 'light', label: localize('com_nav_theme_light') }, ]; return ( @@ -81,27 +81,27 @@ export const LangSelector = ({ // Create an array of options for the Dropdown const languageOptions = [ - { value: 'auto', display: localize('com_nav_lang_auto') }, - { value: 'en-US', display: localize('com_nav_lang_english') }, - { value: 'zh-CN', display: localize('com_nav_lang_chinese') }, - { value: 'zh-TW', display: localize('com_nav_lang_traditionalchinese') }, - { value: 'ar-EG', display: localize('com_nav_lang_arabic') }, - { value: 'de-DE', display: localize('com_nav_lang_german') }, - { value: 'es-ES', display: localize('com_nav_lang_spanish') }, - { value: 'fr-FR', display: localize('com_nav_lang_french') }, - { value: 'it-IT', display: localize('com_nav_lang_italian') }, - { value: 'pl-PL', display: localize('com_nav_lang_polish') }, - { value: 'pt-BR', display: localize('com_nav_lang_brazilian_portuguese') }, - { value: 'ru-RU', display: localize('com_nav_lang_russian') }, - { value: 'ja-JP', display: localize('com_nav_lang_japanese') }, - { value: 'sv-SE', display: localize('com_nav_lang_swedish') }, - { value: 'ko-KR', display: localize('com_nav_lang_korean') }, - { value: 'vi-VN', display: localize('com_nav_lang_vietnamese') }, - { value: 'tr-TR', display: localize('com_nav_lang_turkish') }, - { value: 'nl-NL', display: localize('com_nav_lang_dutch') }, - { value: 'id-ID', display: localize('com_nav_lang_indonesia') }, - { value: 'he-HE', display: localize('com_nav_lang_hebrew') }, - { value: 'fi-FI', display: localize('com_nav_lang_finnish') }, + { value: 'auto', label: localize('com_nav_lang_auto') }, + { value: 'en-US', label: localize('com_nav_lang_english') }, + { value: 'zh-CN', label: localize('com_nav_lang_chinese') }, + { value: 'zh-TW', label: localize('com_nav_lang_traditionalchinese') }, + { value: 'ar-EG', label: localize('com_nav_lang_arabic') }, + { value: 'de-DE', label: localize('com_nav_lang_german') }, + { value: 'es-ES', label: localize('com_nav_lang_spanish') }, + { value: 'fr-FR', label: localize('com_nav_lang_french') }, + { value: 'it-IT', label: localize('com_nav_lang_italian') }, + { value: 'pl-PL', label: localize('com_nav_lang_polish') }, + { value: 'pt-BR', label: localize('com_nav_lang_brazilian_portuguese') }, + { value: 'ru-RU', label: localize('com_nav_lang_russian') }, + { value: 'ja-JP', label: localize('com_nav_lang_japanese') }, + { value: 'sv-SE', label: localize('com_nav_lang_swedish') }, + { value: 'ko-KR', label: localize('com_nav_lang_korean') }, + { value: 'vi-VN', label: localize('com_nav_lang_vietnamese') }, + { value: 'tr-TR', label: localize('com_nav_lang_turkish') }, + { value: 'nl-NL', label: localize('com_nav_lang_dutch') }, + { value: 'id-ID', label: localize('com_nav_lang_indonesia') }, + { value: 'he-HE', label: localize('com_nav_lang_hebrew') }, + { value: 'fi-FI', label: localize('com_nav_lang_finnish') }, ]; return ( diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx index caaedbb8861..9ccba661ccf 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx @@ -14,10 +14,10 @@ const EngineSTTDropdown: React.FC = ({ external }) => { const endpointOptions = external ? [ - { value: 'browser', display: localize('com_nav_browser') }, - { value: 'external', display: localize('com_nav_external') }, + { value: 'browser', label: localize('com_nav_browser') }, + { value: 'external', label: localize('com_nav_external') }, ] - : [{ value: 'browser', display: localize('com_nav_browser') }]; + : [{ value: 'browser', label: localize('com_nav_browser') }]; const handleSelect = (value: string) => { setEngineSTT(value); diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/LanguageSTTDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/LanguageSTTDropdown.tsx index 0904b1dbf6d..8e51ea10048 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/LanguageSTTDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/LanguageSTTDropdown.tsx @@ -8,83 +8,83 @@ export default function LanguageSTTDropdown() { const [languageSTT, setLanguageSTT] = useRecoilState(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' }, + { value: 'af', label: 'Afrikaans' }, + { value: 'eu', label: 'Basque' }, + { value: 'bg', label: 'Bulgarian' }, + { value: 'ca', label: 'Catalan' }, + { value: 'ar-EG', label: 'Arabic (Egypt)' }, + { value: 'ar-JO', label: 'Arabic (Jordan)' }, + { value: 'ar-KW', label: 'Arabic (Kuwait)' }, + { value: 'ar-LB', label: 'Arabic (Lebanon)' }, + { value: 'ar-QA', label: 'Arabic (Qatar)' }, + { value: 'ar-AE', label: 'Arabic (UAE)' }, + { value: 'ar-MA', label: 'Arabic (Morocco)' }, + { value: 'ar-IQ', label: 'Arabic (Iraq)' }, + { value: 'ar-DZ', label: 'Arabic (Algeria)' }, + { value: 'ar-BH', label: 'Arabic (Bahrain)' }, + { value: 'ar-LY', label: 'Arabic (Libya)' }, + { value: 'ar-OM', label: 'Arabic (Oman)' }, + { value: 'ar-SA', label: 'Arabic (Saudi Arabia)' }, + { value: 'ar-TN', label: 'Arabic (Tunisia)' }, + { value: 'ar-YE', label: 'Arabic (Yemen)' }, + { value: 'cs', label: 'Czech' }, + { value: 'nl-NL', label: 'Dutch' }, + { value: 'en-AU', label: 'English (Australia)' }, + { value: 'en-CA', label: 'English (Canada)' }, + { value: 'en-IN', label: 'English (India)' }, + { value: 'en-NZ', label: 'English (New Zealand)' }, + { value: 'en-ZA', label: 'English (South Africa)' }, + { value: 'en-GB', label: 'English (UK)' }, + { value: 'en-US', label: 'English (US)' }, + { value: 'fi', label: 'Finnish' }, + { value: 'fr-FR', label: 'French' }, + { value: 'gl', label: 'Galician' }, + { value: 'de-DE', label: 'German' }, + { value: 'el-GR', label: 'Greek' }, + { value: 'he', label: 'Hebrew' }, + { value: 'hu', label: 'Hungarian' }, + { value: 'is', label: 'Icelandic' }, + { value: 'it-IT', label: 'Italian' }, + { value: 'id', label: 'Indonesian' }, + { value: 'ja', label: 'Japanese' }, + { value: 'ko', label: 'Korean' }, + { value: 'la', label: 'Latin' }, + { value: 'zh-CN', label: 'Mandarin Chinese' }, + { value: 'zh-TW', label: 'Taiwanese' }, + { value: 'zh-HK', label: 'Cantonese' }, + { value: 'ms-MY', label: 'Malaysian' }, + { value: 'no-NO', label: 'Norwegian' }, + { value: 'pl', label: 'Polish' }, + { value: 'xx-piglatin', label: 'Pig Latin' }, + { value: 'pt-PT', label: 'Portuguese' }, + { value: 'pt-br', label: 'Portuguese (Brasil)' }, + { value: 'ro-RO', label: 'Romanian' }, + { value: 'ru', label: 'Russian' }, + { value: 'sr-SP', label: 'Serbian' }, + { value: 'sk', label: 'Slovak' }, + { value: 'es-AR', label: 'Spanish (Argentina)' }, + { value: 'es-BO', label: 'Spanish (Bolivia)' }, + { value: 'es-CL', label: 'Spanish (Chile)' }, + { value: 'es-CO', label: 'Spanish (Colombia)' }, + { value: 'es-CR', label: 'Spanish (Costa Rica)' }, + { value: 'es-DO', label: 'Spanish (Dominican Republic)' }, + { value: 'es-EC', label: 'Spanish (Ecuador)' }, + { value: 'es-SV', label: 'Spanish (El Salvador)' }, + { value: 'es-GT', label: 'Spanish (Guatemala)' }, + { value: 'es-HN', label: 'Spanish (Honduras)' }, + { value: 'es-MX', label: 'Spanish (Mexico)' }, + { value: 'es-NI', label: 'Spanish (Nicaragua)' }, + { value: 'es-PA', label: 'Spanish (Panama)' }, + { value: 'es-PY', label: 'Spanish (Paraguay)' }, + { value: 'es-PE', label: 'Spanish (Peru)' }, + { value: 'es-PR', label: 'Spanish (Puerto Rico)' }, + { value: 'es-ES', label: 'Spanish (Spain)' }, + { value: 'es-US', label: 'Spanish (US)' }, + { value: 'es-UY', label: 'Spanish (Uruguay)' }, + { value: 'es-VE', label: 'Spanish (Venezuela)' }, + { value: 'sv-SE', label: 'Swedish' }, + { value: 'tr', label: 'Turkish' }, + { value: 'zu', label: 'Zulu' }, ]; const handleSelect = (value: string) => { diff --git a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx index 566c7305967..6ae78c06554 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx @@ -44,7 +44,7 @@ function Speech() { 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 [voice, setVoice] = useRecoilState(store.voice); const [cloudBrowserVoices, setCloudBrowserVoices] = useRecoilState( store.cloudBrowserVoices, ); @@ -53,7 +53,7 @@ function Speech() { const [playbackRate, setPlaybackRate] = useRecoilState(store.playbackRate); const updateSetting = useCallback( - (key, newValue) => { + (key: string, newValue: string | number) => { const settings = { sttExternal: { value: sttExternal, setFunc: setSttExternal }, ttsExternal: { value: ttsExternal, setFunc: setTtsExternal }, diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx index 4bff4bc4418..405c78555aa 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx @@ -14,13 +14,13 @@ const EngineTTSDropdown: React.FC = ({ external }) => { const endpointOptions = external ? [ - { value: 'browser', display: localize('com_nav_browser') }, - { value: 'edge', display: localize('com_nav_edge') }, - { value: 'external', display: localize('com_nav_external') }, + { value: 'browser', label: localize('com_nav_browser') }, + { value: 'edge', label: localize('com_nav_edge') }, + { value: 'external', label: localize('com_nav_external') }, ] : [ - { value: 'browser', display: localize('com_nav_browser') }, - { value: 'edge', display: localize('com_nav_edge') }, + { value: 'browser', label: localize('com_nav_browser') }, + { value: 'edge', label: localize('com_nav_edge') }, ]; const handleSelect = (value: string) => { diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/VoiceDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/VoiceDropdown.tsx index 340be50e8db..53a4c4686f2 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/VoiceDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/VoiceDropdown.tsx @@ -1,39 +1,33 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { useRecoilState } from 'recoil'; -import Dropdown from '~/components/ui/DropdownNoState'; +import React from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import type { Option } from '~/common'; +import DropdownNoState from '~/components/ui/DropdownNoState'; import { useLocalize, useTextToSpeech } from '~/hooks'; +import { logger } from '~/utils'; import store from '~/store'; export default function VoiceDropdown() { const localize = useLocalize(); + const { voices = [] } = useTextToSpeech(); const [voice, setVoice] = useRecoilState(store.voice); - const { voices } = useTextToSpeech(); - const [voiceOptions, setVoiceOptions] = useState([]); - const [engineTTS] = useRecoilState(store.engineTTS); + const engineTTS = useRecoilValue(store.engineTTS); - useEffect(() => { - async function fetchVoices() { - const options = await voices(); - setVoiceOptions(options); - - if (!voice && options.length > 0) { - setVoice(options[0]); - } + const handleVoiceChange = (newValue?: string | Option) => { + logger.log('Voice changed:', newValue); + const newVoice = typeof newValue === 'string' ? newValue : newValue?.value; + if (newVoice != null) { + return setVoice(newVoice.toString()); } - - fetchVoices(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [engineTTS]); - - const memoizedVoiceOptions = useMemo(() => voiceOptions, [voiceOptions]); + }; return (
{localize('com_nav_voice_select')}
- void; - options: (string | OptionType)[]; + options: string[] | Option[]; className?: string; anchor?: AnchorPropsWithSelection; sizeClasses?: string; @@ -50,8 +46,7 @@ const Dropdown: FC = ({ = ({ {label} {options - .map((o) => (typeof o === 'string' ? { value: o, display: o } : o)) - .find((o) => o.value === selectedValue)?.display || selectedValue} + .map((o) => (typeof o === 'string' ? { value: o, label: o } : o)) + .find((o) => o.value === selectedValue)?.label ?? selectedValue} = ({ viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" - className="h-4 w-5 rotate-0 transform text-black transition-transform duration-300 ease-in-out dark:text-gray-50" + className="h-4 w-5 rotate-0 transform text-text-primary transition-transform duration-300 ease-in-out" > @@ -82,7 +77,7 @@ const Dropdown: FC = ({ > = ({
- {typeof item === 'string' ? item : (item as OptionType).display} + {typeof item === 'string' ? item : (item as Option).label} {selectedValue === (typeof item === 'string' ? item : item.value) && ( diff --git a/client/src/components/ui/DropdownNoState.tsx b/client/src/components/ui/DropdownNoState.tsx index 67cb826425b..2d6b7a22f6f 100644 --- a/client/src/components/ui/DropdownNoState.tsx +++ b/client/src/components/ui/DropdownNoState.tsx @@ -7,18 +7,14 @@ import { Transition, } from '@headlessui/react'; import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating'; +import type { Option } from '~/common'; import { cn } from '~/utils/'; -type OptionType = { - value: string; - display?: string; -}; - interface DropdownProps { - value: string | OptionType; + value?: string | Option; label?: string; - onChange: (value: string) => void | ((value: OptionType) => void); - options: (string | OptionType)[]; + onChange: (value: string | Option) => void; + options: (string | Option)[]; className?: string; anchor?: AnchorPropsWithSelection; sizeClasses?: string; @@ -35,19 +31,23 @@ const Dropdown: FC = ({ sizeClasses, testId = 'dropdown-menu', }) => { - const getValue = (option: string | OptionType): string => - typeof option === 'string' ? option : option.value; + const getValue = (option?: string | Option) => + typeof option === 'string' ? option : option?.value; - const getDisplay = (option: string | OptionType): string => - typeof option === 'string' ? option : (option.display ?? '') || option.value; + const getDisplay = (option?: string | Option) => + typeof option === 'string' ? option : option?.label ?? option?.value; - const selectedOption = options.find((option) => getValue(option) === getValue(value)); + const isEqual = (a: string | Option, b: string | Option): boolean => getValue(a) === getValue(b); - const displayValue = selectedOption != null ? getDisplay(selectedOption) : getDisplay(value); + const selectedOption = options.find((option) => isEqual(option, value ?? '')) ?? value; + + const handleChange = (newValue: string | Option) => { + onChange(newValue); + }; return (
- +
= ({ > {label} - {displayValue} + {getDisplay(selectedOption)} = ({ style={{ width: '100%' }} data-theme={getValue(item)} > -
- {getDisplay(item)} -
+ {({ selected }) => ( +
+ {getDisplay(item)} + {selected && ( + + + + + + )} +
+ )} ))} diff --git a/client/src/data-provider/queries.ts b/client/src/data-provider/queries.ts index 25bbf840587..da316355599 100644 --- a/client/src/data-provider/queries.ts +++ b/client/src/data-provider/queries.ts @@ -266,7 +266,7 @@ export const useListAssistantsQuery = ( refetchOnMount: false, retry: false, ...config, - enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled, + enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled, }, ); }; @@ -333,7 +333,7 @@ export const useGetAssistantByIdQuery = ( retry: false, ...config, // Query will not execute until the assistant_id exists - enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled, + enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled, }, ); }; @@ -364,7 +364,7 @@ export const useGetActionsQuery = ( refetchOnReconnect: false, refetchOnMount: false, ...config, - enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled, + enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled, }, ); }; @@ -394,7 +394,7 @@ export const useGetAssistantDocsQuery = ( refetchOnReconnect: false, refetchOnMount: false, ...config, - enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled, + enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled, }, ); }; @@ -440,7 +440,7 @@ export const useVoicesQuery = (): UseQueryResult => { }; /* Custom config speech */ -export const useCustomConfigSpeechQuery = (): UseQueryResult => { +export const useCustomConfigSpeechQuery = () => { return useQuery([QueryKeys.customConfigSpeech], () => dataService.getCustomConfigSpeech()); }; @@ -488,7 +488,7 @@ export const useGetPromptGroup = ( refetchOnMount: false, retry: false, ...config, - enabled: config?.enabled !== undefined ? config?.enabled : true, + enabled: config?.enabled !== undefined ? config.enabled : true, }, ); }; @@ -506,7 +506,7 @@ export const useGetPrompts = ( refetchOnMount: false, retry: false, ...config, - enabled: config?.enabled !== undefined ? config?.enabled : true, + enabled: config?.enabled !== undefined ? config.enabled : true, }, ); }; @@ -540,7 +540,7 @@ export const useGetCategories = ( refetchOnMount: false, retry: false, ...config, - enabled: config?.enabled !== undefined ? config?.enabled : true, + enabled: config?.enabled !== undefined ? config.enabled : true, }, ); }; @@ -558,7 +558,7 @@ export const useGetRandomPrompts = ( refetchOnMount: false, retry: false, ...config, - enabled: config?.enabled !== undefined ? config?.enabled : true, + enabled: config?.enabled !== undefined ? config.enabled : true, }, ); }; diff --git a/client/src/hooks/Input/useGetAudioSettings.ts b/client/src/hooks/Input/useGetAudioSettings.ts index d16a2dc4bd1..99e8854f212 100644 --- a/client/src/hooks/Input/useGetAudioSettings.ts +++ b/client/src/hooks/Input/useGetAudioSettings.ts @@ -1,4 +1,5 @@ -import { useRecoilState } from 'recoil'; +import { useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; import store from '~/store'; export enum STTEndpoints { @@ -13,13 +14,16 @@ export enum TTSEndpoints { } const useGetAudioSettings = () => { - const [engineSTT] = useRecoilState(store.engineSTT); - const [engineTTS] = useRecoilState(store.engineTTS); + const engineSTT = useRecoilValue(store.engineSTT); + const engineTTS = useRecoilValue(store.engineTTS); - const speechToTextEndpoint: STTEndpoints = engineSTT as STTEndpoints; - const textToSpeechEndpoint: TTSEndpoints = engineTTS as TTSEndpoints; + const speechToTextEndpoint = engineSTT; + const textToSpeechEndpoint = engineTTS; - return { speechToTextEndpoint, textToSpeechEndpoint }; + return useMemo( + () => ({ speechToTextEndpoint, textToSpeechEndpoint }), + [speechToTextEndpoint, textToSpeechEndpoint], + ); }; export default useGetAudioSettings; diff --git a/client/src/hooks/Input/useTextToSpeech.ts b/client/src/hooks/Input/useTextToSpeech.ts index 4f915a361da..95ac5a6f847 100644 --- a/client/src/hooks/Input/useTextToSpeech.ts +++ b/client/src/hooks/Input/useTextToSpeech.ts @@ -1,13 +1,18 @@ -import { useRef } from 'react'; +import { useRecoilState } from 'recoil'; +import { useRef, useMemo, useEffect } from 'react'; import { parseTextParts } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider'; +import type { Option } from '~/common'; import useTextToSpeechExternal from './useTextToSpeechExternal'; import useTextToSpeechBrowser from './useTextToSpeechBrowser'; import useGetAudioSettings from './useGetAudioSettings'; import useTextToSpeechEdge from './useTextToSpeechEdge'; import { usePauseGlobalAudio } from '../Audio'; +import { logger } from '~/utils'; +import store from '~/store'; const useTextToSpeech = (message?: TMessage, isLast = false, index = 0) => { + const [voice, setVoice] = useRecoilState(store.voice); const { textToSpeechEndpoint } = useGetAudioSettings(); const { pauseGlobalAudio } = usePauseGlobalAudio(index); const audioRef = useRef(null); @@ -33,9 +38,47 @@ const useTextToSpeech = (message?: TMessage, isLast = false, index = 0) => { isLoading: isLoadingExternal, audioRef: audioRefExternal, voices: voicesExternal, - } = useTextToSpeechExternal(message?.messageId || '', isLast, index); + } = useTextToSpeechExternal(message?.messageId ?? '', isLast, index); - let generateSpeech, cancelSpeech, isSpeaking, isLoading, voices; + let generateSpeech, cancelSpeech, isSpeaking, isLoading; + + const voices: Option[] | string[] = useMemo(() => { + const voiceMap = { + external: voicesExternal, + edge: voicesEdge, + browser: voicesLocal, + }; + + return voiceMap[textToSpeechEndpoint]; + }, [textToSpeechEndpoint, voicesEdge, voicesExternal, voicesLocal]); + + useEffect(() => { + const firstVoice = voices[0]; + if (voices.length && typeof firstVoice === 'object') { + const lastSelectedVoice = voices.find((v) => + typeof v === 'object' ? v.value === voice : v === voice, + ); + if (lastSelectedVoice != null) { + const currentVoice = + typeof lastSelectedVoice === 'object' ? lastSelectedVoice.value : lastSelectedVoice; + logger.log('useTextToSpeech.ts - Effect:', { voices, voice: currentVoice }); + setVoice(currentVoice?.toString() ?? undefined); + return; + } + + logger.log('useTextToSpeech.ts - Effect:', { voices, voice: firstVoice.value }); + setVoice(firstVoice.value?.toString() ?? undefined); + } else if (voices.length) { + const lastSelectedVoice = voices.find((v) => v === voice); + if (lastSelectedVoice != null) { + logger.log('useTextToSpeech.ts - Effect:', { voices, voice: lastSelectedVoice }); + setVoice(lastSelectedVoice.toString()); + return; + } + logger.log('useTextToSpeech.ts - Effect:', { voices, voice: firstVoice }); + setVoice(firstVoice.toString()); + } + }, [setVoice, textToSpeechEndpoint, voice, voices]); switch (textToSpeechEndpoint) { case 'external': @@ -43,17 +86,15 @@ const useTextToSpeech = (message?: TMessage, isLast = false, index = 0) => { cancelSpeech = cancelSpeechExternal; isSpeaking = isSpeakingExternal; isLoading = isLoadingExternal; - if (audioRefExternal) { + if (audioRefExternal.current) { audioRef.current = audioRefExternal.current; } - voices = voicesExternal; break; case 'edge': generateSpeech = generateSpeechEdge; cancelSpeech = cancelSpeechEdge; isSpeaking = isSpeakingEdge; isLoading = false; - voices = voicesEdge; break; case 'browser': default: @@ -61,7 +102,6 @@ const useTextToSpeech = (message?: TMessage, isLast = false, index = 0) => { cancelSpeech = cancelSpeechLocal; isSpeaking = isSpeakingLocal; isLoading = false; - voices = voicesLocal; break; } @@ -82,7 +122,7 @@ const useTextToSpeech = (message?: TMessage, isLast = false, index = 0) => { const handleMouseUp = () => { isMouseDownRef.current = false; - if (timerRef.current) { + if (timerRef.current != null) { window.clearTimeout(timerRef.current); } }; @@ -105,8 +145,8 @@ const useTextToSpeech = (message?: TMessage, isLast = false, index = 0) => { toggleSpeech, isSpeaking, isLoading, - voices, audioRef, + voices, }; }; diff --git a/client/src/hooks/Input/useTextToSpeechBrowser.ts b/client/src/hooks/Input/useTextToSpeechBrowser.ts index c8c8158e314..ad399c88d95 100644 --- a/client/src/hooks/Input/useTextToSpeechBrowser.ts +++ b/client/src/hooks/Input/useTextToSpeechBrowser.ts @@ -1,21 +1,46 @@ import { useRecoilState } from 'recoil'; -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import store from '~/store'; interface VoiceOption { value: string; - display: string; + label: string; } function useTextToSpeechBrowser() { const [cloudBrowserVoices] = useRecoilState(store.cloudBrowserVoices); const [isSpeaking, setIsSpeaking] = useState(false); const [voiceName] = useRecoilState(store.voice); + const [voices, setVoices] = useState([]); + + const updateVoices = useCallback(() => { + const availableVoices = window.speechSynthesis + .getVoices() + .filter((v) => cloudBrowserVoices || v.localService === true); + + const voiceOptions: VoiceOption[] = availableVoices.map((v) => ({ + value: v.name, + label: v.name, + })); + + setVoices(voiceOptions); + }, [cloudBrowserVoices]); + + useEffect(() => { + if (window.speechSynthesis.getVoices().length) { + updateVoices(); + } else { + window.speechSynthesis.onvoiceschanged = updateVoices; + } + + return () => { + window.speechSynthesis.onvoiceschanged = null; + }; + }, [updateVoices]); const generateSpeechLocal = (text: string) => { const synth = window.speechSynthesis; - const voices = synth.getVoices().filter((v) => cloudBrowserVoices || v.localService === true); - const voice = voices.find((v) => v.name === voiceName); + const voice = voices.find((v) => v.value === voiceName); if (!voice) { return; @@ -23,7 +48,7 @@ function useTextToSpeechBrowser() { synth.cancel(); const utterance = new SpeechSynthesisUtterance(text); - utterance.voice = voice; + utterance.voice = synth.getVoices().find((v) => v.name === voice.value) || null; utterance.onend = () => { setIsSpeaking(false); }; @@ -32,34 +57,10 @@ function useTextToSpeechBrowser() { }; const cancelSpeechLocal = () => { - const synth = window.speechSynthesis; - synth.cancel(); + window.speechSynthesis.cancel(); setIsSpeaking(false); }; - const voices = (): Promise => { - return new Promise((resolve) => { - const getAndMapVoices = () => { - const availableVoices = speechSynthesis - .getVoices() - .filter((v) => cloudBrowserVoices || v.localService === true); - - const voiceOptions: VoiceOption[] = availableVoices.map((v) => ({ - value: v.name, - display: v.name, - })); - - resolve(voiceOptions); - }; - - if (speechSynthesis.getVoices().length) { - getAndMapVoices(); - } else { - speechSynthesis.onvoiceschanged = getAndMapVoices; - } - }); - }; - return { generateSpeechLocal, cancelSpeechLocal, isSpeaking, voices }; } diff --git a/client/src/hooks/Input/useTextToSpeechEdge.ts b/client/src/hooks/Input/useTextToSpeechEdge.ts index 73cbc6b7942..fd969cd2b04 100644 --- a/client/src/hooks/Input/useTextToSpeechEdge.ts +++ b/client/src/hooks/Input/useTextToSpeechEdge.ts @@ -1,4 +1,4 @@ -import { useRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { useState, useCallback, useRef, useEffect } from 'react'; import { MsEdgeTTS, OUTPUT_FORMAT } from 'msedge-tts'; import { useToastContext } from '~/Providers'; @@ -7,20 +7,21 @@ import store from '~/store'; interface Voice { value: string; - display: string; + label: string; } interface UseTextToSpeechEdgeReturn { - generateSpeechEdge: (text: string) => Promise; + generateSpeechEdge: (text: string) => void; cancelSpeechEdge: () => void; isSpeaking: boolean; - voices: () => Promise; + voices: Voice[]; } function useTextToSpeechEdge(): UseTextToSpeechEdgeReturn { const localize = useLocalize(); + const [voices, setVoices] = useState([]); const [isSpeaking, setIsSpeaking] = useState(false); - const [voiceName] = useRecoilState(store.voice); + const voiceName = useRecoilValue(store.voice); const ttsRef = useRef(null); const audioElementRef = useRef(null); const mediaSourceRef = useRef(null); @@ -28,61 +29,59 @@ function useTextToSpeechEdge(): UseTextToSpeechEdgeReturn { const pendingBuffers = useRef([]); const { showToast } = useToastContext(); - const initializeTTS = useCallback(async (): Promise => { + const fetchVoices = useCallback(() => { if (!ttsRef.current) { ttsRef.current = new MsEdgeTTS(); } - try { - await ttsRef.current.setMetadata(voiceName, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3); - } catch (error) { - console.error('Error initializing TTS:', error); - showToast({ - message: localize('com_nav_tts_init_error', (error as Error).message), - status: 'error', - }); - } - }, [voiceName, showToast, localize]); - - const onSourceOpen = useCallback((): void => { - if (!sourceBufferRef.current && mediaSourceRef.current) { - try { - sourceBufferRef.current = mediaSourceRef.current.addSourceBuffer('audio/mpeg'); - sourceBufferRef.current.addEventListener('updateend', appendNextBuffer); - } catch (error) { - console.error('Error adding source buffer:', error); + ttsRef.current + .getVoices() + .then((voicesList) => { + setVoices( + voicesList.map((v) => ({ + value: v.ShortName, + label: v.FriendlyName, + })), + ); + }) + .catch((error) => { + console.error('Error fetching voices:', error); showToast({ - message: localize('com_nav_source_buffer_error'), + message: localize('com_nav_voices_fetch_error'), status: 'error', }); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps + }); }, [showToast, localize]); - const initializeMediaSource = useCallback(async (): Promise => { - return new Promise((resolve) => { - if (!mediaSourceRef.current) { - mediaSourceRef.current = new MediaSource(); - audioElementRef.current = new Audio(); - audioElementRef.current.src = URL.createObjectURL(mediaSourceRef.current); - } + const initializeTTS = useCallback(() => { + if (!ttsRef.current) { + ttsRef.current = new MsEdgeTTS(); + } + const availableVoice: Voice | undefined = voices.find((v) => v.value === voiceName); - const mediaSource = mediaSourceRef.current; - if (mediaSource.readyState === 'open') { - onSourceOpen(); - resolve(); - } else { - const onSourceOpenWrapper = (): void => { - onSourceOpen(); - resolve(); - mediaSource.removeEventListener('sourceopen', onSourceOpenWrapper); - }; - mediaSource.addEventListener('sourceopen', onSourceOpenWrapper); - } - }); - }, [onSourceOpen]); + if (availableVoice) { + ttsRef.current + .setMetadata(availableVoice.value, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3) + .catch((error) => { + console.error('Error initializing TTS:', error); + showToast({ + message: localize('com_nav_tts_init_error', (error as Error).message), + status: 'error', + }); + }); + } else if (voices.length > 0) { + ttsRef.current + .setMetadata(voices[0].value, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3) + .catch((error) => { + console.error('Error initializing TTS:', error); + showToast({ + message: localize('com_nav_tts_init_error', (error as Error).message), + status: 'error', + }); + }); + } + }, [voiceName, showToast, localize, voices]); - const appendNextBuffer = useCallback((): void => { + const appendNextBuffer = useCallback(() => { if ( sourceBufferRef.current && !sourceBufferRef.current.updating && @@ -104,50 +103,81 @@ function useTextToSpeechEdge(): UseTextToSpeechEdgeReturn { } }, [showToast, localize]); - const generateSpeechEdge = useCallback( - async (text: string): Promise => { + const onSourceOpen = useCallback(() => { + if (!sourceBufferRef.current && mediaSourceRef.current) { try { - await initializeTTS(); - await initializeMediaSource(); + sourceBufferRef.current = mediaSourceRef.current.addSourceBuffer('audio/mpeg'); + sourceBufferRef.current.addEventListener('updateend', appendNextBuffer); + } catch (error) { + console.error('Error adding source buffer:', error); + showToast({ + message: localize('com_nav_source_buffer_error'), + status: 'error', + }); + } + } + }, [showToast, localize, appendNextBuffer]); - if (!ttsRef.current || !audioElementRef.current) { - throw new Error('TTS or Audio element not initialized'); - } + const initializeMediaSource = useCallback(() => { + if (!mediaSourceRef.current) { + mediaSourceRef.current = new MediaSource(); + audioElementRef.current = new Audio(); + audioElementRef.current.src = URL.createObjectURL(mediaSourceRef.current); + } - setIsSpeaking(true); - pendingBuffers.current = []; + const mediaSource = mediaSourceRef.current; + if (mediaSource.readyState === 'open') { + onSourceOpen(); + } else { + mediaSource.addEventListener('sourceopen', onSourceOpen); + } + }, [onSourceOpen]); - const readable = await ttsRef.current.toStream(text); + const generateSpeechEdge = useCallback( + (text: string) => { + const generate = async () => { + try { + if (!ttsRef.current || !audioElementRef.current) { + throw new Error('TTS or Audio element not initialized'); + } - readable.on('data', (chunk: Buffer) => { - pendingBuffers.current.push(new Uint8Array(chunk)); - appendNextBuffer(); - }); + setIsSpeaking(true); + pendingBuffers.current = []; - readable.on('end', () => { - if (mediaSourceRef.current && mediaSourceRef.current.readyState === 'open') { - mediaSourceRef.current.endOfStream(); - } - }); + const readable = await ttsRef.current.toStream(text); + + readable.on('data', (chunk: Buffer) => { + pendingBuffers.current.push(new Uint8Array(chunk)); + appendNextBuffer(); + }); + + readable.on('end', () => { + if (mediaSourceRef.current && mediaSourceRef.current.readyState === 'open') { + mediaSourceRef.current.endOfStream(); + } + }); + + audioElementRef.current.onended = () => { + setIsSpeaking(false); + }; - audioElementRef.current.onended = () => { + await audioElementRef.current.play(); + } catch (error) { + console.error('Error generating speech:', error); + showToast({ + message: localize('com_nav_audio_play_error', (error as Error).message), + status: 'error', + }); setIsSpeaking(false); - }; + } + }; - await audioElementRef.current.play(); - } catch (error) { - console.error('Error generating speech:', error); - showToast({ - message: localize('com_nav_audio_play_error', (error as Error).message), - status: 'error', - }); - setIsSpeaking(false); - } + generate(); }, - [initializeTTS, initializeMediaSource, appendNextBuffer, showToast, localize], + [appendNextBuffer, showToast, localize], ); - const cancelSpeechEdge = useCallback((): void => { + const cancelSpeechEdge = useCallback(() => { try { if (audioElementRef.current) { audioElementRef.current.pause(); @@ -167,33 +197,22 @@ function useTextToSpeechEdge(): UseTextToSpeechEdgeReturn { } }, [showToast, localize]); - const voices = useCallback(async (): Promise => { - if (!ttsRef.current) { - ttsRef.current = new MsEdgeTTS(); - } - try { - const voicesList = await ttsRef.current.getVoices(); - return voicesList.map((v) => ({ - value: v.ShortName, - display: v.FriendlyName, - })); - } catch (error) { - console.error('Error fetching voices:', error); - showToast({ - message: localize('com_nav_voices_fetch_error'), - status: 'error', - }); - return []; - } - }, [showToast, localize]); + useEffect(() => { + fetchVoices(); + }, [fetchVoices]); + + useEffect(() => { + initializeTTS(); + }, [voiceName, initializeTTS]); useEffect(() => { + initializeMediaSource(); return () => { if (mediaSourceRef.current) { - URL.revokeObjectURL(audioElementRef.current?.src || ''); + URL.revokeObjectURL(audioElementRef.current?.src ?? ''); } }; - }, []); + }, [initializeMediaSource]); return { generateSpeechEdge, cancelSpeechEdge, isSpeaking, voices }; } diff --git a/client/src/hooks/Input/useTextToSpeechExternal.ts b/client/src/hooks/Input/useTextToSpeechExternal.ts index ce78cc65a30..e04eadea538 100644 --- a/client/src/hooks/Input/useTextToSpeechExternal.ts +++ b/client/src/hooks/Input/useTextToSpeechExternal.ts @@ -37,7 +37,7 @@ function useTextToSpeechExternal(messageId: string, isLast: boolean, index = 0) const playAudioPromise = (blobUrl: string) => { const newAudio = new Audio(blobUrl); const initializeAudio = () => { - if (playbackRate && playbackRate !== 1 && playbackRate > 0) { + if (playbackRate != null && playbackRate !== 1 && playbackRate > 0) { newAudio.playbackRate = playbackRate; } }; @@ -47,7 +47,7 @@ function useTextToSpeechExternal(messageId: string, isLast: boolean, index = 0) playPromise().catch((error: Error) => { if ( - error?.message && + error.message && error.message.includes('The play() request was interrupted by a call to pause()') ) { console.log('Play request was interrupted by a call to pause()'); @@ -92,7 +92,7 @@ function useTextToSpeechExternal(messageId: string, isLast: boolean, index = 0) if (cacheTTS && inputText) { const cache = await caches.open('tts-responses'); - const request = new Request(inputText!); + const request = new Request(inputText); const response = new Response(audioBlob); cache.put(request, response); } @@ -118,7 +118,7 @@ function useTextToSpeechExternal(messageId: string, isLast: boolean, index = 0) }); const startMutation = (text: string, download: boolean) => { - const formData = createFormData(text, voice); + const formData = createFormData(text, voice ?? ''); setDownloadFile(download); processAudio(formData); }; @@ -178,9 +178,7 @@ function useTextToSpeechExternal(messageId: string, isLast: boolean, index = 0) return isLocalSpeaking || (isLast && globalIsPlaying); }, [isLocalSpeaking, globalIsPlaying, isLast]); - const useVoices = () => { - return useVoicesQuery().data ?? []; - }; + const { data: voicesData = [] } = useVoicesQuery(); return { generateSpeechExternal, @@ -188,7 +186,7 @@ function useTextToSpeechExternal(messageId: string, isLast: boolean, index = 0) isLoading, isSpeaking, audioRef, - voices: useVoices, + voices: voicesData, }; } diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index 6092593cf21..7e57f54639f 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -56,7 +56,7 @@ const localStorageAtoms = { textToSpeech: atomWithLocalStorage('textToSpeech', true), engineTTS: atomWithLocalStorage('engineTTS', 'browser'), - voice: atomWithLocalStorage('voice', ''), + voice: atomWithLocalStorage('voice', undefined), cloudBrowserVoices: atomWithLocalStorage('cloudBrowserVoices', false), languageTTS: atomWithLocalStorage('languageTTS', ''), automaticPlayback: atomWithLocalStorage('automaticPlayback', false), diff --git a/package-lock.json b/package-lock.json index dc927591d0e..d942e032cb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6645,7 +6645,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.2.tgz", "integrity": "sha512-Kb3hgk9gRNRcTZktBrKdHhF3xFhYkca1Rk6e1/im2ENf83dgN54orMW0uSKTXFnUpZOUFZ+wcY05LlipwgZIFQ==", - "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.17.1", @@ -30879,7 +30878,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.4", + "version": "0.7.41", "license": "ISC", "dependencies": { "@types/js-yaml": "^4.0.9",