"use client"; import Link from "next/link"; import { useEffect, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { BarChart3, Save, RotateCcw, ExternalLink } from "lucide-react"; import { api } from "@/lib/client/api"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input, Label } from "@/components/ui/input"; import { Spinner, Switch, Badge } from "@/components/ui/primitives"; import { toast } from "@/components/ui/toast"; import type { SettingsProfile } from "./SettingsView"; type Route = { primary: string; fallback: string; effort: "low" | "medium" | "high"; categories: string[]; avoid?: string[] }; interface ModelsResp { models: { id: string; provider: string; available: boolean; enabled: boolean; benchmark?: { pass_rate?: number } | null }[]; routes: Record; defaultRoutes: Record; taskLabels: Record } const TASK_MODES = [["auto", "Auto (classified per message)"], ["chat", "Chat"], ["write", "Write"], ["reason", "Reason"], ["deep", "Deep"], ["code", "Code"]] as const; export function AISection({ profile, refresh }: { profile: SettingsProfile; refresh: () => void }) { const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ["ai-models"], queryFn: () => api("/api/ai/models"), staleTime: 60_000 }); const [routes, setRoutes] = useState | null>(null); const [saving, setSaving] = useState(false); useEffect(() => { if (data && !routes) setRoutes(data.routes); }, [data, routes]); const [budget, setBudget] = useState(String(profile.aiBudgetUsd)); useEffect(() => setBudget(String(profile.aiBudgetUsd)), [profile.aiBudgetUsd]); const prefs = profile.preferences as { aiDefaultTask?: string; aiAlwaysCite?: boolean }; async function savePref(p: Record) { try { await api("/api/settings", { method: "PATCH", json: { preferences: p } }); refresh(); } catch (e: any) { toast("Could not save", { description: e.message, tone: "danger" }); } } async function saveBudget() { const usd = Number(budget); if (!Number.isFinite(usd) || usd < 0) return; try { await api("/api/ai/models", { method: "POST", json: { action: "budget", usd } }); toast("Daily budget saved", { tone: "success" }); refresh(); qc.invalidateQueries({ queryKey: ["dashboard"] }); } catch (e: any) { toast("Could not save budget", { description: e.message, tone: "danger" }); } } async function saveRoutes(next: Record | Record) { setSaving(true); try { await api("/api/ai/models", { method: "POST", json: { action: "routes", routes: next } }); toast(Object.keys(next).length ? "Routes saved" : "Routes reset to measured defaults", { tone: "success" }); qc.invalidateQueries({ queryKey: ["ai-models"] }); setRoutes(null); } catch (e: any) { toast("Could not save routes", { description: e.message, tone: "danger" }); } finally { setSaving(false); } } const dirty = !!data && !!routes && JSON.stringify(routes) !== JSON.stringify(data.routes); const models = (data?.models ?? []).filter((m) => m.available && m.enabled); const Sel = ({ value, onChange, allowEmpty }: { value: string; onChange: (v: string) => void; allowEmpty?: boolean }) => ; return (

Assistant defaults

Task class pre-selected in new AI conversations.

setBudget(e.target.value)} className="w-32" onKeyDown={(e) => { if (e.key === "Enter") saveBudget(); }} />

Above this spend, Deep and Reason requests are downgraded to cheaper classes for the rest of the day.

Model routes per task class

Override the measured defaults. Primary is tried first, then the fallback, then the best measured eligible model.

Models & benchmarks
{isLoading || !routes ?
: (
{Object.entries(routes).map(([task, r]) => { const d = data!.defaultRoutes[task]; const changed = d && (d.primary !== r.primary || d.fallback !== r.fallback || d.effort !== r.effort); return ( ); })}
TaskPrimaryFallbackEffort
{data!.taskLabels[task] ?? task}
{task}
setRoutes({ ...routes, [task]: { ...r, primary: v } })} /> setRoutes({ ...routes, [task]: { ...r, fallback: v } })} /> {changed ? : default}
)}
Unsaved changes
); }