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 { 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,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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user