feat: added i18n support; added Simplified Chinese support
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import type { SettingsSection } from './SettingsPanel';
|
||||
import { useI18n } from '../../i18n/useI18n';
|
||||
|
||||
interface SettingsSidebarProps {
|
||||
activeSection: SettingsSection;
|
||||
@@ -13,22 +14,23 @@ const SettingsSidebar: React.FC<SettingsSidebarProps> = ({
|
||||
onSectionChange,
|
||||
onClose
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const sections = [
|
||||
{ id: 'general' as SettingsSection, label: 'General' },
|
||||
{ id: 'audio_io' as SettingsSection, label: 'Audio I/O' },
|
||||
{ id: 'behavior' as SettingsSection, label: 'Behavior' },
|
||||
{ id: 'templates' as SettingsSection, label: 'Templates' },
|
||||
{ id: 'chord_guide' as SettingsSection, label: 'Chord Guide' }
|
||||
{ id: 'general' as SettingsSection, label: t('settings.sidebar.general') },
|
||||
{ id: 'audio_io' as SettingsSection, label: t('settings.sidebar.audioIo') },
|
||||
{ id: 'behavior' as SettingsSection, label: t('settings.sidebar.behavior') },
|
||||
{ id: 'templates' as SettingsSection, label: t('settings.sidebar.templates') },
|
||||
{ id: 'chord_guide' as SettingsSection, label: t('settings.sidebar.chordGuide') }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="settings-sidebar">
|
||||
<div className="settings-sidebar-header">
|
||||
<h2>Settings</h2>
|
||||
<h2>{t('settings.sidebar.title')}</h2>
|
||||
<button
|
||||
className="settings-close-btn"
|
||||
onClick={onClose}
|
||||
title="Close Settings"
|
||||
title={t('settings.sidebar.close')}
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import { I18nContext } from '../../../i18n/I18nProvider';
|
||||
import { translate } from '../../../i18n/translate';
|
||||
|
||||
const { localSeparatorModelCacheMock } = vi.hoisted(() => ({
|
||||
localSeparatorModelCacheMock: {
|
||||
@@ -11,6 +13,7 @@ const { localSeparatorModelCacheMock } = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
const configState = new Map<string, unknown>([
|
||||
['general.language', 'auto'],
|
||||
['general.llm_provider', 'local_browser'],
|
||||
['general.persist_api_keys_non_localhost', false],
|
||||
['general.openai.api_key', ''],
|
||||
@@ -86,7 +89,23 @@ vi.mock('../../../util/local-separator/modelCache', () => ({
|
||||
}));
|
||||
|
||||
describe('GeneralSettings', () => {
|
||||
const renderSettings = () => render(
|
||||
<I18nContext.Provider
|
||||
value={{
|
||||
languageSetting: 'auto',
|
||||
resolvedLocale: 'en_us',
|
||||
setLanguageSetting: async (value) => {
|
||||
await configManagerMock.set('general.language', value);
|
||||
},
|
||||
t: (key, params) => translate(key, params, 'en_us'),
|
||||
}}
|
||||
>
|
||||
<GeneralSettings />
|
||||
</I18nContext.Provider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
configState.set('general.language', 'auto');
|
||||
configState.set('general.local_browser.context_length', 65536);
|
||||
configManagerMock.get.mockClear();
|
||||
configManagerMock.set.mockClear();
|
||||
@@ -111,7 +130,7 @@ describe('GeneralSettings', () => {
|
||||
});
|
||||
|
||||
it('renders the local context length selector and VRAM hint', async () => {
|
||||
render(<GeneralSettings />);
|
||||
renderSettings();
|
||||
|
||||
expect(await screen.findByText('Gemma 4 E4B Local Runtime')).toBeTruthy();
|
||||
expect(screen.getByLabelText('Context Length')).toBeTruthy();
|
||||
@@ -119,14 +138,14 @@ describe('GeneralSettings', () => {
|
||||
});
|
||||
|
||||
it('initializes the local context length from config', async () => {
|
||||
render(<GeneralSettings />);
|
||||
renderSettings();
|
||||
|
||||
const select = await screen.findByLabelText('Context Length');
|
||||
expect((select as HTMLSelectElement).value).toBe('65536');
|
||||
});
|
||||
|
||||
it('persists local context length changes', async () => {
|
||||
render(<GeneralSettings />);
|
||||
renderSettings();
|
||||
|
||||
const select = await screen.findByLabelText('Context Length');
|
||||
fireEvent.change(select, { target: { value: '131072' } });
|
||||
@@ -137,7 +156,7 @@ describe('GeneralSettings', () => {
|
||||
});
|
||||
|
||||
it('renders and persists local runtime download URLs', async () => {
|
||||
render(<GeneralSettings />);
|
||||
renderSettings();
|
||||
|
||||
expect(await screen.findByDisplayValue('https://huggingface.co/notabilia/gemma-4-E4B-it-litert-lm/resolve/main/gemma-4-E4B-it-web.task')).toBeTruthy();
|
||||
expect(screen.getByDisplayValue('https://huggingface.co/notabilia/uvr5-models/resolve/main/UVR-MDX-NET-Inst_HQ_3.onnx')).toBeTruthy();
|
||||
@@ -176,7 +195,7 @@ describe('GeneralSettings', () => {
|
||||
.mockResolvedValueOnce(false)
|
||||
.mockResolvedValueOnce(false);
|
||||
|
||||
render(<GeneralSettings />);
|
||||
renderSettings();
|
||||
|
||||
expect(await screen.findByText('UVR5 Web Runtime')).toBeTruthy();
|
||||
|
||||
@@ -219,10 +238,24 @@ describe('GeneralSettings', () => {
|
||||
reason: 'This host may not support the local browser runtime reliably because cross-origin isolation or SharedArrayBuffer is unavailable. COOP/COEP headers may be missing.',
|
||||
};
|
||||
|
||||
render(<GeneralSettings />);
|
||||
renderSettings();
|
||||
|
||||
expect(await screen.findByText('Gemma 4 E4B Local Runtime')).toBeTruthy();
|
||||
expect(screen.queryByText(/may not support the local browser runtime reliably/i)).toBeNull();
|
||||
expect(screen.getByText(/The local model downloads automatically/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders language first and persists language changes', async () => {
|
||||
renderSettings();
|
||||
|
||||
const languageSelect = await screen.findByLabelText('Language');
|
||||
expect(languageSelect).toBeTruthy();
|
||||
expect(screen.getAllByRole('combobox')[0]).toBe(languageSelect);
|
||||
|
||||
fireEvent.change(languageSelect, { target: { value: 'zh_cn' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(configManagerMock.set).toHaveBeenCalledWith('general.language', 'zh_cn');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ 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 { useI18n } from '../../../i18n/useI18n';
|
||||
import type { LanguageSetting } from '../../../i18n/types';
|
||||
import {
|
||||
formatLocalLLMContextLength,
|
||||
LOCAL_LLM_CONTEXT_LENGTH_OPTIONS,
|
||||
@@ -18,6 +20,8 @@ import {
|
||||
} from '../../../util/local-separator/config';
|
||||
|
||||
const GeneralSettings: React.FC = () => {
|
||||
const { t, setLanguageSetting } = useI18n();
|
||||
const [language, setLanguage] = useState<LanguageSetting>('auto');
|
||||
const [llmProvider, setLlmProvider] = useState<string>(LOCAL_LLM_PROVIDER_KEY);
|
||||
const [openaiKey, setOpenaiKey] = useState<string>('');
|
||||
const [openaiModel, setOpenaiModel] = useState<string>('');
|
||||
@@ -90,6 +94,7 @@ const GeneralSettings: React.FC = () => {
|
||||
await configManager.initialize();
|
||||
}
|
||||
|
||||
setLanguage(((configManager.get('general.language') as LanguageSetting | undefined) ?? 'auto'));
|
||||
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) || '');
|
||||
@@ -153,6 +158,15 @@ const GeneralSettings: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = async (value: LanguageSetting) => {
|
||||
setLanguage(value);
|
||||
try {
|
||||
await setLanguageSetting(value);
|
||||
} catch (error) {
|
||||
console.error('Failed to save language:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenaiKeyChange = (value: string) => {
|
||||
setOpenaiKey(value);
|
||||
debouncedSave('general.openai.api_key', value);
|
||||
@@ -322,45 +336,66 @@ const GeneralSettings: React.FC = () => {
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div className="settings-section-header">
|
||||
<h3>General</h3>
|
||||
<h3>{t('settings.general.title')}</h3>
|
||||
</div>
|
||||
|
||||
<div className="settings-section-content">
|
||||
<div className="settings-group">
|
||||
<h4>LLM Provider</h4>
|
||||
<div className="settings-item">
|
||||
<label className="settings-label" htmlFor="general-language-select">
|
||||
{t('settings.general.language.label')}
|
||||
</label>
|
||||
<select
|
||||
id="general-language-select"
|
||||
className="settings-select"
|
||||
value={language}
|
||||
onChange={(e) => void handleLanguageChange(e.target.value as LanguageSetting)}
|
||||
>
|
||||
<option value="auto">{t('settings.general.language.auto')}</option>
|
||||
<option value="en_us">{t('settings.general.language.en_us')}</option>
|
||||
<option value="zh_cn">{t('settings.general.language.zh_cn')}</option>
|
||||
</select>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
{t('settings.general.language.help')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>{t('settings.general.llmProvider.section')}</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
LLM Provider
|
||||
{t('settings.general.llmProvider.label')}
|
||||
</label>
|
||||
<select
|
||||
className="settings-select"
|
||||
value={llmProvider}
|
||||
onChange={(e) => handleLlmProviderChange(e.target.value)}
|
||||
>
|
||||
<option value={LOCAL_LLM_PROVIDER_KEY}>Local LLM (Browser)</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value={LOCAL_LLM_PROVIDER_KEY}>{t('settings.general.llmProvider.local')}</option>
|
||||
<option value="openai">{t('settings.general.llmProvider.openai')}</option>
|
||||
{/* <option value="gemini">Gemini</option>
|
||||
<option value="claude">Claude</option> */}
|
||||
<option value="claude_openrouter">Claude (via OpenRouter)</option>
|
||||
<option value="openai_compatible">OpenAI Compatible (e.g. OpenRouter, Ollama)</option>
|
||||
<option value="claude_openrouter">{t('settings.general.llmProvider.claudeOpenRouter')}</option>
|
||||
<option value="openai_compatible">{t('settings.general.llmProvider.openaiCompatible')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Persist API Keys on Non-Localhost
|
||||
{t('settings.general.persistKeys.label')}
|
||||
</label>
|
||||
<select
|
||||
className="settings-select"
|
||||
value={persistApiKeysNonLocalhost ? 'yes' : 'no'}
|
||||
onChange={(e) => handlePersistApiKeysNonLocalhostChange(e.target.value)}
|
||||
>
|
||||
<option value="no">No</option>
|
||||
<option value="yes">Yes</option>
|
||||
<option value="no">{t('settings.no')}</option>
|
||||
<option value="yes">{t('settings.yes')}</option>
|
||||
</select>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
When enabled, API keys will be saved to browser storage even on non-localhost environments. Warning: This may increase security vulnerability to XSS attacks.
|
||||
{t('settings.general.persistKeys.help')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,20 +411,20 @@ const GeneralSettings: React.FC = () => {
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Cached Model Status
|
||||
{t('settings.general.localRuntime.cachedStatus')}
|
||||
</label>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
{localModelState.isChecking
|
||||
? 'Checking local model cache...'
|
||||
? t('settings.general.localRuntime.cacheChecking')
|
||||
: localModelState.isCached
|
||||
? 'Downloaded in browser cache.'
|
||||
: 'Not downloaded yet.'}
|
||||
? t('settings.general.localRuntime.cacheDownloaded')
|
||||
: t('settings.general.localRuntime.cacheMissing')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label" htmlFor="local-llm-context-length">
|
||||
Context Length
|
||||
{t('settings.general.localRuntime.contextLength')}
|
||||
</label>
|
||||
<select
|
||||
id="local-llm-context-length"
|
||||
@@ -404,13 +439,13 @@ const GeneralSettings: React.FC = () => {
|
||||
))}
|
||||
</select>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
Larger context lengths require more VRAM and may also reduce performance as conversations become longer.
|
||||
{t('settings.general.localRuntime.contextHelp')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Download URL
|
||||
{t('settings.general.localRuntime.downloadUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -420,7 +455,7 @@ const GeneralSettings: React.FC = () => {
|
||||
onChange={(e) => handleLocalModelUrlChange(e.target.value)}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
Changing this URL may break downloads or point to an incompatible model file.{' '}
|
||||
{t('settings.general.localRuntime.downloadHelp')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
@@ -429,14 +464,14 @@ const GeneralSettings: React.FC = () => {
|
||||
}}
|
||||
style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }}
|
||||
>
|
||||
Restore default
|
||||
{t('settings.restoreDefault')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!localModelState.isCached && !localModelState.isDownloading && localModelState.runtimeSupport.supported && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px', marginBottom: '8px' }}>
|
||||
The local model downloads automatically the next time you chat with `Local LLM (Browser)`.
|
||||
{t('settings.general.localRuntime.autoDownload')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -470,17 +505,17 @@ const GeneralSettings: React.FC = () => {
|
||||
onClick={() => void handleDeleteLocalModel()}
|
||||
disabled={localModelState.isDeleting || localModelState.isDownloading || !localModelState.isCached}
|
||||
>
|
||||
{localModelState.isDeleting ? 'Deleting...' : 'Delete Cached Model'}
|
||||
{localModelState.isDeleting ? t('settings.deleting') : t('settings.deleteCachedModel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>UVR5 Web Runtime</h4>
|
||||
<h4>{t('settings.general.uvr5.section')}</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
UVR-MDX-NET-Inst_HQ_3 Download URL
|
||||
{t('settings.general.uvr5.downloadUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -490,7 +525,7 @@ const GeneralSettings: React.FC = () => {
|
||||
onChange={(e) => handleUvr5ModelUrlChange(e.target.value)}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
Changing this URL may break downloads or point to an incompatible model file.{' '}
|
||||
{t('settings.general.modelUrl.help')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
@@ -501,7 +536,7 @@ const GeneralSettings: React.FC = () => {
|
||||
}}
|
||||
style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }}
|
||||
>
|
||||
Restore default
|
||||
{t('settings.restoreDefault')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -513,13 +548,13 @@ const GeneralSettings: React.FC = () => {
|
||||
onClick={() => void handleDeleteUvr5Model()}
|
||||
disabled={isCheckingUvr5ModelCache || isDeletingUvr5Model || !isUvr5ModelCached}
|
||||
>
|
||||
{isDeletingUvr5Model ? 'Deleting...' : 'Delete Cached Model'}
|
||||
{isDeletingUvr5Model ? t('settings.deleting') : t('settings.deleteCachedModel')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
htdemucs_4s Download URL
|
||||
{t('settings.general.htdemucs.downloadUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -529,7 +564,7 @@ const GeneralSettings: React.FC = () => {
|
||||
onChange={(e) => handleHtdemucsModelUrlChange(e.target.value)}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
Changing this URL may break downloads or point to an incompatible model file.{' '}
|
||||
{t('settings.general.modelUrl.help')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
@@ -540,7 +575,7 @@ const GeneralSettings: React.FC = () => {
|
||||
}}
|
||||
style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }}
|
||||
>
|
||||
Restore default
|
||||
{t('settings.restoreDefault')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -552,37 +587,37 @@ const GeneralSettings: React.FC = () => {
|
||||
onClick={() => void handleDeleteHtdemucsModel()}
|
||||
disabled={isCheckingUvr5ModelCache || isDeletingHtdemucsModel || !isHtdemucsModelCached}
|
||||
>
|
||||
{isDeletingHtdemucsModel ? 'Deleting...' : 'Delete Cached Model'}
|
||||
{isDeletingHtdemucsModel ? t('settings.deleting') : t('settings.deleteCachedModel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>OpenAI</h4>
|
||||
<h4>{t('settings.general.openai.section')}</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Key
|
||||
{t('settings.general.openai.key')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="settings-input"
|
||||
placeholder="Enter your OpenAI API key"
|
||||
placeholder={t('settings.general.openai.keyPlaceholder')}
|
||||
value={openaiKey}
|
||||
onChange={(e) => handleOpenaiKeyChange(e.target.value)}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
{isLocalEnvironment
|
||||
? 'Keys are persisted locally (the IndexedDB in your browser).'
|
||||
? t('settings.general.keys.persisted')
|
||||
: 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.'}
|
||||
? t('settings.general.keys.persisted')
|
||||
: t('settings.general.keys.sessionOnly')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Model
|
||||
{t('settings.general.openai.model')}
|
||||
</label>
|
||||
<select
|
||||
className="settings-select"
|
||||
@@ -601,18 +636,18 @@ const GeneralSettings: React.FC = () => {
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Flex Mode
|
||||
{t('settings.general.openai.flexMode')}
|
||||
</label>
|
||||
<select
|
||||
className="settings-select"
|
||||
value={openaiFlex ? 'yes' : 'no'}
|
||||
onChange={(e) => handleOpenaiFlexChange(e.target.value)}
|
||||
>
|
||||
<option value="no">No</option>
|
||||
<option value="yes">Yes</option>
|
||||
<option value="no">{t('settings.no')}</option>
|
||||
<option value="yes">{t('settings.yes')}</option>
|
||||
</select>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
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.
|
||||
{t('settings.general.openai.flexHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -678,31 +713,31 @@ const GeneralSettings: React.FC = () => {
|
||||
</div> */}
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>Anthropic Claude (via OpenRouter)</h4>
|
||||
<h4>{t('settings.general.claudeOpenRouter.section')}</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Key
|
||||
{t('settings.general.openai.key')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="settings-input"
|
||||
placeholder="Enter your Claude API key"
|
||||
placeholder={t('settings.general.claudeOpenRouter.keyPlaceholder')}
|
||||
value={claudeOpenRouterKey}
|
||||
onChange={(e) => handleClaudeOpenRouterKeyChange(e.target.value)}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
{isLocalEnvironment
|
||||
? 'Keys are persisted locally (the IndexedDB in your browser).'
|
||||
? t('settings.general.keys.persisted')
|
||||
: 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.'}
|
||||
? t('settings.general.keys.persisted')
|
||||
: t('settings.general.keys.sessionOnly')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Base URL
|
||||
{t('settings.general.baseUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -712,13 +747,13 @@ const GeneralSettings: React.FC = () => {
|
||||
onChange={(e) => handleClaudeOpenRouterBaseUrlChange(e.target.value)}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
This is the base URL for the OpenRouter API. Please do not change this unless you know what you are doing.
|
||||
{t('settings.general.claudeOpenRouter.baseUrlHelp')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Model
|
||||
{t('settings.general.openai.model')}
|
||||
</label>
|
||||
<select
|
||||
className="settings-select"
|
||||
@@ -736,41 +771,41 @@ const GeneralSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>OpenAI Compatible Server</h4>
|
||||
<h4>{t('settings.general.openaiCompatible.section')}</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Key
|
||||
{t('settings.general.openai.key')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="settings-input"
|
||||
placeholder="Enter your API key"
|
||||
placeholder={t('settings.general.openaiCompatible.keyPlaceholder')}
|
||||
value={compatibleKey}
|
||||
onChange={(e) => handleCompatibleKeyChange(e.target.value)}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
{isLocalEnvironment
|
||||
? 'Keys are persisted locally (the IndexedDB in your browser).'
|
||||
? t('settings.general.keys.persisted')
|
||||
: 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.'}
|
||||
? t('settings.general.keys.persisted')
|
||||
: t('settings.general.keys.sessionOnly')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Base URL
|
||||
{t('settings.general.baseUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="settings-input"
|
||||
placeholder="e.g. https://openrouter.ai/api/v1"
|
||||
placeholder={t('settings.general.openaiCompatible.baseUrlPlaceholder')}
|
||||
value={compatibleBaseUrl}
|
||||
onChange={(e) => handleCompatibleBaseUrlChange(e.target.value)}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
Quick presets:{' '}
|
||||
{t('settings.general.openaiCompatible.baseUrlHelp')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
@@ -819,12 +854,12 @@ const GeneralSettings: React.FC = () => {
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Model
|
||||
{t('settings.general.openai.model')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="settings-input"
|
||||
placeholder="e.g. qwen3:30b"
|
||||
placeholder={t('settings.general.openaiCompatible.modelPlaceholder')}
|
||||
value={compatibleModel}
|
||||
onChange={(e) => handleCompatibleModelChange(e.target.value)}
|
||||
/>
|
||||
@@ -832,17 +867,17 @@ const GeneralSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>Soundfont Settings</h4>
|
||||
<h4>{t('settings.general.soundfont.section')}</h4>
|
||||
|
||||
{soundfontServerManaged && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px', marginBottom: '8px' }}>
|
||||
Soundfont configuration is managed by the server (kgone-server.json). Settings are read-only.
|
||||
{t('settings.general.soundfont.managed')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Base URL
|
||||
{t('settings.general.soundfont.baseUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -853,7 +888,7 @@ const GeneralSettings: React.FC = () => {
|
||||
disabled={soundfontServerManaged}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
Changing this URL to an incompatible soundfont source may cause some instruments to sound wrong or not play.{' '}
|
||||
{t('settings.general.soundfont.baseUrlHelp')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
@@ -862,24 +897,24 @@ const GeneralSettings: React.FC = () => {
|
||||
}}
|
||||
style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }}
|
||||
>
|
||||
Restore default
|
||||
{t('settings.restoreDefault')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>K.G.One Settings</h4>
|
||||
<h4>{t('settings.general.kgone.section')}</h4>
|
||||
|
||||
{kgoneServerManaged && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px', marginBottom: '8px' }}>
|
||||
K.G.One configuration is managed by the server (kgone-server.json). Settings are read-only.
|
||||
{t('settings.general.kgone.managed')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Enable K.G.One Integration
|
||||
{t('settings.general.kgone.enabled')}
|
||||
</label>
|
||||
<select
|
||||
className="settings-select"
|
||||
@@ -887,14 +922,14 @@ const GeneralSettings: React.FC = () => {
|
||||
onChange={(e) => handleKgoneEnabledChange(e.target.value === 'true')}
|
||||
disabled={kgoneServerManaged}
|
||||
>
|
||||
<option value="false">Disabled</option>
|
||||
<option value="true">Enabled</option>
|
||||
<option value="false">{t('settings.general.kgone.disabled')}</option>
|
||||
<option value="true">{t('settings.general.kgone.enabledOption')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Server Base URL
|
||||
{t('settings.general.kgone.serverBaseUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -905,7 +940,7 @@ const GeneralSettings: React.FC = () => {
|
||||
disabled={kgoneServerManaged}
|
||||
/>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
Base URL of a running K.G.One Music Studio server. Used for full-song generation, clip generation, and stem separation.
|
||||
{t('settings.general.kgone.serverBaseUrlHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user