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]);