Fix: ElevenLabs API Key Not Persisting (#6557)

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
This commit is contained in:
Abhijay Jain
2026-01-27 20:13:01 +05:30
committed by GitHub
parent e9024b7c19
commit 2a41f5daed
6 changed files with 127 additions and 85 deletions
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { Input } from '../../ui/input'; import { Input } from '../../ui/input';
import { Button } from '../../ui/button';
import { useConfig } from '../../ConfigContext'; import { useConfig } from '../../ConfigContext';
import { ELEVENLABS_API_KEY, isSecretKeyConfigured } from '../../../hooks/dictationConstants'; import { ELEVENLABS_API_KEY, isSecretKeyConfigured } from '../../../hooks/dictationConstants';
import { setElevenLabsKeyCache } from '../../../hooks/useDictationSettings'; import { setElevenLabsKeyCache } from '../../../hooks/useDictationSettings';
@@ -8,71 +9,75 @@ export const ElevenLabsKeyInput = () => {
const [elevenLabsApiKey, setElevenLabsApiKey] = useState(''); const [elevenLabsApiKey, setElevenLabsApiKey] = useState('');
const [isLoadingKey, setIsLoadingKey] = useState(false); const [isLoadingKey, setIsLoadingKey] = useState(false);
const [hasElevenLabsKey, setHasElevenLabsKey] = useState(false); const [hasElevenLabsKey, setHasElevenLabsKey] = useState(false);
const elevenLabsApiKeyRef = useRef(''); const [validationError, setValidationError] = useState('');
const { upsert, read } = useConfig(); const [isEditing, setIsEditing] = useState(false);
const { upsert, read, remove } = useConfig();
useEffect(() => { const loadKey = useCallback(async () => {
const loadKey = async () => { setIsLoadingKey(true);
setIsLoadingKey(true); try {
try { const response = await read(ELEVENLABS_API_KEY, true);
const response = await read(ELEVENLABS_API_KEY, true); const hasKey = isSecretKeyConfigured(response);
if (isSecretKeyConfigured(response)) { setHasElevenLabsKey(hasKey);
setHasElevenLabsKey(true); setElevenLabsKeyCache(hasKey);
setElevenLabsKeyCache(true); } catch (error) {
} else { console.error(error);
setElevenLabsKeyCache(false); setElevenLabsKeyCache(false);
} } finally {
} catch (error) { setIsLoadingKey(false);
console.error('Error checking ElevenLabs API key:', error); }
setElevenLabsKeyCache(false);
} finally {
setIsLoadingKey(false);
}
};
loadKey();
}, [read]); }, [read]);
// Save key on unmount to avoid losing unsaved changes
useEffect(() => { useEffect(() => {
return () => { loadKey();
if (elevenLabsApiKeyRef.current) { }, [loadKey]);
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]);
const handleElevenLabsKeyChange = (key: string) => { const handleElevenLabsKeyChange = (key: string) => {
setElevenLabsApiKey(key); setElevenLabsApiKey(key);
elevenLabsApiKeyRef.current = key; if (validationError) {
if (key.length > 0) { setValidationError('');
setHasElevenLabsKey(false);
} }
}; };
const saveElevenLabsKey = async () => { const handleSave = async () => {
try { try {
if (elevenLabsApiKey.trim()) { const trimmedKey = elevenLabsApiKey.trim();
await upsert(ELEVENLABS_API_KEY, elevenLabsApiKey, true);
setHasElevenLabsKey(true); if (!trimmedKey) {
setElevenLabsKeyCache(true); setValidationError('API key is required');
} else { return;
await upsert(ELEVENLABS_API_KEY, null, true);
setHasElevenLabsKey(false);
setElevenLabsKeyCache(false);
} }
await upsert(ELEVENLABS_API_KEY, trimmedKey, true);
setElevenLabsApiKey('');
setValidationError('');
setIsEditing(false);
await loadKey();
} catch (error) { } 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 ( return (
<div className="py-2 px-2 bg-background-subtle rounded-lg"> <div className="py-2 px-2 bg-background-subtle rounded-lg">
<div className="mb-2"> <div className="mb-2">
@@ -82,17 +87,42 @@ export const ElevenLabsKeyInput = () => {
{hasElevenLabsKey && <span className="text-green-600 ml-2">(Configured)</span>} {hasElevenLabsKey && <span className="text-green-600 ml-2">(Configured)</span>}
</p> </p>
</div> </div>
<Input
type="password" {!isEditing ? (
value={elevenLabsApiKey} <Button
onChange={(e) => handleElevenLabsKeyChange(e.target.value)} variant="outline"
onBlur={saveElevenLabsKey} size="sm"
placeholder={ onClick={() => setIsEditing(true)}
hasElevenLabsKey ? 'Enter new API key to update' : 'Enter your ElevenLabs API key' disabled={isLoadingKey}
} >
className="max-w-md" {hasElevenLabsKey ? 'Update API Key' : 'Add API Key'}
disabled={isLoadingKey} </Button>
/> ) : (
<div className="space-y-2">
<Input
type="password"
value={elevenLabsApiKey}
onChange={(e) => handleElevenLabsKeyChange(e.target.value)}
placeholder="Enter your ElevenLabs API key"
className="max-w-md"
autoFocus
/>
{validationError && <p className="text-xs text-red-600 mt-1">{validationError}</p>}
<div className="flex gap-2">
<Button size="sm" onClick={handleSave}>
Save
</Button>
<Button variant="outline" size="sm" onClick={handleCancel}>
Cancel
</Button>
{hasElevenLabsKey && (
<Button variant="destructive" size="sm" onClick={handleRemove}>
Remove
</Button>
)}
</div>
</div>
)}
</div> </div>
); );
}; };
@@ -1,7 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings'; 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 { useConfig } from '../../ConfigContext';
import { ElevenLabsKeyInput } from './ElevenLabsKeyInput'; import { ElevenLabsKeyInput } from './ElevenLabsKeyInput';
import { ProviderInfo } from './ProviderInfo'; import { ProviderInfo } from './ProviderInfo';
@@ -56,7 +59,7 @@ export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelecto
const getProviderLabel = (provider: DictationProvider): string => { const getProviderLabel = (provider: DictationProvider): string => {
switch (provider) { switch (provider) {
case 'openai': case DICTATION_PROVIDER_OPENAI:
return 'OpenAI Whisper'; return 'OpenAI Whisper';
case DICTATION_PROVIDER_ELEVENLABS: case DICTATION_PROVIDER_ELEVENLABS:
return 'ElevenLabs'; return 'ElevenLabs';
@@ -84,25 +87,31 @@ export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelecto
</button> </button>
{showProviderDropdown && ( {showProviderDropdown && (
<div className="absolute right-0 mt-1 w-48 bg-background-default border border-border-default rounded-md shadow-lg z-10"> <div className="absolute right-0 mt-1 w-max min-w-[250px] max-w-[350px] bg-background-default border border-border-default rounded-md shadow-lg z-50">
<button <button
onClick={() => handleProviderChange('openai')} onClick={() => handleProviderChange(DICTATION_PROVIDER_OPENAI)}
className={`w-full px-3 py-2 text-left text-sm transition-colors hover:bg-background-subtle text-text-default ${!VOICE_DICTATION_ELEVENLABS_ENABLED ? 'first:rounded-t-md last:rounded-b-md' : 'first:rounded-t-md'}`} className={`w-full px-3 py-2 text-left text-sm transition-colors hover:bg-background-subtle text-text-default whitespace-nowrap ${!VOICE_DICTATION_ELEVENLABS_ENABLED ? 'first:rounded-t-md last:rounded-b-md' : 'first:rounded-t-md'}`}
> >
OpenAI Whisper <span className="flex items-center justify-between gap-2">
{!hasOpenAIKey && <span className="text-xs ml-1">(not configured)</span>} <span>
{settings.provider === 'openai' && <span className="float-right"></span>} OpenAI Whisper
{!hasOpenAIKey && (
<span className="text-xs ml-1 text-text-muted">(not configured)</span>
)}
</span>
{settings.provider === DICTATION_PROVIDER_OPENAI && <span></span>}
</span>
</button> </button>
{VOICE_DICTATION_ELEVENLABS_ENABLED && ( {VOICE_DICTATION_ELEVENLABS_ENABLED && (
<button <button
onClick={() => handleProviderChange(DICTATION_PROVIDER_ELEVENLABS)} onClick={() => handleProviderChange(DICTATION_PROVIDER_ELEVENLABS)}
className="w-full px-3 py-2 text-left text-sm hover:bg-background-subtle transition-colors text-text-default last:rounded-b-md" className="w-full px-3 py-2 text-left text-sm hover:bg-background-subtle transition-colors text-text-default last:rounded-b-md whitespace-nowrap"
> >
ElevenLabs <span className="flex items-center justify-between gap-2">
{settings.provider === DICTATION_PROVIDER_ELEVENLABS && ( <span>ElevenLabs</span>
<span className="float-right"></span> {settings.provider === DICTATION_PROVIDER_ELEVENLABS && <span></span>}
)} </span>
</button> </button>
)} )}
</div> </div>
@@ -3,6 +3,7 @@ import { Switch } from '../../ui/switch';
import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings'; import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings';
import { import {
DICTATION_SETTINGS_KEY, DICTATION_SETTINGS_KEY,
DICTATION_PROVIDER_OPENAI,
DICTATION_PROVIDER_ELEVENLABS, DICTATION_PROVIDER_ELEVENLABS,
getDefaultDictationSettings, getDefaultDictationSettings,
} from '../../../hooks/dictationConstants'; } from '../../../hooks/dictationConstants';
@@ -35,7 +36,7 @@ export const VoiceDictationToggle = () => {
) { ) {
loadedSettings = { loadedSettings = {
...loadedSettings, ...loadedSettings,
provider: 'openai', provider: DICTATION_PROVIDER_OPENAI,
}; };
localStorage.setItem(DICTATION_SETTINGS_KEY, JSON.stringify(loadedSettings)); localStorage.setItem(DICTATION_SETTINGS_KEY, JSON.stringify(loadedSettings));
} }
@@ -59,7 +60,7 @@ export const VoiceDictationToggle = () => {
saveSettings({ saveSettings({
...settings, ...settings,
enabled, enabled,
provider: settings.provider === null ? 'openai' : settings.provider, provider: settings.provider === null ? DICTATION_PROVIDER_OPENAI : settings.provider,
}); });
trackSettingToggled('voice_dictation', enabled); trackSettingToggled('voice_dictation', enabled);
}; };
@@ -83,7 +84,7 @@ export const VoiceDictationToggle = () => {
</div> </div>
<div <div
className={`overflow-hidden transition-all duration-300 ease-in-out ${ className={`overflow-visible transition-all duration-300 ease-in-out ${
settings.enabled ? 'max-h-96 opacity-100 mt-2' : 'max-h-0 opacity-0 mt-0' settings.enabled ? 'max-h-96 opacity-100 mt-2' : 'max-h-0 opacity-0 mt-0'
}`} }`}
> >
+2 -3
View File
@@ -2,6 +2,7 @@ import { DictationSettings, DictationProvider } from './useDictationSettings';
export const DICTATION_SETTINGS_KEY = 'dictation_settings'; export const DICTATION_SETTINGS_KEY = 'dictation_settings';
export const ELEVENLABS_API_KEY = 'ELEVENLABS_API_KEY'; 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 DICTATION_PROVIDER_ELEVENLABS = 'elevenlabs' as const;
export const isSecretKeyConfigured = (response: unknown): boolean => export const isSecretKeyConfigured = (response: unknown): boolean =>
@@ -14,14 +15,12 @@ export const getDefaultDictationSettings = async (
getProviders: (refresh: boolean) => Promise<Array<{ name: string; is_configured: boolean }>> getProviders: (refresh: boolean) => Promise<Array<{ name: string; is_configured: boolean }>>
): Promise<DictationSettings> => { ): Promise<DictationSettings> => {
const providers = await getProviders(false); const providers = await getProviders(false);
// Check if we have an OpenAI API key as primary default
const openAIProvider = providers.find((p) => p.name === 'openai'); const openAIProvider = providers.find((p) => p.name === 'openai');
if (openAIProvider && openAIProvider.is_configured) { if (openAIProvider && openAIProvider.is_configured) {
return { return {
enabled: true, enabled: true,
provider: 'openai' as DictationProvider, provider: DICTATION_PROVIDER_OPENAI,
}; };
} else { } else {
return { return {
+5 -2
View File
@@ -3,12 +3,16 @@ import { useConfig } from '../components/ConfigContext';
import { import {
DICTATION_SETTINGS_KEY, DICTATION_SETTINGS_KEY,
ELEVENLABS_API_KEY, ELEVENLABS_API_KEY,
DICTATION_PROVIDER_OPENAI,
DICTATION_PROVIDER_ELEVENLABS, DICTATION_PROVIDER_ELEVENLABS,
getDefaultDictationSettings, getDefaultDictationSettings,
isSecretKeyConfigured, isSecretKeyConfigured,
} from './dictationConstants'; } 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 { export interface DictationSettings {
enabled: boolean; enabled: boolean;
@@ -28,7 +32,6 @@ export const useDictationSettings = () => {
useEffect(() => { useEffect(() => {
const loadSettings = async () => { const loadSettings = async () => {
// Load settings from localStorage
const saved = localStorage.getItem(DICTATION_SETTINGS_KEY); const saved = localStorage.getItem(DICTATION_SETTINGS_KEY);
let currentSettings: DictationSettings; let currentSettings: DictationSettings;
+3 -3
View File
@@ -2,7 +2,7 @@ import { useState, useRef, useCallback, useEffect } from 'react';
import { useConfig } from '../components/ConfigContext'; import { useConfig } from '../components/ConfigContext';
import { getApiUrl } from '../config'; import { getApiUrl } from '../config';
import { useDictationSettings } from './useDictationSettings'; import { useDictationSettings } from './useDictationSettings';
import { DICTATION_PROVIDER_ELEVENLABS } from './dictationConstants'; import { DICTATION_PROVIDER_OPENAI, DICTATION_PROVIDER_ELEVENLABS } from './dictationConstants';
import { safeJsonParse } from '../utils/conversionUtils'; import { safeJsonParse } from '../utils/conversionUtils';
interface UseWhisperOptions { interface UseWhisperOptions {
@@ -75,7 +75,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
// Check provider availability // Check provider availability
switch (dictationSettings.provider) { switch (dictationSettings.provider) {
case 'openai': case DICTATION_PROVIDER_OPENAI:
setCanUseDictation(hasOpenAIKey); setCanUseDictation(hasOpenAIKey);
break; break;
case DICTATION_PROVIDER_ELEVENLABS: case DICTATION_PROVIDER_ELEVENLABS:
@@ -178,7 +178,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
// Choose endpoint based on provider // Choose endpoint based on provider
switch (dictationSettings.provider) { switch (dictationSettings.provider) {
case 'openai': case DICTATION_PROVIDER_OPENAI:
endpoint = '/audio/transcribe'; endpoint = '/audio/transcribe';
break; break;
case DICTATION_PROVIDER_ELEVENLABS: case DICTATION_PROVIDER_ELEVENLABS: