fix: update dictation settings handling and improve user feedback (#4093)

Signed-off-by: Parsa Rahimi <mail@parsuli.net>
This commit is contained in:
Parsa Rahimi
2025-08-14 13:17:06 -07:00
committed by GitHub
parent 28b106a039
commit d9b0e7f4c6
4 changed files with 94 additions and 70 deletions
+19 -6
View File
@@ -1006,7 +1006,7 @@ export default function ChatInput({
{/* Inline action buttons on the right */} {/* Inline action buttons on the right */}
<div className="flex items-center gap-1 px-2 relative"> <div className="flex items-center gap-1 px-2 relative">
{/* Microphone button - show if dictation is enabled, disable if not configured */} {/* Microphone button - show if dictation is enabled, disable if not configured */}
{dictationSettings?.enabled && ( {(dictationSettings?.enabled || dictationSettings?.provider === null) && (
<> <>
{!canUseDictation ? ( {!canUseDictation ? (
<Tooltip> <Tooltip>
@@ -1026,11 +1026,24 @@ export default function ChatInput({
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{dictationSettings.provider === 'openai' {dictationSettings.provider === 'openai' ? (
? 'OpenAI API key is not configured. Set it up in Settings > Models.' <p>
: dictationSettings.provider === 'elevenlabs' OpenAI API key is not configured. Set it up in <b>Settings</b> {'>'}{' '}
? 'ElevenLabs API key is not configured. Set it up in Settings > Chat > Voice Dictation.' <b>Models.</b>
: 'Dictation provider is not properly configured.'} </p>
) : dictationSettings.provider === 'elevenlabs' ? (
<p>
ElevenLabs API key is not configured. Set it up in <b>Settings</b> {'>'}{' '}
<b>Chat</b> {'>'} <b>Voice Dictation.</b>
</p>
) : dictationSettings.provider === null ? (
<p>
Dictation is not configured. Configure it in <b>Settings</b> {'>'}{' '}
<b>Chat</b> {'>'} <b>Voice Dictation.</b>
</p>
) : (
<p>Dictation provider is not properly configured.</p>
)}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
) : ( ) : (
@@ -3,21 +3,15 @@ import { Switch } from '../../ui/switch';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { Input } from '../../ui/input'; import { Input } from '../../ui/input';
import { useConfig } from '../../ConfigContext'; import { useConfig } from '../../ConfigContext';
import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings';
type DictationProvider = 'openai' | 'elevenlabs';
interface DictationSettings {
enabled: boolean;
provider: DictationProvider;
}
const DICTATION_SETTINGS_KEY = 'dictation_settings'; const DICTATION_SETTINGS_KEY = 'dictation_settings';
const ELEVENLABS_API_KEY = 'ELEVENLABS_API_KEY'; const ELEVENLABS_API_KEY = 'ELEVENLABS_API_KEY';
export default function DictationSection() { export default function DictationSection() {
const [settings, setSettings] = useState<DictationSettings>({ const [settings, setSettings] = useState<DictationSettings>({
enabled: true, enabled: false,
provider: 'openai', provider: null,
}); });
const [hasOpenAIKey, setHasOpenAIKey] = useState(false); const [hasOpenAIKey, setHasOpenAIKey] = useState(false);
const [showProviderDropdown, setShowProviderDropdown] = useState(false); const [showProviderDropdown, setShowProviderDropdown] = useState(false);
@@ -120,7 +114,11 @@ export default function DictationSection() {
}; };
const handleToggle = (enabled: boolean) => { const handleToggle = (enabled: boolean) => {
saveSettings({ ...settings, enabled }); saveSettings({
...settings,
enabled,
provider: settings.provider === null ? 'openai' : settings.provider,
});
}; };
const handleProviderChange = (provider: DictationProvider) => { const handleProviderChange = (provider: DictationProvider) => {
@@ -157,7 +155,7 @@ export default function DictationSection() {
case 'elevenlabs': case 'elevenlabs':
return 'ElevenLabs'; return 'ElevenLabs';
default: default:
return provider; return 'None (disabled)';
} }
}; };
+17 -9
View File
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useConfig } from '../components/ConfigContext'; import { useConfig } from '../components/ConfigContext';
export type DictationProvider = 'openai' | 'elevenlabs'; export type DictationProvider = 'openai' | 'elevenlabs' | null;
export interface DictationSettings { export interface DictationSettings {
enabled: boolean; enabled: boolean;
@@ -14,7 +14,7 @@ const ELEVENLABS_API_KEY = 'ELEVENLABS_API_KEY';
export const useDictationSettings = () => { export const useDictationSettings = () => {
const [settings, setSettings] = useState<DictationSettings | null>(null); const [settings, setSettings] = useState<DictationSettings | null>(null);
const [hasElevenLabsKey, setHasElevenLabsKey] = useState<boolean>(false); const [hasElevenLabsKey, setHasElevenLabsKey] = useState<boolean>(false);
const { read } = useConfig(); const { read, getProviders } = useConfig();
useEffect(() => { useEffect(() => {
const loadSettings = async () => { const loadSettings = async () => {
@@ -23,12 +23,20 @@ export const useDictationSettings = () => {
if (saved) { if (saved) {
setSettings(JSON.parse(saved)); setSettings(JSON.parse(saved));
} else { } else {
// Default settings const providers = await getProviders(false);
const defaultSettings: DictationSettings = { // Check if we have an OpenAI API key as primary default
enabled: true, const openAIProvider = providers.find((p) => p.name === 'openai');
provider: 'openai', if (openAIProvider && openAIProvider.is_configured) {
}; setSettings({
setSettings(defaultSettings); enabled: true,
provider: 'openai',
});
} else {
setSettings({
enabled: false,
provider: null,
});
}
} }
// Load ElevenLabs API key from storage (non-secret for frontend access) // Load ElevenLabs API key from storage (non-secret for frontend access)
@@ -54,7 +62,7 @@ export const useDictationSettings = () => {
window.addEventListener('storage', handleStorageChange); window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange); return () => window.removeEventListener('storage', handleStorageChange);
}, [read]); }, [read, getProviders]);
return { settings, hasElevenLabsKey }; return { settings, hasElevenLabsKey };
}; };
+49 -44
View File
@@ -85,9 +85,54 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
} }
}, [dictationSettings, hasOpenAIKey, hasElevenLabsKey]); }, [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( const transcribeAudio = useCallback(
async (audioBlob: Blob) => { async (audioBlob: Blob) => {
if (!dictationSettings) { if (!dictationSettings) {
stopRecording();
onError?.(new Error('Dictation settings not loaded')); onError?.(new Error('Dictation settings not loaded'));
return; return;
} }
@@ -169,6 +214,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
} }
} catch (error) { } catch (error) {
console.error('Error transcribing audio:', error); console.error('Error transcribing audio:', error);
stopRecording();
onError?.(error as Error); onError?.(error as Error);
} finally { } finally {
setIsTranscribing(false); setIsTranscribing(false);
@@ -176,54 +222,12 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
setEstimatedSize(0); 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 () => { const startRecording = useCallback(async () => {
if (!dictationSettings) { if (!dictationSettings) {
stopRecording();
onError?.(new Error('Dictation settings not loaded')); onError?.(new Error('Dictation settings not loaded'));
return; return;
} }
@@ -301,6 +305,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
setIsRecording(true); setIsRecording(true);
} catch (error) { } catch (error) {
console.error('Error starting recording:', error); console.error('Error starting recording:', error);
stopRecording();
onError?.(error as Error); onError?.(error as Error);
} }
}, [onError, onSizeWarning, transcribeAudio, stopRecording, dictationSettings]); }, [onError, onSizeWarning, transcribeAudio, stopRecording, dictationSettings]);