a341d254e1
Vier-Etappen-Umbau der MC2-Oberfläche für Endnutzer (kein Fachjargon): 1. Klartext-Schicht (lib/roleMeta.ts): Rollen-Kürzel → Zweck-Namen mit Icons (hermes=Lucys Hirn, embed=Lucys Gedächtnis, vision=Lucys Augen, …), zentral in ModelBadges/ModelsCard/Cockpit. Veraltete Rollenliste korrigiert (Phantom-„fast" raus; embed/coder-lite ergänzt). 2. „Geht's Lucy gut?"-Panel (lib/useLucyHealth.ts + LucyHealthCard): 5 Fähigkeiten in Klartext, Ampel-Verdikt, Speicher-Ampel, 1-Klick-Reparatur (echte Unit-Namen). 3. Einfach/Experte-Schalter (lib/useExpertMode.ts + ExpertToggle) + Schutzgeländer: Hirn/Gedächtnis (protected) nicht löschbar/entladbar; Ctx/Spec/Slots/Lane-Details im Einfach-Modus versteckt. 4. „Immer-bereit-Set"-Manager (WarmSetManager): brains-Gruppe in Klartext + ehrliches Speicher-Budget (inkl. KV) mit Verdrängungs-Hinweis. Live gegen die Box verifiziert. frontend/dist mitgebaut. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
137 lines
6.9 KiB
TypeScript
137 lines
6.9 KiB
TypeScript
import { useEffect, useRef, useState } from "react"
|
|
import { Boxes, Zap, Moon } from "lucide-react"
|
|
import { useModels, useTokenStats, useSystemStatus } from "@/lib/queries"
|
|
import { fmtSize, gb } from "@/lib/format"
|
|
import { cn } from "@/lib/utils"
|
|
import { ROLES, RoleLabel } from "@/components/models/ModelBadges"
|
|
|
|
// Vereint die frühere RolesCard (Rolle→Modell) + ActiveModelsCard (warm/Inferenz/Größe)
|
|
// zu EINER Karte — dieselbe Info stand vorher doppelt auf der Zentrale.
|
|
export function ModelsCard() {
|
|
const { data } = useModels(2_000)
|
|
const { data: stats } = useTokenStats(2_000)
|
|
const { data: sysStatus } = useSystemStatus()
|
|
const models = data?.models ?? []
|
|
const running = data?.running ?? []
|
|
|
|
// Echte Pool-Belegung (Unified-RAM inkl. KV) — schlanke Glance-Bar. Die volle
|
|
// Segment-Bar + „Alle entladen" bleibt im Modell-Manager.
|
|
const gttTotal = sysStatus?.gpu?.gtt_total || sysStatus?.gpu?.vram_total || 0
|
|
const gttUsed = sysStatus?.gpu?.gtt_used || 0
|
|
const poolPct = gttTotal > 0 ? Math.min(100, (gttUsed / gttTotal) * 100) : 0
|
|
const poolBar = poolPct >= 88 ? "bg-red-500" : poolPct >= 70 ? "bg-amber-500" : "bg-teal-500"
|
|
const poolText = poolPct >= 88 ? "text-red-400" : poolPct >= 70 ? "text-amber-400" : "text-foreground"
|
|
|
|
// „Inferenz aktiv": total_tokens seit letztem Poll gestiegen → es wird generiert.
|
|
const prev = useRef<number | null>(null)
|
|
const [generating, setGenerating] = useState(false)
|
|
useEffect(() => {
|
|
if (!stats) return
|
|
const total = stats.total_tokens
|
|
if (prev.current !== null && total > prev.current) {
|
|
setGenerating(true)
|
|
const t = setTimeout(() => setGenerating(false), 4_000)
|
|
prev.current = total
|
|
return () => clearTimeout(t)
|
|
}
|
|
prev.current = total
|
|
}, [stats?.total_tokens])
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<Boxes className="h-4.5 w-4.5 text-primary" />
|
|
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Modelle</h2>
|
|
</div>
|
|
<span className={cn(
|
|
"flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider font-mono px-2 py-0.5 rounded-full border transition-colors",
|
|
generating
|
|
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/30"
|
|
: "bg-background/30 text-muted-foreground/70 border-border/40"
|
|
)}>
|
|
{generating ? <Zap className="h-3 w-3 animate-pulse" /> : <Moon className="h-3 w-3" />}
|
|
{generating ? "Inferenz aktiv" : "Idle"}
|
|
</span>
|
|
</div>
|
|
|
|
{gttTotal > 0 && (
|
|
<div className="mb-4">
|
|
<div className="flex items-center justify-between text-[10px] font-mono text-muted-foreground/80 mb-1">
|
|
<span className="uppercase tracking-wider font-semibold">Speicher-Pool</span>
|
|
<span><span className={cn("font-bold", poolText)}>{gb(gttUsed)}</span> / {gb(gttTotal)} GB belegt</span>
|
|
</div>
|
|
<div className="h-1.5 w-full rounded-full bg-background/50 border border-border/30 overflow-hidden">
|
|
<div className={cn("h-full rounded-full transition-all duration-500", poolBar)} style={{ width: `${poolPct}%` }} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2 flex-1">
|
|
{ROLES.map((role) => {
|
|
const m = models.find((x) => x.role === role)
|
|
const isRunning = m ? running.includes(m.name) : false
|
|
|
|
return (
|
|
<div
|
|
key={role}
|
|
className={cn(
|
|
"flex items-center justify-between gap-2 p-2 rounded-xl border transition-all duration-300",
|
|
isRunning
|
|
? "border-emerald-500/30 bg-emerald-500/5 shadow-sm shadow-emerald-500/5"
|
|
: m
|
|
? "border-primary/20 bg-primary/5"
|
|
: "border-border/30 bg-background/10 opacity-60"
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<RoleLabel role={role} className="shrink-0" />
|
|
<div className="flex flex-col min-w-0">
|
|
<span className="text-xs font-semibold truncate font-mono text-foreground">
|
|
{m ? m.name.split("/").pop()?.replace(/\.gguf$/i, "") : "nicht zugewiesen"}
|
|
</span>
|
|
{m && (
|
|
<div className="flex gap-1 items-center mt-0.5 flex-wrap text-muted-foreground/70">
|
|
<span className="text-[9px] font-mono">{fmtSize(m.size_bytes)}</span>
|
|
{m.prompt_cache && (
|
|
<span className="text-[7px] leading-none font-mono font-bold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 px-1 py-0.5 rounded" title="Prompt Caching aktiv">PC</span>
|
|
)}
|
|
{m.spec_active && (
|
|
<span className="text-[7px] leading-none font-mono font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-1 py-0.5 rounded" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>SPEC</span>
|
|
)}
|
|
{m.parallel_slots > 1 && (
|
|
<span className="text-[7px] leading-none font-mono font-bold bg-violet-500/10 text-violet-400 border border-violet-500/20 px-1 py-0.5 rounded" title={`${m.parallel_slots} parallele Slots aktiv`}>SLOTS: {m.parallel_slots}</span>
|
|
)}
|
|
{m.incomplete && (
|
|
<span className="text-[7px] leading-none font-mono font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 px-1 py-0.5 rounded" title="GGUF-Datei fehlt">⚠ fehlt</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<span className="shrink-0">
|
|
{m ? (
|
|
isRunning ? (
|
|
<span className="flex items-center gap-1 text-[8px] font-semibold text-emerald-400 uppercase tracking-wider font-mono">
|
|
<span className={cn("h-1.5 w-1.5 rounded-full bg-emerald-500", generating && "animate-pulse")} /> warm
|
|
</span>
|
|
) : (
|
|
<span className="text-[8px] font-semibold text-muted-foreground uppercase tracking-wider font-mono">bereit</span>
|
|
)
|
|
) : (
|
|
<span className="text-[8px] font-semibold text-muted-foreground/60 uppercase tracking-wider font-mono">—</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<div className="mt-4 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3">
|
|
Laden erfolgt automatisch per Auto-Swap. Details & Routing im Modell-Manager.
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|