Fix: ElevenLabs API Key Not Persisting (#6557)
Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
This commit is contained in:
@@ -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,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user