"use client"; import { useEffect, useState, type FormEvent, type ReactNode } from "react"; import useSWR from "swr"; import { Monitor, Moon, Sun, LogOut, ShieldCheck, Smartphone, Download } from "lucide-react"; import { api, fetcher, relTime } from "@/lib/client/fetcher"; import { usePrefs, setPrefs, type Prefs } from "@/lib/client/prefs"; import { useModels } from "@/lib/client/use-models"; import { Badge, Button, Input, Toggle, Spinner, cn, toast } from "../ui"; export function Section({ id, title, hint, children }: { id: string; title: string; hint?: string; children: ReactNode }) { return

{title}

{hint &&

{hint}

}
{children}
; } export function Row({ label, hint, children }: { label: string; hint?: string; children: ReactNode }) { return
{label}{hint && {hint}}{children}
; } // ---------------------------------------------------------------- Apparence export function AppearanceSection() { const prefs = usePrefs(); const themes: { v: Prefs["theme"]; l: string; I: typeof Sun }[] = [{ v: "dark", l: "Sombre", I: Moon }, { v: "light", l: "Clair", I: Sun }, { v: "system", l: "Système", I: Monitor }]; return (
{themes.map(t => )}
setPrefs({ sendWithEnter: v })} /> setPrefs({ showReasoning: v })} />
); } // ---------------------------------------------------------------- Modèles & routage export function ModelsSection({ routing, onSave }: { routing: { defaultMode?: string }; onSave: (key: string, value: unknown) => Promise }) { const prefs = usePrefs(); const { chat, defaultModel, mutate } = useModels(); const [saving, setSaving] = useState(false); async function setDefault(id: string) { setSaving(true); try { await api("/api/models", { method: "PATCH", json: { id, default: true } }); await mutate(); toast("Modèle par défaut enregistré", "ok"); } finally { setSaving(false); } } const modes = [{ v: "manual", l: "Manuel" }, { v: "auto", l: "Automatique" }, { v: "fast", l: "Rapide" }, { v: "quality", l: "Qualité" }, { v: "economy", l: "Économie" }]; return (
{saving && }
Détails, favoris, masquage et résultats de benchmark : Laboratoire de modèles.
); } // ---------------------------------------------------------------- Dépenses export interface Spending { dailyLimitUsd: number; monthlyLimitUsd: number; alertAtPercent: number } export function SpendingSection({ value, onSave }: { value: Spending; onSave: (key: string, value: unknown) => Promise }) { const [v, setV] = useState(value); const [busy, setBusy] = useState(false); useEffect(() => setV(value), [value]); const num = (k: keyof Spending) => (e: React.ChangeEvent) => setV(x => ({ ...x, [k]: Number(e.target.value) })); return (

Mettre 0 désactive la limite. Ce suivi est une estimation locale : la facturation des fournisseurs peut inclure d'autres usages (autres applications, Claude Code, etc.). Détails dans Coûts & usage.

); } // ---------------------------------------------------------------- Outils & intégrations export interface ToolInfo { name: string; description: string; permission: string; category: string } export function ToolsSection({ tools }: { tools: ToolInfo[] }) { return (
{tools.map(t =>
{t.name}{t.description}{t.permission === "auto" ? "auto" : "approbation"}
)}

L'outil Python s'exécute dans un bac à sable (sandbox-exec, sans réseau, 20 s, répertoire temporaire isolé). Aucun outil n'a d'accès shell au cluster. Les permissions par projet se règlent dans chaque projet.

); } export function IntegrationsSection({ integrations }: { integrations: { openai: boolean; anthropic: boolean; tavily: boolean } }) { const items = [{ k: "openai", l: "OpenAI", d: "Modèles GPT / o-series, images, voix, embeddings" }, { k: "anthropic", l: "Anthropic", d: "Modèles Claude" }, { k: "tavily", l: "Tavily", d: "Recherche web et extraction de pages" }] as const; return (
{items.map(i => {integrations[i.k] ? "configurée" : "absente"})} à venir
); } // ---------------------------------------------------------------- Sécurité interface SessionInfo { id: string; createdAt: string; expiresAt: string; lastSeen: string; userAgent: string | null; ip: string | null; current: boolean } export function SecuritySection({ owner }: { owner: { username: string; createdAt: string; totp: boolean } | null }) { const { data, mutate } = useSWR<{ sessions: SessionInfo[] }>("/api/auth/sessions", fetcher); const [cur, setCur] = useState(""); const [next, setNext] = useState(""); const [confirm2, setConfirm2] = useState(""); const [busy, setBusy] = useState(false); async function changePw(e: FormEvent) { e.preventDefault(); if (next !== confirm2) { toast("Les mots de passe ne correspondent pas", "error"); return; } setBusy(true); try { const r = await api<{ note?: string }>("/api/settings", { method: "PATCH", json: { password: { current: cur, next } } }); toast(r.note ?? "Mot de passe changé", "ok"); setCur(""); setNext(""); setConfirm2(""); void mutate(); } catch (err) { toast((err as Error).message, "error"); } finally { setBusy(false); } } async function revoke(id?: string) { await api("/api/auth/sessions", { method: "DELETE", json: id ? { id } : { all: true } }); toast(id ? "Session révoquée" : "Autres sessions révoquées", "ok"); void mutate(); } async function logout() { await api("/api/auth/logout", { method: "POST" }); navigator.serviceWorker?.controller?.postMessage("logout"); location.href = "/login"; } const ua = (s: string | null) => { if (!s) return "appareil inconnu"; const m = /(iPhone|iPad|Android|Macintosh|Windows|Linux)/.exec(s)?.[1] ?? "navigateur"; const b = /(Safari|Chrome|Firefox|Edg)/.exec(s)?.[1] ?? ""; return `${m}${b ? ` · ${b}` : ""}`; }; return (
{owner?.username ?? "—"} {owner?.totp ? "activée" : "désactivée"}
Changer le mot de passe
setCur(e.target.value)} required /> setNext(e.target.value)} required /> setConfirm2(e.target.value)} required />
Toutes les autres sessions seront révoquées.
Sessions actives{(data?.sessions.length ?? 0) > 1 && }
{!data && }
{data?.sessions.map(s => (
{ua(s.userAgent)}{s.current && cette session}{s.ip ?? "IP inconnue"} · vue {relTime(s.lastSeen)} · expire {new Date(s.expiresAt).toLocaleDateString("fr-CA")} {!s.current && }
))}
); } // ---------------------------------------------------------------- PWA interface BeforeInstallPromptEvent extends Event { prompt(): Promise; userChoice: Promise<{ outcome: "accepted" | "dismissed" }> } export function InstallHint() { const [evt, setEvt] = useState(null); useEffect(() => { const h = (e: Event) => { e.preventDefault(); setEvt(e as BeforeInstallPromptEvent); }; window.addEventListener("beforeinstallprompt", h); return () => window.removeEventListener("beforeinstallprompt", h); }, []); if (!evt) return null; return
Installe Chat SPBoucher AI comme application (écran d'accueil, plein écran, démarrage rapide).
; }