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 { Button } from '../../ui/button';
import { useConfig } from '../../ConfigContext';
import { ELEVENLABS_API_KEY, isSecretKeyConfigured } from '../../../hooks/dictationConstants';
import { setElevenLabsKeyCache } from '../../../hooks/useDictationSettings';
@@ -8,71 +9,75 @@ export const ElevenLabsKeyInput = () => {
const [elevenLabsApiKey, setElevenLabsApiKey] = useState('');
const [isLoadingKey, setIsLoadingKey] = useState(false);
const [hasElevenLabsKey, setHasElevenLabsKey] = useState(false);
const elevenLabsApiKeyRef = useRef('');
const { upsert, read } = useConfig();
const [validationError, setValidationError] = useState('');
const [isEditing, setIsEditing] = useState(false);
const { upsert, read, remove } = useConfig();
useEffect(() => {
const loadKey = async () => {
setIsLoadingKey(true);
try {
const response = await read(ELEVENLABS_API_KEY, true);
if (isSecretKeyConfigured(response)) {
setHasElevenLabsKey(true);
setElevenLabsKeyCache(true);
} else {
setElevenLabsKeyCache(false);
}
} catch (error) {
console.error('Error checking ElevenLabs API key:', error);
setElevenLabsKeyCache(false);
} finally {
setIsLoadingKey(false);
}
};
loadKey();
const loadKey = useCallback(async () => {
setIsLoadingKey(true);
try {
const response = await read(ELEVENLABS_API_KEY, true);
const hasKey = isSecretKeyConfigured(response);
setHasElevenLabsKey(hasKey);
setElevenLabsKeyCache(hasKey);
} catch (error) {
console.error(error);
setElevenLabsKeyCache(false);
} finally {
setIsLoadingKey(false);
}
}, [read]);
// Save key on unmount to avoid losing unsaved changes
useEffect(() => {
return () => {
if (elevenLabsApiKeyRef.current) {
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]);
loadKey();
}, [loadKey]);
const handleElevenLabsKeyChange = (key: string) => {
setElevenLabsApiKey(key);
elevenLabsApiKeyRef.current = key;
if (key.length > 0) {
setHasElevenLabsKey(false);
if (validationError) {
setValidationError('');
}
};
const saveElevenLabsKey = async () => {
const handleSave = async () => {
try {
if (elevenLabsApiKey.trim()) {
await upsert(ELEVENLABS_API_KEY, elevenLabsApiKey, true);
setHasElevenLabsKey(true);
setElevenLabsKeyCache(true);
} else {
await upsert(ELEVENLABS_API_KEY, null, true);
setHasElevenLabsKey(false);
setElevenLabsKeyCache(false);
const trimmedKey = elevenLabsApiKey.trim();
if (!trimmedKey) {
setValidationError('API key is required');
return;
}
await upsert(ELEVENLABS_API_KEY, trimmedKey, true);
setElevenLabsApiKey('');
setValidationError('');
setIsEditing(false);
await loadKey();
} 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 (
<div className="py-2 px-2 bg-background-subtle rounded-lg">
<div className="mb-2">
@@ -82,17 +87,42 @@ export const ElevenLabsKeyInput = () => {
{hasElevenLabsKey && <span className="text-green-600 ml-2">(Configured)</span>}
</p>
</div>
<Input
type="password"
value={elevenLabsApiKey}
onChange={(e) => handleElevenLabsKeyChange(e.target.value)}
onBlur={saveElevenLabsKey}
placeholder={
hasElevenLabsKey ? 'Enter new API key to update' : 'Enter your ElevenLabs API key'
}
className="max-w-md"
disabled={isLoadingKey}
/>
{!isEditing ? (
<Button
variant="outline"
size="sm"
onClick={() => setIsEditing(true)}
disabled={isLoadingKey}
>
{hasElevenLabsKey ? 'Update API Key' : 'Add API Key'}
</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>
);
};
@@ -1,7 +1,10 @@
import { useState, useEffect } from 'react';
import { ChevronDown } from 'lucide-react';
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 { ElevenLabsKeyInput } from './ElevenLabsKeyInput';
import { ProviderInfo } from './ProviderInfo';
@@ -56,7 +59,7 @@ export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelecto
const getProviderLabel = (provider: DictationProvider): string => {
switch (provider) {
case 'openai':
case DICTATION_PROVIDER_OPENAI:
return 'OpenAI Whisper';
case DICTATION_PROVIDER_ELEVENLABS:
return 'ElevenLabs';
@@ -84,25 +87,31 @@ export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelecto
</button>
{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
onClick={() => handleProviderChange('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'}`}
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 whitespace-nowrap ${!VOICE_DICTATION_ELEVENLABS_ENABLED ? 'first:rounded-t-md last:rounded-b-md' : 'first:rounded-t-md'}`}
>
OpenAI Whisper
{!hasOpenAIKey && <span className="text-xs ml-1">(not configured)</span>}
{settings.provider === 'openai' && <span className="float-right"></span>}
<span className="flex items-center justify-between gap-2">
<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>
{VOICE_DICTATION_ELEVENLABS_ENABLED && (
<button
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
{settings.provider === DICTATION_PROVIDER_ELEVENLABS && (
<span className="float-right"></span>
)}
<span className="flex items-center justify-between gap-2">
<span>ElevenLabs</span>
{settings.provider === DICTATION_PROVIDER_ELEVENLABS && <span></span>}
</span>
</button>
)}
</div>
@@ -3,6 +3,7 @@ import { Switch } from '../../ui/switch';
import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings';
import {
DICTATION_SETTINGS_KEY,
DICTATION_PROVIDER_OPENAI,
DICTATION_PROVIDER_ELEVENLABS,
getDefaultDictationSettings,
} from '../../../hooks/dictationConstants';
@@ -35,7 +36,7 @@ export const VoiceDictationToggle = () => {
) {
loadedSettings = {
...loadedSettings,
provider: 'openai',
provider: DICTATION_PROVIDER_OPENAI,
};
localStorage.setItem(DICTATION_SETTINGS_KEY, JSON.stringify(loadedSettings));
}
@@ -59,7 +60,7 @@ export const VoiceDictationToggle = () => {
saveSettings({
...settings,
enabled,
provider: settings.provider === null ? 'openai' : settings.provider,
provider: settings.provider === null ? DICTATION_PROVIDER_OPENAI : settings.provider,
});
trackSettingToggled('voice_dictation', enabled);
};
@@ -83,7 +84,7 @@ export const VoiceDictationToggle = () => {
</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'
}`}
>
+2 -3
View File
@@ -2,6 +2,7 @@ import { DictationSettings, DictationProvider } from './useDictationSettings';
export const DICTATION_SETTINGS_KEY = 'dictation_settings';
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 isSecretKeyConfigured = (response: unknown): boolean =>
@@ -14,14 +15,12 @@ export const getDefaultDictationSettings = async (
getProviders: (refresh: boolean) => Promise<Array<{ name: string; is_configured: boolean }>>
): Promise<DictationSettings> => {
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) {
return {
enabled: true,
provider: 'openai' as DictationProvider,
provider: DICTATION_PROVIDER_OPENAI,
};
} else {
return {
+5 -2
View File
@@ -3,12 +3,16 @@ import { useConfig } from '../components/ConfigContext';
import {
DICTATION_SETTINGS_KEY,
ELEVENLABS_API_KEY,
DICTATION_PROVIDER_OPENAI,
DICTATION_PROVIDER_ELEVENLABS,
getDefaultDictationSettings,
isSecretKeyConfigured,
} 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 {
enabled: boolean;
@@ -28,7 +32,6 @@ export const useDictationSettings = () => {
useEffect(() => {
const loadSettings = async () => {
// Load settings from localStorage
const saved = localStorage.getItem(DICTATION_SETTINGS_KEY);
let currentSettings: DictationSettings;
+3 -3
View File
@@ -2,7 +2,7 @@ import { useState, useRef, useCallback, useEffect } from 'react';
import { useConfig } from '../components/ConfigContext';
import { getApiUrl } from '../config';
import { useDictationSettings } from './useDictationSettings';
import { DICTATION_PROVIDER_ELEVENLABS } from './dictationConstants';
import { DICTATION_PROVIDER_OPENAI, DICTATION_PROVIDER_ELEVENLABS } from './dictationConstants';
import { safeJsonParse } from '../utils/conversionUtils';
interface UseWhisperOptions {
@@ -75,7 +75,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
// Check provider availability
switch (dictationSettings.provider) {
case 'openai':
case DICTATION_PROVIDER_OPENAI:
setCanUseDictation(hasOpenAIKey);
break;
case DICTATION_PROVIDER_ELEVENLABS:
@@ -178,7 +178,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
// Choose endpoint based on provider
switch (dictationSettings.provider) {
case 'openai':
case DICTATION_PROVIDER_OPENAI:
endpoint = '/audio/transcribe';
break;
case DICTATION_PROVIDER_ELEVENLABS: