initial public release.
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
import SettingsSidebar from './SettingsSidebar';
|
||||
import GeneralSettings from './sections/GeneralSettings';
|
||||
import BehaviorSettings from './sections/BehaviorSettings';
|
||||
import TemplatesSettings from './sections/TemplatesSettings';
|
||||
|
||||
export type SettingsSection = 'general' | 'behavior' | 'templates';
|
||||
|
||||
interface SettingsPanelProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SettingsPanel: React.FC<SettingsPanelProps> = ({ onClose }) => {
|
||||
const [activeSection, setActiveSection] = useState<SettingsSection>('general');
|
||||
|
||||
const renderActiveSection = () => {
|
||||
switch (activeSection) {
|
||||
case 'general':
|
||||
return <GeneralSettings />;
|
||||
case 'behavior':
|
||||
return <BehaviorSettings />;
|
||||
case 'templates':
|
||||
return <TemplatesSettings />;
|
||||
default:
|
||||
return <GeneralSettings />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-panel">
|
||||
<div className="settings-container">
|
||||
<SettingsSidebar
|
||||
activeSection={activeSection}
|
||||
onSectionChange={setActiveSection}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<div className="settings-content">
|
||||
{renderActiveSection()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPanel;
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import type { SettingsSection } from './SettingsPanel';
|
||||
|
||||
interface SettingsSidebarProps {
|
||||
activeSection: SettingsSection;
|
||||
onSectionChange: (section: SettingsSection) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SettingsSidebar: React.FC<SettingsSidebarProps> = ({
|
||||
activeSection,
|
||||
onSectionChange,
|
||||
onClose
|
||||
}) => {
|
||||
const sections = [
|
||||
{ id: 'general' as SettingsSection, label: 'General' },
|
||||
{ id: 'behavior' as SettingsSection, label: 'Behavior' },
|
||||
{ id: 'templates' as SettingsSection, label: 'Templates' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="settings-sidebar">
|
||||
<div className="settings-sidebar-header">
|
||||
<h2>Settings</h2>
|
||||
<button
|
||||
className="settings-close-btn"
|
||||
onClick={onClose}
|
||||
title="Close Settings"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="settings-nav">
|
||||
{sections.map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
className={`settings-nav-item ${activeSection === section.id ? 'active' : ''}`}
|
||||
onClick={() => onSectionChange(section.id)}
|
||||
>
|
||||
{section.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsSidebar;
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as SettingsPanel } from './SettingsPanel.tsx';
|
||||
export { default as SettingsSidebar } from './SettingsSidebar.tsx';
|
||||
export { default as GeneralSettings } from './sections/GeneralSettings.tsx';
|
||||
export { default as BehaviorSettings } from './sections/BehaviorSettings.tsx';
|
||||
export { default as TemplatesSettings } from './sections/TemplatesSettings.tsx';
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ConfigManager } from '../../../core/config/ConfigManager';
|
||||
|
||||
const BehaviorSettings: React.FC = () => {
|
||||
const [chatboxDefaultOpen, setChatboxDefaultOpen] = useState<boolean>(true);
|
||||
|
||||
const configManager = ConfigManager.instance();
|
||||
|
||||
// Load configuration values on component mount
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
if (!configManager.getIsInitialized()) {
|
||||
await configManager.initialize();
|
||||
}
|
||||
|
||||
setChatboxDefaultOpen((configManager.get('chatbox.default_open') as boolean) ?? true);
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, [configManager]);
|
||||
|
||||
// Save configuration when values change
|
||||
const handleChatboxDefaultOpenChange = async (value: string) => {
|
||||
const boolValue = value === 'yes';
|
||||
setChatboxDefaultOpen(boolValue);
|
||||
await configManager.set('chatbox.default_open', boolValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div className="settings-section-header">
|
||||
<h3>Behavior</h3>
|
||||
</div>
|
||||
|
||||
<div className="settings-section-content">
|
||||
<div className="settings-group">
|
||||
<h4>Chat Box</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Open at Start Up
|
||||
</label>
|
||||
<select
|
||||
className="settings-select"
|
||||
value={chatboxDefaultOpen ? 'yes' : 'no'}
|
||||
onChange={(e) => handleChatboxDefaultOpenChange(e.target.value)}
|
||||
>
|
||||
<option value="no">No</option>
|
||||
<option value="yes">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BehaviorSettings;
|
||||
@@ -0,0 +1,359 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { ConfigManager } from '../../../core/config/ConfigManager';
|
||||
|
||||
const GeneralSettings: React.FC = () => {
|
||||
const [llmProvider, setLlmProvider] = useState<string>('openai');
|
||||
const [openaiKey, setOpenaiKey] = useState<string>('');
|
||||
const [openaiModel, setOpenaiModel] = useState<string>('');
|
||||
const [geminiKey, setGeminiKey] = useState<string>('');
|
||||
const [geminiModel, setGeminiModel] = useState<string>('');
|
||||
const [claudeKey, setClaudeKey] = useState<string>('');
|
||||
const [claudeModel, setClaudeModel] = useState<string>('');
|
||||
const [openaiFlex, setOpenaiFlex] = useState<boolean>(false);
|
||||
const [compatibleKey, setCompatibleKey] = useState<string>('');
|
||||
const [compatibleBaseUrl, setCompatibleBaseUrl] = useState<string>('');
|
||||
const [compatibleModel, setCompatibleModel] = useState<string>('');
|
||||
const [soundfontBaseUrl, setSoundfontBaseUrl] = useState<string>('');
|
||||
|
||||
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;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load configuration values on component mount
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
if (!configManager.getIsInitialized()) {
|
||||
await configManager.initialize();
|
||||
}
|
||||
|
||||
setLlmProvider((configManager.get('general.llm_provider') as string) || 'openai');
|
||||
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);
|
||||
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) || '');
|
||||
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) || '');
|
||||
setSoundfontBaseUrl((configManager.get('general.soundfont.base_url') as string) || '');
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, [configManager]);
|
||||
|
||||
// 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 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 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);
|
||||
};
|
||||
|
||||
// NOTE: Gemini and Claude are not supported yet due to CORS issues.
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div className="settings-section-header">
|
||||
<h3>General</h3>
|
||||
</div>
|
||||
|
||||
<div className="settings-section-content">
|
||||
<div className="settings-group">
|
||||
<h4>LLM Provider</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
LLM Provider
|
||||
</label>
|
||||
<select
|
||||
className="settings-select"
|
||||
value={llmProvider}
|
||||
onChange={(e) => handleLlmProviderChange(e.target.value)}
|
||||
>
|
||||
<option value="openai">OpenAI</option>
|
||||
{/* <option value="gemini">Gemini</option>
|
||||
<option value="claude">Claude</option> */}
|
||||
<option value="openai_compatible">OpenAI Compatible (e.g. OpenRouter, Ollama)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>OpenAI</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="settings-input"
|
||||
placeholder="Enter your OpenAI API key"
|
||||
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).'
|
||||
: 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
className="settings-select"
|
||||
value={openaiModel}
|
||||
onChange={(e) => handleOpenaiModelChange(e.target.value)}
|
||||
>
|
||||
{/* <option value="gpt-5">gpt-5</option>
|
||||
<option value="gpt-5-mini">gpt-5-mini</option>
|
||||
<option value="gpt-5-nano">gpt-5-nano</option> */}
|
||||
<option value="gpt-4o">gpt-4o</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Flex Mode
|
||||
</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>
|
||||
</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.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="settings-group">
|
||||
<h4>Gemini</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="settings-input"
|
||||
placeholder="Enter your Gemini API key"
|
||||
value={geminiKey}
|
||||
onChange={(e) => handleGeminiKeyChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Model
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="settings-input"
|
||||
placeholder="e.g. gemini-2.5-flash"
|
||||
value={geminiModel}
|
||||
onChange={(e) => handleGeminiModelChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>Claude</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="settings-input"
|
||||
placeholder="Enter your Claude API key"
|
||||
value={claudeKey}
|
||||
onChange={(e) => handleClaudeKeyChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Model
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="settings-input"
|
||||
placeholder="e.g. claude-sonnet-4-0"
|
||||
value={claudeModel}
|
||||
onChange={(e) => handleClaudeModelChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>OpenAI Compatible Server</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="settings-input"
|
||||
placeholder="Enter your API key"
|
||||
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).'
|
||||
: 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Base URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="settings-input"
|
||||
placeholder="e.g. https://api.openrouter.ai/v1"
|
||||
value={compatibleBaseUrl}
|
||||
onChange={(e) => handleCompatibleBaseUrlChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Model
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="settings-input"
|
||||
placeholder="e.g. qwen3:30b"
|
||||
value={compatibleModel}
|
||||
onChange={(e) => handleCompatibleModelChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>Soundfont Settings</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Base URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="settings-input"
|
||||
placeholder="e.g. https://cdn.jsdelivr.net/npm/soundfont-for-samplers/FluidR3_GM/"
|
||||
value={soundfontBaseUrl}
|
||||
onChange={(e) => handleSoundfontBaseUrlChange(e.target.value)}
|
||||
/>
|
||||
<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.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralSettings;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { ConfigManager } from '../../../core/config/ConfigManager';
|
||||
|
||||
const TemplatesSettings: React.FC = () => {
|
||||
const [customInstructions, setCustomInstructions] = useState<string>('');
|
||||
|
||||
const configManager = ConfigManager.instance();
|
||||
|
||||
// Load configuration values on component mount
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
if (!configManager.getIsInitialized()) {
|
||||
await configManager.initialize();
|
||||
}
|
||||
|
||||
setCustomInstructions((configManager.get('templates.custom_instructions') as string) || '');
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, [configManager]);
|
||||
|
||||
// Debounced save function for textarea
|
||||
const debouncedSave = useCallback((value: string) => {
|
||||
const timeoutId = setTimeout(async () => {
|
||||
try {
|
||||
await configManager.set('templates.custom_instructions', value);
|
||||
console.log('Custom instructions saved');
|
||||
} catch (error) {
|
||||
console.error('Failed to save custom instructions:', error);
|
||||
}
|
||||
}, 1000); // 1 second debounce for longer text
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [configManager]);
|
||||
|
||||
// Save configuration when value changes
|
||||
const handleCustomInstructionsChange = (value: string) => {
|
||||
setCustomInstructions(value);
|
||||
debouncedSave(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div className="settings-section-header">
|
||||
<h3>Templates</h3>
|
||||
</div>
|
||||
|
||||
<div className="settings-section-content">
|
||||
<div className="settings-group">
|
||||
<h4>Custom Instructions</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<textarea
|
||||
className="settings-textarea"
|
||||
placeholder="Please input your custom instructions for the K.G.Studio Musician Assistant"
|
||||
rows={8}
|
||||
value={customInstructions}
|
||||
onChange={(e) => handleCustomInstructionsChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplatesSettings;
|
||||
Reference in New Issue
Block a user