Bonsai-Chat-WebGPU / src /app /components /SettingsSheet.tsx
Valeriy Selitskiy
Use 4096-token release defaults
36a170e
Raw
History Blame Contribute Delete
5.74 kB
import { useEffect, useRef } from 'react';
import type { ModelOption, RuntimeSettings, TelemetrySnapshot } from '../../lib/contracts';
import { DEFAULT_MAX_TOKENS } from '../../lib/runtime-defaults';
import type { ThemeMode } from '../theme';
interface SettingsSheetProps {
open: boolean;
themeMode: ThemeMode;
onThemeModeChange: (theme: ThemeMode) => void;
activeModel: ModelOption;
telemetry: TelemetrySnapshot;
settings: RuntimeSettings;
onSettingsChange: (settings: RuntimeSettings) => void;
onPersistStorage: () => void;
onClearStorage: () => void;
onClearConversation: () => void;
onClose: () => void;
}
export function SettingsSheet({
open,
themeMode,
onThemeModeChange,
activeModel,
telemetry,
settings,
onSettingsChange,
onPersistStorage,
onClearStorage,
onClearConversation,
onClose,
}: SettingsSheetProps) {
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!open) return;
closeButtonRef.current?.focus();
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [open, onClose]);
if (!open) return null;
const contextLimit = activeModel.id === 'bonsai-27b'
? activeModel.defaultContext
: activeModel.contextLimit;
const update = <Key extends keyof RuntimeSettings>(key: Key, value: RuntimeSettings[Key]) => {
onSettingsChange({ ...settings, [key]: value });
};
return (
<div className="settings-layer" role="presentation" onMouseDown={(event) => {
if (event.target === event.currentTarget) onClose();
}}>
<section className="settings-sheet" role="dialog" aria-modal="true" aria-labelledby="settings-title">
<header>
<h2 id="settings-title">Chat settings</h2>
<button ref={closeButtonRef} className="sheet-close" type="button" onClick={onClose} aria-label="Close settings">×</button>
</header>
<label className="setting-group setting-select">
<div><small>Appearance</small><strong>Follow the system or choose a theme</strong></div>
<select aria-label="Color theme" value={themeMode} onChange={(event) => onThemeModeChange(event.target.value as ThemeMode)}>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
<label className="setting-group setting-select">
<div><small>Compute backend</small><strong>Auto prefers WebGPU</strong></div>
<select value={settings.backend} onChange={(event) => update('backend', event.target.value as RuntimeSettings['backend'])}>
<option value="auto">Auto</option>
<option value="webgpu">WebGPU</option>
{activeModel.id !== 'bonsai-27b' && <option value="wasm">CPU · WASM</option>}
</select>
</label>
<div className="setting-group">
<div><small>Selected model</small><strong>{activeModel.label}</strong></div>
<span className="setting-locked">{activeModel.footprint}</span>
</div>
<label className="range-setting">
<span><small>Context allocation · applied on load</small><strong>{settings.contextSize.toLocaleString()} tokens</strong></span>
<input
type="range"
min={Math.min(1024, activeModel.defaultContext)}
max={contextLimit}
step={16}
value={settings.contextSize}
onChange={(event) => update('contextSize', Number(event.target.value))}
/>
<span className="range-limits"><i>{Math.min(1024, activeModel.defaultContext).toLocaleString()}</i><i>{contextLimit.toLocaleString()}</i></span>
</label>
<label className="range-setting">
<span><small>Temperature</small><strong>{settings.temperature.toFixed(2)}</strong></span>
<input type="range" min="0" max="2" step="0.05" value={settings.temperature} onChange={(event) => update('temperature', Number(event.target.value))} />
<span className="range-limits"><i>deterministic</i><i>exploratory</i></span>
</label>
<label className="range-setting">
<span><small>Maximum completion</small><strong>{settings.maxTokens} tokens</strong></span>
<input type="range" min="64" max={DEFAULT_MAX_TOKENS} step="16" value={settings.maxTokens} onChange={(event) => update('maxTokens', Number(event.target.value))} />
<span className="range-limits"><i>64</i><i>{DEFAULT_MAX_TOKENS.toLocaleString()}</i></span>
</label>
<label className="system-prompt-setting">
<span><small>System instructions</small><strong>Saved with this chat</strong></span>
<textarea rows={5} value={settings.systemPrompt} onChange={(event) => update('systemPrompt', event.target.value.slice(0, 8192))} />
</label>
<div className="settings-actions">
<button type="button" onClick={onPersistStorage}>Request persistent storage</button>
<button type="button" onClick={onClearConversation}>Clear conversation</button>
<button className="danger-action" type="button" onClick={onClearStorage}>Delete cached models</button>
</div>
<div className="settings-note">
<span className="signal-dot" aria-hidden="true" />
<p><strong>{telemetry.backend} boundary.</strong> Sampling and tools run in local workers. Changing backend or context reloads the selected model on the next run.</p>
</div>
</section>
</div>
);
}