7a11fd2846
DashboardView 849 -> ~40 Zeilen Orchestrator. Sechs eigenständige Karten unter components/dashboard/, jede mit eigenem Daten-Hook (react-query, geteilte Keys): - RadialGauge, SystemStatusCard (useSystemStatus) - UpdatesCard (useUpdates+useJobs; OS/Engine-Update, Reboot, Modell-Upgrade, Sudo-Modal, invalidiert nach Aktionen) - AgentStatusCard (useAgentStatus+useModels; Gehirn-Wechsel-Modal) - RolesCard (useModels), MemoryInputCard (useMemory), TokenStatsCard (useTokenStats) Kein manuelles useEffect+setInterval mehr; useDialog statt lokalem Dialog-State. Verifiziert: tsc grün, Build grün, alle 6 Karten rendern, Live-Daten + Pricing- Footer korrekt, keine Konsolenfehler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
5.1 KiB
TypeScript
105 lines
5.1 KiB
TypeScript
import { Layers } from "lucide-react"
|
|
import { useModels } from "@/lib/queries"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const ROLES = ["fast", "heavy", "coder", "reasoning", "vision", "scout"]
|
|
|
|
export function RolesCard() {
|
|
const { data } = useModels(3_000)
|
|
const models = data?.models ?? []
|
|
const running = data?.running ?? []
|
|
|
|
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 justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Layers className="h-4.5 w-4.5 text-primary" />
|
|
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Rollen-Belegung</h2>
|
|
</div>
|
|
|
|
<div className="space-y-2 max-h-48 overflow-y-auto pr-1 scrollbar-thin">
|
|
{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 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="min-w-0 flex-1 mr-2">
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<span className={cn("text-[9px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0",
|
|
role === "fast" ? "bg-cyan-500/15 text-cyan-400 border-cyan-500/25" :
|
|
role === "heavy" ? "bg-amber-500/15 text-amber-400 border-amber-500/25" :
|
|
role === "coder" ? "bg-violet-500/15 text-violet-400 border-violet-500/25" :
|
|
role === "reasoning" ? "bg-emerald-500/15 text-emerald-400 border-emerald-500/25" :
|
|
role === "vision" ? "bg-pink-500/15 text-pink-400 border-pink-500/25" :
|
|
"bg-teal-500/15 text-teal-400 border-teal-500/25"
|
|
)}>
|
|
{role}
|
|
</span>
|
|
|
|
<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">
|
|
{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_draft_model && (
|
|
<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>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 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="h-1.5 w-1.5 rounded-full bg-emerald-500 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>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</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.
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|