Optimise load config in UI (#6662)

This commit is contained in:
Lifei Zhou
2026-01-24 03:45:29 +11:00
committed by GitHub
parent 346ff7ab84
commit 6375aace13
10 changed files with 113 additions and 66 deletions
+23 -15
View File
@@ -17,10 +17,10 @@ import { AlertType, useAlerts } from './alerts';
import { useConfig } from './ConfigContext'; import { useConfig } from './ConfigContext';
import { useModelAndProvider } from './ModelAndProviderContext'; import { useModelAndProvider } from './ModelAndProviderContext';
import { useWhisper } from '../hooks/useWhisper'; import { useWhisper } from '../hooks/useWhisper';
import { DICTATION_PROVIDER_ELEVENLABS } from '../hooks/dictationConstants';
import { WaveformVisualizer } from './WaveformVisualizer'; import { WaveformVisualizer } from './WaveformVisualizer';
import { toastError } from '../toasts'; import { toastError } from '../toasts';
import MentionPopover, { DisplayItemWithMatch } from './MentionPopover'; import MentionPopover, { DisplayItemWithMatch } from './MentionPopover';
import { useDictationSettings } from '../hooks/useDictationSettings';
import { COST_TRACKING_ENABLED, VOICE_DICTATION_ELEVENLABS_ENABLED } from '../updates'; import { COST_TRACKING_ENABLED, VOICE_DICTATION_ELEVENLABS_ENABLED } from '../updates';
import { CostTracker } from './bottom_menu/CostTracker'; import { CostTracker } from './bottom_menu/CostTracker';
import { DroppedFile, useFileDrop } from '../hooks/useFileDrop'; import { DroppedFile, useFileDrop } from '../hooks/useFileDrop';
@@ -265,6 +265,7 @@ export default function ChatInput({
stopRecording, stopRecording,
recordingDuration, recordingDuration,
estimatedSize, estimatedSize,
dictationSettings,
} = useWhisper({ } = useWhisper({
onTranscription: (text) => { onTranscription: (text) => {
trackVoiceDictation('transcribed'); trackVoiceDictation('transcribed');
@@ -289,8 +290,6 @@ export default function ChatInput({
}); });
}, },
}); });
const { settings: dictationSettings } = useDictationSettings();
const internalTextAreaRef = useRef<HTMLTextAreaElement>(null); const internalTextAreaRef = useRef<HTMLTextAreaElement>(null);
const textAreaRef = inputRef || internalTextAreaRef; const textAreaRef = inputRef || internalTextAreaRef;
const timeoutRefsRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set()); const timeoutRefsRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
@@ -618,17 +617,20 @@ export default function ChatInput({
return [...pastedImageData, ...droppedImageData]; return [...pastedImageData, ...droppedImageData];
}, [pastedImages, allDroppedFiles]); }, [pastedImages, allDroppedFiles]);
const appendDroppedFilePaths = useCallback((text: string): string => { const appendDroppedFilePaths = useCallback(
const droppedFilePaths = allDroppedFiles (text: string): string => {
.filter((file) => !file.isImage && !file.error && !file.isLoading) const droppedFilePaths = allDroppedFiles
.map((file) => file.path); .filter((file) => !file.isImage && !file.error && !file.isLoading)
.map((file) => file.path);
if (droppedFilePaths.length > 0) { if (droppedFilePaths.length > 0) {
const pathsString = droppedFilePaths.join(' '); const pathsString = droppedFilePaths.join(' ');
return text ? `${text} ${pathsString}` : pathsString; return text ? `${text} ${pathsString}` : pathsString;
} }
return text; return text;
}, [allDroppedFiles]); },
[allDroppedFiles]
);
const clearInputState = useCallback(() => { const clearInputState = useCallback(() => {
setDisplayValue(''); setDisplayValue('');
@@ -1189,7 +1191,13 @@ export default function ChatInput({
onDrop={handleLocalDrop} onDrop={handleLocalDrop}
onDragOver={handleLocalDragOver} onDragOver={handleLocalDragOver}
> >
<input ref={fileInputRef} type="file" onChange={handleFileInputChange} style={{ display: 'none' }} accept="*/*" /> <input
ref={fileInputRef}
type="file"
onChange={handleFileInputChange}
style={{ display: 'none' }}
accept="*/*"
/>
{/* Message Queue Display */} {/* Message Queue Display */}
{queuedMessages.length > 0 && ( {queuedMessages.length > 0 && (
<MessageQueue <MessageQueue
@@ -1270,7 +1278,7 @@ export default function ChatInput({
<b>Models.</b> <b>Models.</b>
</p> </p>
) : VOICE_DICTATION_ELEVENLABS_ENABLED && ) : VOICE_DICTATION_ELEVENLABS_ENABLED &&
dictationSettings.provider === 'elevenlabs' ? ( dictationSettings.provider === DICTATION_PROVIDER_ELEVENLABS ? (
<p> <p>
ElevenLabs API key is not configured. Set it up in <b>Settings</b> {'>'}{' '} ElevenLabs API key is not configured. Set it up in <b>Settings</b> {'>'}{' '}
<b>Chat</b> {'>'} <b>Voice Dictation.</b> <b>Chat</b> {'>'} <b>Voice Dictation.</b>
+17 -16
View File
@@ -66,6 +66,10 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
const [extensionsList, setExtensionsList] = useState<FixedExtensionEntry[]>([]); const [extensionsList, setExtensionsList] = useState<FixedExtensionEntry[]>([]);
const [extensionWarnings, setExtensionWarnings] = useState<string[]>([]); const [extensionWarnings, setExtensionWarnings] = useState<string[]>([]);
// Ref to access providersList in getProviders without recreating the callback
const providersListRef = React.useRef<ProviderDetails[]>(providersList);
providersListRef.current = providersList;
const reloadConfig = useCallback(async () => { const reloadConfig = useCallback(async () => {
const response = await readAllConfig(); const response = await readAllConfig();
setConfig(response.data?.config || {}); setConfig(response.data?.config || {});
@@ -168,23 +172,20 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
[addExtension, getExtensions] [addExtension, getExtensions]
); );
const getProviders = useCallback( const getProviders = useCallback(async (forceRefresh = false): Promise<ProviderDetails[]> => {
async (forceRefresh = false): Promise<ProviderDetails[]> => { if (forceRefresh || providersListRef.current.length === 0) {
if (forceRefresh || providersList.length === 0) { try {
try { const response = await providers();
const response = await providers(); const providersData = response.data || [];
const providersData = response.data || []; setProvidersList(providersData);
setProvidersList(providersData); return providersData;
return providersData; } catch (error) {
} catch (error) { console.error('Failed to fetch providers:', error);
console.error('Failed to fetch providers:', error); return [];
return [];
}
} }
return providersList; }
}, return providersListRef.current;
[providersList] }, []);
);
const getProviderModels = useCallback(async (providerName: string): Promise<string[]> => { const getProviderModels = useCallback(async (providerName: string): Promise<string[]> => {
try { try {
@@ -28,6 +28,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
const [pendingSort, setPendingSort] = useState(false); const [pendingSort, setPendingSort] = useState(false);
const [togglingExtension, setTogglingExtension] = useState<string | null>(null); const [togglingExtension, setTogglingExtension] = useState<string | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0); const [refreshTrigger, setRefreshTrigger] = useState(0);
const [isSessionExtensionsLoaded, setIsSessionExtensionsLoaded] = useState(false);
const sortTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); const sortTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const { extensionsList: allExtensions } = useConfig(); const { extensionsList: allExtensions } = useConfig();
const isHubView = !sessionId; const isHubView = !sessionId;
@@ -70,12 +71,15 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
if (response.data?.extensions) { if (response.data?.extensions) {
setSessionExtensions(response.data.extensions); setSessionExtensions(response.data.extensions);
setIsSessionExtensionsLoaded(true);
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch session extensions:', error); console.error('Failed to fetch session extensions:', error);
setIsSessionExtensionsLoaded(true);
} }
}; };
setIsSessionExtensionsLoaded(false);
fetchExtensions(); fetchExtensions();
}, [sessionId, isOpen, refreshTrigger]); }, [sessionId, isOpen, refreshTrigger]);
@@ -225,7 +229,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
> >
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <button
className="flex items-center cursor-pointer [&_svg]:size-4 text-text-default/70 hover:text-text-default hover:scale-100 hover:bg-transparent text-xs" className={`flex items-center [&_svg]:size-4 text-text-default/70 hover:text-text-default hover:scale-100 hover:bg-transparent text-xs cursor-pointer ${allExtensions.length === 0 || (!isHubView && !isSessionExtensionsLoaded) ? 'invisible' : ''}`}
title="manage extensions" title="manage extensions"
> >
<Puzzle className="mr-1 h-4 w-4" /> <Puzzle className="mr-1 h-4 w-4" />
@@ -1,7 +1,8 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Input } from '../../ui/input'; import { Input } from '../../ui/input';
import { useConfig } from '../../ConfigContext'; import { useConfig } from '../../ConfigContext';
import { ELEVENLABS_API_KEY } from '../../../hooks/dictationConstants'; import { ELEVENLABS_API_KEY, isSecretKeyConfigured } from '../../../hooks/dictationConstants';
import { setElevenLabsKeyCache } from '../../../hooks/useDictationSettings';
export const ElevenLabsKeyInput = () => { export const ElevenLabsKeyInput = () => {
const [elevenLabsApiKey, setElevenLabsApiKey] = useState(''); const [elevenLabsApiKey, setElevenLabsApiKey] = useState('');
@@ -14,12 +15,16 @@ export const ElevenLabsKeyInput = () => {
const loadKey = async () => { const loadKey = async () => {
setIsLoadingKey(true); setIsLoadingKey(true);
try { try {
const keyExists = await read(ELEVENLABS_API_KEY, true); const response = await read(ELEVENLABS_API_KEY, true);
if (keyExists === true) { if (isSecretKeyConfigured(response)) {
setHasElevenLabsKey(true); setHasElevenLabsKey(true);
setElevenLabsKeyCache(true);
} else {
setElevenLabsKeyCache(false);
} }
} catch (error) { } catch (error) {
console.error('Error checking ElevenLabs API key:', error); console.error('Error checking ElevenLabs API key:', error);
setElevenLabsKeyCache(false);
} finally { } finally {
setIsLoadingKey(false); setIsLoadingKey(false);
} }
@@ -34,9 +39,11 @@ export const ElevenLabsKeyInput = () => {
if (elevenLabsApiKeyRef.current) { if (elevenLabsApiKeyRef.current) {
const keyToSave = elevenLabsApiKeyRef.current; const keyToSave = elevenLabsApiKeyRef.current;
if (keyToSave.trim()) { if (keyToSave.trim()) {
upsert(ELEVENLABS_API_KEY, keyToSave, true).catch((error) => { upsert(ELEVENLABS_API_KEY, keyToSave, true)
console.error('Error saving ElevenLabs API key on unmount:', error); .then(() => setElevenLabsKeyCache(true))
}); .catch((error) => {
console.error('Error saving ElevenLabs API key on unmount:', error);
});
} }
} }
}; };
@@ -53,15 +60,13 @@ export const ElevenLabsKeyInput = () => {
const saveElevenLabsKey = async () => { const saveElevenLabsKey = async () => {
try { try {
if (elevenLabsApiKey.trim()) { if (elevenLabsApiKey.trim()) {
console.log('Saving ElevenLabs API key to secure storage...');
await upsert(ELEVENLABS_API_KEY, elevenLabsApiKey, true); await upsert(ELEVENLABS_API_KEY, elevenLabsApiKey, true);
setHasElevenLabsKey(true); setHasElevenLabsKey(true);
console.log('ElevenLabs API key saved successfully'); setElevenLabsKeyCache(true);
} else { } else {
console.log('Removing ElevenLabs API key from secure storage...');
await upsert(ELEVENLABS_API_KEY, null, true); await upsert(ELEVENLABS_API_KEY, null, true);
setHasElevenLabsKey(false); setHasElevenLabsKey(false);
console.log('ElevenLabs API key removed successfully'); setElevenLabsKeyCache(false);
} }
} catch (error) { } catch (error) {
console.error('Error saving ElevenLabs API key:', error); console.error('Error saving ElevenLabs API key:', error);
@@ -1,4 +1,5 @@
import { DictationProvider } from '../../../hooks/useDictationSettings'; import { DictationProvider } from '../../../hooks/useDictationSettings';
import { DICTATION_PROVIDER_ELEVENLABS } from '../../../hooks/dictationConstants';
import { VOICE_DICTATION_ELEVENLABS_ENABLED } from '../../../updates'; import { VOICE_DICTATION_ELEVENLABS_ENABLED } from '../../../updates';
interface ProviderInfoProps { interface ProviderInfoProps {
@@ -16,7 +17,7 @@ export const ProviderInfo = ({ provider }: ProviderInfoProps) => {
configured in the Models section. configured in the Models section.
</p> </p>
)} )}
{VOICE_DICTATION_ELEVENLABS_ENABLED && provider === 'elevenlabs' && ( {VOICE_DICTATION_ELEVENLABS_ENABLED && provider === DICTATION_PROVIDER_ELEVENLABS && (
<div> <div>
<p className="text-xs text-text-muted"> <p className="text-xs text-text-muted">
Uses ElevenLabs speech-to-text API for high-quality transcription. Uses ElevenLabs speech-to-text API for high-quality transcription.
@@ -1,6 +1,7 @@
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 { useConfig } from '../../ConfigContext'; import { useConfig } from '../../ConfigContext';
import { ElevenLabsKeyInput } from './ElevenLabsKeyInput'; import { ElevenLabsKeyInput } from './ElevenLabsKeyInput';
import { ProviderInfo } from './ProviderInfo'; import { ProviderInfo } from './ProviderInfo';
@@ -57,7 +58,7 @@ export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelecto
switch (provider) { switch (provider) {
case 'openai': case 'openai':
return 'OpenAI Whisper'; return 'OpenAI Whisper';
case 'elevenlabs': case DICTATION_PROVIDER_ELEVENLABS:
return 'ElevenLabs'; return 'ElevenLabs';
default: default:
return 'None (disabled)'; return 'None (disabled)';
@@ -95,11 +96,13 @@ export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelecto
{VOICE_DICTATION_ELEVENLABS_ENABLED && ( {VOICE_DICTATION_ELEVENLABS_ENABLED && (
<button <button
onClick={() => handleProviderChange('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"
> >
ElevenLabs ElevenLabs
{settings.provider === 'elevenlabs' && <span className="float-right"></span>} {settings.provider === DICTATION_PROVIDER_ELEVENLABS && (
<span className="float-right"></span>
)}
</button> </button>
)} )}
</div> </div>
@@ -107,9 +110,8 @@ export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelecto
</div> </div>
</div> </div>
{VOICE_DICTATION_ELEVENLABS_ENABLED && settings.provider === 'elevenlabs' && ( {VOICE_DICTATION_ELEVENLABS_ENABLED &&
<ElevenLabsKeyInput /> settings.provider === DICTATION_PROVIDER_ELEVENLABS && <ElevenLabsKeyInput />}
)}
<ProviderInfo provider={settings.provider} /> <ProviderInfo provider={settings.provider} />
</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_ELEVENLABS,
getDefaultDictationSettings, getDefaultDictationSettings,
} from '../../../hooks/dictationConstants'; } from '../../../hooks/dictationConstants';
import { useConfig } from '../../ConfigContext'; import { useConfig } from '../../ConfigContext';
@@ -28,7 +29,10 @@ export const VoiceDictationToggle = () => {
loadedSettings = parsed; loadedSettings = parsed;
// If ElevenLabs is disabled and user has it selected, reset to OpenAI // If ElevenLabs is disabled and user has it selected, reset to OpenAI
if (!VOICE_DICTATION_ELEVENLABS_ENABLED && loadedSettings.provider === 'elevenlabs') { if (
!VOICE_DICTATION_ELEVENLABS_ENABLED &&
loadedSettings.provider === DICTATION_PROVIDER_ELEVENLABS
) {
loadedSettings = { loadedSettings = {
...loadedSettings, ...loadedSettings,
provider: 'openai', provider: 'openai',
@@ -2,6 +2,13 @@ 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_ELEVENLABS = 'elevenlabs' as const;
export const isSecretKeyConfigured = (response: unknown): boolean =>
typeof response === 'object' &&
response !== null &&
'maskedValue' in response &&
!!(response as { maskedValue: string }).maskedValue;
export const getDefaultDictationSettings = async ( export const getDefaultDictationSettings = async (
getProviders: (refresh: boolean) => Promise<Array<{ name: string; is_configured: boolean }>> getProviders: (refresh: boolean) => Promise<Array<{ name: string; is_configured: boolean }>>
+27 -14
View File
@@ -3,19 +3,27 @@ import { useConfig } from '../components/ConfigContext';
import { import {
DICTATION_SETTINGS_KEY, DICTATION_SETTINGS_KEY,
ELEVENLABS_API_KEY, ELEVENLABS_API_KEY,
DICTATION_PROVIDER_ELEVENLABS,
getDefaultDictationSettings, getDefaultDictationSettings,
isSecretKeyConfigured,
} from './dictationConstants'; } from './dictationConstants';
export type DictationProvider = 'openai' | 'elevenlabs' | null; export type DictationProvider = 'openai' | typeof DICTATION_PROVIDER_ELEVENLABS | null;
export interface DictationSettings { export interface DictationSettings {
enabled: boolean; enabled: boolean;
provider: DictationProvider; provider: DictationProvider;
} }
let elevenLabsKeyCache: boolean | null = null;
export const setElevenLabsKeyCache = (value: boolean) => {
elevenLabsKeyCache = value;
};
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>(elevenLabsKeyCache ?? false);
const { read, getProviders } = useConfig(); const { read, getProviders } = useConfig();
useEffect(() => { useEffect(() => {
@@ -23,22 +31,27 @@ export const useDictationSettings = () => {
// Load settings from localStorage // Load settings from localStorage
const saved = localStorage.getItem(DICTATION_SETTINGS_KEY); const saved = localStorage.getItem(DICTATION_SETTINGS_KEY);
let currentSettings: DictationSettings;
if (saved) { if (saved) {
const parsedSettings = JSON.parse(saved); currentSettings = JSON.parse(saved);
setSettings(parsedSettings);
} else { } else {
const defaultSettings = await getDefaultDictationSettings(getProviders); currentSettings = await getDefaultDictationSettings(getProviders);
setSettings(defaultSettings);
} }
setSettings(currentSettings);
// Load ElevenLabs API key from storage (non-secret for frontend access) if (
try { currentSettings.provider === DICTATION_PROVIDER_ELEVENLABS &&
const keyExists = await read(ELEVENLABS_API_KEY, true); elevenLabsKeyCache === null
if (keyExists === true) { ) {
setHasElevenLabsKey(true); try {
const response = await read(ELEVENLABS_API_KEY, true);
const hasKey = isSecretKeyConfigured(response);
elevenLabsKeyCache = hasKey;
setHasElevenLabsKey(hasKey);
} catch (error) {
elevenLabsKeyCache = false;
setHasElevenLabsKey(false);
console.error('[useDictationSettings] Error checking ElevenLabs API key:', error);
} }
} catch (error) {
console.error('[useDictationSettings] Error loading ElevenLabs API key:', error);
} }
}; };
+4 -2
View File
@@ -2,6 +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 { safeJsonParse } from '../utils/conversionUtils'; import { safeJsonParse } from '../utils/conversionUtils';
interface UseWhisperOptions { interface UseWhisperOptions {
@@ -77,7 +78,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
case 'openai': case 'openai':
setCanUseDictation(hasOpenAIKey); setCanUseDictation(hasOpenAIKey);
break; break;
case 'elevenlabs': case DICTATION_PROVIDER_ELEVENLABS:
setCanUseDictation(hasElevenLabsKey); setCanUseDictation(hasElevenLabsKey);
break; break;
default: default:
@@ -180,7 +181,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
case 'openai': case 'openai':
endpoint = '/audio/transcribe'; endpoint = '/audio/transcribe';
break; break;
case 'elevenlabs': case DICTATION_PROVIDER_ELEVENLABS:
endpoint = '/audio/transcribe/elevenlabs'; endpoint = '/audio/transcribe/elevenlabs';
break; break;
default: default:
@@ -373,5 +374,6 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp
stopRecording, stopRecording,
recordingDuration, recordingDuration,
estimatedSize, estimatedSize,
dictationSettings,
}; };
}; };