Add admin AI models panel (list, test, select Mistral models)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
defc07648c
commit
13677b3ce3
4 files changed
+162
-5
No files matched your search
@@ -670,13 +670,78 @@ function BlacklistPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
function AiModelsPanel() {
|
||||
const { toast } = useToast();
|
||||
const [data, setData] = useState<{ filter: string; synthesis: string; available: string[]; error: string | null } | null>(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [synthesis, setSynthesis] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const res = await adminFetch("/api/admin/ai/models");
|
||||
if (!res.ok) return;
|
||||
const d = await res.json();
|
||||
setData(d); setFilter(d.filter); setSynthesis(d.synthesis);
|
||||
}, []);
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const test = async (model: string) => {
|
||||
setBusy(true);
|
||||
const r = await adminFetch("/api/admin/ai/test", { method: "POST", body: JSON.stringify({ model }) });
|
||||
const d = await r.json().catch(() => ({}));
|
||||
setBusy(false);
|
||||
toast(d.ok
|
||||
? { title: "Modèle OK", description: model }
|
||||
: { title: "Échec", description: d.error ?? `Erreur ${r.status}`, variant: "destructive" });
|
||||
return !!d.ok;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setBusy(true);
|
||||
const r = await adminFetch("/api/admin/ai/models", { method: "POST", body: JSON.stringify({ filter, synthesis }) });
|
||||
setBusy(false);
|
||||
toast(r.ok ? { title: "Modèles enregistrés" } : { title: "Erreur", variant: "destructive" });
|
||||
if (r.ok) load();
|
||||
};
|
||||
|
||||
const opts = data?.available ?? [];
|
||||
const field = (label: string, value: string, set: (v: string) => void) => (
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs text-muted-foreground">{label}</label>
|
||||
<div className="flex gap-2">
|
||||
<Input list="ai-models" value={value} onChange={e => set(e.target.value)} className="font-mono text-xs" />
|
||||
<Button size="sm" variant="outline" disabled={busy || !value} onClick={() => test(value)}>Tester</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="font-serif font-semibold text-lg text-primary">Modèles IA</h2>
|
||||
{data?.error && (
|
||||
<div className="text-xs border border-destructive/40 rounded-md p-3 bg-destructive/5 text-destructive">
|
||||
Clé API inutilisable ou fournisseur injoignable : <span className="font-mono">{data.error}</span>
|
||||
<p className="mt-1 text-muted-foreground">La clé se change dans <code>MISTRAL_API_KEY</code> (.env) puis redémarrage de l'API.</p>
|
||||
</div>
|
||||
)}
|
||||
<datalist id="ai-models">{opts.map(m => <option key={m} value={m} />)}</datalist>
|
||||
{field("Modèle de filtrage (rapide)", filter, setFilter)}
|
||||
{field("Modèle de synthèse (qualité)", synthesis, setSynthesis)}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button size="sm" onClick={save} disabled={busy}>Enregistrer</Button>
|
||||
<span className="text-xs text-muted-foreground">{opts.length} modèle(s) accessible(s) avec la clé actuelle</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Composant principal Admin ────────────────────────────────────────────────
|
||||
|
||||
export default function Admin() {
|
||||
const { authed, login, logout } = useAdminAuth();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [activePanel, setActivePanel] = useState<"contributions" | "consultations" | "blacklist">("contributions");
|
||||
const [activePanel, setActivePanel] = useState<"contributions" | "consultations" | "blacklist" | "ia">("contributions");
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [list, setList] = useState<IdeaList | null>(null);
|
||||
const [status, setStatus] = useState("all");
|
||||
@@ -875,6 +940,16 @@ export default function Admin() {
|
||||
>
|
||||
<ShieldBan className="h-3 w-3" /> Blacklist
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActivePanel("ia")}
|
||||
className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${
|
||||
activePanel === "ia"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
IA
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
@@ -904,6 +979,8 @@ export default function Admin() {
|
||||
<ConsultationsPanel />
|
||||
)}
|
||||
|
||||
{activePanel === "ia" && <AiModelsPanel />}
|
||||
|
||||
{/* ─── Panel Blacklist IP ─── */}
|
||||
{activePanel === "blacklist" && (
|
||||
<BlacklistPanel />
|
||||
|
||||
Reference in new issue
Block a user