/** * SetupTab/SettingsPanel.tsx — comprehensive adjustable-settings panel. * * Renders every MEGACOMPACT_* setting from /api/rag-settings, grouped into * collapsible categories. Each setting renders by type (boolean → Switch, * number/string → styled input) and writes back via POST /api/rag-settings on * change. Switches POST immediately; text/number inputs POST after a 500ms * debounce. Requires-LLM settings show an amber badge when no LLM embedder is * active. A restart-required toast appears after a successful write. * * PREVENT-PI-004: all data comes from localhost dashboard API endpoints. */ import type React from "react"; import { useState, useEffect, useCallback, useRef } from "react"; import type { SettingsResponse, SettingState } from "@contracts"; import { fetchSettings, postSetting } from "../../api/client"; import { Switch } from "../../components/ui/switch"; import SettingsSection from "./SettingsSection"; const INPUT_CLASS = "rounded-md border border-border bg-bg-elevated/50 px-2 py-1 text-sm outline-none transition-colors focus:border-primary disabled:cursor-not-allowed disabled:opacity-50"; function LlmBadge(): React.ReactElement { return ( LLM ); } interface RowProps { readonly setting: SettingState; readonly id: string; readonly llmActive: boolean; readonly saving: boolean; readonly onUpdate: (key: string, raw: string) => Promise; } function SettingRow({ setting, id, llmActive, saving, onUpdate, }: RowProps): React.ReactElement { const [error, setError] = useState(null); const locked = setting.requiresLlm && !llmActive; const timerRef = useRef | null>(null); useEffect(() => { return () => { if (timerRef.current) clearTimeout(timerRef.current); }; }, []); const submit = useCallback( (raw: string) => { onUpdate(setting.key, raw) .then((msg) => setError(msg)) .catch((e: unknown) => setError(e instanceof Error ? e.message : String(e)), ); }, [onUpdate, setting.key], ); const scheduleSave = useCallback( (raw: string) => { if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => submit(raw), 500); }, [submit], ); const inputDisabled = saving || locked; return ( {setting.label} {setting.requiresLlm && !llmActive && } {setting.description} {setting.type === "boolean" ? ( submit(String(checked))} aria-label={setting.label} /> ) : ( <> scheduleSave(e.target.value)} className={`${INPUT_CLASS} ${setting.type === "number" ? "w-24" : "w-56"}`} /> {setting.unit && ( {setting.unit} )} > )} {error && Error: {error}} ); } export default function SettingsPanel(): React.ReactElement { const [settings, setSettings] = useState(null); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [toast, setToast] = useState(null); const load = useCallback(() => { fetchSettings() .then(setSettings) .catch((e: unknown) => setError(e instanceof Error ? e.message : String(e)), ); }, []); useEffect(() => { load(); }, [load]); const handleUpdate = useCallback( async (key: string, raw: string): Promise => { setSaving(true); try { const res = await postSetting({ key, value: raw }); setToast( res.restartRequired ? "Saved — restart pi to apply changes." : "Saved.", ); // Refresh to pick up the server's normalized value (and to keep the // controlled Switch in sync with the persisted state). load(); return null; } catch (e) { return e instanceof Error ? e.message : String(e); } finally { setSaving(false); } }, [load], ); return ( {error && Error: {error}} {!settings && !error && ( Loading settings... )} {settings?.categories.map((cat) => ( {cat.settings.map((setting) => ( ))} ))} {toast && ( {toast} )} ); }
Error: {error}
Loading settings...