'use client'; import { useCallback, useEffect, useState } from 'react'; import { api } from '@/lib/api'; import { authClient, useSession } from '@/lib/auth-client'; import type { SsoProviderDiscovery } from '@/lib/sso'; import { SsoProviderSection } from '@/components/settings/sso-provider-section'; // ─── Types ──────────────────────────────────────────────────────────────────── interface ModelInfo { id: string; provider: string; name: string; reasoning: boolean; contextWindow: number; maxTokens: number; inputTypes: ('text' | 'image')[]; cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; } interface ProviderInfo { id: string; name: string; available: boolean; models: ModelInfo[]; } interface TestConnectionResult { providerId: string; reachable: boolean; latencyMs?: number; error?: string; discoveredModels?: string[]; } type TestState = 'idle' | 'testing' | 'success' | 'error'; interface ProviderTestStatus { state: TestState; result?: TestConnectionResult; } interface Preference { key: string; value: unknown; category: string; } type Theme = 'light' | 'dark' | 'system'; type SaveState = 'idle' | 'saving' | 'saved' | 'error'; type Tab = 'profile' | 'appearance' | 'notifications' | 'providers'; // ─── Helpers ────────────────────────────────────────────────────────────────── function prefValue(prefs: Preference[], key: string, fallback: T): T { const p = prefs.find((x) => x.key === key); if (p === undefined) return fallback; return p.value as T; } // ─── Main Page ──────────────────────────────────────────────────────────────── export default function SettingsPage(): React.ReactElement { const { data: session } = useSession(); const [activeTab, setActiveTab] = useState('profile'); const tabs: { id: Tab; label: string }[] = [ { id: 'profile', label: 'Profile' }, { id: 'appearance', label: 'Appearance' }, { id: 'notifications', label: 'Notifications' }, { id: 'providers', label: 'Providers' }, ]; return (

Settings

{/* Tab bar */}
{tabs.map((tab) => ( ))}
{activeTab === 'profile' && } {activeTab === 'appearance' && } {activeTab === 'notifications' && } {activeTab === 'providers' && }
); } // ─── Profile Tab ────────────────────────────────────────────────────────────── function ProfileTab({ session, }: { session: { user: { id: string; name: string; email: string; image?: string | null } } | null; }): React.ReactElement { const [name, setName] = useState(session?.user.name ?? ''); const [image, setImage] = useState(session?.user.image ?? ''); const [saveState, setSaveState] = useState('idle'); const [errorMsg, setErrorMsg] = useState(''); // Sync from session when it loads useEffect(() => { if (session?.user) { setName(session.user.name ?? ''); setImage(session.user.image ?? ''); } }, [session]); const handleSave = async (): Promise => { setSaveState('saving'); setErrorMsg(''); try { const result = await authClient.updateUser({ name, image: image || null }); if (result.error) { setErrorMsg(result.error.message ?? 'Failed to update profile'); setSaveState('error'); return; } setSaveState('saved'); setTimeout(() => setSaveState('idle'), 2000); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to update profile'; setErrorMsg(message); setSaveState('error'); } }; return (

Profile

setName(e.target.value)} placeholder="Your name" className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent" />

Email cannot be changed here.

setImage(e.target.value)} placeholder="https://example.com/avatar.png" className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent" />
{saveState === 'error' && errorMsg &&

{errorMsg}

}
); } // ─── Appearance Tab ─────────────────────────────────────────────────────────── function AppearanceTab(): React.ReactElement { const [loading, setLoading] = useState(true); const [theme, setTheme] = useState('system'); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [defaultModel, setDefaultModel] = useState(''); const [saveState, setSaveState] = useState('idle'); const [errorMsg, setErrorMsg] = useState(''); useEffect(() => { api('/api/memory/preferences?category=appearance') .catch(() => [] as Preference[]) .then((p) => { setTheme(prefValue(p, 'ui.theme', 'system')); setSidebarCollapsed(prefValue(p, 'ui.sidebar_collapsed', false)); setDefaultModel(prefValue(p, 'ui.default_model', '')); }) .finally(() => setLoading(false)); }, []); const handleSave = async (): Promise => { setSaveState('saving'); setErrorMsg(''); try { await Promise.all([ api('/api/memory/preferences', { method: 'POST', body: { key: 'ui.theme', value: theme, category: 'appearance', source: 'user' }, }), api('/api/memory/preferences', { method: 'POST', body: { key: 'ui.sidebar_collapsed', value: sidebarCollapsed, category: 'appearance', source: 'user', }, }), ...(defaultModel ? [ api('/api/memory/preferences', { method: 'POST', body: { key: 'ui.default_model', value: defaultModel, category: 'appearance', source: 'user', }, }), ] : []), ]); setSaveState('saved'); setTimeout(() => setSaveState('idle'), 2000); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to save preferences'; setErrorMsg(message); setSaveState('error'); } }; if (loading) { return (

Appearance

Loading preferences...

); } return (

Appearance

{/* Theme */}
{(['system', 'light', 'dark'] as Theme[]).map((t) => ( ))}
{/* Sidebar collapsed default */}

Collapse sidebar by default

Start with sidebar collapsed on page load

{/* Default model */} setDefaultModel(e.target.value)} placeholder="e.g. ollama/llama3.2" className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent" />

Model ID to pre-select for new conversations.

{saveState === 'error' && errorMsg &&

{errorMsg}

}
); } // ─── Notifications Tab ──────────────────────────────────────────────────────── function NotificationsTab(): React.ReactElement { const [loading, setLoading] = useState(true); const [emailAgentComplete, setEmailAgentComplete] = useState(false); const [emailMentions, setEmailMentions] = useState(true); const [emailDigest, setEmailDigest] = useState(false); const [saveState, setSaveState] = useState('idle'); const [errorMsg, setErrorMsg] = useState(''); useEffect(() => { api('/api/memory/preferences?category=communication') .catch(() => [] as Preference[]) .then((p) => { setEmailAgentComplete(prefValue(p, 'notify.email_agent_complete', false)); setEmailMentions(prefValue(p, 'notify.email_mentions', true)); setEmailDigest(prefValue(p, 'notify.email_digest', false)); }) .finally(() => setLoading(false)); }, []); const handleSave = async (): Promise => { setSaveState('saving'); setErrorMsg(''); try { await Promise.all([ api('/api/memory/preferences', { method: 'POST', body: { key: 'notify.email_agent_complete', value: emailAgentComplete, category: 'communication', source: 'user', }, }), api('/api/memory/preferences', { method: 'POST', body: { key: 'notify.email_mentions', value: emailMentions, category: 'communication', source: 'user', }, }), api('/api/memory/preferences', { method: 'POST', body: { key: 'notify.email_digest', value: emailDigest, category: 'communication', source: 'user', }, }), ]); setSaveState('saved'); setTimeout(() => setSaveState('idle'), 2000); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to save preferences'; setErrorMsg(message); setSaveState('error'); } }; if (loading) { return (

Notifications

Loading preferences...

); } return (

Notifications

Configure when you receive email notifications.

{saveState === 'error' && errorMsg &&

{errorMsg}

}
); } // ─── Providers Tab ──────────────────────────────────────────────────────────── function ProvidersTab(): React.ReactElement { const [providers, setProviders] = useState([]); const [ssoProviders, setSsoProviders] = useState([]); const [loading, setLoading] = useState(true); const [ssoLoading, setSsoLoading] = useState(true); const [testStatuses, setTestStatuses] = useState>({}); useEffect(() => { api('/api/providers') .catch(() => [] as ProviderInfo[]) .then((p) => setProviders(p)) .finally(() => setLoading(false)); }, []); useEffect(() => { api('/api/sso/providers') .catch(() => [] as SsoProviderDiscovery[]) .then((providers) => setSsoProviders(providers)) .finally(() => setSsoLoading(false)); }, []); const testConnection = useCallback(async (providerId: string): Promise => { setTestStatuses((prev) => ({ ...prev, [providerId]: { state: 'testing' }, })); try { const result = await api('/api/providers/test', { method: 'POST', body: { providerId }, }); setTestStatuses((prev) => ({ ...prev, [providerId]: { state: result.reachable ? 'success' : 'error', result }, })); } catch { setTestStatuses((prev) => ({ ...prev, [providerId]: { state: 'error', result: { providerId, reachable: false, error: 'Request failed' }, }, })); } }, []); const defaultModel: ModelInfo | undefined = providers .flatMap((p) => p.models) .find((m) => providers.find((p) => p.id === m.provider)?.available); return (

SSO Providers

LLM Providers

{loading ? (

Loading providers...

) : providers.length === 0 ? (

No providers configured. Set{' '} OLLAMA_BASE_URL {' '} or{' '} MOSAIC_CUSTOM_PROVIDERS {' '} to add providers.

) : (
{providers.map((provider) => ( void testConnection(provider.id)} /> ))}
)}
); } // ─── Shared UI Components ───────────────────────────────────────────────────── function FormField({ label, id, children, }: { label: string; id: string; children: React.ReactNode; }): React.ReactElement { return (
{children}
); } function Toggle({ checked, onChange, }: { checked: boolean; onChange: (v: boolean) => void; }): React.ReactElement { return ( ); } function NotifyRow({ label, description, checked, onChange, }: { label: string; description: string; checked: boolean; onChange: (v: boolean) => void; }): React.ReactElement { return (

{label}

{description}

); } function SaveButton({ state, onClick, }: { state: SaveState; onClick: () => void; }): React.ReactElement { return ( ); } // ─── Provider Card (from original page) ────────────────────────────────────── interface ProviderCardProps { provider: ProviderInfo; defaultModel: ModelInfo | undefined; testStatus: ProviderTestStatus; onTest: () => void; } function ProviderCard({ provider, defaultModel, testStatus, onTest, }: ProviderCardProps): React.ReactElement { const [expanded, setExpanded] = useState(false); return (
{/* Header row */}
{provider.name}

{provider.models.length} model{provider.models.length !== 1 ? 's' : ''}

{/* Test result banner */} {testStatus.state !== 'idle' && testStatus.state !== 'testing' && testStatus.result && ( )} {/* Model list */} {expanded && (
{provider.models.map((model) => ( ))}
Model Capabilities Context Cost (in/out) Default
)}
); } interface ModelRowProps { model: ModelInfo; isDefault: boolean; } function ModelRow({ model, isDefault }: ModelRowProps): React.ReactElement { return ( {model.name}
{model.reasoning && } {model.inputTypes.includes('image') && }
{formatContext(model.contextWindow)} {model.cost.input === 0 && model.cost.output === 0 ? 'free' : `$${model.cost.input} / $${model.cost.output}`} {isDefault && ( default )} ); } function ProviderAvatar({ id }: { id: string }): React.ReactElement { const letter = id.charAt(0).toUpperCase(); return (
{letter}
); } function ProviderStatusBadge({ available }: { available: boolean }): React.ReactElement { return ( {available ? 'Active' : 'Inactive'} ); } interface TestConnectionButtonProps { status: ProviderTestStatus; onTest: () => void; } function TestConnectionButton({ status, onTest }: TestConnectionButtonProps): React.ReactElement { const isTesting = status.state === 'testing'; return ( ); } function TestResultBanner({ result }: { result: TestConnectionResult }): React.ReactElement { return (
{result.reachable ? ( <> Connected {result.latencyMs !== undefined && ( ({result.latencyMs}ms) )} {result.discoveredModels && result.discoveredModels.length > 0 && ( — {result.discoveredModels.length} model {result.discoveredModels.length !== 1 ? 's' : ''} discovered )} ) : ( <>Connection failed{result.error ? `: ${result.error}` : ''} )}
); } function CapabilityBadge({ label, color = 'default', }: { label: string; color?: 'default' | 'purple' | 'blue'; }): React.ReactElement { const colorClass = color === 'purple' ? 'bg-purple-500/20 text-purple-400' : color === 'blue' ? 'bg-blue-500/20 text-blue-400' : 'bg-surface-elevated text-text-muted'; return {label}; } function formatContext(tokens: number): string { if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`; if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`; return String(tokens); }