import { useEffect, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faRobot, faRotateLeft } from '@fortawesome/pro-duotone-svg-icons'; import { Button } from '@/components/base/buttons/button'; import { TopBar } from '@/components/layout/top-bar'; import { AiForm, emptyAiFormValues, type AiFormValues, type AiProvider, } from '@/components/forms/ai-form'; import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; import { markSetupStepComplete } from '@/lib/setup-state'; // /settings/ai — Pattern B page for the AI assistant config. Backed by // /api/config/ai which is file-backed (data/ai.json) and hot-reloaded through // AiConfigService — no restart needed. // // Temperature is a string in the form for input UX (so users can partially // type '0.', '0.5', etc) then clamped to 0..2 on save. type ServerAiConfig = { provider?: AiProvider; model?: string; temperature?: number; systemPromptAddendum?: string; }; const clampTemperature = (raw: string): number => { const n = Number(raw); if (Number.isNaN(n)) return 0.7; return Math.min(2, Math.max(0, n)); }; export const AiSettingsPage = () => { const [values, setValues] = useState(emptyAiFormValues); const [loading, setLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isResetting, setIsResetting] = useState(false); const loadConfig = async () => { try { const data = await apiClient.get('/api/config/ai'); setValues({ provider: data.provider ?? 'openai', model: data.model ?? 'gpt-4o-mini', temperature: data.temperature != null ? String(data.temperature) : '0.7', systemPromptAddendum: data.systemPromptAddendum ?? '', }); } catch { // toast already shown } finally { setLoading(false); } }; useEffect(() => { loadConfig(); }, []); const handleSave = async () => { if (!values.model.trim()) { notify.error('Model is required'); return; } setIsSaving(true); try { await apiClient.put('/api/config/ai', { provider: values.provider, model: values.model.trim(), temperature: clampTemperature(values.temperature), systemPromptAddendum: values.systemPromptAddendum, }); notify.success('AI settings updated', 'Changes are live for new conversations.'); markSetupStepComplete('ai').catch(() => {}); await loadConfig(); } catch (err) { console.error('[ai] save failed', err); } finally { setIsSaving(false); } }; const handleReset = async () => { if (!confirm('Reset AI settings to defaults? The system prompt addendum will be cleared.')) { return; } setIsResetting(true); try { await apiClient.post('/api/config/ai/reset'); notify.info('AI reset', 'Provider, model, and prompt have been restored to defaults.'); await loadConfig(); } catch (err) { console.error('[ai] reset failed', err); } finally { setIsResetting(false); } }; return (

API keys live in environment variables

The actual OPENAI_API_KEY and ANTHROPIC_API_KEY are set at deploy time and can't be edited here. If you change the provider, make sure the matching key is configured on the sidecar or the assistant will silently fall back to the other provider.

{loading ? (

Loading AI settings...

) : (
)}
); };