import { useEffect, useState } from "react"; import { useStore } from "../store/useStore"; import { applyPreset, deleteModel, getBenchScores, getHardware, listMcp, listPresets, pullModel, runBench, savePreset, testMcp, updateMcp, } from "../api/client"; import type { AgentConfig, BenchResult, HardwareInfo, McpServer, } from "../api/client"; import { DownloadIcon, RefreshIcon } from "../components/Icon"; const TOOL_DESC: Record = { read_file: "Lire un fichier du projet", write_file: "Créer / modifier un fichier", edit_file: "Modification chirurgicale (recherche/remplacement)", list_dir: "Lister un répertoire", grep_search: "Chercher dans les fichiers", code_task: "Moteur code (multi-fichiers, git)", web_search: "Recherche web", run_shell: "Exécuter une commande", }; const SENSITIVE = new Set(["run_shell"]); /** Vue Configuration — Modèle & génération, outils, invite système. */ export function SettingsView() { const { models, selectedModel, setSelectedModel, refreshModels, loadedModels, warmingModel, warmError, status, config, availableTools, refreshConfig, updateConfig, } = useStore(); const [draft, setDraft] = useState(config); const [saved, setSaved] = useState(false); useEffect(() => { refreshConfig(); }, [refreshConfig, selectedModel]); useEffect(() => { if (config) setDraft(config); }, [config]); const [pullName, setPullName] = useState(""); const [pullStatus, setPullStatus] = useState(null); const [pullPct, setPullPct] = useState(0); const [deletingModel, setDeletingModel] = useState(null); const [deleteError, setDeleteError] = useState(null); const doDeleteModel = async (name: string) => { if (!window.confirm(`Supprimer définitivement le modèle ${name} ?`)) return; setDeletingModel(name); setDeleteError(null); try { await deleteModel(name); await refreshModels(); } catch (err) { setDeleteError(err instanceof Error ? err.message : "suppression impossible"); } finally { setDeletingModel(null); } }; const doPull = async () => { if (!pullName.trim()) return; setPullStatus("démarrage…"); setPullPct(0); try { await pullModel(pullName.trim(), (s, p) => { setPullStatus(s); if (p) setPullPct(p); }); setPullStatus("terminé"); await refreshModels(); } catch { setPullStatus("échec"); } }; const dirty = draft && config && JSON.stringify(draft) !== JSON.stringify(config); const save = async () => { if (!draft) return; await updateConfig(draft); setSaved(true); setTimeout(() => setSaved(false), 2000); }; const set = (key: K, value: AgentConfig[K]) => setDraft((d) => (d ? { ...d, [key]: value } : d)); const activeTools = draft ? Object.values(draft.tools).filter(Boolean).length : 0; return (
{/* En-tête */}
MODÈLE & GÉNÉRATION
Choisis le modèle local et règle son comportement. Les changements sont sauvegardés pour le modèle sélectionné.
{status?.connected ? "Ollama connecté" : "Ollama déconnecté"}
{!draft ? (
Chargement de la configuration…
) : ( <>
{/* Modèle Ollama */}
MODÈLE OLLAMA
{models.length === 0 && (
Aucun modèle installé. Télécharge-en un ci-dessous.
)} {models.map((m) => { const on = m.name === selectedModel; const loaded = loadedModels.find((item) => item.name === m.name); const warming = warmingModel === m.name; return (
); })} {deleteError && (
{deleteError}
)} {warmError && (
Préchargement : {warmError}
)}
TÉLÉCHARGER
setPullName(e.target.value)} placeholder="gemma2:9b" className="h-9 flex-1 border-[3px] border-line bg-card-soft px-3 text-[13px] text-ink outline-none placeholder:text-muted-3" />
{pullStatus && (
{pullName} {pullStatus} {pullPct ? `· ${pullPct}%` : ""}
)} {/* Génération + Outils */}
GÉNÉRATION
{/* Les deux seuls réglages qui comptent au quotidien. */} v.toFixed(2)} onChange={(v) => set("temperature", v)} /> `${Math.round(v / 1024)}K`} onChange={(v) => set("num_ctx", Math.round(v))} /> v.toFixed(2)} onChange={(v) => set("top_p", v)} /> String(Math.round(v))} onChange={(v) => set("top_k", Math.round(v))} /> String(Math.round(v))} onChange={(v) => set("max_tokens", Math.round(v))} /> (v < 0 ? "auto" : String(Math.round(v)))} onChange={(v) => set("num_gpu", Math.round(v))} />
Laisse « auto » : forcer un nombre de couches qui ne tient pas en VRAM bascule toute l'inférence sur le CPU.
String(Math.round(v))} onChange={(v) => set("num_batch", Math.round(v))} last />
La précision KV (f16/q8_0) est un réglage global du serveur Ollama et nécessite son redémarrage.
Mode réflexion
Désactive si le modèle ne renvoie que du raisonnement sans réponse.
set("think", !draft.think)} />
OUTILS
{activeTools} / {availableTools.length} actifs
{availableTools.map((name, i) => { const on = draft.tools[name] ?? false; return (
0 ? "border-t-2 border-line-soft" : "" }`} > {name} {TOOL_DESC[name] ?? ""} {SENSITIVE.has(name) && ( · sensible )} set("tools", { ...draft.tools, [name]: !on })} />
); })} {draft.tools.run_shell && (
Demander une validation avant chaque commande shell set("confirm_shell", !draft.confirm_shell)} />
)}
INTELLIGENCE
{/* Au quotidien : comment il code, et s'il planifie. */} {([ ["ponytail", "Code minimal", "Va au plus simple qui marche, sans dépendance ni extra"] as const, ["plan_mode", "Plan-puis-exécute", "Découpe une nouvelle construction en étapes"] as const, ]).map(([key, label, desc], i) => ( set(key, !draft[key])} /> ))} {([ ["skills_enabled", "Skills automatiques", "Méthodes expertes selon la tâche (débogage, web…)"] as const, ["self_review", "Auto-critique", "Relit et révise la réponse — plus lent"] as const, ]).map(([key, label, desc], i) => ( set(key, !draft[key])} /> ))}
Mémoire Notes Markdown que l'agent écrit lui-même, relisibles dans /data/MEMORY
Maintien en VRAM (préchargement) Garde le modèle chargé pour des réponses instantanées
{/* Invite système */}
INVITE SYSTÈME
{draft.system_prompt.length} caractères