"use client"; // Modèles : catalogue OpenRouter avec surcharges (activer/désactiver, favori, note) // et éditeur des préréglages ordonnés. import { useMemo, useState } from "react"; import { ArrowDown, ArrowUp, Check, Pencil, Plus, Search, Sparkles, Star, X } from "lucide-react"; import { PageHeader } from "@/components/app-shell"; import { Badge, Button, Card, EmptyState, Input, Skeleton, Spinner, Tabs, cn } from "@/components/ui"; import { ErrorBanner, SectionTitle, SuccessFlash, Toggle, fmtInt, postJson, useFetchJson } from "./shared"; type ModelInfo = { id: string; name: string; provider: string; description: string; contextLength: number; pricing: { prompt: number; completion: number }; supportsImages: boolean; supportsFiles: boolean; supportsTools: boolean; supportsReasoning: boolean; supportsStructured: boolean; isFree: boolean; costTier: "économique" | "modéré" | "coûteux"; enabled: boolean; favorite: boolean; note: string; }; type Preset = { label: string; models: string[]; description: string }; type ModelsResponse = { models: ModelInfo[]; presets: Record; defaultPresets: Record }; const FILTERS = [ { key: "all", label: "Tous" }, { key: "enabled", label: "Activés" }, { key: "disabled", label: "Désactivés" }, { key: "vision", label: "Vision" }, { key: "free", label: "Gratuits" }, ]; const TIER_TONE: Record = { "économique": "green", "modéré": "amber", "coûteux": "red", }; const TIER_LABEL: Record = { "économique": "Économique", "modéré": "Modéré", "coûteux": "Coûteux", }; const PAGE_SIZE = 50; export function AdminModels() { const { data, error, loading, reload, setData } = useFetchJson("/api/admin/models"); const [search, setSearch] = useState(""); const [filter, setFilter] = useState("all"); const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); const [actionError, setActionError] = useState(null); const [editingNote, setEditingNote] = useState(null); const [noteDraft, setNoteDraft] = useState(""); const [savingNote, setSavingNote] = useState(false); // Préréglages en édition locale const [presets, setPresets] = useState | null>(null); const [savingPresets, setSavingPresets] = useState(false); const [presetsFlash, setPresetsFlash] = useState(null); const effectivePresets = presets ?? data?.presets ?? null; const models = useMemo(() => data?.models ?? [], [data]); const filtered = useMemo(() => { const q = search.trim().toLowerCase(); return models.filter((m) => { if (filter === "enabled" && !m.enabled) return false; if (filter === "disabled" && m.enabled) return false; if (filter === "vision" && !m.supportsImages) return false; if (filter === "free" && !m.isFree) return false; if (q && !m.name.toLowerCase().includes(q) && !m.id.toLowerCase().includes(q) && !m.provider.toLowerCase().includes(q)) return false; return true; }); }, [models, search, filter]); async function override(modelId: string, patch: { enabled?: boolean; favorite?: boolean; note?: string }) { setActionError(null); const before = models; // Mise à jour optimiste setData((d) => d ? { ...d, models: d.models.map((m) => (m.id === modelId ? { ...m, ...patch } : m)) } : d ); try { await postJson("/api/admin/models", { action: "override", modelId, ...patch }); } catch (e) { setData((d) => (d ? { ...d, models: before } : d)); setActionError(e instanceof Error ? e.message : "L'action a échoué."); } } async function saveNote(modelId: string) { setSavingNote(true); await override(modelId, { note: noteDraft.slice(0, 300) }); setSavingNote(false); setEditingNote(null); } async function savePresets() { if (!effectivePresets) return; setSavingPresets(true); setActionError(null); setPresetsFlash(null); try { await postJson("/api/admin/models", { action: "presets", presets: effectivePresets }); setPresetsFlash("Préréglages enregistrés."); setTimeout(() => setPresetsFlash(null), 3500); } catch (e) { setActionError(e instanceof Error ? e.message : "L'enregistrement des préréglages a échoué."); } finally { setSavingPresets(false); } } function mutatePreset(key: string, fn: (p: Preset) => Preset) { if (!effectivePresets) return; setPresets({ ...effectivePresets, [key]: fn(effectivePresets[key]) }); } if (loading) { return (
); } if (error || !data) { return (
); } const visible = filtered.slice(0, visibleCount); return (
m.enabled).length)} activés`} /> {actionError &&
}
{ setSearch(e.target.value); setVisibleCount(PAGE_SIZE); }} placeholder="Rechercher un modèle…" className="pl-9" aria-label="Rechercher un modèle" />
{ setFilter(k); setVisibleCount(PAGE_SIZE); }} />
{filtered.length === 0 ? ( } title="Aucun modèle ne correspond" description="Modifiez la recherche ou le filtre." /> ) : (
{visible.map((m) => ( ))}
Modèle Fournisseur Entrée / sortie ($/M) Contexte Capacités Palier Activé Favori Note

{m.name}

{m.id}

{m.provider} {m.isFree ? Gratuit : `${m.pricing.prompt.toFixed(2)} / ${m.pricing.completion.toFixed(2)}`} {Math.round(m.contextLength / 1000)} k
{m.supportsImages && Vision} {m.supportsFiles && Fichiers} {m.supportsReasoning && Raisonnement} {m.supportsTools && Outils}
{TIER_LABEL[m.costTier]} override(m.id, { enabled: v })} label={`Activer ${m.name}`} /> {editingNote === m.id ? (
setNoteDraft(e.target.value)} maxLength={300} autoFocus className="h-8 text-[12.5px]" onKeyDown={(e) => { if (e.key === "Enter") saveNote(m.id); if (e.key === "Escape") setEditingNote(null); }} aria-label={`Note pour ${m.name}`} />
) : ( )}
{filtered.length > visibleCount && (
)}
)} {/* ---------------- Préréglages ---------------- */}
Préréglages
{!effectivePresets ? ( } title="Aucun préréglage" description="Les préréglages par défaut apparaîtront après le premier chargement des modèles." /> ) : (
{Object.entries(effectivePresets).map(([key, preset]) => ( mutatePreset(key, fn)} /> ))}
)}
); } function PresetEditor({ presetKey, preset, models, onChange, }: { presetKey: string; preset: Preset; models: ModelInfo[]; onChange: (fn: (p: Preset) => Preset) => void; }) { const [query, setQuery] = useState(""); const matches = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return []; return models .filter((m) => m.enabled && !preset.models.includes(m.id) && (m.name.toLowerCase().includes(q) || m.id.toLowerCase().includes(q))) .slice(0, 8); }, [query, models, preset.models]); function move(index: number, delta: number) { onChange((p) => { const next = [...p.models]; const j = index + delta; if (j < 0 || j >= next.length) return p; [next[index], next[j]] = [next[j], next[index]]; return { ...p, models: next }; }); } return (

{preset.label}

{preset.description}

{presetKey}
{preset.models.length === 0 ? (

Aucun modèle — ajoutez-en ci-dessous.

) : (
    {preset.models.map((id, i) => { const m = models.find((x) => x.id === id); return (
  • {i + 1}.

    {m?.name ?? id}

    {(!m || !m.enabled) &&

    {!m ? "Introuvable au catalogue" : "Désactivé"}

    }
  • ); })}
)} {preset.models.length < 10 && (
setQuery(e.target.value)} placeholder="Ajouter un modèle (recherche)…" className="h-8 pl-8 text-[12.5px]" aria-label={`Ajouter un modèle au préréglage ${preset.label}`} />
{matches.length > 0 && (
    {matches.map((m) => (
  • ))}
)}
)}
); }