fix: simplify agent graph to show the 4 tiles and fix compilation errors

This commit is contained in:
Hitonabi
2026-06-26 08:12:30 +02:00
parent 18f361d485
commit 4acea97f03
7 changed files with 453 additions and 717 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-izDdu6A_.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-mQ1DUIFB.css">
<script type="module" crossorigin src="/assets/index-CiscfPzK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ChiXEUW1.css">
</head>
<body>
<div id="root"></div>
+89 -353
View File
@@ -1,6 +1,6 @@
import { useEffect, useState, useRef, useCallback } from "react"
import { Bot, ExternalLink, Activity, Cpu, Wrench, Shield, X, Check, Copy } from "lucide-react"
import { api, type AgentStatus, type ModelInfo, type RoutingResp, type ConnectResp } from "@/lib/api"
import { Bot, ExternalLink, Activity, Cpu, Wrench, Shield } from "lucide-react"
import { api, type AgentStatus } from "@/lib/api"
import { cn, resolveExternalUrl } from "@/lib/utils"
function Tile({ label, ok, detail, icon: Icon }: { label: string; ok: boolean; detail?: string; icon: any }) {
@@ -30,22 +30,11 @@ function Tile({ label, ok, detail, icon: Icon }: { label: string; ok: boolean; d
)
}
const ROLES = ["fast", "heavy", "coder", "reasoning", "agent", "vision", "scout"]
export function AgentView() {
const [s, setS] = useState<AgentStatus | null>(null)
const [error, setError] = useState("")
// Graph states
const [models, setModels] = useState<ModelInfo[]>([])
const [running, setRunning] = useState<string[]>([])
const [routing, setRouting] = useState<RoutingResp | null>(null)
const [connectData, setConnectData] = useState<ConnectResp | null>(null)
// Graph UI states
const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null)
const [activeRoleDrop, setActiveRoleDrop] = useState<string | null>(null)
const [copied, setCopied] = useState(false)
// Graph UI state
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
// Canvas pixel tracking for pixel-perfect connection graph without non-uniform scaling
@@ -71,47 +60,14 @@ export function AgentView() {
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}`
// Helper to generate curve between any two points
const getCurvePath = (startX: number, startY: number, endX: number, endY: number) => {
const midX = (startX + endX) / 2
return `M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`
}
function loadData() {
api<AgentStatus>("/api/agent/status").then(setS).catch((e) => setError(String(e)))
Promise.all([
api<{ models: ModelInfo[]; running?: string[] }>("/api/models"),
api<RoutingResp>("/api/routing"),
api<ConnectResp>("/api/connect")
])
.then(([mResp, rResp, cResp]) => {
setModels(mResp.models || [])
setRunning(mResp.running || [])
setRouting(rResp)
setConnectData(cResp)
})
.catch((e) => console.error("Error loading graph data in AgentView", e))
}
useEffect(() => {
@@ -120,32 +76,6 @@ export function AgentView() {
return () => clearInterval(t)
}, [])
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
}
async function handleRoleChange(role: string, modelName: string) {
setActiveRoleDrop(null)
try {
await api(`/api/models/${encodeURIComponent(modelName)}/role`, {
method: "POST",
body: JSON.stringify({ role: role || null }),
})
loadData()
} catch (e) {
alert(`Fehler beim Zuweisen der Rolle: ${e}`)
}
}
async function copySnippet(snippet?: string) {
if (!snippet) return
await navigator.clipboard.writeText(snippet)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}
return (
<div className="space-y-6">
{/* Styles inside AgentView for dash flow animations */}
@@ -155,7 +85,7 @@ export function AgentView() {
stroke-dashoffset: -20;
}
}
.animate-flow-cyan {
.svg-flow-path {
stroke-dasharray: 4 6;
animation: flow-dash 1s linear infinite;
}
@@ -229,9 +159,9 @@ export function AgentView() {
{/* Interactive Gateway Graph */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-6 shadow-lg shadow-black/10 space-y-4">
<div>
<span className="text-xs font-bold uppercase tracking-wider text-foreground">Interactive Gateway Graph (Duplikat)</span>
<span className="text-xs font-bold uppercase tracking-wider text-foreground">Interactive Agent Flow Graph</span>
<p className="text-[10px] text-muted-foreground mt-0.5 leading-relaxed">
Visualisiert die Kopplung der Client-Editoren mit dem OpenAI-Gateway und den aktiven Modell-Rollen.
Visualisiert das Zusammenspiel und die Verbindungspfade zwischen den Hermes-Systemkomponenten.
</p>
</div>
@@ -249,309 +179,115 @@ export function AgentView() {
</linearGradient>
</defs>
{/* Bezier Curves: Clients to Gateway */}
{/* Roo Code (y=10) */}
<path d={getClientPath(0.10)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "roocode" || activeClient === "roocode") && (
<path d={getClientPath(0.10)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
{/* Connection: WebUI to Gateway */}
<path d={getCurvePath(w * 0.15, h * 0.5, w * 0.5, h * 0.5)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "webui" || s.webui_reachable) && (
<path d={getCurvePath(w * 0.15, h * 0.5, w * 0.5, h * 0.5)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* Cursor (y=30) */}
<path d={getClientPath(0.30)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "cursor" || activeClient === "cursor") && (
<path d={getClientPath(0.30)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
{/* Connection: Gateway to Gehirn */}
<path d={getCurvePath(w * 0.5, h * 0.5, w * 0.85, h * 0.3)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "gateway" || hoveredNode === "brain" || s.gateway_reachable) && (
<path d={getCurvePath(w * 0.5, h * 0.5, w * 0.85, h * 0.3)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* OpenCode (y=50) */}
<path d={getClientPath(0.50)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "opencode" || activeClient === "opencode") && (
<path d={getClientPath(0.50)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
)}
{/* Zed (y=70) */}
<path d={getClientPath(0.70)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "zed" || activeClient === "zed") && (
<path d={getClientPath(0.70)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
)}
{/* Continue (y=90) */}
<path d={getClientPath(0.90)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "continue" || activeClient === "continue") && (
<path d={getClientPath(0.90)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
)}
{/* Bezier Curves: Gateway to Roles */}
{/* fast (y=12) */}
<path d={getRolePath(0.12)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("fast") && (
<path d={getRolePath(0.12)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
)}
{/* heavy (y=31) */}
<path d={getRolePath(0.31)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("heavy") && (
<path d={getRolePath(0.31)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
)}
{/* coder (y=50) */}
<path d={getRolePath(0.50)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("coder") && (
<path d={getRolePath(0.50)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
)}
{/* vision (y=69) */}
<path d={getRolePath(0.69)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("vision") && (
<path d={getRolePath(0.69)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
)}
{/* scout (y=88) */}
<path d={getRolePath(0.88)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("scout") && (
<path d={getRolePath(0.88)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
{/* Connection: Gateway to Verdrahtung */}
<path d={getCurvePath(w * 0.5, h * 0.5, w * 0.85, h * 0.7)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "gateway" || hoveredNode === "wiring" || s.gateway_reachable) && (
<path d={getCurvePath(w * 0.5, h * 0.5, w * 0.85, h * 0.7)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
</svg>
{/* Client Nodes */}
{/* COLUMN 1: Interface */}
<div
className="absolute cursor-pointer select-none z-10 w-28 h-8 -translate-y-1/2 -translate-x-1/2 rounded-xl border border-border/60 bg-card/75 backdrop-blur-md flex items-center justify-center gap-1.5 px-2 hover:border-primary transition-all text-[10px] font-semibold text-foreground shadow shadow-black/20"
style={{ left: "10%", top: "10%" }}
onMouseEnter={() => setHoveredNode("roocode")}
className="absolute cursor-pointer select-none z-10 w-36 h-9 -translate-y-1/2 -translate-x-1/2 rounded-xl border border-border/60 bg-card/75 backdrop-blur-md flex items-center justify-center gap-1.5 px-2.5 hover:border-primary transition-all text-[10px] font-semibold text-foreground shadow shadow-black/20"
style={{ left: "15%", top: "50%" }}
onMouseEnter={() => setHoveredNode("webui")}
onMouseLeave={() => setHoveredNode(null)}
onClick={() => setActiveClient(c => c === "roocode" ? null : "roocode")}
onClick={() => s.webui_reachable && window.open(resolveExternalUrl(s.webui_url), "_blank")}
title={s.webui_reachable ? "Klicken um Chat-WebUI zu öffnen" : "WebUI Offline"}
>
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
<span>Roo Code</span>
<Activity className={cn("h-3.5 w-3.5", s.webui_reachable ? "text-emerald-400" : "text-amber-500")} />
<span>Agent WebUI</span>
<span className={cn("w-1.5 h-1.5 rounded-full animate-pulse ml-auto", s.webui_reachable ? "bg-emerald-500" : "bg-amber-500")} />
</div>
{/* COLUMN 2: Gateway */}
<div
className="absolute cursor-pointer select-none z-10 w-28 h-8 -translate-y-1/2 -translate-x-1/2 rounded-xl border border-border/60 bg-card/75 backdrop-blur-md flex items-center justify-center gap-1.5 px-2 hover:border-primary transition-all text-[10px] font-semibold text-foreground shadow shadow-black/20"
style={{ left: "10%", top: "30%" }}
onMouseEnter={() => setHoveredNode("cursor")}
onMouseLeave={() => setHoveredNode(null)}
onClick={() => setActiveClient(c => c === "cursor" ? null : "cursor")}
>
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
<span>Cursor IDE</span>
</div>
<div
className="absolute cursor-pointer select-none z-10 w-28 h-8 -translate-y-1/2 -translate-x-1/2 rounded-xl border border-border/60 bg-card/75 backdrop-blur-md flex items-center justify-center gap-1.5 px-2 hover:border-primary transition-all text-[10px] font-semibold text-foreground shadow shadow-black/20"
style={{ left: "10%", top: "50%" }}
onMouseEnter={() => setHoveredNode("opencode")}
onMouseLeave={() => setHoveredNode(null)}
onClick={() => setActiveClient(c => c === "opencode" ? null : "opencode")}
>
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
<span>OpenCode</span>
</div>
<div
className="absolute cursor-pointer select-none z-10 w-28 h-8 -translate-y-1/2 -translate-x-1/2 rounded-xl border border-border/60 bg-card/75 backdrop-blur-md flex items-center justify-center gap-1.5 px-2 hover:border-primary transition-all text-[10px] font-semibold text-foreground shadow shadow-black/20"
style={{ left: "10%", top: "70%" }}
onMouseEnter={() => setHoveredNode("zed")}
onMouseLeave={() => setHoveredNode(null)}
onClick={() => setActiveClient(c => c === "zed" ? null : "zed")}
>
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
<span>Zed</span>
</div>
<div
className="absolute cursor-pointer select-none z-10 w-28 h-8 -translate-y-1/2 -translate-x-1/2 rounded-xl border border-border/60 bg-card/75 backdrop-blur-md flex items-center justify-center gap-1.5 px-2 hover:border-primary transition-all text-[10px] font-semibold text-foreground shadow shadow-black/20"
style={{ left: "10%", top: "90%" }}
onMouseEnter={() => setHoveredNode("continue")}
onMouseLeave={() => setHoveredNode(null)}
onClick={() => setActiveClient(c => c === "continue" ? null : "continue")}
>
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
<span>Continue</span>
</div>
{/* Gateway Node */}
<div
className="absolute select-none z-10 w-36 py-3 px-4 -translate-y-1/2 -translate-x-1/2 rounded-2xl border border-primary/50 bg-card/90 backdrop-blur-md flex flex-col items-center justify-center text-center shadow-lg shadow-primary/5"
className="absolute select-none z-10 w-40 py-2.5 px-3 -translate-y-1/2 -translate-x-1/2 rounded-2xl border border-primary/50 bg-card/90 backdrop-blur-md flex flex-col items-center justify-center text-center shadow-lg shadow-primary/5 cursor-pointer"
style={{ left: "50%", top: "50%" }}
onMouseEnter={() => setHoveredNode("gateway")}
onMouseLeave={() => setHoveredNode(null)}
>
<div className="text-[10px] uppercase font-bold text-primary font-space tracking-wide">Gateway Auto</div>
<div className="text-[10px] font-mono text-muted-foreground mt-0.5">
Schwelle: &gt; {routing?.heavy_threshold_chars ? (routing.heavy_threshold_chars / 1000) : "4"}k Zeichen
<div className="flex items-center gap-1">
<Bot className="h-3.5 w-3.5 text-primary" />
<span className="text-[10px] uppercase font-bold text-primary font-space tracking-wide">Agent Gateway</span>
</div>
<div className="mt-1 px-1.5 py-0.5 rounded bg-primary/10 border border-primary/20 text-[9px] font-semibold text-primary uppercase font-mono">
Auto-Swap
<div className="text-[9px] font-mono text-muted-foreground mt-0.5">Port :8642</div>
<div className={cn("mt-1 px-1.5 py-0.5 rounded text-[8px] font-semibold uppercase font-mono border",
s.gateway_reachable
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
: "bg-red-500/10 border-red-500/20 text-red-400"
)}>
{s.gateway_reachable ? "Online" : "Offline"}
</div>
</div>
{/* Role Nodes */}
{ROLES.map((role) => {
const yPositions = ["12%", "31%", "50%", "69%", "88%"]
const activeModel = getModelForRole(role)
const isWarm = activeModel ? running.includes(activeModel.name) : false
if (role === "reasoning" || role === "agent") return null
const indexMap = { fast: 0, heavy: 1, coder: 2, vision: 3, scout: 4 }[role] as number
return (
<div
key={role}
className={cn(
"absolute z-10 w-44 py-1.5 px-3 -translate-y-1/2 -translate-x-1/2 rounded-xl border flex flex-col justify-center hover:border-primary/50 transition-all text-left shadow select-none cursor-pointer",
isWarm
? "border-emerald-500/50 bg-emerald-500/5 shadow-emerald-500/5"
: activeModel
? "border-border/60 bg-card/75"
: "border-dashed border-border/40 bg-background/20"
)}
style={{ left: "90%", top: yPositions[indexMap] }}
onClick={() => setActiveRoleDrop(activeRoleDrop === role ? null : role)}
>
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{role}</span>
{isWarm && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" />}
</div>
<div className="text-[10px] font-mono font-medium truncate mt-0.5 text-foreground max-w-[150px]">
{activeModel ? activeModel.name.split("/").pop()?.replace(".gguf", "") : "Keine Zuweisung"}
</div>
{/* Zuweisung Popover */}
{activeRoleDrop === role && (
<div className="absolute right-0 top-full mt-1 z-35 w-52 rounded-xl border border-border/80 bg-popover/95 backdrop-blur-md p-1.5 shadow-2xl space-y-1">
<div className="text-[9px] uppercase font-bold text-muted-foreground/60 px-2 py-1 select-none">Modell zuweisen:</div>
<button
onClick={() => handleRoleChange(role, "")}
className="w-full text-left px-2.5 py-1.5 rounded-lg text-[10px] hover:bg-accent text-red-400 font-semibold cursor-pointer"
>
Zuweisung entfernen
</button>
{models.map((m) => (
<button
key={m.name}
onClick={() => handleRoleChange(role, m.name)}
className={cn(
"w-full text-left px-2.5 py-1.5 rounded-lg text-[10px] hover:bg-accent flex items-center justify-between font-mono cursor-pointer",
m.role === role ? "text-primary font-bold" : "text-foreground"
)}
>
<span className="truncate max-w-[150px]">{m.name.split("/").pop()?.replace(".gguf", "")}</span>
{m.role === role && <Check className="h-3 w-3 shrink-0" />}
</button>
))}
</div>
)}
{/* COLUMN 3: Brain & Wiring */}
<div
className={cn(
"absolute z-10 w-44 py-1.5 px-3 -translate-y-1/2 -translate-x-1/2 rounded-xl border flex flex-col justify-center hover:border-primary/50 transition-all text-left shadow select-none",
s.gateway_reachable ? "border-emerald-500/50 bg-emerald-500/5 shadow-emerald-500/5" : "border-border/60 bg-card/75"
)}
style={{ left: "85%", top: "30%" }}
onMouseEnter={() => setHoveredNode("brain")}
onMouseLeave={() => setHoveredNode(null)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-muted-foreground">
<Cpu className="h-3 w-3 text-primary" />
<span className="text-[10px] font-bold uppercase tracking-wider">Aktives Gehirn</span>
</div>
)
})}
{/* Floating Setup Popover */}
{activeClient && connectData && (
<div
className="absolute z-30 md:w-96 w-[90%] rounded-2xl border border-border/80 bg-card/95 backdrop-blur-xl p-4 shadow-2xl space-y-3 flex flex-col justify-between"
style={{
left: "22%",
top: activeClient === "roocode" ? "5%"
: activeClient === "cursor" ? "20%"
: activeClient === "opencode" ? "40%"
: activeClient === "zed" ? "55%"
: "65%"
}}
>
<div className="flex items-center justify-between border-b border-border/20 pb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-primary">
{activeClient === "roocode" && "Roo Code Setup"}
{activeClient === "cursor" && "Cursor Setup"}
{activeClient === "opencode" && "OpenCode Setup"}
{activeClient === "zed" && "Zed Setup"}
{activeClient === "continue" && "Continue Setup"}
</span>
<button
onClick={() => setActiveClient(null)}
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
<div className="text-[10px] text-muted-foreground leading-relaxed space-y-2">
{activeClient === "roocode" && (
<ul className="list-decimal pl-4 space-y-1">
<li>Suche in VS Code nach der Erweiterung <strong>Roo Code</strong> und installiere sie.</li>
<li>Wähle in den Roo Code Einstellungen: Provider: <strong>OpenAI Compatible</strong>.</li>
<li>Füge das untenstehende JSON-Snippet in die <code>settings.json</code> ein.</li>
</ul>
)}
{activeClient === "cursor" && (
<ul className="list-decimal pl-4 space-y-1">
<li>Öffne Cursor Settings <strong>Models</strong>.</li>
<li>Deaktiviere Cloud-Modelle, klappe <strong>OpenAI API</strong> auf.</li>
<li>Trage die Base URL unten ein und aktiviere das Modell <strong>auto</strong>.</li>
</ul>
)}
{activeClient === "opencode" && (
<ul className="list-decimal pl-4 space-y-1">
<li>Öffne die <code>opencode.jsonc</code> Konfigurationsdatei.</li>
<li>Ersetze den Provider-Eintrag unter <code>provider</code> mit dem Snippet unten.</li>
</ul>
)}
{activeClient === "zed" && (
<ul className="list-decimal pl-4 space-y-1">
<li>Öffne die Zed Settings (<code>ctrl+,</code>).</li>
<li>Füge das untenstehende JSON-Segment unter <code>language_models</code> ein.</li>
</ul>
)}
{activeClient === "continue" && (
<ul className="list-decimal pl-4 space-y-1">
<li>Klicke auf das Zahnrad-Symbol in der Continue-Erweiterung.</li>
<li>Füge den Gateway-Eintrag zum <code>models</code>-Array hinzu.</li>
</ul>
)}
</div>
{connectData.tools && (
<div className="relative rounded-xl border border-border/40 bg-black/40 overflow-hidden mt-1 shrink-0">
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border/20 bg-black/20">
<span className="text-[8px] font-mono text-muted-foreground">JSON Config</span>
<button
onClick={() => copySnippet(
activeClient === "roocode" ? connectData.tools.cline?.snippet :
activeClient === "cursor" ? connectData.tools.cursor?.snippet :
activeClient === "opencode" ? connectData.tools.opencode?.snippet :
activeClient === "zed" ? connectData.tools.zed?.snippet :
connectData.tools.continue?.snippet
)}
className="text-[9px] font-semibold text-primary hover:text-primary-foreground flex items-center gap-1 cursor-pointer"
>
{copied ? <Check className="h-3 w-3 text-emerald-400" /> : <Copy className="h-3 w-3" />}
<span>{copied ? "Kopiert" : "Kopieren"}</span>
</button>
</div>
<pre className="p-3 max-h-36 overflow-y-auto text-[9px] font-mono text-cyan-200/90 whitespace-pre scrollbar-thin select-text">
<code>
{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}
</code>
</pre>
</div>
)}
{s.gateway_reachable && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" />}
</div>
)}
<div className="text-[10px] font-mono font-medium truncate mt-0.5 text-foreground font-space" title={s.brain_model}>
{s.brain_model || "auto"}
</div>
</div>
<div
className={cn(
"absolute z-10 w-44 py-1.5 px-3 -translate-y-1/2 -translate-x-1/2 rounded-xl border flex flex-col justify-center hover:border-primary/50 transition-all text-left shadow select-none",
s.has_config ? "border-emerald-500/50 bg-emerald-500/5 shadow-emerald-500/5" : "border-border/60 bg-card/75"
)}
style={{ left: "85%", top: "70%" }}
onMouseEnter={() => setHoveredNode("wiring")}
onMouseLeave={() => setHoveredNode(null)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-muted-foreground">
<Wrench className="h-3 w-3 text-primary" />
<span className="text-[10px] font-bold uppercase tracking-wider">Verdrahtung</span>
</div>
{s.has_config && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" />}
</div>
<div className="text-[9px] font-mono mt-0.5 text-muted-foreground flex gap-1 justify-between">
<span>Config: {s.has_config ? "✓" : "—"}</span>
<span>Skills: {s.has_skills ? "✓" : "—"}</span>
<span>Memory: {s.has_memories ? "✓" : "—"}</span>
</div>
</div>
</div>
{/* Legend */}
<div className="flex flex-wrap items-center gap-6 mt-3 text-[10px] text-muted-foreground border-t border-border/20 pt-3">
<span className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-cyan-400" />
<span>Cyan-Fluss: Client-Anfrage an Gateway</span>
<span>Cyan-Fluss: Client-Anfrage an Agenten</span>
</span>
<span className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
<span>Grüner Puls: Aktive Verbindung / Warmes Modell geladen</span>
</span>
<span className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded bg-gradient-to-r from-teal-500 to-emerald-500" />
<span>VRAM-Verlauf: Modellspezifischer Speicheranteil</span>
<span>Grüner Puls: Verbindung hergestellt / Modul online</span>
</span>
</div>
</div>
+11 -11
View File
@@ -327,7 +327,7 @@ function Cockpit() {
stroke-dashoffset: -20;
}
}
.animate-flow-cyan {
.svg-flow-path {
stroke-dasharray: 4 6;
animation: flow-dash 1s linear infinite;
}
@@ -420,62 +420,62 @@ function Cockpit() {
{/* Roo Code (y=10) */}
<path d={getClientPath(0.10)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "roocode" || activeClient === "roocode") && (
<path d={getClientPath(0.10)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getClientPath(0.10)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* Cursor (y=30) */}
<path d={getClientPath(0.30)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "cursor" || activeClient === "cursor") && (
<path d={getClientPath(0.30)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getClientPath(0.30)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* OpenCode (y=50) */}
<path d={getClientPath(0.50)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "opencode" || activeClient === "opencode") && (
<path d={getClientPath(0.50)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getClientPath(0.50)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* Zed (y=70) */}
<path d={getClientPath(0.70)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "zed" || activeClient === "zed") && (
<path d={getClientPath(0.70)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getClientPath(0.70)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* Continue (y=90) */}
<path d={getClientPath(0.90)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "continue" || activeClient === "continue") && (
<path d={getClientPath(0.90)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getClientPath(0.90)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* Bezier Curves: Gateway to Roles (50% -> 90%) */}
{/* fast (y=12) */}
<path d={getRolePath(0.12)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("fast") && (
<path d={getRolePath(0.12)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getRolePath(0.12)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* heavy (y=31) */}
<path d={getRolePath(0.31)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("heavy") && (
<path d={getRolePath(0.31)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getRolePath(0.31)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* coder (y=50) */}
<path d={getRolePath(0.50)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("coder") && (
<path d={getRolePath(0.50)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getRolePath(0.50)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* vision (y=69) */}
<path d={getRolePath(0.69)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("vision") && (
<path d={getRolePath(0.69)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getRolePath(0.69)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
{/* scout (y=88) */}
<path d={getRolePath(0.88)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{isRoleRunning("scout") && (
<path d={getRolePath(0.88)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="animate-flow-cyan" />
<path d={getRolePath(0.88)} stroke="url(#active-glow)" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
</svg>