Refactor: UI-Konsolidierung über Zentrale, Modell-Manager, Hermes & Verbinden
Beseitigt Redundanzen/Mismatches, die sich mit den Lanes angesammelt hatten: - Modell-Manager: großen animierten SVG-Gateway-Graph (~310 Z.) entfernt — Rolle→Modell deckt das Slot-Grid ab, Lanes der Lane-Editor, IDE-Config die "Verbinden"-Seite. Cockpit-Brain-Switch raus → Verweis auf Hermes-Tab. - Hermes: zweiten SVG-Graph + 4 Status-Kacheln durch einen ruhigen Box→Pfeil- Fluss ersetzt (Terminal → Gateway → Hirn/Verdrahtung/PC), Stil wie "Verbinden". - Hirn-Wechsel vereinheitlicht: nur noch im Hermes-Tab. Installiert = warm-bewusst (/api/agent/brain/set), Alias = /api/agent/brain; Lane-Aliase chat/coding/fast/heavy. - Terminologie auf Lane-Sprache: ConnectView "model auto" → chat/coding; connect.py-Snippets defaulten auf 'coding' (GATEWAY_MODELS mit Lanes vorn). - Zentrale: ActiveModelsCard + RolesCard zu einer ModelsCard verschmolzen (Rollen + warm + Inferenz + Größe); Layout entdoppelt. ~600 Zeilen SVG-Graph-Code raus, 2 tote Karten gelöscht. Verifiziert: npm run build (tsc strict) clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { Activity, Zap, Moon } from "lucide-react"
|
||||
import { useModels, useTokenStats } from "@/lib/queries"
|
||||
import { fmtSize } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { roleTone } from "@/components/models/ModelBadges"
|
||||
|
||||
export function ActiveModelsCard() {
|
||||
const { data } = useModels(2_000)
|
||||
const { data: stats } = useTokenStats(2_000)
|
||||
const models = data?.models ?? []
|
||||
const running = data?.running ?? []
|
||||
const loaded = models.filter((m) => running.includes(m.name))
|
||||
|
||||
// "Inferenz aktiv": total_tokens ist seit dem letzten 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">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4.5 w-4.5 text-primary" />
|
||||
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Aktive Modelle (Live)</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>
|
||||
|
||||
{loaded.length === 0 ? (
|
||||
<div className="flex items-center justify-center gap-2 h-20 text-xs text-muted-foreground/70 italic">
|
||||
Kein Modell geladen — Auto-Swap lädt bei Anfrage.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-2">
|
||||
{loaded.map((m) => (
|
||||
<div key={m.name} className="flex items-center justify-between gap-2 p-3 rounded-xl bg-emerald-500/5 border border-emerald-500/20">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
{m.role && (
|
||||
<span className={cn("text-[8px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0",
|
||||
roleTone(m.role))}>
|
||||
{m.role}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs font-semibold font-mono text-foreground truncate">
|
||||
{m.name.split("/").pop()?.replace(/\.gguf$/i, "")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[9px] font-mono text-muted-foreground/70 mt-0.5">
|
||||
{fmtSize(m.size_bytes)} im Unified-RAM
|
||||
</div>
|
||||
</div>
|
||||
<span className="flex items-center gap-1 text-[8px] font-semibold text-emerald-400 uppercase tracking-wider font-mono shrink-0">
|
||||
<span className={cn("h-1.5 w-1.5 rounded-full bg-emerald-500", generating && "animate-pulse")} /> warm
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { Boxes, Zap, Moon } from "lucide-react"
|
||||
import { useModels, useTokenStats } from "@/lib/queries"
|
||||
import { fmtSize } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ROLES, roleTone } 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 models = data?.models ?? []
|
||||
const running = data?.running ?? []
|
||||
|
||||
// „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>
|
||||
|
||||
<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">
|
||||
<span className={cn("text-[9px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0", roleTone(role))}>
|
||||
{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 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>
|
||||
)
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Layers } from "lucide-react"
|
||||
import { useModels } from "@/lib/queries"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ROLES, roleTone } from "@/components/models/ModelBadges"
|
||||
|
||||
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",
|
||||
roleTone(role))}>
|
||||
{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_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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { cn } from "@/lib/utils"
|
||||
// Die 5 kanonischen Serving-Rollen (identisch zu Backend sources.py/llamaswap.ROLE_IDS).
|
||||
export const ROLES = ["fast", "heavy", "coder", "vision", "scout"]
|
||||
|
||||
// Rolle → Farbton. EINE Quelle der Wahrheit für alle Rollen-Badges (Cockpit, RolesCard,
|
||||
// ActiveModelsCard, …). `hermes` = Agent-Hirn (taucht als geladenes Modell auf).
|
||||
// Rolle → Farbton. EINE Quelle der Wahrheit für alle Rollen-Badges (Cockpit, ModelsCard,
|
||||
// …). `hermes` = Agent-Hirn (taucht als geladenes Modell auf).
|
||||
export const ROLE_TONE: Record<string, string> = {
|
||||
fast: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
|
||||
heavy: "bg-amber-500/15 text-amber-400 border-amber-500/25",
|
||||
|
||||
Reference in New Issue
Block a user