"use client"; import { useMemo, useState } from "react"; import { ChevronDown, Star, Eye, Wrench, Braces, Brain, Zap, Gauge, Coins, Sparkles, Check, Search } from "lucide-react"; import { useModels, PROVIDER_LABEL, shortName } from "@/lib/client/use-models"; import type { AIModel, RoutingMode } from "@/lib/client/types"; import { api } from "@/lib/client/fetcher"; import { cn, Dialog, Badge } from "../ui"; const MODES: { id: RoutingMode; label: string; desc: string; icon: React.ComponentType<{ className?: string }> }[] = [ { id: "auto", label: "Automatique", desc: "Classe la tâche et choisit le meilleur compromis", icon: Sparkles }, { id: "fast", label: "Rapide", desc: "Latence la plus basse (≥ 89 % au benchmark)", icon: Zap }, { id: "quality", label: "Qualité", desc: "Score maximal, palier le plus élevé", icon: Gauge }, { id: "economy", label: "Économie", desc: "Prix le plus bas (score 100 %)", icon: Coins }, ]; export function ModelPicker({ value, mode, onChange, compact }: { value: string | null; mode: RoutingMode; onChange: (v: { modelId: string | null; mode: RoutingMode }) => void; compact?: boolean }) { const { chat, byId, mutate } = useModels(); const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const current = byId(value); const label = mode !== "manual" ? MODES.find(m => m.id === mode)!.label : shortName(current, value); const groups = useMemo(() => { const ql = q.toLowerCase(); const list = chat.filter(m => !ql || m.id.includes(ql) || m.displayName.toLowerCase().includes(ql)); const fav = list.filter(m => m.favorite); const g: Record = {}; for (const m of list) (g[m.provider] ??= []).push(m); return { fav, g }; }, [chat, q]); async function toggleFav(e: React.MouseEvent, m: AIModel) { e.stopPropagation(); await api("/api/models", { method: "PATCH", json: { id: m.id, favorite: !m.favorite } }); void mutate(); } return ( <> setOpen(false)} width="max-w-2xl" title="Choisir un modèle">
{MODES.map(m => )}
setQ(e.target.value)} placeholder="Filtrer les modèles vérifiés…" className="input !pl-8" autoFocus />
{groups.fav.length > 0 && !q && {groups.fav.map(m => { onChange({ modelId: m.id, mode: "manual" }); setOpen(false); }} onFav={e => toggleFav(e, m)} />)}} {Object.entries(groups.g).map(([prov, ms]) => {ms.map(m => { onChange({ modelId: m.id, mode: "manual" }); setOpen(false); }} onFav={e => toggleFav(e, m)} />)})}
Intégration future de l'inférence locale sur le cluster — aucun modèle vérifié pour l'instant.
Seuls les modèles vérifiés par requête réelle apparaissent ici.Laboratoire de modèles →
); } function Group({ title, count, children }: { title: string; count?: number; children: React.ReactNode }) { return
{title}{count != null && · {count}}
{children}
; } export function CapIcons({ m, className }: { m: AIModel; className?: string }) { const c = m.capabilities; return {c.vision === true && }{c.tools === true && }{c.structuredOutput === true && }{c.reasoning === true && } ; } function Row({ m, selected, onSelect, onFav }: { m: AIModel; selected: boolean; onSelect: () => void; onFav: (e: React.MouseEvent) => void }) { const p = m.pricing; const b = m.benchmark; return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelect(); } }} className={cn("flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors", selected ? "bg-accent/10" : "hover:bg-surface")}>
{m.displayName}{m.kind === "chat-pro" && pro · lent}{m.kind === "search" && recherche web}
{b?.qualityScore != null && = 100 ? "bg-ok" : b.qualityScore >= 89 ? "bg-accent" : "bg-warn")} style={{ width: `${b.qualityScore}%` }} />{b.qualityScore} %} {b?.p50LatencyMs != null && {b.p50LatencyMs} ms} {p?.input != null && p?.output != null ? {p.input} $ / {p.output} $ par M : prix non vérifié} {m.contextWindow && {Math.round(m.contextWindow / 1000)}k ctx}
{selected && }
); }