Spaces:
Running
Running
| 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> | |
| ); | |
| } | |