import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { ConfigManager } from '../../../core/config/ConfigManager'; import { LocalLLMModelManager, type LocalLLMModelState } from '../../../util/localLLMModelManager'; import { LocalSeparatorModelCache } from '../../../util/local-separator/modelCache'; import { formatLocalLLMContextLength, LOCAL_LLM_CONTEXT_LENGTH_OPTIONS, LOCAL_LLM_DEFAULT_MODEL_URL, LOCAL_LLM_DEFAULT_CONTEXT_LENGTH, LOCAL_LLM_DISPLAY_NAME, LOCAL_LLM_PROVIDER_KEY, normalizeLocalLLMContextLength, type LocalLLMContextLength, } from '../../../util/localLLMConfig'; import { LOCAL_SEPARATOR_DEFAULT_MODEL_URL } from '../../../util/local-separator/config'; const GeneralSettings: React.FC = () => { const [llmProvider, setLlmProvider] = useState(LOCAL_LLM_PROVIDER_KEY); const [openaiKey, setOpenaiKey] = useState(''); const [openaiModel, setOpenaiModel] = useState(''); const [geminiKey, setGeminiKey] = useState(''); const [geminiModel, setGeminiModel] = useState(''); const [claudeKey, setClaudeKey] = useState(''); const [claudeModel, setClaudeModel] = useState(''); const [claudeOpenRouterKey, setClaudeOpenRouterKey] = useState(''); const [claudeOpenRouterBaseUrl, setClaudeOpenRouterBaseUrl] = useState(''); const [claudeOpenRouterModel, setClaudeOpenRouterModel] = useState(''); const [openaiFlex, setOpenaiFlex] = useState(false); const [persistApiKeysNonLocalhost, setPersistApiKeysNonLocalhost] = useState(false); const [compatibleKey, setCompatibleKey] = useState(''); const [compatibleBaseUrl, setCompatibleBaseUrl] = useState(''); const [compatibleModel, setCompatibleModel] = useState(''); const [soundfontBaseUrl, setSoundfontBaseUrl] = useState(''); const [kgoneEnabled, setKgoneEnabled] = useState(false); const [kgoneBaseUrl, setKgoneBaseUrl] = useState(''); const [kgoneServerManaged, setKgoneServerManaged] = useState(false); const [soundfontServerManaged, setSoundfontServerManaged] = useState(false); const [localContextLength, setLocalContextLength] = useState(LOCAL_LLM_DEFAULT_CONTEXT_LENGTH); const [localModelState, setLocalModelState] = useState(LocalLLMModelManager.getState()); const [localModelUrl, setLocalModelUrl] = useState(''); const [uvr5ModelUrl, setUvr5ModelUrl] = useState(''); const [isUvr5ModelCached, setIsUvr5ModelCached] = useState(false); const [isCheckingUvr5ModelCache, setIsCheckingUvr5ModelCache] = useState(false); const [isDeletingUvr5Model, setIsDeletingUvr5Model] = useState(false); const configManager = ConfigManager.instance(); const isLocalEnvironment = useMemo(() => { try { if (typeof window === 'undefined' || typeof window.location === 'undefined') { return false; } const { protocol, hostname } = window.location; if (protocol === 'file:') return true; const localHosts = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0']); return localHosts.has(hostname); } catch { return false; } }, []); const refreshUvr5ModelCacheState = useCallback(async () => { setIsCheckingUvr5ModelCache(true); try { setIsUvr5ModelCached(await LocalSeparatorModelCache.exists()); } catch (error) { console.error('Failed to check UVR5 cached model state:', error); setIsUvr5ModelCached(false); } finally { setIsCheckingUvr5ModelCache(false); } }, []); // Load configuration values on component mount useEffect(() => { const loadConfig = async () => { if (!configManager.getIsInitialized()) { await configManager.initialize(); } setLlmProvider((configManager.get('general.llm_provider') as string) || LOCAL_LLM_PROVIDER_KEY); setOpenaiKey((configManager.get('general.openai.api_key') as string) || ''); setOpenaiModel((configManager.get('general.openai.model') as string) || ''); setOpenaiFlex((configManager.get('general.openai.flex') as boolean) ?? false); setPersistApiKeysNonLocalhost((configManager.get('general.persist_api_keys_non_localhost') as boolean) ?? false); setGeminiKey((configManager.get('general.gemini.api_key') as string) || ''); setGeminiModel((configManager.get('general.gemini.model') as string) || ''); setClaudeKey((configManager.get('general.claude.api_key') as string) || ''); setClaudeModel((configManager.get('general.claude.model') as string) || ''); setClaudeOpenRouterKey((configManager.get('general.claude_openrouter.api_key') as string) || ''); setClaudeOpenRouterBaseUrl((configManager.get('general.claude_openrouter.base_url') as string) || ''); setClaudeOpenRouterModel((configManager.get('general.claude_openrouter.model') as string) || ''); setCompatibleKey((configManager.get('general.openai_compatible.api_key') as string) || ''); setCompatibleBaseUrl((configManager.get('general.openai_compatible.base_url') as string) || ''); setCompatibleModel((configManager.get('general.openai_compatible.model') as string) || ''); setLocalContextLength(normalizeLocalLLMContextLength(configManager.get('general.local_browser.context_length'))); setLocalModelUrl((configManager.get('general.local_browser.model_url') as string) || LOCAL_LLM_DEFAULT_MODEL_URL); setUvr5ModelUrl((configManager.get('general.uvr5_web_runtime.mdx_net_model_url') as string) || LOCAL_SEPARATOR_DEFAULT_MODEL_URL); setSoundfontBaseUrl((configManager.get('general.soundfont.base_url') as string) || ''); setKgoneEnabled((configManager.get('general.kgone.enabled') as boolean) ?? false); setKgoneBaseUrl((configManager.get('general.kgone.base_url') as string) || ''); setKgoneServerManaged(configManager.isKGOneServerManaged()); setSoundfontServerManaged(configManager.isSoundfontServerManaged()); }; loadConfig(); const unsubscribe = LocalLLMModelManager.subscribe(setLocalModelState); void refreshUvr5ModelCacheState(); return unsubscribe; }, [configManager, refreshUvr5ModelCacheState]); // Debounced save function for text inputs const debouncedSave = useCallback((key: string, value: string) => { const timeoutId = setTimeout(async () => { try { await configManager.set(key, value); console.log(`Settings saved: ${key} = ${value}`); } catch (error) { console.error(`Failed to save setting ${key}:`, error); } }, 500); // 500ms debounce return () => clearTimeout(timeoutId); }, [configManager]); // Save configuration when values change const handleLlmProviderChange = async (value: string) => { setLlmProvider(value); try { await configManager.set('general.llm_provider', value); console.log('LLM provider changed to:', value); } catch (error) { console.error('Failed to save LLM provider:', error); } }; const handleOpenaiKeyChange = (value: string) => { setOpenaiKey(value); debouncedSave('general.openai.api_key', value); }; const handleOpenaiModelChange = (value: string) => { setOpenaiModel(value); debouncedSave('general.openai.model', value); }; const handleOpenaiFlexChange = async (value: string) => { const boolValue = value === 'yes'; setOpenaiFlex(boolValue); try { await configManager.set('general.openai.flex', boolValue); console.log('OpenAI Flex Mode changed to:', boolValue); } catch (error) { console.error('Failed to save OpenAI Flex Mode:', error); } }; const handlePersistApiKeysNonLocalhostChange = async (value: string) => { const boolValue = value === 'yes'; setPersistApiKeysNonLocalhost(boolValue); try { await configManager.set('general.persist_api_keys_non_localhost', boolValue); console.log('Persist API Keys Non-Localhost changed to:', boolValue); } catch (error) { console.error('Failed to save Persist API Keys Non-Localhost:', error); } }; const handleGeminiKeyChange = (value: string) => { setGeminiKey(value); debouncedSave('general.gemini.api_key', value); }; const handleGeminiModelChange = (value: string) => { setGeminiModel(value); debouncedSave('general.gemini.model', value); }; const handleClaudeKeyChange = (value: string) => { setClaudeKey(value); debouncedSave('general.claude.api_key', value); }; const handleClaudeModelChange = (value: string) => { setClaudeModel(value); debouncedSave('general.claude.model', value); }; const handleClaudeOpenRouterKeyChange = (value: string) => { setClaudeOpenRouterKey(value); debouncedSave('general.claude_openrouter.api_key', value); }; const handleClaudeOpenRouterModelChange = (value: string) => { setClaudeOpenRouterModel(value); debouncedSave('general.claude_openrouter.model', value); }; const handleClaudeOpenRouterBaseUrlChange = (value: string) => { setClaudeOpenRouterBaseUrl(value); debouncedSave('general.claude_openrouter.base_url', value); }; const handleCompatibleKeyChange = (value: string) => { setCompatibleKey(value); debouncedSave('general.openai_compatible.api_key', value); }; const handleCompatibleBaseUrlChange = (value: string) => { setCompatibleBaseUrl(value); debouncedSave('general.openai_compatible.base_url', value); }; const handleCompatibleModelChange = (value: string) => { setCompatibleModel(value); debouncedSave('general.openai_compatible.model', value); }; const handleSoundfontBaseUrlChange = (value: string) => { setSoundfontBaseUrl(value); debouncedSave('general.soundfont.base_url', value); }; const handleKgoneEnabledChange = async (value: boolean) => { setKgoneEnabled(value); try { await configManager.set('general.kgone.enabled', value); } catch (error) { console.error('Failed to save K.G.One enabled:', error); } }; const handleKgoneBaseUrlChange = (value: string) => { setKgoneBaseUrl(value); debouncedSave('general.kgone.base_url', value); }; const handleLocalModelUrlChange = (value: string) => { setLocalModelUrl(value); debouncedSave('general.local_browser.model_url', value); }; const handleUvr5ModelUrlChange = (value: string) => { setUvr5ModelUrl(value); debouncedSave('general.uvr5_web_runtime.mdx_net_model_url', value); }; const handleDeleteLocalModel = async () => { try { await LocalLLMModelManager.deleteCachedModel(); } catch (error) { console.error('Failed to delete local language model cache:', error); } }; const handleDeleteUvr5Model = async () => { setIsDeletingUvr5Model(true); try { await LocalSeparatorModelCache.delete(); setIsUvr5ModelCached(false); } catch (error) { console.error('Failed to delete UVR5 cached model:', error); } finally { setIsDeletingUvr5Model(false); await refreshUvr5ModelCacheState(); } }; const handleLocalContextLengthChange = async (value: string) => { const parsed = Number(value); const normalized = normalizeLocalLLMContextLength(parsed); setLocalContextLength(normalized); try { await configManager.set('general.local_browser.context_length', normalized); console.log('Local browser context length changed to:', normalized); } catch (error) { console.error('Failed to save local browser context length:', error); } }; const localRuntimeMessage = localModelState.runtimeSupport.reason; const hasLocalRuntimeHardFailure = !localModelState.runtimeSupport.supported; // NOTE: Gemini and Claude are not supported yet due to CORS issues. return (

General

LLM Provider

When enabled, API keys will be saved to browser storage even on non-localhost environments. Warning: This may increase security vulnerability to XSS attacks.

{LOCAL_LLM_DISPLAY_NAME} Local Runtime

{hasLocalRuntimeHardFailure && (
{localRuntimeMessage}
)}
{localModelState.isChecking ? 'Checking local model cache...' : localModelState.isCached ? 'Downloaded in browser cache.' : 'Not downloaded yet.'}
Larger context lengths require more VRAM and may also reduce performance as conversations become longer.
handleLocalModelUrlChange(e.target.value)} />
{!localModelState.isCached && !localModelState.isDownloading && localModelState.runtimeSupport.supported && (
The local model downloads automatically the next time you chat with `Local LLM (Browser)`.
)} {(localModelState.isDownloading || localModelState.progressText) && (
{localModelState.progressText}
)} {localModelState.error && (
{localModelState.error}
)}

UVR5 Web Runtime

handleUvr5ModelUrlChange(e.target.value)} />

OpenAI

handleOpenaiKeyChange(e.target.value)} />
{isLocalEnvironment ? 'Keys are persisted locally (the IndexedDB in your browser).' : persistApiKeysNonLocalhost ? 'Keys are persisted locally (the IndexedDB in your browser).' : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
Flex Mode uses OpenAI's flexible service tier. Pros: potential cost savings and higher throughput during busy periods. Cons: variable latency and possible queueing/deprioritization. Applies only to the OpenAI provider; no effect for OpenAI Compatible servers.
{/*

Gemini

handleGeminiKeyChange(e.target.value)} />
handleGeminiModelChange(e.target.value)} />

Claude

handleClaudeKeyChange(e.target.value)} />
handleClaudeModelChange(e.target.value)} />
*/}

Anthropic Claude (via OpenRouter)

handleClaudeOpenRouterKeyChange(e.target.value)} />
{isLocalEnvironment ? 'Keys are persisted locally (the IndexedDB in your browser).' : persistApiKeysNonLocalhost ? 'Keys are persisted locally (the IndexedDB in your browser).' : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
handleClaudeOpenRouterBaseUrlChange(e.target.value)} />
This is the base URL for the OpenRouter API. Please do not change this unless you know what you are doing.

OpenAI Compatible Server

handleCompatibleKeyChange(e.target.value)} />
{isLocalEnvironment ? 'Keys are persisted locally (the IndexedDB in your browser).' : persistApiKeysNonLocalhost ? 'Keys are persisted locally (the IndexedDB in your browser).' : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
handleCompatibleBaseUrlChange(e.target.value)} />
Quick presets:{' '} { e.preventDefault(); handleCompatibleBaseUrlChange('http://localhost:11434/v1/chat/completions'); }} style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }} > Ollama {' | '} { e.preventDefault(); handleCompatibleBaseUrlChange('http://localhost:8080/v1/chat/completions'); }} style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }} > llama.cpp {' | '} { e.preventDefault(); handleCompatibleBaseUrlChange('http://127.0.0.1:8317/v1/chat/completions'); }} style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }} > CLIProxyAPI {' | '} { e.preventDefault(); handleCompatibleBaseUrlChange('https://openrouter.ai/api/v1/chat/completions'); }} style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }} > OpenRouter
handleCompatibleModelChange(e.target.value)} />

Soundfont Settings

{soundfontServerManaged && (
Soundfont configuration is managed by the server (kgone-server.json). Settings are read-only.
)}
handleSoundfontBaseUrlChange(e.target.value)} disabled={soundfontServerManaged} />

K.G.One Settings

{kgoneServerManaged && (
K.G.One configuration is managed by the server (kgone-server.json). Settings are read-only.
)}
handleKgoneBaseUrlChange(e.target.value)} disabled={kgoneServerManaged} />
Base URL of a running K.G.One Music Studio server. Used for full-song generation, clip generation, and stem separation.
); }; export default GeneralSettings;