fix: update dictation settings handling and improve user feedback (#4093)
Signed-off-by: Parsa Rahimi <mail@parsuli.net>
This commit is contained in:
@@ -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)';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user