import { useEffect, useState, useRef, useCallback } from "react" import { Download, Search, Trash2, Edit3, Star, Layers, Activity, HardDrive, X, Check, Copy, Bot, Eye, Code, Brain, Compass, ChevronDown, ChevronUp } from "lucide-react" import { api, type DiscoverResp, type Fit, type Job, type ModelInfo, type RoutingResp, type UpdatesResp, type ConnectResp } from "@/lib/api" import { CapsChips } from "@/components/CapsChips" import { cn } from "@/lib/utils" import { CustomDialog } from "@/components/CustomDialog" function fmtBytes(b?: number) { if (!b) return "" if (b > 1024 ** 3) return `${(b / 1024 ** 3).toFixed(1)} GB` return `${(b / 1024 ** 2).toFixed(0)} MB` } function fmtEta(s?: number) { if (!s) return "" const m = Math.floor(s / 60) return m > 0 ? `${m} min` : `${s} s` } function JobsBar({ onError }: { onError?: (msg: string) => void }) { const [jobs, setJobs] = useState([]) const [errorMsg, setErrorMsg] = useState(null) function load() { api<{ jobs: Job[] }>("/api/jobs") .then((d) => setJobs(d.jobs || [])) .catch(() => {}) } useEffect(() => { load() const t = setInterval(load, 2000) return () => clearInterval(t) }, []) async function cancelJob(jobId: string) { try { await api(`/api/jobs/${jobId}/cancel`, { method: "POST" }) load() } catch (e: any) { if (onError) onError(e.message) else setErrorMsg(e.message) } } const active = jobs.filter((j) => j.state === "running" || j.state === "queued") const recent = jobs.filter((j) => j.state !== "running" && j.state !== "queued").slice(-3) if (active.length === 0 && recent.length === 0) return null return (
Aktive Downloads
{active.map((j) => (
{j.label}
{j.progress ?? 0}% • {fmtBytes(j.done_bytes)}/{fmtBytes(j.total_bytes)} {j.eta_s ? ` • ETA ${fmtEta(j.eta_s)}` : ""}
))} {recent.map((j) => (
{j.label} {j.state}
))} {errorMsg && ( setErrorMsg(null)} /> )}
) } function fmtSize(b?: number | null) { if (!b) return "—" const gb = b / 1024 ** 3 return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(b / 1024 ** 2).toFixed(0)} MB` } function fmtCtx(c: number | null) { return c ? `${Math.round(c / 1024)}k` : "—" } function FitBadge({ fit }: { fit: Fit }) { const tone = { perfect: "bg-emerald-500/15 text-emerald-400 border border-emerald-500/20", marginal: "bg-amber-500/15 text-amber-400 border border-amber-500/20", too_tight: "bg-red-500/15 text-red-400 border border-red-500/20", }[fit.level] return ( {fit.text} • {fit.req_gb} GB RAM ) } const ROLES = ["fast", "heavy", "coder", "reasoning", "agent", "vision", "scout"] function getBrandInfo(name: string) { const low = name.toLowerCase() if (low.includes("qwen")) return { name: "Qwen", color: "bg-purple-500/20 text-purple-300 border-purple-500/30", initial: "Q" } if (low.includes("gemma")) return { name: "Gemma", color: "bg-blue-500/20 text-blue-300 border-blue-500/30", initial: "G" } if (low.includes("llama")) return { name: "Llama", color: "bg-red-500/20 text-red-300 border-red-500/30", initial: "🦙" } if (low.includes("mistral") || low.includes("mixtral")) return { name: "Mistral", color: "bg-orange-500/20 text-orange-300 border-orange-500/30", initial: "M" } if (low.includes("deepseek")) return { name: "DeepSeek", color: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30", initial: "D" } if (low.includes("hermes") || low.includes("nous")) return { name: "Hermes", color: "bg-amber-500/20 text-amber-300 border-amber-500/30", initial: "H" } if (low.includes("phi")) return { name: "Phi", color: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30", initial: "Φ" } return { name: "Other", color: "bg-slate-500/20 text-slate-300 border-slate-500/30", initial: "AI" } } function Cockpit() { const [models, setModels] = useState([]) const [running, setRunning] = useState([]) const [routing, setRouting] = useState(null) const [connectData, setConnectData] = useState(null) const [updates, setUpdates] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState("") // 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") // Custom Dialog State const [dialog, setDialog] = useState<{ type: "alert" | "confirm" | "prompt" title: string message: string defaultValue?: string onConfirm: (val?: string) => void onCancel?: () => void } | null>(null) function showAlert(title: string, message: string, onConfirm?: () => void) { setDialog({ type: "alert", title, message, onConfirm: () => { setDialog(null) if (onConfirm) onConfirm() } }) } function showConfirm(title: string, message: string, onConfirm: () => void, onCancel?: () => void) { setDialog({ type: "confirm", title, message, onConfirm: () => { setDialog(null) onConfirm() }, onCancel: () => { setDialog(null) if (onCancel) onCancel() } }) } function showPrompt(title: string, message: string, defaultValue: string, onConfirm: (val?: string) => void, onCancel?: () => void) { setDialog({ type: "prompt", title, message, defaultValue, onConfirm: (val) => { setDialog(null) onConfirm(val) }, onCancel: () => { setDialog(null) if (onCancel) onCancel() } }) } 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}` } function load() { Promise.all([ api<{ models: ModelInfo[]; running?: string[] }>("/api/models"), api("/api/routing"), api("/api/connect"), api("/api/maintenance/updates") ]) .then(([mResp, rResp, cResp, uResp]) => { setModels(mResp.models || []) setRunning(mResp.running || []) setRouting(rResp) setConnectData(cResp) setUpdates(uResp) }) .catch((e) => setError(String(e))) .finally(() => setLoading(false)) } useEffect(() => { load() const t = setInterval(load, 4000) return () => clearInterval(t) }, []) async function handleLoadModel(name: string) { try { await api(`/api/models/${encodeURIComponent(name)}/load`, { method: "POST" }) load() } 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" }) load() } catch (e: any) { showAlert("Fehler", `Fehler beim Entladen des Modells: ${e.message}`) } } async function handleUnloadAll() { try { await api("/api/models/unload", { method: "POST" }) load() } 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 }), }) load() } 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) }), }) load() } 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" }) load() } 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 // Skip "reasoning" & "agent" to keep layout neat and identical to canonical roles if (role === "reasoning" || 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", "reasoning", "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} )}
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} )} {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) => ( ))}
)} {dialog && ( )}
) } function AddModel() { const [repo, setRepo] = useState("") const [quants, setQuants] = useState([]) const [quant, setQuant] = useState("Q4_K_M") const [msg, setMsg] = useState("") const [q, setQ] = useState("") const [results, setResults] = useState<{ repo: string; downloads: number }[]>([]) async function loadQuants(r?: string) { const rr = r ?? repo if (!rr.trim()) return setMsg("Analysiere HuggingFace Repository...") try { const d = await api<{ repo: string; quants: string[] }>(`/api/hf/quants?repo=${encodeURIComponent(rr)}`) setRepo(d.repo) setQuants(d.quants) if (d.quants.length) setQuant(d.quants.includes("Q4_K_M") ? "Q4_K_M" : d.quants[0]) setMsg(d.quants.length ? "" : "Keine GGUF-Dateien in diesem Repository gefunden.") } catch (e) { setMsg(`Fehler: ${e}`) } } async function search() { if (!q.trim()) return setMsg("Durchsuche HuggingFace...") try { const d = await api<{ results: { repo: string; downloads: number }[] }>(`/api/hf/search?q=${encodeURIComponent(q)}`) setResults(d.results) setMsg(d.results.length ? "" : "Keine Ergebnisse gefunden.") } catch (e) { setMsg(`Suche fehlgeschlagen: ${e}`) } } async function install() { if (!repo.trim()) return setMsg("Download-Job wird initiiert...") try { await api("/api/models/install", { method: "POST", body: JSON.stringify({ repo, quant, jinja: true }), }) setMsg(`Download gestartet: ${repo} (${quant}). Fortschritt wird oben angezeigt.`) } catch (e) { setMsg(`Download-Fehler: ${e}`) } } return (
HF Download & Suche
setRepo(e.target.value)} placeholder="HF-URL oder org/repo (z.B. unsloth/Qwen2.5-Coder-7B-Instruct-GGUF)" className="flex-1 h-9 rounded-lg border border-border/60 bg-background/40 px-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground" />
{quants.length > 0 && ( <> )}
setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && search()} placeholder="HuggingFace durchsuchen (z.B. Llama-3.1)..." className="w-full h-9 pl-9 pr-3 rounded-lg border border-border/60 bg-background/40 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground" />
{results.length > 0 && (
{results.map((r) => ( ))}
)} {msg &&
{msg}
}
) } const ROLE_METADATA: Record = { vision: { title: "Bilder & Vision", desc: "Verarbeitet Bilder, Screenshots und visuelle Diagramme in multimodalen Chats.", icon: Eye }, coder: { title: "Coden & Entwicklung", desc: "Autovervollständigung, Refactoring und Codegenerierung direkt in deiner IDE.", icon: Code }, reasoning: { title: "Logik & Nachdenken", desc: "Komplexe logische Gedankengänge, Mathematik und tiefgründiges Planen (Reasoning).", icon: Brain }, agent: { title: "Autonomer Agent (Hermes)", desc: "Führt selbstständig Terminalbefehle aus und interagiert mit deinem Homelab.", icon: Bot }, scout: { title: "Allrounder & Chat", desc: "Schnelle Antworten, Zusammenfassungen, Übersetzungen und alltägliche Fragen.", icon: Compass } } function Discover() { const [data, setData] = useState(null) const [models, setModels] = useState([]) const [updates, setUpdates] = useState(null) const [error, setError] = useState("") const [loading, setLoading] = useState(true) const [installing, setInstalling] = useState>({}) const [expandedAlternatives, setExpandedAlternatives] = useState>({}) const [showExpert, setShowExpert] = useState(false) useEffect(() => { Promise.all([ api("/api/discover"), api<{ models: ModelInfo[] }>("/api/models"), api("/api/maintenance/updates").catch(() => null) ]) .then(([discoverData, modelsData, updatesData]) => { setData(discoverData) setModels(modelsData.models || []) if (updatesData) setUpdates(updatesData) }) .catch((e) => setError(String(e))) .finally(() => setLoading(false)) }, []) async function install(repo: string, role: string, quant: string, toolCapable: boolean) { setInstalling((s) => ({ ...s, [repo]: "Starte..." })) try { await api("/api/models/install", { method: "POST", body: JSON.stringify({ repo, role, quant, jinja: toolCapable }), }) setInstalling((s) => ({ ...s, [repo]: "Download läuft" })) } catch (e) { setInstalling((s) => ({ ...s, [repo]: "Fehler" })) } } if (loading) return
Analysiere Hardware und suche passende GGUF-Empfehlungen…
if (error || !data) return (
Empfehlungsdienst temporär nicht erreichbar ({error}).
) return (
{/* Informational Header */}
Modell-Registry geladen für {data.sys_ram_gb} GB System-RAM.
Empfehlungen sind automatisch auf deine Box-Hardware optimiert.
{/* Sockets/Slots Grid */}
{data.categories.map((cat) => { const meta = ROLE_METADATA[cat.role] || { title: cat.title || cat.role, desc: "Spezifisches Modell für diese Systemrolle.", icon: Layers } const IconComponent = meta.icon // Check if a model is installed for this role const installedModel = models.find((m) => m.role === cat.role) // Check if an upgrade is available for this role const hasUpgrade = updates?.model_list.find((u) => u.role === cat.role) // Get the primary recommended model const recommendedModel = cat.models.find((m) => m.repo === cat.recommended) || cat.models[0] if (!recommendedModel) return null const isInstallingRecommended = installing[recommendedModel.repo] const alternativeModels = cat.models.filter((m) => m.repo !== cat.recommended) const isExpanded = !!expandedAlternatives[cat.role] return (
{/* Socket Header */}

{meta.title}

Rolle: {cat.role}
{/* Status Badges */} {installedModel ? ( Aktiviert ) : ( Frei )}
{/* Role Description */}

{meta.desc}

{/* Current vs Recommended model card */}
{installedModel ? (
Aktive GGUF-Belegung
{installedModel.name.split("/").pop()}
Größe: {fmtBytes(installedModel.size_bytes || 0)} Quant: {installedModel.quant || "GGUF"}
) : (
Empfohlenes Modell
{recommendedModel.name}
Ersteller: {recommendedModel.author} Quant: {recommendedModel.quant}
)}
{/* Main Action Button */}
{installedModel ? ( hasUpgrade ? (
Bessere Version in der Registry: {hasUpgrade.repo.split("/").pop()}
) : (
Auf neuestem Stand
) ) : ( )}
{/* Collapsible alternatives list */} {alternativeModels.length > 0 && (
{isExpanded && (
{alternativeModels.map((alt) => (
{alt.name}
Quant: {alt.quant} {alt.fit.text}
))}
)}
)}
) })}
{/* Collapsible Custom Hugging Face Downloader */}
{showExpert && (
)}
) } export function ModelsView() { const [tab, setTab] = useState<"cockpit" | "discover">("cockpit") return (

Modell-Manager

Verwalte deine Modelle und passe das Gateway-Routing live über das interaktive Cockpit an.

{(["cockpit", "discover"] as const).map((t) => ( ))}
{tab === "cockpit" ? : }
) }