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,20 +1,18 @@
|
||||
import { useState, useRef, useCallback } from "react"
|
||||
import { Download, Trash2, Edit3, Activity, HardDrive, X, Check, Copy, Zap, Bot } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { Download, Trash2, Edit3, Activity, HardDrive, X, Check, Zap, Bot } from "lucide-react"
|
||||
import { api, setGroup, type ModelInfo, type RoleRecResp } from "@/lib/api"
|
||||
import { useModels, useGroups, useRouting, useConnect, useUpdates, useHermesBrain, useSystemStatus, useQueryClient, qk } from "@/lib/queries"
|
||||
import { useModels, useGroups, useUpdates, useHermesBrain, useSystemStatus, 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, roleTone } from "@/components/models/ModelBadges"
|
||||
import { getBrandInfo, roleTone } from "@/components/models/ModelBadges"
|
||||
import { SpecDraftModal } from "@/components/models/SpecDraftModal"
|
||||
import { LaneEditor } from "@/components/models/LaneEditor"
|
||||
|
||||
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 { data: brain } = useHermesBrain()
|
||||
const { data: sysStatus } = useSystemStatus()
|
||||
@@ -53,13 +51,9 @@ export function Cockpit() {
|
||||
}
|
||||
|
||||
// UI state
|
||||
const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null)
|
||||
const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null)
|
||||
const [roleRec, setRoleRec] = useState<RoleRecResp | null>(null)
|
||||
const [specModel, setSpecModel] = useState<ModelInfo | null>(null)
|
||||
const [showBrainSwitch, setShowBrainSwitch] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
|
||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid")
|
||||
const [filterMode, setFilterMode] = useState<"all" | "in_use">("all")
|
||||
|
||||
@@ -70,55 +64,6 @@ export function Cockpit() {
|
||||
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<ResizeObserver | null>(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 doLoadModel(name: string) {
|
||||
try {
|
||||
await api(`/api/models/${encodeURIComponent(name)}/load`, { method: "POST" })
|
||||
@@ -265,30 +210,6 @@ export function Cockpit() {
|
||||
)
|
||||
}
|
||||
|
||||
async function handleSetBrain(modelName: string) {
|
||||
showConfirm(
|
||||
"Agent-Hirn wechseln?",
|
||||
`'${modelName.split("/").pop()}' als Agent-Hirn (Alias hermes) setzen? Es wird warm gehalten (brains-Gruppe); Hermes nutzt es nach einem kurzen Gateway-Restart.`,
|
||||
async () => {
|
||||
try {
|
||||
await api("/api/agent/brain/set", { method: "POST", body: JSON.stringify({ model_id: modelName }) })
|
||||
showAlert("Erledigt", "Agent-Hirn gewechselt. Gateway wurde neugestartet.")
|
||||
setShowBrainSwitch(false)
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Wechsel fehlgeschlagen: ${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 <div className="text-xs text-muted-foreground py-12 text-center">Initialisiere HUD Cockpit…</div>
|
||||
if (error) {
|
||||
return (
|
||||
@@ -308,13 +229,6 @@ export function Cockpit() {
|
||||
? gttTotal
|
||||
: (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 (
|
||||
<div className="space-y-8">
|
||||
{/* Styles inside Cockpit for dash flow animations */}
|
||||
@@ -388,320 +302,6 @@ export function Cockpit() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ZONE B: SVG Node Router */}
|
||||
<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</span>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5 leading-relaxed">
|
||||
Klicke links auf Editoren, um Configs zu kopieren. Klicke rechts auf Rollen, um GGUF-Zuweisungen live zu ändern.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* The Graph Canvas Area */}
|
||||
<div ref={containerRef} className="relative w-full h-96 border border-border/30 rounded-xl bg-black/25 overflow-hidden flex">
|
||||
{/* SVG Overlay behind HTML Nodes */}
|
||||
<svg className="absolute inset-0 pointer-events-none w-full h-full">
|
||||
{/* Gradients */}
|
||||
<defs>
|
||||
<linearGradient id="cyan-to-teal" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#06b6d4" stopOpacity="0.45" />
|
||||
<stop offset="100%" stopColor="#0d9488" stopOpacity="0.45" />
|
||||
</linearGradient>
|
||||
<linearGradient id="active-glow" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.8" />
|
||||
<stop offset="100%" stopColor="#10b981" stopOpacity="0.8" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* Bezier Curves: Clients to Gateway (10% -> 50%) */}
|
||||
{/* 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="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="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="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="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="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="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="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="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="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="svg-flow-path" />
|
||||
)}
|
||||
</svg>
|
||||
|
||||
{/* HTML Nodes */}
|
||||
{/* COLUMN 1: Client nodes */}
|
||||
<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")}
|
||||
onMouseLeave={() => setHoveredNode(null)}
|
||||
onClick={() => setActiveClient(c => c === "roocode" ? null : "roocode")}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
|
||||
<span>Roo Code</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: "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>
|
||||
|
||||
{/* COLUMN 2: Central Gateway Node — virtuelle Lanes (Router pickt das echte Modell) */}
|
||||
<div
|
||||
className="absolute select-none z-10 w-40 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"
|
||||
style={{ left: "50%", top: "50%" }}
|
||||
>
|
||||
<div className="text-[10px] uppercase font-bold text-primary font-space tracking-wide">Gateway · Lanes</div>
|
||||
<div className="mt-1 flex flex-col gap-0.5 text-[9px] font-mono text-muted-foreground">
|
||||
{routing?.lanes?.length ? routing.lanes.map((l) => (
|
||||
<span key={l.name}>
|
||||
<span className="text-foreground font-semibold">{l.name}</span>
|
||||
{l.threshold_chars ? ` ›${(l.threshold_chars / 1000).toFixed(0)}k` : l.escalate_chars ? ` ⇧${(l.escalate_chars / 1000).toFixed(0)}k` : ""}
|
||||
</span>
|
||||
)) : <span>chat · coding</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">
|
||||
Router
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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 (
|
||||
<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={() => openRoleAssign(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>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* CLIENT SETUP MODAL WINDOW */}
|
||||
{activeClient && connectData && (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200 flex flex-col justify-between">
|
||||
{/* Modal Header */}
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<span className="text-xs 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-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Guide Steps */}
|
||||
<div className="text-xs 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>
|
||||
|
||||
{/* Snippet box */}
|
||||
{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-[10px] 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-xs font-semibold text-primary hover:text-primary-foreground flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
{copied ? <Check className="h-3.5 w-3.5 text-emerald-400" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
<span>{copied ? "Kopiert" : "Kopieren"}</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 max-h-48 overflow-y-auto text-xs 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>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setActiveClient(null)}
|
||||
className="w-full h-9 rounded-lg bg-primary text-primary-foreground hover:opacity-90 font-semibold text-xs transition-opacity cursor-pointer mt-2"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legend for the connection graph */}
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ZONE B.4: Lane-Routing & Policy (UI-editierbar, hot-reload) */}
|
||||
<LaneEditor />
|
||||
|
||||
@@ -759,10 +359,11 @@ export function Cockpit() {
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowBrainSwitch(true)}
|
||||
onClick={() => window.dispatchEvent(new CustomEvent("mc-navigate", { detail: { view: "agent" } }))}
|
||||
className="h-7 px-3 rounded-lg border border-indigo-500/30 bg-indigo-500/5 text-indigo-300 text-[9px] font-bold uppercase hover:bg-indigo-500/15 transition-all cursor-pointer"
|
||||
title="Hirn-Wechsel passiert jetzt zentral im Hermes-Tab"
|
||||
>
|
||||
Hirn wechseln
|
||||
Im Hermes-Tab wechseln →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1314,60 +915,6 @@ export function Cockpit() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Agent-Hirn wechseln */}
|
||||
{showBrainSwitch && (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-1.5">
|
||||
<Bot className="h-4 w-4" /> Agent-Hirn wechseln
|
||||
</h3>
|
||||
<button onClick={() => setShowBrainSwitch(false)} className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
Wähle ein <strong className="text-foreground">installiertes</strong> Modell als Agent-Hirn. Es bekommt den
|
||||
<code className="text-primary"> hermes</code>-Alias, wird warm gehalten (brains-Gruppe), und Hermes nutzt es nach
|
||||
einem kurzen Gateway-Restart. Neues Modell (z.B. Hermes-4.3 oder Gemma-4)? Erst über „Modelle finden" laden.
|
||||
</p>
|
||||
<div className="space-y-1.5 max-h-72 overflow-y-auto pr-1">
|
||||
{models.map((m) => {
|
||||
const isBrain = m.role === "hermes"
|
||||
return (
|
||||
<button
|
||||
key={m.name}
|
||||
onClick={() => !isBrain && handleSetBrain(m.name)}
|
||||
disabled={isBrain || m.incomplete}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-2.5 rounded-lg text-xs flex items-center justify-between font-mono border transition-all",
|
||||
isBrain ? "border-indigo-500/40 bg-indigo-500/10 text-indigo-300"
|
||||
: m.incomplete ? "border-border/20 bg-background/10 text-muted-foreground/50 cursor-not-allowed"
|
||||
: "border-border/30 bg-background/20 text-foreground hover:bg-accent cursor-pointer"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="font-semibold truncate max-w-[280px]">{m.name.split("/").pop()?.replace(/\.gguf$/i, "")}</span>
|
||||
<span className="text-[9px] text-muted-foreground mt-0.5">
|
||||
{m.capabilities.params_b ? `${m.capabilities.params_b}B` : "?"} · {fmtSize(m.size_bytes)}
|
||||
{m.role && ` · Rolle: ${m.role}`}{m.capabilities.tools !== "no" ? " · Tools ✓" : " · ohne Tools"}
|
||||
</span>
|
||||
</div>
|
||||
{isBrain
|
||||
? <span className="text-[9px] font-bold uppercase text-indigo-300 shrink-0 flex items-center gap-1"><Check className="h-3.5 w-3.5" /> Aktiv</span>
|
||||
: <span className="text-[9px] font-bold uppercase text-primary/70 shrink-0">Als Hirn setzen →</span>}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="text-[10px] text-amber-400/80 flex items-start gap-1.5 border-t border-border/20 pt-2.5">
|
||||
<span>💡</span>
|
||||
<span>Für einen <strong>Agenten</strong> sind Hermes-Modelle (natives Tool-Calling) meist die robustere Wahl als allgemeine Modelle.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dialogElement}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user