'use client'; import { useCallback, useEffect, useState } from 'react'; interface Settings { provider: 'openai' | 'anthropic' | 'ollama'; apiKeys: { openai: string; anthropic: string }; ollamaBase: string; models: { openai: string; anthropic: string; ollama: string }; synthesisPrompt: string; } interface KeySource { openai: boolean; anthropic: boolean; ollama: boolean; } export default function SettingsForm() { const [settings, setSettings] = useState(null); const [keyFromDb, setKeyFromDb] = useState(null); const [loadError, setLoadError] = useState(null); const [saving, setSaving] = useState(false); const [saveMsg, setSaveMsg] = useState<{ ok: boolean; text: string } | null>(null); const [testMsg, setTestMsg] = useState<{ ok: boolean; text: string } | null>(null); const [testing, setTesting] = useState(false); const adminHeaders = useCallback( () => ({ 'x-admin-key': sessionStorage.getItem('mb_admin_key') ?? '', 'Content-Type': 'application/json', }), [], ); useEffect(() => { (async () => { try { const res = await fetch('/api/settings', { headers: { 'x-admin-key': sessionStorage.getItem('mb_admin_key') ?? '' }, }); const data = await res.json(); if (!res.ok) throw new Error(data.error ?? 'load failed'); setSettings(data.settings as Settings); setKeyFromDb(data.keySources as KeySource); } catch (e) { setLoadError(e instanceof Error ? e.message : 'load failed'); } })(); }, []); if (loadError) { return

Could not load settings: {loadError}

; } if (!settings || !keyFromDb) { return

Loading…

; } const prompt: Settings = { provider: settings.provider, apiKeys: { ...settings.apiKeys }, ollamaBase: settings.ollamaBase ?? 'http://localhost:11434', models: { ...settings.models }, synthesisPrompt: settings.synthesisPrompt, }; function patch(key: K, value: Settings[K]) { setSettings((s) => (s ? { ...s, [key]: value } : s)); setSaveMsg(null); } async function save(e: React.FormEvent) { e.preventDefault(); setSaving(true); setSaveMsg(null); setTestMsg(null); try { const res = await fetch('/api/settings', { method: 'PUT', headers: adminHeaders(), body: JSON.stringify(prompt), }); const data = await res.json(); setSaveMsg({ ok: res.ok, text: res.ok ? 'Settings saved.' : (data.error ?? 'save failed') }); if (res.ok) { setKeyFromDb(data.keySources as KeySource); } } catch { setSaveMsg({ ok: false, text: 'Network error' }); } finally { setSaving(false); } } async function test() { setTesting(true); setTestMsg(null); try { const res = await fetch('/api/settings/test', { method: 'POST', headers: adminHeaders(), }); const data = await res.json(); setTestMsg( data.ok ? { ok: true, text: data.message ?? 'Connection OK.' } : { ok: false, text: data.error ?? 'Test failed' }, ); } catch { setTestMsg({ ok: false, text: 'Network error' }); } finally { setTesting(false); } } return (
{/* Provider selection */}

Active provider

{(['openai', 'anthropic', 'ollama'] as const).map((p) => ( ))}
{/* Model */} {/* API key — only cloud providers */} {prompt.provider !== 'ollama' ? ( ) : ( )}
{/* Synthesis prompt */}

Synthesis system prompt

This is the standing instruction the LLM receives on every synthesis. Edit to change tone, length, or structure — keep the “strict JSON only” contract so parsing stays reliable.