import { useState, useRef, useCallback } from "react" import { Download, Trash2, Edit3, Activity, HardDrive, X, Check, Copy } from "lucide-react" import { api } from "@/lib/api" import { useModels, useRouting, useConnect, useUpdates, useQueryClient, qk } from "@/lib/queries" import { useDialog } from "@/lib/useDialog" import { CapsChips } from "@/components/CapsChips" import { cn } from "@/lib/utils" import { fmtSize, fmtCtx } from "@/lib/format" import { getBrandInfo, ROLES } from "@/components/models/ModelBadges" export function Cockpit() { const qc = useQueryClient() const { data: modelsResp, isLoading: loading, error: loadErr } = useModels(4_000) const { data: routing } = useRouting(4_000) const { data: connectData } = useConnect() const { data: updates } = useUpdates(4_000) const { showAlert, showConfirm, showPrompt, dialogElement } = useDialog() const models = modelsResp?.models ?? [] const running = modelsResp?.running ?? [] const error = loadErr ? String(loadErr) : "" const reload = () => { qc.invalidateQueries({ queryKey: qk.models }) qc.invalidateQueries({ queryKey: qk.routing }) } // UI state const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null) const [activeRoleForAssign, setActiveRoleForAssign] = useState(null) const [copied, setCopied] = useState(false) const [hoveredNode, setHoveredNode] = useState(null) const [viewMode, setViewMode] = useState<"grid" | "list">("grid") const [filterMode, setFilterMode] = useState<"all" | "in_use">("all") const filteredModels = models.filter((m) => { if (filterMode === "in_use") { return !!m.role || running.includes(m.name) } return true }) // Canvas pixel tracking for pixel-perfect connection graph without non-uniform scaling const [dimensions, setDimensions] = useState({ width: 800, height: 360 }) const observerRef = useRef(null) const containerRef = useCallback((node: HTMLDivElement | null) => { if (observerRef.current) { observerRef.current.disconnect() observerRef.current = null } if (node) { const observer = new ResizeObserver((entries) => { if (!entries || entries.length === 0) return const rect = entries[0].contentRect setDimensions({ width: rect.width, height: rect.height }) }) observer.observe(node) observerRef.current = observer } }, []) const w = dimensions.width const h = dimensions.height // Helper to generate curve from client to gateway const getClientPath = (yPercent: number) => { const startX = w * 0.1 const startY = h * yPercent const endX = w * 0.5 const endY = h * 0.5 const cp1X = w * 0.3 const cp1Y = startY const cp2X = w * 0.3 const cp2Y = endY return `M ${startX} ${startY} C ${cp1X} ${cp1Y}, ${cp2X} ${cp2Y}, ${endX} ${endY}` } // Helper to generate curve from gateway to role const getRolePath = (yPercent: number) => { const startX = w * 0.5 const startY = h * 0.5 const endX = w * 0.9 const endY = h * yPercent const cp1X = w * 0.7 const cp1Y = startY const cp2X = w * 0.7 const cp2Y = endY return `M ${startX} ${startY} C ${cp1X} ${cp1Y}, ${cp2X} ${cp2Y}, ${endX} ${endY}` } async function handleLoadModel(name: string) { try { await api(`/api/models/${encodeURIComponent(name)}/load`, { method: "POST" }) reload() } catch (e: any) { showAlert("Fehler", `Fehler beim Laden des Modells: ${e.message}`) } } async function handleUnloadModel(name: string) { try { await api(`/api/models/${encodeURIComponent(name)}/unload`, { method: "POST" }) reload() } catch (e: any) { showAlert("Fehler", `Fehler beim Entladen des Modells: ${e.message}`) } } async function handleUnloadAll() { try { await api("/api/models/unload", { method: "POST" }) reload() } catch (e: any) { showAlert("Fehler", `Fehler beim Entladen aller Modelle: ${e.message}`) } } async function handleRoleChange(role: string, modelName: string) { try { await api(`/api/models/${encodeURIComponent(modelName)}/role`, { method: "POST", body: JSON.stringify({ role: role || null }), }) reload() } catch (e: any) { showAlert("Fehler", `Fehler beim Zuweisen der Rolle: ${e.message || e}`) } } async function handleSetCtx(name: string, cur: number | null) { showPrompt( "Kontextlänge anpassen", "Gib die gewünschte Kontextlänge in Tokens an:", String(cur || 32768), async (v) => { if (!v) return try { await api(`/api/models/${encodeURIComponent(name)}/ctx`, { method: "POST", body: JSON.stringify({ ctx: parseInt(v, 10) }), }) reload() } catch (e: any) { showAlert("Fehler", `Fehler beim Setzen des Kontexts: ${e.message || e}`) } } ) } async function handleDelete(name: string) { showConfirm( "Modell löschen?", `Modell '${name}' und alle zugehörigen GGUF-Dateien unwiderruflich von der Box löschen?`, async () => { try { await api(`/api/models/${encodeURIComponent(name)}`, { method: "DELETE" }) reload() } catch (e: any) { showAlert("Fehler", `Fehler beim Löschen: ${e.message || e}`) } } ) } async function handleSmartUpgrade(repo: string, role: string, quant: string, toolCapable: boolean) { try { await api("/api/models/install", { method: "POST", body: JSON.stringify({ repo, role, quant, jinja: toolCapable }), }) showAlert("Herunterladen gestartet", `Download für '${repo}' gestartet! Der Fortschritt wird oben angezeigt.`) } catch (e: any) { showAlert("Fehler", `Fehler beim Starten des Upgrades: ${e.message || e}`) } } async function copySnippet(snippet?: string) { if (!snippet) return await navigator.clipboard.writeText(snippet) setCopied(true) setTimeout(() => setCopied(false), 1500) } if (loading) return
Initialisiere HUD Cockpit…
if (error) { return (
Gateway oder Engine nicht erreichbar ({error}).
) } // VRAM calculation: APU fallback 16GB if sysfs returns 0 const runningWithInfo = models.filter((m) => running.includes(m.name)) const totalRunningSize = runningWithInfo.reduce((acc, m) => acc + (m.size_bytes || 0), 0) const virtualMax = 16 * 1024 ** 3 // 16 GB Default const capacity = totalRunningSize > virtualMax ? totalRunningSize * 1.2 : virtualMax // Find model by role const getModelForRole = (role: string) => models.find((m) => m.role === role) const isRoleRunning = (role: string) => { const m = getModelForRole(role) return m ? running.includes(m.name) : false } return (
{/* Styles inside Cockpit for dash flow animations */} {/* ZONE A: Widescreen VRAM HUD */}
VRAM / Memory Belegung
Llama Swap VRAM-Pool: {fmtSize(totalRunningSize)} / {fmtSize(capacity)} geladen {running.length > 0 && ( )}
{/* The memory bar */}
{runningWithInfo.length === 0 ? (
VRAM Leer — Auto-Swap lädt Modelle bei Anfrage
) : ( runningWithInfo.map((m, idx) => { const widthPct = ((m.size_bytes || 0) / capacity) * 100 const bgClass = [ "from-teal-500 to-emerald-500", "from-indigo-500 to-blue-500", "from-purple-500 to-pink-500", "from-cyan-500 to-sky-500" ][idx % 4] return (
{m.role ? `[${m.role}] ` : ""}{m.name.split("/").pop()?.replace(".gguf", "")} {fmtSize(m.size_bytes)}
) }) )}
{/* ZONE B: SVG Node Router */}
Interactive Gateway Graph

Klicke links auf Editoren, um Configs zu kopieren. Klicke rechts auf Rollen, um GGUF-Zuweisungen live zu ändern.

{/* The Graph Canvas Area */}
{/* SVG Overlay behind HTML Nodes */} {/* Gradients */} {/* Bezier Curves: Clients to Gateway (10% -> 50%) */} {/* Roo Code (y=10) */} {(hoveredNode === "roocode" || activeClient === "roocode") && ( )} {/* Cursor (y=30) */} {(hoveredNode === "cursor" || activeClient === "cursor") && ( )} {/* OpenCode (y=50) */} {(hoveredNode === "opencode" || activeClient === "opencode") && ( )} {/* Zed (y=70) */} {(hoveredNode === "zed" || activeClient === "zed") && ( )} {/* Continue (y=90) */} {(hoveredNode === "continue" || activeClient === "continue") && ( )} {/* Bezier Curves: Gateway to Roles (50% -> 90%) */} {/* fast (y=12) */} {isRoleRunning("fast") && ( )} {/* heavy (y=31) */} {isRoleRunning("heavy") && ( )} {/* coder (y=50) */} {isRoleRunning("coder") && ( )} {/* vision (y=69) */} {isRoleRunning("vision") && ( )} {/* scout (y=88) */} {isRoleRunning("scout") && ( )} {/* HTML Nodes */} {/* COLUMN 1: Client nodes */}
setHoveredNode("roocode")} onMouseLeave={() => setHoveredNode(null)} onClick={() => setActiveClient(c => c === "roocode" ? null : "roocode")} > Roo Code
setHoveredNode("cursor")} onMouseLeave={() => setHoveredNode(null)} onClick={() => setActiveClient(c => c === "cursor" ? null : "cursor")} > Cursor IDE
setHoveredNode("opencode")} onMouseLeave={() => setHoveredNode(null)} onClick={() => setActiveClient(c => c === "opencode" ? null : "opencode")} > OpenCode
setHoveredNode("zed")} onMouseLeave={() => setHoveredNode(null)} onClick={() => setActiveClient(c => c === "zed" ? null : "zed")} > Zed
setHoveredNode("continue")} onMouseLeave={() => setHoveredNode(null)} onClick={() => setActiveClient(c => c === "continue" ? null : "continue")} > Continue
{/* COLUMN 2: Central Gateway Node */}
Gateway Auto
Schwelle: > {routing?.heavy_threshold_chars ? (routing.heavy_threshold_chars / 1000) : "4"}k Zeichen
Auto-Swap
{/* COLUMN 3: Role nodes (fast, heavy, coder, vision, scout) */} {ROLES.map((role) => { const yPositions = ["12%", "31%", "50%", "69%", "88%"] const activeModel = getModelForRole(role) const isWarm = activeModel ? running.includes(activeModel.name) : false if (role === "agent") return null const indexMap = { fast: 0, heavy: 1, coder: 2, vision: 3, scout: 4 }[role] as number return (
setActiveRoleForAssign(role)} >
{role} {isWarm && }
{activeModel ? activeModel.name.split("/").pop()?.replace(".gguf", "") : "Keine Zuweisung"}
) })} {/* CLIENT SETUP MODAL WINDOW */} {activeClient && connectData && (
{/* Modal Header */}
{activeClient === "roocode" && "Roo Code Setup"} {activeClient === "cursor" && "Cursor Setup"} {activeClient === "opencode" && "OpenCode Setup"} {activeClient === "zed" && "Zed Setup"} {activeClient === "continue" && "Continue Setup"}
{/* Modal Guide Steps */}
{activeClient === "roocode" && (
  • Suche in VS Code nach der Erweiterung Roo Code und installiere sie.
  • Wähle in den Roo Code Einstellungen: Provider: OpenAI Compatible.
  • Füge das untenstehende JSON-Snippet in die settings.json ein.
)} {activeClient === "cursor" && (
  • Öffne Cursor Settings ➔ Models.
  • Deaktiviere Cloud-Modelle, klappe OpenAI API auf.
  • Trage die Base URL unten ein und aktiviere das Modell auto.
)} {activeClient === "opencode" && (
  • Öffne die opencode.jsonc Konfigurationsdatei.
  • Ersetze den Provider-Eintrag unter provider mit dem Snippet unten.
)} {activeClient === "zed" && (
  • Öffne die Zed Settings (ctrl+,).
  • Füge das untenstehende JSON-Segment unter language_models ein.
)} {activeClient === "continue" && (
  • Klicke auf das Zahnrad-Symbol in der Continue-Erweiterung.
  • Füge den Gateway-Eintrag zum models-Array hinzu.
)}
{/* Snippet box */} {connectData.tools && (
JSON Config
                      
                        {activeClient === "roocode" && connectData.tools.cline?.snippet}
                        {activeClient === "cursor" && connectData.tools.cursor?.snippet}
                        {activeClient === "opencode" && connectData.tools.opencode?.snippet}
                        {activeClient === "zed" && connectData.tools.zed?.snippet}
                        {activeClient === "continue" && connectData.tools.continue?.snippet}
                      
                    
)}
)}
{/* Legend for the connection graph */}
Cyan-Fluss: Client-Anfrage an Gateway Grüner Puls: Aktive Verbindung / Warmes Modell geladen VRAM-Verlauf: Modellspezifischer Speicheranteil
{/* ZONE B.5: Slot-Belegung */}
Gateway Steckplatz-Belegung (Slot-Zuweisung)
{["fast", "heavy", "coder", "vision", "scout"].map((role) => { const m = models.find((x) => x.role === role) const isWarm = m ? running.includes(m.name) : false return (
setActiveRoleForAssign(role)} className={cn( "rounded-xl border p-3 flex flex-col justify-between gap-2.5 transition-all cursor-pointer select-none text-left bg-background/25 hover:border-primary/45 hover:bg-background/40 group min-h-[90px]", isWarm ? "border-emerald-500/40 shadow-sm shadow-emerald-500/5" : m ? "border-primary/20 bg-primary/5" : "border-border/30 border-dashed opacity-75" )} >
{role} {isWarm && }
{m ? m.name.split("/").pop()?.replace(/\.gguf$/i, "") : "nicht zugewiesen"}
Ändern ➔
) })}
{/* ZONE C: Library list cards & Upgrade Radar */}
Installierte Modell-Bibliothek ({filteredModels.length} von {models.length})
{/* Filter mode selector */}
{/* Grid/List layout selector */}
{viewMode === "grid" ? (
{filteredModels.length === 0 ? (
{filterMode === "in_use" ? "Keine Modelle in Benutzung (keine Zuweisung oder warm geladene Modelle)." : "Keine Modelle konfiguriert. Verwende den Tab \"Modelle finden\" zum Herunterladen."}
) : ( filteredModels.map((m) => { const isRunning = running.includes(m.name) const hasUpgrade = updates?.model_list.find((u) => u.role === m.role) const brand = getBrandInfo(m.name) return (
{/* Brand Icon Badge */}
{brand.initial}

{m.name.split("/").pop()}

{m.quant || "GGUF"} {isRunning && ( Warm )} {m.role && ( {m.role} )} {m.prompt_cache && ( PC )} {m.spec_draft_model && ( SPEC )} {m.parallel_slots > 1 && ( SLOTS: {m.parallel_slots} )} {m.incomplete && ( ⚠ Datei fehlt )}
Größe
{fmtSize(m.size_bytes)}
Kontext
{fmtCtx(m.ctx)}
{/* Smart Upgrade radar trigger banner */} {hasUpgrade && (
Upgrade verfügbar: {hasUpgrade.repo.split("/").pop()}
)} {/* Actions row at the bottom of Grid Card */}
) }) )}
) : (
{filteredModels.length === 0 ? (
{filterMode === "in_use" ? "Keine Modelle in Benutzung (keine Zuweisung oder warm geladene Modelle)." : "Keine Modelle konfiguriert. Verwende den Tab \"Modelle finden\" zum Herunterladen."}
) : ( filteredModels.map((m) => { const isRunning = running.includes(m.name) const brand = getBrandInfo(m.name) return (
{/* Brand Icon */}
{brand.initial}

{m.name.split("/").pop()}

{m.role && ( {m.role} )} {m.prompt_cache && ( PC )} {m.spec_draft_model && ( SPEC )} {m.parallel_slots > 1 && ( SLOTS: {m.parallel_slots} )} {m.incomplete && ( ⚠ Datei fehlt )} {isRunning && ( warm )}
Größe: {fmtSize(m.size_bytes)} Kontext: {fmtCtx(m.ctx)} {m.quant || "GGUF"}
{/* Chips and Actions */}
) }) )}
)}
{/* Role Assignment Modal */} {activeRoleForAssign && (

Rolle '{activeRoleForAssign}' konfigurieren

Wähle ein Modell aus deiner Bibliothek für die Rolle {activeRoleForAssign}:

{models.map((m) => ( ))}
)} {dialogElement}
) }