diff --git a/ui/desktop/src/components/settings/dictation/ElevenLabsKeyInput.tsx b/ui/desktop/src/components/settings/dictation/ElevenLabsKeyInput.tsx index a305faf05404..1856a8c5e5c5 100644 --- a/ui/desktop/src/components/settings/dictation/ElevenLabsKeyInput.tsx +++ b/ui/desktop/src/components/settings/dictation/ElevenLabsKeyInput.tsx @@ -1,5 +1,6 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Input } from '../../ui/input'; +import { Button } from '../../ui/button'; import { useConfig } from '../../ConfigContext'; import { ELEVENLABS_API_KEY, isSecretKeyConfigured } from '../../../hooks/dictationConstants'; import { setElevenLabsKeyCache } from '../../../hooks/useDictationSettings'; @@ -8,71 +9,75 @@ export const ElevenLabsKeyInput = () => { const [elevenLabsApiKey, setElevenLabsApiKey] = useState(''); const [isLoadingKey, setIsLoadingKey] = useState(false); const [hasElevenLabsKey, setHasElevenLabsKey] = useState(false); - const elevenLabsApiKeyRef = useRef(''); - const { upsert, read } = useConfig(); + const [validationError, setValidationError] = useState(''); + const [isEditing, setIsEditing] = useState(false); + const { upsert, read, remove } = useConfig(); - useEffect(() => { - const loadKey = async () => { - setIsLoadingKey(true); - try { - const response = await read(ELEVENLABS_API_KEY, true); - if (isSecretKeyConfigured(response)) { - setHasElevenLabsKey(true); - setElevenLabsKeyCache(true); - } else { - setElevenLabsKeyCache(false); - } - } catch (error) { - console.error('Error checking ElevenLabs API key:', error); - setElevenLabsKeyCache(false); - } finally { - setIsLoadingKey(false); - } - }; - - loadKey(); + const loadKey = useCallback(async () => { + setIsLoadingKey(true); + try { + const response = await read(ELEVENLABS_API_KEY, true); + const hasKey = isSecretKeyConfigured(response); + setHasElevenLabsKey(hasKey); + setElevenLabsKeyCache(hasKey); + } catch (error) { + console.error(error); + setElevenLabsKeyCache(false); + } finally { + setIsLoadingKey(false); + } }, [read]); - // Save key on unmount to avoid losing unsaved changes useEffect(() => { - return () => { - if (elevenLabsApiKeyRef.current) { - const keyToSave = elevenLabsApiKeyRef.current; - if (keyToSave.trim()) { - upsert(ELEVENLABS_API_KEY, keyToSave, true) - .then(() => setElevenLabsKeyCache(true)) - .catch((error) => { - console.error('Error saving ElevenLabs API key on unmount:', error); - }); - } - } - }; - }, [upsert]); + loadKey(); + }, [loadKey]); const handleElevenLabsKeyChange = (key: string) => { setElevenLabsApiKey(key); - elevenLabsApiKeyRef.current = key; - if (key.length > 0) { - setHasElevenLabsKey(false); + if (validationError) { + setValidationError(''); } }; - const saveElevenLabsKey = async () => { + const handleSave = async () => { try { - if (elevenLabsApiKey.trim()) { - await upsert(ELEVENLABS_API_KEY, elevenLabsApiKey, true); - setHasElevenLabsKey(true); - setElevenLabsKeyCache(true); - } else { - await upsert(ELEVENLABS_API_KEY, null, true); - setHasElevenLabsKey(false); - setElevenLabsKeyCache(false); + const trimmedKey = elevenLabsApiKey.trim(); + + if (!trimmedKey) { + setValidationError('API key is required'); + return; } + + await upsert(ELEVENLABS_API_KEY, trimmedKey, true); + setElevenLabsApiKey(''); + setValidationError(''); + setIsEditing(false); + await loadKey(); } catch (error) { - console.error('Error saving ElevenLabs API key:', error); + console.error(error); + setValidationError('Failed to save API key'); } }; + const handleRemove = async () => { + try { + await remove(ELEVENLABS_API_KEY, true); + await loadKey(); + setElevenLabsApiKey(''); + setValidationError(''); + setIsEditing(false); + } catch (error) { + console.error(error); + setValidationError('Failed to remove API key'); + } + }; + + const handleCancel = () => { + setElevenLabsApiKey(''); + setValidationError(''); + setIsEditing(false); + }; + return (
@@ -82,17 +87,42 @@ export const ElevenLabsKeyInput = () => { {hasElevenLabsKey && (Configured)}

- handleElevenLabsKeyChange(e.target.value)} - onBlur={saveElevenLabsKey} - placeholder={ - hasElevenLabsKey ? 'Enter new API key to update' : 'Enter your ElevenLabs API key' - } - className="max-w-md" - disabled={isLoadingKey} - /> + + {!isEditing ? ( + + ) : ( +
+ handleElevenLabsKeyChange(e.target.value)} + placeholder="Enter your ElevenLabs API key" + className="max-w-md" + autoFocus + /> + {validationError &&

{validationError}

} +
+ + + {hasElevenLabsKey && ( + + )} +
+
+ )}
); }; diff --git a/ui/desktop/src/components/settings/dictation/ProviderSelector.tsx b/ui/desktop/src/components/settings/dictation/ProviderSelector.tsx index 5c8e917d4356..4b3878509a06 100644 --- a/ui/desktop/src/components/settings/dictation/ProviderSelector.tsx +++ b/ui/desktop/src/components/settings/dictation/ProviderSelector.tsx @@ -1,7 +1,10 @@ import { useState, useEffect } from 'react'; import { ChevronDown } from 'lucide-react'; import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings'; -import { DICTATION_PROVIDER_ELEVENLABS } from '../../../hooks/dictationConstants'; +import { + DICTATION_PROVIDER_OPENAI, + DICTATION_PROVIDER_ELEVENLABS, +} from '../../../hooks/dictationConstants'; import { useConfig } from '../../ConfigContext'; import { ElevenLabsKeyInput } from './ElevenLabsKeyInput'; import { ProviderInfo } from './ProviderInfo'; @@ -56,7 +59,7 @@ export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelecto const getProviderLabel = (provider: DictationProvider): string => { switch (provider) { - case 'openai': + case DICTATION_PROVIDER_OPENAI: return 'OpenAI Whisper'; case DICTATION_PROVIDER_ELEVENLABS: return 'ElevenLabs'; @@ -84,25 +87,31 @@ export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelecto {showProviderDropdown && ( -
+
{VOICE_DICTATION_ELEVENLABS_ENABLED && ( )}
diff --git a/ui/desktop/src/components/settings/dictation/VoiceDictationToggle.tsx b/ui/desktop/src/components/settings/dictation/VoiceDictationToggle.tsx index dcb4fea9cec1..6af1c71c8916 100644 --- a/ui/desktop/src/components/settings/dictation/VoiceDictationToggle.tsx +++ b/ui/desktop/src/components/settings/dictation/VoiceDictationToggle.tsx @@ -3,6 +3,7 @@ import { Switch } from '../../ui/switch'; import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings'; import { DICTATION_SETTINGS_KEY, + DICTATION_PROVIDER_OPENAI, DICTATION_PROVIDER_ELEVENLABS, getDefaultDictationSettings, } from '../../../hooks/dictationConstants'; @@ -35,7 +36,7 @@ export const VoiceDictationToggle = () => { ) { loadedSettings = { ...loadedSettings, - provider: 'openai', + provider: DICTATION_PROVIDER_OPENAI, }; localStorage.setItem(DICTATION_SETTINGS_KEY, JSON.stringify(loadedSettings)); } @@ -59,7 +60,7 @@ export const VoiceDictationToggle = () => { saveSettings({ ...settings, enabled, - provider: settings.provider === null ? 'openai' : settings.provider, + provider: settings.provider === null ? DICTATION_PROVIDER_OPENAI : settings.provider, }); trackSettingToggled('voice_dictation', enabled); }; @@ -83,7 +84,7 @@ export const VoiceDictationToggle = () => {
diff --git a/ui/desktop/src/hooks/dictationConstants.ts b/ui/desktop/src/hooks/dictationConstants.ts index c9c7ee65d9d5..1863c6decace 100644 --- a/ui/desktop/src/hooks/dictationConstants.ts +++ b/ui/desktop/src/hooks/dictationConstants.ts @@ -2,6 +2,7 @@ import { DictationSettings, DictationProvider } from './useDictationSettings'; export const DICTATION_SETTINGS_KEY = 'dictation_settings'; export const ELEVENLABS_API_KEY = 'ELEVENLABS_API_KEY'; +export const DICTATION_PROVIDER_OPENAI = 'openai' as const; export const DICTATION_PROVIDER_ELEVENLABS = 'elevenlabs' as const; export const isSecretKeyConfigured = (response: unknown): boolean => @@ -14,14 +15,12 @@ export const getDefaultDictationSettings = async ( getProviders: (refresh: boolean) => Promise> ): Promise => { const providers = await getProviders(false); - - // Check if we have an OpenAI API key as primary default const openAIProvider = providers.find((p) => p.name === 'openai'); if (openAIProvider && openAIProvider.is_configured) { return { enabled: true, - provider: 'openai' as DictationProvider, + provider: DICTATION_PROVIDER_OPENAI, }; } else { return { diff --git a/ui/desktop/src/hooks/useDictationSettings.ts b/ui/desktop/src/hooks/useDictationSettings.ts index 778006f7e6d4..4afe4e0f2574 100644 --- a/ui/desktop/src/hooks/useDictationSettings.ts +++ b/ui/desktop/src/hooks/useDictationSettings.ts @@ -3,12 +3,16 @@ import { useConfig } from '../components/ConfigContext'; import { DICTATION_SETTINGS_KEY, ELEVENLABS_API_KEY, + DICTATION_PROVIDER_OPENAI, DICTATION_PROVIDER_ELEVENLABS, getDefaultDictationSettings, isSecretKeyConfigured, } from './dictationConstants'; -export type DictationProvider = 'openai' | typeof DICTATION_PROVIDER_ELEVENLABS | null; +export type DictationProvider = + | typeof DICTATION_PROVIDER_OPENAI + | typeof DICTATION_PROVIDER_ELEVENLABS + | null; export interface DictationSettings { enabled: boolean; @@ -28,7 +32,6 @@ export const useDictationSettings = () => { useEffect(() => { const loadSettings = async () => { - // Load settings from localStorage const saved = localStorage.getItem(DICTATION_SETTINGS_KEY); let currentSettings: DictationSettings; diff --git a/ui/desktop/src/hooks/useWhisper.ts b/ui/desktop/src/hooks/useWhisper.ts index 0cc557f807e6..caf8a1a3040a 100644 --- a/ui/desktop/src/hooks/useWhisper.ts +++ b/ui/desktop/src/hooks/useWhisper.ts @@ -2,7 +2,7 @@ import { useState, useRef, useCallback, useEffect } from 'react'; import { useConfig } from '../components/ConfigContext'; import { getApiUrl } from '../config'; import { useDictationSettings } from './useDictationSettings'; -import { DICTATION_PROVIDER_ELEVENLABS } from './dictationConstants'; +import { DICTATION_PROVIDER_OPENAI, DICTATION_PROVIDER_ELEVENLABS } from './dictationConstants'; import { safeJsonParse } from '../utils/conversionUtils'; interface UseWhisperOptions { @@ -75,7 +75,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp // Check provider availability switch (dictationSettings.provider) { - case 'openai': + case DICTATION_PROVIDER_OPENAI: setCanUseDictation(hasOpenAIKey); break; case DICTATION_PROVIDER_ELEVENLABS: @@ -178,7 +178,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp // Choose endpoint based on provider switch (dictationSettings.provider) { - case 'openai': + case DICTATION_PROVIDER_OPENAI: endpoint = '/audio/transcribe'; break; case DICTATION_PROVIDER_ELEVENLABS: