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:
+11
-10
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Connect: erzeugt saubere, getestete Konfig-Snippets für IDEs/Agenten auf dem
|
Connect: erzeugt saubere, getestete Konfig-Snippets für IDEs/Agenten auf dem
|
||||||
LOKALEN PC (separate Maschine im LAN). Alle zeigen auf den **Gateway** der Box
|
LOKALEN PC (separate Maschine im LAN). Alle zeigen auf den **Gateway** der Box
|
||||||
(`model: auto`, Cockpit-Port :9001/v1) + den **Shared-Memory-MCP** (MC :9001).
|
(Lanes `coding`/`chat`, Cockpit-Port :9001/v1) + den **Shared-Memory-MCP** (MC :9001).
|
||||||
|
|
||||||
Wichtig: Host ist die LAN-IP der Box (NICHT eine Proxy-Domain) — das war in v1
|
Wichtig: Host ist die LAN-IP der Box (NICHT eine Proxy-Domain) — das war in v1
|
||||||
die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
|
die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
|
||||||
@@ -15,8 +15,9 @@ from config import LLAMA_SWAP_URL, MEM0_SERVICE_URL, PORT
|
|||||||
|
|
||||||
DEFAULT_HOST = "192.168.178.151"
|
DEFAULT_HOST = "192.168.178.151"
|
||||||
|
|
||||||
# Modelle, die der Gateway anbietet (model:auto = Standard).
|
# Modelle/Lanes, die der Gateway anbietet. Lanes zuerst: 'coding' (agentischer Coder) ist der
|
||||||
GATEWAY_MODELS = ["auto", "fast", "heavy", "coder", "vision"]
|
# Standard für IDEs, 'chat' für Allgemeines; dahinter die direkten Aliase.
|
||||||
|
GATEWAY_MODELS = ["coding", "chat", "fast", "heavy", "coder", "vision"]
|
||||||
|
|
||||||
|
|
||||||
def _gw(host: str) -> str:
|
def _gw(host: str) -> str:
|
||||||
@@ -34,7 +35,7 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
"apiProvider": "openai",
|
"apiProvider": "openai",
|
||||||
"openAiBaseUrl": gw,
|
"openAiBaseUrl": gw,
|
||||||
"openAiApiKey": "local",
|
"openAiApiKey": "local",
|
||||||
"openAiModelId": "auto",
|
"openAiModelId": "coding",
|
||||||
}, indent=2)
|
}, indent=2)
|
||||||
|
|
||||||
opencode = json.dumps({
|
opencode = json.dumps({
|
||||||
@@ -51,7 +52,7 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
cursor = json.dumps({
|
cursor = json.dumps({
|
||||||
"Base URL": gw,
|
"Base URL": gw,
|
||||||
"API Key": "local",
|
"API Key": "local",
|
||||||
"Active Model": "auto"
|
"Active Model": "coding"
|
||||||
}, indent=2)
|
}, indent=2)
|
||||||
|
|
||||||
zed = json.dumps({
|
zed = json.dumps({
|
||||||
@@ -69,7 +70,7 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
"assistant": {
|
"assistant": {
|
||||||
"default_model": {
|
"default_model": {
|
||||||
"provider": "openai_compatible",
|
"provider": "openai_compatible",
|
||||||
"model": "auto"
|
"model": "coding"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, indent=2)
|
}, indent=2)
|
||||||
@@ -77,7 +78,7 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
cont = json.dumps({
|
cont = json.dumps({
|
||||||
"models": [
|
"models": [
|
||||||
{"title": f"Bosgame / {m}", "provider": "openai", "model": m,
|
{"title": f"Bosgame / {m}", "provider": "openai", "model": m,
|
||||||
"apiBase": gw, "apiKey": "local"} for m in ("auto", "coder", "heavy")
|
"apiBase": gw, "apiKey": "local"} for m in ("coding", "chat", "coder")
|
||||||
]
|
]
|
||||||
}, indent=2)
|
}, indent=2)
|
||||||
|
|
||||||
@@ -89,12 +90,12 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
f"# und hat kein /v1/messages. Dazwischen muss ein Übersetzer (Anthropic ⇄ OpenAI) laufen:\n"
|
f"# und hat kein /v1/messages. Dazwischen muss ein Übersetzer (Anthropic ⇄ OpenAI) laufen:\n"
|
||||||
f"# • claude-code-router (leichtgewichtig, npm)\n"
|
f"# • claude-code-router (leichtgewichtig, npm)\n"
|
||||||
f"# • oder LiteLLM mit /v1/messages-Bridge\n"
|
f"# • oder LiteLLM mit /v1/messages-Bridge\n"
|
||||||
f"# Den Übersetzer auf den Gateway zeigen lassen: baseURL={gw}, model=auto, apiKey=local.\n"
|
f"# Den Übersetzer auf den Gateway zeigen lassen: baseURL={gw}, model=coding, apiKey=local.\n"
|
||||||
f"# Dann Claude Code auf den lokalen Übersetzer richten (Beispiel-Port 3456):\n"
|
f"# Dann Claude Code auf den lokalen Übersetzer richten (Beispiel-Port 3456):\n"
|
||||||
f"\n"
|
f"\n"
|
||||||
f'export ANTHROPIC_BASE_URL="http://localhost:3456"\n'
|
f'export ANTHROPIC_BASE_URL="http://localhost:3456"\n'
|
||||||
f'export ANTHROPIC_AUTH_TOKEN="local"\n'
|
f'export ANTHROPIC_AUTH_TOKEN="local"\n'
|
||||||
f'export ANTHROPIC_MODEL="auto"'
|
f'export ANTHROPIC_MODEL="coding"'
|
||||||
)
|
)
|
||||||
|
|
||||||
memory_mcp = json.dumps({
|
memory_mcp = json.dumps({
|
||||||
@@ -114,7 +115,7 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
# Leitung 1 — das MODELL. Alle Snippets zeigen auf den OpenAI-kompatiblen Gateway.
|
# Leitung 1 — das MODELL. Alle Snippets zeigen auf den OpenAI-kompatiblen Gateway.
|
||||||
"tools": {
|
"tools": {
|
||||||
"cline": {"label": "Roo Code / Cline", "lang": "json", "snippet": cline,
|
"cline": {"label": "Roo Code / Cline", "lang": "json", "snippet": cline,
|
||||||
"note": "OpenAI-Provider → Gateway. Modell 'auto' (schnell, eskaliert bei Bedarf)."},
|
"note": "OpenAI-Provider → Gateway. Lane 'coding' (agentischer Coder); 'chat' für Allgemeines."},
|
||||||
"cursor": {"label": "Cursor", "lang": "json", "snippet": cursor,
|
"cursor": {"label": "Cursor", "lang": "json", "snippet": cursor,
|
||||||
"note": "Einstellungen ➔ Models ➔ OpenAI API key + Base URL."},
|
"note": "Einstellungen ➔ Models ➔ OpenAI API key + Base URL."},
|
||||||
"opencode": {"label": "OpenCode", "lang": "jsonc", "snippet": opencode,
|
"opencode": {"label": "OpenCode", "lang": "jsonc", "snippet": opencode,
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-5476
File diff suppressed because one or more lines are too long
+5466
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CyyjRWEt.js"></script>
|
<script type="module" crossorigin src="/assets/index-DSZM1C9y.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CHGCSDst.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-UDlAwulM.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react"
|
|
||||||
import { Activity, Zap, Moon } from "lucide-react"
|
|
||||||
import { useModels, useTokenStats } from "@/lib/queries"
|
|
||||||
import { fmtSize } from "@/lib/format"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { roleTone } from "@/components/models/ModelBadges"
|
|
||||||
|
|
||||||
export function ActiveModelsCard() {
|
|
||||||
const { data } = useModels(2_000)
|
|
||||||
const { data: stats } = useTokenStats(2_000)
|
|
||||||
const models = data?.models ?? []
|
|
||||||
const running = data?.running ?? []
|
|
||||||
const loaded = models.filter((m) => running.includes(m.name))
|
|
||||||
|
|
||||||
// "Inferenz aktiv": total_tokens ist seit dem letzten Poll gestiegen → es wird generiert.
|
|
||||||
const prev = useRef<number | null>(null)
|
|
||||||
const [generating, setGenerating] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!stats) return
|
|
||||||
const total = stats.total_tokens
|
|
||||||
if (prev.current !== null && total > prev.current) {
|
|
||||||
setGenerating(true)
|
|
||||||
const t = setTimeout(() => setGenerating(false), 4_000)
|
|
||||||
prev.current = total
|
|
||||||
return () => clearTimeout(t)
|
|
||||||
}
|
|
||||||
prev.current = total
|
|
||||||
}, [stats?.total_tokens])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Activity className="h-4.5 w-4.5 text-primary" />
|
|
||||||
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Aktive Modelle (Live)</h2>
|
|
||||||
</div>
|
|
||||||
<span className={cn(
|
|
||||||
"flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider font-mono px-2 py-0.5 rounded-full border transition-colors",
|
|
||||||
generating
|
|
||||||
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/30"
|
|
||||||
: "bg-background/30 text-muted-foreground/70 border-border/40"
|
|
||||||
)}>
|
|
||||||
{generating ? <Zap className="h-3 w-3 animate-pulse" /> : <Moon className="h-3 w-3" />}
|
|
||||||
{generating ? "Inferenz aktiv" : "Idle"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loaded.length === 0 ? (
|
|
||||||
<div className="flex items-center justify-center gap-2 h-20 text-xs text-muted-foreground/70 italic">
|
|
||||||
Kein Modell geladen — Auto-Swap lädt bei Anfrage.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid gap-2">
|
|
||||||
{loaded.map((m) => (
|
|
||||||
<div key={m.name} className="flex items-center justify-between gap-2 p-3 rounded-xl bg-emerald-500/5 border border-emerald-500/20">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="flex items-center gap-1.5 min-w-0">
|
|
||||||
{m.role && (
|
|
||||||
<span className={cn("text-[8px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0",
|
|
||||||
roleTone(m.role))}>
|
|
||||||
{m.role}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-xs font-semibold font-mono text-foreground truncate">
|
|
||||||
{m.name.split("/").pop()?.replace(/\.gguf$/i, "")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-[9px] font-mono text-muted-foreground/70 mt-0.5">
|
|
||||||
{fmtSize(m.size_bytes)} im Unified-RAM
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="flex items-center gap-1 text-[8px] font-semibold text-emerald-400 uppercase tracking-wider font-mono shrink-0">
|
|
||||||
<span className={cn("h-1.5 w-1.5 rounded-full bg-emerald-500", generating && "animate-pulse")} /> warm
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { Boxes, Zap, Moon } from "lucide-react"
|
||||||
|
import { useModels, useTokenStats } from "@/lib/queries"
|
||||||
|
import { fmtSize } from "@/lib/format"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ROLES, roleTone } from "@/components/models/ModelBadges"
|
||||||
|
|
||||||
|
// Vereint die frühere RolesCard (Rolle→Modell) + ActiveModelsCard (warm/Inferenz/Größe)
|
||||||
|
// zu EINER Karte — dieselbe Info stand vorher doppelt auf der Zentrale.
|
||||||
|
export function ModelsCard() {
|
||||||
|
const { data } = useModels(2_000)
|
||||||
|
const { data: stats } = useTokenStats(2_000)
|
||||||
|
const models = data?.models ?? []
|
||||||
|
const running = data?.running ?? []
|
||||||
|
|
||||||
|
// „Inferenz aktiv": total_tokens seit letztem Poll gestiegen → es wird generiert.
|
||||||
|
const prev = useRef<number | null>(null)
|
||||||
|
const [generating, setGenerating] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stats) return
|
||||||
|
const total = stats.total_tokens
|
||||||
|
if (prev.current !== null && total > prev.current) {
|
||||||
|
setGenerating(true)
|
||||||
|
const t = setTimeout(() => setGenerating(false), 4_000)
|
||||||
|
prev.current = total
|
||||||
|
return () => clearTimeout(t)
|
||||||
|
}
|
||||||
|
prev.current = total
|
||||||
|
}, [stats?.total_tokens])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Boxes className="h-4.5 w-4.5 text-primary" />
|
||||||
|
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Modelle</h2>
|
||||||
|
</div>
|
||||||
|
<span className={cn(
|
||||||
|
"flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider font-mono px-2 py-0.5 rounded-full border transition-colors",
|
||||||
|
generating
|
||||||
|
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/30"
|
||||||
|
: "bg-background/30 text-muted-foreground/70 border-border/40"
|
||||||
|
)}>
|
||||||
|
{generating ? <Zap className="h-3 w-3 animate-pulse" /> : <Moon className="h-3 w-3" />}
|
||||||
|
{generating ? "Inferenz aktiv" : "Idle"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 flex-1">
|
||||||
|
{ROLES.map((role) => {
|
||||||
|
const m = models.find((x) => x.role === role)
|
||||||
|
const isRunning = m ? running.includes(m.name) : false
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={role}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between gap-2 p-2 rounded-xl border transition-all duration-300",
|
||||||
|
isRunning
|
||||||
|
? "border-emerald-500/30 bg-emerald-500/5 shadow-sm shadow-emerald-500/5"
|
||||||
|
: m
|
||||||
|
? "border-primary/20 bg-primary/5"
|
||||||
|
: "border-border/30 bg-background/10 opacity-60"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className={cn("text-[9px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0", roleTone(role))}>
|
||||||
|
{role}
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-col min-w-0">
|
||||||
|
<span className="text-xs font-semibold truncate font-mono text-foreground">
|
||||||
|
{m ? m.name.split("/").pop()?.replace(/\.gguf$/i, "") : "nicht zugewiesen"}
|
||||||
|
</span>
|
||||||
|
{m && (
|
||||||
|
<div className="flex gap-1 items-center mt-0.5 flex-wrap text-muted-foreground/70">
|
||||||
|
<span className="text-[9px] font-mono">{fmtSize(m.size_bytes)}</span>
|
||||||
|
{m.prompt_cache && (
|
||||||
|
<span className="text-[7px] leading-none font-mono font-bold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 px-1 py-0.5 rounded" title="Prompt Caching aktiv">PC</span>
|
||||||
|
)}
|
||||||
|
{m.spec_active && (
|
||||||
|
<span className="text-[7px] leading-none font-mono font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-1 py-0.5 rounded" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>SPEC</span>
|
||||||
|
)}
|
||||||
|
{m.parallel_slots > 1 && (
|
||||||
|
<span className="text-[7px] leading-none font-mono font-bold bg-violet-500/10 text-violet-400 border border-violet-500/20 px-1 py-0.5 rounded" title={`${m.parallel_slots} parallele Slots aktiv`}>SLOTS: {m.parallel_slots}</span>
|
||||||
|
)}
|
||||||
|
{m.incomplete && (
|
||||||
|
<span className="text-[7px] leading-none font-mono font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 px-1 py-0.5 rounded" title="GGUF-Datei fehlt">⚠ fehlt</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="shrink-0">
|
||||||
|
{m ? (
|
||||||
|
isRunning ? (
|
||||||
|
<span className="flex items-center gap-1 text-[8px] font-semibold text-emerald-400 uppercase tracking-wider font-mono">
|
||||||
|
<span className={cn("h-1.5 w-1.5 rounded-full bg-emerald-500", generating && "animate-pulse")} /> warm
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-[8px] font-semibold text-muted-foreground uppercase tracking-wider font-mono">bereit</span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span className="text-[8px] font-semibold text-muted-foreground/60 uppercase tracking-wider font-mono">—</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3">
|
||||||
|
Laden erfolgt automatisch per Auto-Swap. Details & Routing im Modell-Manager.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Layers } from "lucide-react"
|
|
||||||
import { useModels } from "@/lib/queries"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { ROLES, roleTone } from "@/components/models/ModelBadges"
|
|
||||||
|
|
||||||
export function RolesCard() {
|
|
||||||
const { data } = useModels(3_000)
|
|
||||||
const models = data?.models ?? []
|
|
||||||
const running = data?.running ?? []
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<Layers className="h-4.5 w-4.5 text-primary" />
|
|
||||||
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Rollen-Belegung</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 max-h-48 overflow-y-auto pr-1 scrollbar-thin">
|
|
||||||
{ROLES.map((role) => {
|
|
||||||
const m = models.find((x) => x.role === role)
|
|
||||||
const isRunning = m ? running.includes(m.name) : false
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={role}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-between p-2 rounded-xl border transition-all duration-300",
|
|
||||||
isRunning
|
|
||||||
? "border-emerald-500/30 bg-emerald-500/5 shadow-sm shadow-emerald-500/5"
|
|
||||||
: m
|
|
||||||
? "border-primary/20 bg-primary/5"
|
|
||||||
: "border-border/30 bg-background/10 opacity-60"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="min-w-0 flex-1 mr-2">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<span className={cn("text-[9px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0",
|
|
||||||
roleTone(role))}>
|
|
||||||
{role}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className="flex flex-col min-w-0">
|
|
||||||
<span className="text-xs font-semibold truncate font-mono text-foreground">
|
|
||||||
{m ? m.name.split("/").pop()?.replace(/\.gguf$/i, "") : "nicht zugewiesen"}
|
|
||||||
</span>
|
|
||||||
{m && (
|
|
||||||
<div className="flex gap-1 items-center mt-0.5 flex-wrap">
|
|
||||||
{m.prompt_cache && (
|
|
||||||
<span className="text-[7px] leading-none font-mono font-bold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 px-1 py-0.5 rounded" title="Prompt Caching aktiv">
|
|
||||||
PC
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{m.spec_active && (
|
|
||||||
<span className="text-[7px] leading-none font-mono font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-1 py-0.5 rounded" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
|
||||||
SPEC
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{m.parallel_slots > 1 && (
|
|
||||||
<span className="text-[7px] leading-none font-mono font-bold bg-violet-500/10 text-violet-400 border border-violet-500/20 px-1 py-0.5 rounded" title={`${m.parallel_slots} parallele Slots aktiv`}>
|
|
||||||
SLOTS: {m.parallel_slots}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{m.incomplete && (
|
|
||||||
<span className="text-[7px] leading-none font-mono font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 px-1 py-0.5 rounded" title="GGUF-Datei fehlt">
|
|
||||||
⚠ fehlt
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
|
||||||
{m ? (
|
|
||||||
isRunning ? (
|
|
||||||
<span className="flex items-center gap-1 text-[8px] font-semibold text-emerald-400 uppercase tracking-wider font-mono">
|
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> warm
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-[8px] font-semibold text-muted-foreground uppercase tracking-wider font-mono">
|
|
||||||
bereit
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className="text-[8px] font-semibold text-muted-foreground/60 uppercase tracking-wider font-mono">
|
|
||||||
—
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3">
|
|
||||||
Laden erfolgt automatisch per Auto-Swap.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,8 @@ import { cn } from "@/lib/utils"
|
|||||||
// Die 5 kanonischen Serving-Rollen (identisch zu Backend sources.py/llamaswap.ROLE_IDS).
|
// Die 5 kanonischen Serving-Rollen (identisch zu Backend sources.py/llamaswap.ROLE_IDS).
|
||||||
export const ROLES = ["fast", "heavy", "coder", "vision", "scout"]
|
export const ROLES = ["fast", "heavy", "coder", "vision", "scout"]
|
||||||
|
|
||||||
// Rolle → Farbton. EINE Quelle der Wahrheit für alle Rollen-Badges (Cockpit, RolesCard,
|
// Rolle → Farbton. EINE Quelle der Wahrheit für alle Rollen-Badges (Cockpit, ModelsCard,
|
||||||
// ActiveModelsCard, …). `hermes` = Agent-Hirn (taucht als geladenes Modell auf).
|
// …). `hermes` = Agent-Hirn (taucht als geladenes Modell auf).
|
||||||
export const ROLE_TONE: Record<string, string> = {
|
export const ROLE_TONE: Record<string, string> = {
|
||||||
fast: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
|
fast: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
|
||||||
heavy: "bg-amber-500/15 text-amber-400 border-amber-500/25",
|
heavy: "bg-amber-500/15 text-amber-400 border-amber-500/25",
|
||||||
|
|||||||
+161
-290
@@ -1,53 +1,12 @@
|
|||||||
import { useState, useRef, useCallback, useMemo } from "react"
|
import { useState } from "react"
|
||||||
import { Bot, ExternalLink, Activity, Cpu, Wrench, Monitor, Shield, Check, X } from "lucide-react"
|
import { Bot, ExternalLink, Activity, Cpu, Wrench, Monitor, Shield, Check, X } from "lucide-react"
|
||||||
import { api } from "@/lib/api"
|
import { api } from "@/lib/api"
|
||||||
import { useAgentStatus, useModels, useQueryClient, qk } from "@/lib/queries"
|
import { useAgentStatus, useModels, useQueryClient, qk } from "@/lib/queries"
|
||||||
import { useDialog } from "@/lib/useDialog"
|
import { useDialog } from "@/lib/useDialog"
|
||||||
import { cn, resolveExternalUrl } from "@/lib/utils"
|
import { cn, resolveExternalUrl } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Lane-/Gateway-Aliase, auf die das Hirn zeigen kann (Lane-Sprache statt altem „auto").
|
||||||
function Tile({ label, ok, detail, icon: Icon, onClick }: { label: string; ok: boolean; detail?: string; icon: any; onClick?: () => void }) {
|
const ALIASES = ["chat", "coding", "fast", "heavy"]
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={onClick}
|
|
||||||
className={cn(
|
|
||||||
"rounded-2xl border bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 flex flex-col justify-between gap-3 hover:border-primary/40 transition-all duration-300",
|
|
||||||
ok ? "border-border/60" : "border-amber-500/30",
|
|
||||||
onClick && "cursor-pointer hover:bg-card/70"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{label}</span>
|
|
||||||
<Icon className={cn("h-4.5 w-4.5", ok ? "text-primary" : "text-amber-500")} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={cn(
|
|
||||||
"h-2 w-2 rounded-full ring-2 ring-black/40",
|
|
||||||
ok ? "bg-emerald-500 animate-pulse" : "bg-amber-500"
|
|
||||||
)} />
|
|
||||||
<span className="text-xs font-semibold text-foreground">
|
|
||||||
{ok ? "Bereit / Online" : "Offline / Inaktiv"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{detail && <div className="text-[10px] font-mono text-muted-foreground truncate max-w-[200px]" title={detail}>{detail}</div>}
|
|
||||||
</div>
|
|
||||||
{onClick && (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onClick()
|
|
||||||
}}
|
|
||||||
className="mt-2 flex items-center justify-center gap-1.5 w-full py-1.5 px-3 rounded-lg border border-primary/30 bg-primary/10 hover:bg-primary/20 text-primary text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer font-space"
|
|
||||||
>
|
|
||||||
<Cpu className="h-3.5 w-3.5" />
|
|
||||||
<span>Gehirn wechseln</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AgentView() {
|
export function AgentView() {
|
||||||
const { data: s, error: sErr } = useAgentStatus(5_000)
|
const { data: s, error: sErr } = useAgentStatus(5_000)
|
||||||
@@ -55,75 +14,37 @@ export function AgentView() {
|
|||||||
const { showAlert, dialogElement } = useDialog()
|
const { showAlert, dialogElement } = useDialog()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const error = sErr ? String(sErr) : ""
|
const error = sErr ? String(sErr) : ""
|
||||||
|
|
||||||
const availableModels = useMemo(() => {
|
|
||||||
const names = (modelsData?.models ?? []).map(
|
|
||||||
(m) => m.name.split("/").pop()?.replace(".gguf", "") || m.name)
|
|
||||||
return ["auto", "fast", "heavy", ...names]
|
|
||||||
}, [modelsData])
|
|
||||||
|
|
||||||
// Graph UI state
|
|
||||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
|
|
||||||
const [showBrainSelect, setShowBrainSelect] = useState(false)
|
const [showBrainSelect, setShowBrainSelect] = useState(false)
|
||||||
|
|
||||||
// Canvas pixel tracking for pixel-perfect connection graph without non-uniform scaling
|
const installed = modelsData?.models ?? []
|
||||||
const [dimensions, setDimensions] = useState({ width: 800, height: 360 })
|
|
||||||
const observerRef = useRef<ResizeObserver | null>(null)
|
|
||||||
|
|
||||||
const containerRef = useCallback((node: HTMLDivElement | null) => {
|
// Alias setzen → roher Config-Write (model.default zeigt aufs Alias).
|
||||||
if (observerRef.current) {
|
async function setBrainAlias(alias: string) {
|
||||||
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 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}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async function changeBrainModel(model: string) {
|
|
||||||
try {
|
try {
|
||||||
await api("/api/agent/brain", {
|
await api("/api/agent/brain", { method: "POST", body: JSON.stringify({ model: alias }) })
|
||||||
method: "POST",
|
showAlert("Erledigt", `Hermes-Hirn zeigt jetzt auf das Alias '${alias}'. Der Gateway-Dienst wurde neu gestartet.`)
|
||||||
body: JSON.stringify({ model })
|
|
||||||
})
|
|
||||||
showAlert("Erfolgreich", `Hermes-Gehirn wurde auf '${model}' geändert. Der Gateway-Dienst wurde neu gestartet.`)
|
|
||||||
qc.invalidateQueries({ queryKey: qk.agentStatus })
|
qc.invalidateQueries({ queryKey: qk.agentStatus })
|
||||||
setShowBrainSelect(false)
|
setShowBrainSelect(false)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showAlert("Fehler", `Fehler beim Wechseln des Gehirns: ${e.message}`)
|
showAlert("Fehler", `Wechsel fehlgeschlagen: ${e.message || e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installiertes Modell → warm-bewusst (hermes-Alias + brains-Gruppe + Config + Restart).
|
||||||
|
async function setBrainWarm(modelName: string) {
|
||||||
|
try {
|
||||||
|
await api("/api/agent/brain/set", { method: "POST", body: JSON.stringify({ model_id: modelName }) })
|
||||||
|
showAlert("Erledigt", "Agent-Hirn gewechselt (warm gehalten). Gateway wurde neugestartet.")
|
||||||
|
qc.invalidateQueries({ queryKey: qk.agentStatus })
|
||||||
|
qc.invalidateQueries({ queryKey: qk.models })
|
||||||
|
setShowBrainSelect(false)
|
||||||
|
} catch (e: any) {
|
||||||
|
showAlert("Fehler", `Wechsel fehlgeschlagen: ${e.message || e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Styles inside AgentView for dash flow animations */}
|
|
||||||
<style>{`
|
|
||||||
@keyframes flow-dash {
|
|
||||||
to {
|
|
||||||
stroke-dashoffset: -20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.svg-flow-path {
|
|
||||||
stroke-dasharray: 4 6;
|
|
||||||
animation: flow-dash 1s linear infinite;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-4">
|
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -134,7 +55,7 @@ export function AgentView() {
|
|||||||
Hermes ist ein autonomer Agent, der im Hintergrund der Box läuft (Port <code>:8642</code>) und seine eigene UI besitzt.
|
Hermes ist ein autonomer Agent, der im Hintergrund der Box läuft (Port <code>:8642</code>) und seine eigene UI besitzt.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{s?.terminal_url && (
|
{s?.terminal_url && (
|
||||||
<a
|
<a
|
||||||
href={resolveExternalUrl(s.terminal_url)}
|
href={resolveExternalUrl(s.terminal_url)}
|
||||||
@@ -161,170 +82,89 @@ export function AgentView() {
|
|||||||
|
|
||||||
{s && (
|
{s && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Tile Grid */}
|
{/* Agent-Fluss: Terminal → Gateway → Hirn / Verdrahtung / PC (ruhig, statt SVG-Graph) */}
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10">
|
||||||
<Tile
|
<div className="mb-4 flex items-center gap-2">
|
||||||
label="Agent Gateway"
|
<Bot className="h-4.5 w-4.5 text-primary" />
|
||||||
ok={s.gateway_reachable}
|
<span className="text-xs font-bold uppercase tracking-wider text-foreground">Agent-Fluss</span>
|
||||||
detail="Port :8642 (REST API)"
|
|
||||||
icon={Bot}
|
|
||||||
/>
|
|
||||||
<Tile
|
|
||||||
label="Terminal"
|
|
||||||
ok={s.terminal_reachable}
|
|
||||||
detail="Web-Terminal (hermes chat)"
|
|
||||||
icon={Activity}
|
|
||||||
/>
|
|
||||||
<Tile
|
|
||||||
label="Aktives Gehirn"
|
|
||||||
ok={s.gateway_reachable}
|
|
||||||
detail={s.brain_model ? `Model: ${s.brain_model}` : "Model: auto"}
|
|
||||||
icon={Cpu}
|
|
||||||
onClick={() => setShowBrainSelect(true)}
|
|
||||||
/>
|
|
||||||
<Tile
|
|
||||||
label="Verdrahtung"
|
|
||||||
ok={s.has_config}
|
|
||||||
detail={`Config: ${s.has_config ? "✓" : "—"} · Skills: ${s.has_skills ? "✓" : "—"} · Memory: ${s.has_memories ? "✓" : "—"}`}
|
|
||||||
icon={Wrench}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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 Agent Flow Graph</span>
|
|
||||||
<p className="text-[10px] text-muted-foreground mt-0.5 leading-relaxed">
|
|
||||||
Visualisiert das Zusammenspiel und die Verbindungspfade zwischen den Hermes-Systemkomponenten.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The Graph Canvas Area */}
|
<div className="flex flex-col lg:flex-row lg:items-stretch gap-3">
|
||||||
<div ref={containerRef} className="relative w-full h-96 border border-border/30 rounded-xl bg-black/25 overflow-hidden flex">
|
{/* Quelle: Terminal/Clients */}
|
||||||
<svg className="absolute inset-0 pointer-events-none w-full h-full">
|
<button
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Connection: Terminal 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 === "terminal" || s.terminal_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" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 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" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
{/* COLUMN 1: Interface */}
|
|
||||||
<div
|
|
||||||
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("terminal")}
|
|
||||||
onMouseLeave={() => setHoveredNode(null)}
|
|
||||||
onClick={() => s.terminal_reachable && window.open(resolveExternalUrl(s.terminal_url), "_blank")}
|
onClick={() => s.terminal_reachable && window.open(resolveExternalUrl(s.terminal_url), "_blank")}
|
||||||
title={s.terminal_reachable ? "Klicken um das Hermes-Terminal zu öffnen" : "Terminal offline"}
|
title={s.terminal_reachable ? "Hermes-Terminal öffnen" : "Terminal offline"}
|
||||||
|
className="lg:w-40 shrink-0 rounded-xl border border-border/60 bg-background/30 p-3 text-left hover:border-primary/40 transition-all cursor-pointer flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<Activity className={cn("h-3.5 w-3.5", s.terminal_reachable ? "text-emerald-400" : "text-amber-500")} />
|
<div className="flex items-center gap-1.5">
|
||||||
<span>Terminal</span>
|
<Activity className={cn("h-3.5 w-3.5", s.terminal_reachable ? "text-emerald-400" : "text-amber-500")} />
|
||||||
<span className={cn("w-1.5 h-1.5 rounded-full animate-pulse ml-auto", s.terminal_reachable ? "bg-emerald-500" : "bg-amber-500")} />
|
<span className="text-xs font-semibold text-foreground">Terminal</span>
|
||||||
</div>
|
<span className={cn("ml-auto h-1.5 w-1.5 rounded-full animate-pulse", s.terminal_reachable ? "bg-emerald-500" : "bg-amber-500")} />
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-muted-foreground font-mono mt-0.5">hermes chat · Clients</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* COLUMN 2: Gateway */}
|
<div className="hidden lg:flex items-center text-muted-foreground text-lg select-none">→</div>
|
||||||
<div
|
|
||||||
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"
|
{/* Gateway + nachgelagerte Module */}
|
||||||
style={{ left: "50%", top: "50%" }}
|
<div className="flex-1 flex flex-col gap-2.5">
|
||||||
onMouseEnter={() => setHoveredNode("gateway")}
|
{/* Agent Gateway */}
|
||||||
onMouseLeave={() => setHoveredNode(null)}
|
<div className="rounded-xl border border-primary/40 bg-primary/5 px-3.5 py-2.5 flex items-center gap-2">
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Bot className="h-3.5 w-3.5 text-primary" />
|
<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>
|
<span className="text-xs font-semibold text-primary">Agent Gateway</span>
|
||||||
|
<span className="text-[11px] font-mono text-muted-foreground">:8642</span>
|
||||||
|
<span className={cn(
|
||||||
|
"ml-auto inline-flex items-center gap-1.5 text-[10px] font-bold uppercase font-mono px-2 py-0.5 rounded-full border",
|
||||||
|
s.gateway_reachable
|
||||||
|
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
|
||||||
|
: "bg-red-500/10 text-red-400 border-red-500/20"
|
||||||
|
)}>
|
||||||
|
<span className={cn("h-1.5 w-1.5 rounded-full", s.gateway_reachable ? "bg-emerald-500 animate-pulse" : "bg-red-500")} />
|
||||||
|
{s.gateway_reachable ? "online" : "offline"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* COLUMN 3: Brain & Wiring */}
|
{/* Drei Module: Hirn (klickbar), Verdrahtung, PC */}
|
||||||
<div
|
<div className="grid gap-2.5 sm:grid-cols-3">
|
||||||
className={cn(
|
<button
|
||||||
"absolute z-20 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",
|
onClick={() => setShowBrainSelect(true)}
|
||||||
s.gateway_reachable ? "border-emerald-500/50 bg-emerald-500/5 shadow-emerald-500/5" : "border-border/60 bg-card/75"
|
className="rounded-xl border border-border/60 bg-background/30 p-3 text-left hover:border-primary/45 transition-all cursor-pointer"
|
||||||
)}
|
>
|
||||||
style={{ left: "85%", top: "30%" }}
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
onMouseEnter={() => setHoveredNode("brain")}
|
<Cpu className="h-3.5 w-3.5 text-primary" />
|
||||||
onMouseLeave={() => setHoveredNode(null)}
|
<span className="text-[10px] font-bold uppercase tracking-wider">Hirn</span>
|
||||||
onClick={() => setShowBrainSelect(true)}
|
<span className="ml-auto text-[9px] text-primary/70 font-bold uppercase">wechseln →</span>
|
||||||
>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="text-[11px] font-mono font-medium text-foreground truncate mt-1" title={s.brain_model}>
|
||||||
<div className="flex items-center gap-1 text-muted-foreground">
|
{s.brain_model || "chat"}
|
||||||
<Cpu className="h-3 w-3 text-primary" />
|
</div>
|
||||||
<span className="text-[10px] font-bold uppercase tracking-wider">Aktives Gehirn</span>
|
</button>
|
||||||
</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
|
<div className="rounded-xl border border-border/60 bg-background/30 p-3">
|
||||||
className={cn(
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
"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",
|
<Wrench className="h-3.5 w-3.5 text-primary" />
|
||||||
s.has_config ? "border-emerald-500/50 bg-emerald-500/5 shadow-emerald-500/5" : "border-border/60 bg-card/75"
|
<span className="text-[10px] font-bold uppercase tracking-wider">Verdrahtung</span>
|
||||||
)}
|
</div>
|
||||||
style={{ left: "85%", top: "70%" }}
|
<div className="text-[10px] font-mono mt-1 flex flex-wrap gap-x-2 text-muted-foreground">
|
||||||
onMouseEnter={() => setHoveredNode("wiring")}
|
<span>Config: {s.has_config ? "✓" : "—"}</span>
|
||||||
onMouseLeave={() => setHoveredNode(null)}
|
<span>Skills: {s.has_skills ? "✓" : "—"}</span>
|
||||||
>
|
<span>Memory: {s.has_memories ? "✓" : "—"}</span>
|
||||||
<div className="flex items-center justify-between">
|
</div>
|
||||||
<div className="flex items-center gap-1 text-muted-foreground">
|
</div>
|
||||||
<Wrench className="h-3 w-3 text-primary" />
|
|
||||||
<span className="text-[10px] font-bold uppercase tracking-wider">Verdrahtung</span>
|
<div className="rounded-xl border border-border/60 bg-background/30 p-3">
|
||||||
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
|
<Monitor className="h-3.5 w-3.5 text-primary" />
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-wider">PC</span>
|
||||||
|
<span className={cn("ml-auto h-1.5 w-1.5 rounded-full", s.pc_executor_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
|
||||||
|
</div>
|
||||||
|
<div className={cn("text-[11px] font-mono mt-1", s.pc_executor_reachable ? "text-emerald-400" : "text-muted-foreground")}>
|
||||||
|
:7777 {s.pc_executor_reachable ? "verbunden" : "offline"}
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
</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 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: Verbindung hergestellt / Modul online</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PC Executor Status Card */}
|
{/* PC Executor Status Card */}
|
||||||
@@ -383,7 +223,7 @@ export function AgentView() {
|
|||||||
<Shield className="h-5 w-5 text-amber-500" />
|
<Shield className="h-5 w-5 text-amber-500" />
|
||||||
<h3 className="text-sm font-bold uppercase tracking-wider text-foreground">Hermes-Agent Diagnostics</h3>
|
<h3 className="text-sm font-bold uppercase tracking-wider text-foreground">Hermes-Agent Diagnostics</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-muted-foreground space-y-3 leading-relaxed">
|
<div className="text-xs text-muted-foreground space-y-3 leading-relaxed">
|
||||||
<p>
|
<p>
|
||||||
Der Hermes-Dienst ist auf der Box derzeit offline oder kann sich nicht mit dem lokalen llama-swap verbinden.
|
Der Hermes-Dienst ist auf der Box derzeit offline oder kann sich nicht mit dem lokalen llama-swap verbinden.
|
||||||
@@ -391,7 +231,7 @@ export function AgentView() {
|
|||||||
<p>
|
<p>
|
||||||
Stelle sicher, dass die systemd-Dienste auf der Box laufen. Du kannst den Status im <strong>OS & Updates Drawer</strong> prüfen und die Dienste bei Bedarf neu starten.
|
Stelle sicher, dass die systemd-Dienste auf der Box laufen. Du kannst den Status im <strong>OS & Updates Drawer</strong> prüfen und die Dienste bei Bedarf neu starten.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="p-3.5 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] space-y-1">
|
<div className="p-3.5 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] space-y-1">
|
||||||
<div className="text-muted-foreground/60"># Dienste manuell auf der Box prüfen:</div>
|
<div className="text-muted-foreground/60"># Dienste manuell auf der Box prüfen:</div>
|
||||||
<div className="text-primary">systemctl --user status hermes-gateway</div>
|
<div className="text-primary">systemctl --user status hermes-gateway</div>
|
||||||
@@ -408,54 +248,85 @@ export function AgentView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Brain Selection Modal */}
|
{/* Brain Selection Modal — eine Stelle für den Hirn-Wechsel (warm-bewusst + Alias) */}
|
||||||
{s && showBrainSelect && (
|
{s && showBrainSelect && (
|
||||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
<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="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">
|
<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">
|
<h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-1.5">
|
||||||
<Cpu className="h-4 w-4" />
|
<Cpu className="h-4 w-4" />
|
||||||
<span>Hermes-Gehirn konfigurieren</span>
|
<span>Hermes-Hirn konfigurieren</span>
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowBrainSelect(false)}
|
onClick={() => setShowBrainSelect(false)}
|
||||||
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
|
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
||||||
Das „Gehirn" ist das primäre LLM für Hermes. Wähle ein Modell aus deiner Bibliothek oder nutze ein Gateway-Alias (<code className="text-primary font-semibold">auto</code> / <code className="text-primary font-semibold">fast</code> / <code className="text-primary font-semibold">heavy</code>):
|
{/* Lane / Gateway-Alias */}
|
||||||
</p>
|
<div className="space-y-1.5">
|
||||||
|
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Lane / Gateway-Alias</div>
|
||||||
<div className="space-y-1.5 max-h-60 overflow-y-auto pr-1">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{availableModels.map((m) => {
|
{ALIASES.map((a) => {
|
||||||
const isAlias = ["auto", "fast", "heavy"].includes(m);
|
const cur = s.brain_model === a
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={m}
|
key={a}
|
||||||
onClick={() => changeBrainModel(m)}
|
onClick={() => setBrainAlias(a)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full text-left px-3 py-2.5 rounded-lg text-xs hover:bg-accent flex items-center justify-between font-mono cursor-pointer border border-border/30",
|
"px-3 py-1.5 rounded-lg text-xs font-mono font-semibold border transition-all cursor-pointer flex items-center gap-1.5",
|
||||||
s.brain_model === m || (!s.brain_model && m === "auto")
|
cur ? "border-primary/40 bg-primary/10 text-primary" : "border-border/30 bg-background/20 text-foreground hover:bg-accent",
|
||||||
? "text-primary font-bold bg-primary/10 border-primary/30"
|
)}
|
||||||
: "text-foreground bg-background/20"
|
>
|
||||||
)}
|
{a}{cur && <Check className="h-3.5 w-3.5" />}
|
||||||
>
|
</button>
|
||||||
<div className="flex flex-col text-left">
|
)
|
||||||
<span className="font-semibold truncate max-w-[280px]">
|
})}
|
||||||
{m}
|
</div>
|
||||||
</span>
|
<p className="text-[10px] text-muted-foreground/70">
|
||||||
<span className="text-[9px] text-muted-foreground mt-0.5">
|
Schlank & flexibel: das Hirn folgt dem Lane-Routing (z.B. <code className="text-primary">chat</code> = fast↔heavy).
|
||||||
{isAlias ? "Gateway Routing Alias" : "Installiertes GGUF Modell"}
|
</p>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
{(s.brain_model === m || (!s.brain_model && m === "auto")) && (
|
{/* Installierte Modelle (warm-bewusst) */}
|
||||||
<Check className="h-4 w-4 shrink-0 text-primary" />
|
<div className="space-y-1.5">
|
||||||
)}
|
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Installiertes Modell (warm gehalten)</div>
|
||||||
</button>
|
<div className="space-y-1.5 max-h-56 overflow-y-auto pr-1">
|
||||||
);
|
{installed.map((m) => {
|
||||||
})}
|
const isBrain = m.role === "hermes"
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={m.name}
|
||||||
|
onClick={() => !isBrain && setBrainWarm(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` : "?"} · {m.capabilities.tools !== "no" ? "Tools ✓" : "ohne Tools"}
|
||||||
|
{m.role && ` · Rolle: ${m.role}`}
|
||||||
|
</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">Warm setzen →</span>}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</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. Neues Modell? Erst über „Modell-Manager → Modelle finden" laden.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export function ConnectView() {
|
|||||||
<StatusBadge line={health?.gateway} loading={healthLoading} />
|
<StatusBadge line={health?.gateway} loading={healthLoading} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[11px] font-mono text-primary/80">
|
<div className="mt-1 text-[11px] font-mono text-primary/80">
|
||||||
Gateway · :9001/v1 · model auto
|
Gateway · :9001/v1 · Lanes chat / coding
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { ActiveModelsCard } from "@/components/dashboard/ActiveModelsCard"
|
|
||||||
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
||||||
import { UpdatesCard } from "@/components/dashboard/UpdatesCard"
|
import { UpdatesCard } from "@/components/dashboard/UpdatesCard"
|
||||||
import { AgentStatusCard } from "@/components/dashboard/AgentStatusCard"
|
import { AgentStatusCard } from "@/components/dashboard/AgentStatusCard"
|
||||||
import { RolesCard } from "@/components/dashboard/RolesCard"
|
|
||||||
import { MemoryInputCard } from "@/components/dashboard/MemoryInputCard"
|
import { MemoryInputCard } from "@/components/dashboard/MemoryInputCard"
|
||||||
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
||||||
import { ServicesCard } from "@/components/dashboard/ServicesCard"
|
import { ServicesCard } from "@/components/dashboard/ServicesCard"
|
||||||
import { VoiceLatencyCard } from "@/components/dashboard/VoiceLatencyCard"
|
import { VoiceLatencyCard } from "@/components/dashboard/VoiceLatencyCard"
|
||||||
|
import { ModelsCard } from "@/components/dashboard/ModelsCard"
|
||||||
|
|
||||||
function ZoneLabel({ children }: { children: React.ReactNode }) {
|
function ZoneLabel({ children }: { children: React.ReactNode }) {
|
||||||
return <p className="mb-2 ml-0.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">{children}</p>
|
return <p className="mb-2 ml-0.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">{children}</p>
|
||||||
@@ -22,28 +21,22 @@ export function DashboardView() {
|
|||||||
<p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p>
|
<p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hero-Banner — die 3 relevantesten, gleich prominent */}
|
{/* Hero-Banner — System, Durchsatz, Modelle (Rollen + warm + Inferenz in einer Karte) */}
|
||||||
<div className="grid items-stretch gap-6 lg:grid-cols-3">
|
<div className="grid items-stretch gap-6 lg:grid-cols-3">
|
||||||
<SystemStatusCard />
|
<SystemStatusCard />
|
||||||
<TokenPerformanceCard />
|
<TokenPerformanceCard />
|
||||||
<ActiveModelsCard />
|
<ModelsCard />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stack-Status */}
|
{/* Stack & Telemetrie */}
|
||||||
<section>
|
<section>
|
||||||
<ZoneLabel>Stack-Status</ZoneLabel>
|
<ZoneLabel>Stack & Telemetrie</ZoneLabel>
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<RolesCard />
|
|
||||||
<ServicesCard />
|
<ServicesCard />
|
||||||
|
<VoiceLatencyCard />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Sprach-Latenz (Voice/Lucy-Pipeline, C2) */}
|
|
||||||
<section>
|
|
||||||
<ZoneLabel>Sprach-Latenz</ZoneLabel>
|
|
||||||
<VoiceLatencyCard />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Betrieb & Wissen */}
|
{/* Betrieb & Wissen */}
|
||||||
<section>
|
<section>
|
||||||
<ZoneLabel>Betrieb & Wissen</ZoneLabel>
|
<ZoneLabel>Betrieb & Wissen</ZoneLabel>
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { useState, useRef, useCallback } from "react"
|
import { useState } from "react"
|
||||||
import { Download, Trash2, Edit3, Activity, HardDrive, X, Check, Copy, Zap, Bot } from "lucide-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 { 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 { useDialog } from "@/lib/useDialog"
|
||||||
import { CapsChips } from "@/components/CapsChips"
|
import { CapsChips } from "@/components/CapsChips"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { fmtSize, fmtCtx } from "@/lib/format"
|
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 { SpecDraftModal } from "@/components/models/SpecDraftModal"
|
||||||
import { LaneEditor } from "@/components/models/LaneEditor"
|
import { LaneEditor } from "@/components/models/LaneEditor"
|
||||||
|
|
||||||
export function Cockpit() {
|
export function Cockpit() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const { data: modelsResp, isLoading: loading, error: loadErr } = useModels(4_000)
|
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: updates } = useUpdates(4_000)
|
||||||
const { data: brain } = useHermesBrain()
|
const { data: brain } = useHermesBrain()
|
||||||
const { data: sysStatus } = useSystemStatus()
|
const { data: sysStatus } = useSystemStatus()
|
||||||
@@ -53,13 +51,9 @@ export function Cockpit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null)
|
|
||||||
const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null)
|
const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null)
|
||||||
const [roleRec, setRoleRec] = useState<RoleRecResp | null>(null)
|
const [roleRec, setRoleRec] = useState<RoleRecResp | null>(null)
|
||||||
const [specModel, setSpecModel] = useState<ModelInfo | 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 [viewMode, setViewMode] = useState<"grid" | "list">("grid")
|
||||||
const [filterMode, setFilterMode] = useState<"all" | "in_use">("all")
|
const [filterMode, setFilterMode] = useState<"all" | "in_use">("all")
|
||||||
|
|
||||||
@@ -70,55 +64,6 @@ export function Cockpit() {
|
|||||||
return true
|
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) {
|
async function doLoadModel(name: string) {
|
||||||
try {
|
try {
|
||||||
await api(`/api/models/${encodeURIComponent(name)}/load`, { method: "POST" })
|
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 (loading) return <div className="text-xs text-muted-foreground py-12 text-center">Initialisiere HUD Cockpit…</div>
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@@ -308,13 +229,6 @@ export function Cockpit() {
|
|||||||
? gttTotal
|
? gttTotal
|
||||||
: (totalRunningSize > virtualMax ? totalRunningSize * 1.2 : virtualMax)
|
: (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 (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Styles inside Cockpit for dash flow animations */}
|
{/* Styles inside Cockpit for dash flow animations */}
|
||||||
@@ -388,320 +302,6 @@ export function Cockpit() {
|
|||||||
</div>
|
</div>
|
||||||
</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) */}
|
{/* ZONE B.4: Lane-Routing & Policy (UI-editierbar, hot-reload) */}
|
||||||
<LaneEditor />
|
<LaneEditor />
|
||||||
|
|
||||||
@@ -759,10 +359,11 @@ export function Cockpit() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
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>
|
</button>
|
||||||
</div>
|
</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}
|
{dialogElement}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user