diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index bf7c387d3e77..057cebdb592c 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1006,7 +1006,7 @@ export default function ChatInput({ {/* Inline action buttons on the right */}
{/* Microphone button - show if dictation is enabled, disable if not configured */} - {dictationSettings?.enabled && ( + {(dictationSettings?.enabled || dictationSettings?.provider === null) && ( <> {!canUseDictation ? ( @@ -1026,11 +1026,24 @@ export default function ChatInput({ - {dictationSettings.provider === 'openai' - ? 'OpenAI API key is not configured. Set it up in Settings > Models.' - : dictationSettings.provider === 'elevenlabs' - ? 'ElevenLabs API key is not configured. Set it up in Settings > Chat > Voice Dictation.' - : 'Dictation provider is not properly configured.'} + {dictationSettings.provider === 'openai' ? ( +

+ OpenAI API key is not configured. Set it up in Settings {'>'}{' '} + Models. +

+ ) : dictationSettings.provider === 'elevenlabs' ? ( +

+ ElevenLabs API key is not configured. Set it up in Settings {'>'}{' '} + Chat {'>'} Voice Dictation. +

+ ) : dictationSettings.provider === null ? ( +

+ Dictation is not configured. Configure it in Settings {'>'}{' '} + Chat {'>'} Voice Dictation. +

+ ) : ( +

Dictation provider is not properly configured.

+ )}
) : ( diff --git a/ui/desktop/src/components/settings/dictation/DictationSection.tsx b/ui/desktop/src/components/settings/dictation/DictationSection.tsx index f80cbe85c201..23f8e16b5fb3 100644 --- a/ui/desktop/src/components/settings/dictation/DictationSection.tsx +++ b/ui/desktop/src/components/settings/dictation/DictationSection.tsx @@ -3,21 +3,15 @@ import { Switch } from '../../ui/switch'; import { ChevronDown } from 'lucide-react'; import { Input } from '../../ui/input'; import { useConfig } from '../../ConfigContext'; - -type DictationProvider = 'openai' | 'elevenlabs'; - -interface DictationSettings { - enabled: boolean; - provider: DictationProvider; -} +import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings'; const DICTATION_SETTINGS_KEY = 'dictation_settings'; const ELEVENLABS_API_KEY = 'ELEVENLABS_API_KEY'; export default function DictationSection() { const [settings, setSettings] = useState({ - enabled: true, - provider: 'openai', + enabled: false, + provider: null, }); const [hasOpenAIKey, setHasOpenAIKey] = useState(false); const [showProviderDropdown, setShowProviderDropdown] = useState(false); @@ -120,7 +114,11 @@ export default function DictationSection() { }; const handleToggle = (enabled: boolean) => { - saveSettings({ ...settings, enabled }); + saveSettings({ + ...settings, + enabled, + provider: settings.provider === null ? 'openai' : settings.provider, + }); }; const handleProviderChange = (provider: DictationProvider) => { @@ -157,7 +155,7 @@ export default function DictationSection() { case 'elevenlabs': return 'ElevenLabs'; default: - return provider; + return 'None (disabled)'; } }; diff --git a/ui/desktop/src/hooks/useDictationSettings.ts b/ui/desktop/src/hooks/useDictationSettings.ts index 340cd8c3e7e2..57ab0abb8906 100644 --- a/ui/desktop/src/hooks/useDictationSettings.ts +++ b/ui/desktop/src/hooks/useDictationSettings.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useConfig } from '../components/ConfigContext'; -export type DictationProvider = 'openai' | 'elevenlabs'; +export type DictationProvider = 'openai' | 'elevenlabs' | null; export interface DictationSettings { enabled: boolean; @@ -14,7 +14,7 @@ const ELEVENLABS_API_KEY = 'ELEVENLABS_API_KEY'; export const useDictationSettings = () => { const [settings, setSettings] = useState(null); const [hasElevenLabsKey, setHasElevenLabsKey] = useState(false); - const { read } = useConfig(); + const { read, getProviders } = useConfig(); useEffect(() => { const loadSettings = async () => { @@ -23,12 +23,20 @@ export const useDictationSettings = () => { if (saved) { setSettings(JSON.parse(saved)); } else { - // Default settings - const defaultSettings: DictationSettings = { - enabled: true, - provider: 'openai', - }; - setSettings(defaultSettings); + 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) { + setSettings({ + enabled: true, + provider: 'openai', + }); + } else { + setSettings({ + enabled: false, + provider: null, + }); + } } // Load ElevenLabs API key from storage (non-secret for frontend access) @@ -54,7 +62,7 @@ export const useDictationSettings = () => { window.addEventListener('storage', handleStorageChange); return () => window.removeEventListener('storage', handleStorageChange); - }, [read]); + }, [read, getProviders]); return { settings, hasElevenLabsKey }; }; diff --git a/ui/desktop/src/hooks/useWhisper.ts b/ui/desktop/src/hooks/useWhisper.ts index 92f9527aff95..24c9847a0a4e 100644 --- a/ui/desktop/src/hooks/useWhisper.ts +++ b/ui/desktop/src/hooks/useWhisper.ts @@ -85,9 +85,54 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp } }, [dictationSettings, hasOpenAIKey, hasElevenLabsKey]); + // Define stopRecording before startRecording to avoid circular dependency + const stopRecording = useCallback(() => { + setIsRecording(false); // Always update the visual state + + if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { + mediaRecorderRef.current.stop(); + } + + // Clear interval + if (durationIntervalRef.current) { + clearInterval(durationIntervalRef.current); + durationIntervalRef.current = null; + } + + // Stop all tracks in the stream + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + streamRef.current = null; + } + + // Close audio context + if (audioContext && audioContext.state !== 'closed') { + audioContext.close().catch(console.error); + setAudioContext(null); + setAnalyser(null); + } + }, [audioContext]); + + // Cleanup effect to prevent memory leaks + useEffect(() => { + return () => { + // Cleanup on unmount + if (durationIntervalRef.current) { + clearInterval(durationIntervalRef.current); + } + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + } + if (audioContext && audioContext.state !== 'closed') { + audioContext.close().catch(console.error); + } + }; + }, [audioContext]); + const transcribeAudio = useCallback( async (audioBlob: Blob) => { if (!dictationSettings) { + stopRecording(); onError?.(new Error('Dictation settings not loaded')); return; } @@ -169,6 +214,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp } } catch (error) { console.error('Error transcribing audio:', error); + stopRecording(); onError?.(error as Error); } finally { setIsTranscribing(false); @@ -176,54 +222,12 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp setEstimatedSize(0); } }, - [onTranscription, onError, dictationSettings] + [onTranscription, onError, dictationSettings, stopRecording] ); - // Define stopRecording before startRecording to avoid circular dependency - const stopRecording = useCallback(() => { - if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { - mediaRecorderRef.current.stop(); - setIsRecording(false); - } - - // Clear interval - if (durationIntervalRef.current) { - clearInterval(durationIntervalRef.current); - durationIntervalRef.current = null; - } - - // Stop all tracks in the stream - if (streamRef.current) { - streamRef.current.getTracks().forEach((track) => track.stop()); - streamRef.current = null; - } - - // Close audio context - if (audioContext && audioContext.state !== 'closed') { - audioContext.close().catch(console.error); - setAudioContext(null); - setAnalyser(null); - } - }, [audioContext]); - - // Cleanup effect to prevent memory leaks - useEffect(() => { - return () => { - // Cleanup on unmount - if (durationIntervalRef.current) { - clearInterval(durationIntervalRef.current); - } - if (streamRef.current) { - streamRef.current.getTracks().forEach((track) => track.stop()); - } - if (audioContext && audioContext.state !== 'closed') { - audioContext.close().catch(console.error); - } - }; - }, [audioContext]); - const startRecording = useCallback(async () => { if (!dictationSettings) { + stopRecording(); onError?.(new Error('Dictation settings not loaded')); return; } @@ -301,6 +305,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp setIsRecording(true); } catch (error) { console.error('Error starting recording:', error); + stopRecording(); onError?.(error as Error); } }, [onError, onSizeWarning, transcribeAudio, stopRecording, dictationSettings]);