26224804ed
- Gedaechtnis: 'Im Terminal starten' navigierte zu nicht existenter View 'terminal'
-> 'konsole'; interne Duplikate (AUTO-Set, CAT_LABEL_MAP) entfernt
- Konsole: box-console jetzt in Dienste-Liste + Restart-Allowlist; Offline-Overlay
bekommt Ein-Klick-'Dienst neu starten' statt SSH-Anweisung
- Cockpit Werkzeuge: Doppel-Eintrag ('Box-Shell' + 'Interaktives Terminal', beide
-> Konsole) zu EINEM Eintrag 'Konsole (Box-Shell)' zusammengelegt
- Auftragsbuch: 'Nichts zu entscheiden'-Banner beruecksichtigt blockierte
Ideen-Karten; Abschnitt heisst neutral 'Patch-Vorschlaege'
- Hermes-Diagnose: Klickweg (Dienste-Schublade oeffnen) statt veraltetem
Drawer-Namen + systemctl-Kommandos
- Entdecken: Upgrade-Knopf uebernimmt fremde Quant nicht mehr blind
- Skills: Status-Badge zeigt Klartext (abgeschaltet/defekt/...) statt rohem State
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
345 lines
19 KiB
TypeScript
345 lines
19 KiB
TypeScript
import { useState } from "react"
|
|
import { Bot, ExternalLink, Activity, Cpu, Wrench, Monitor, Shield, Check, X } from "lucide-react"
|
|
import { api } from "@/lib/api"
|
|
import { useAgentStatus, useModels, useQueryClient, qk } from "@/lib/queries"
|
|
import { useDialog } from "@/lib/useDialog"
|
|
import { cn, resolveExternalUrl } from "@/lib/utils"
|
|
|
|
// Lane-/Gateway-Aliase, auf die das Hirn zeigen kann (Lane-Sprache statt altem „auto").
|
|
const ALIASES = ["chat", "coding", "fast", "heavy"]
|
|
|
|
export function AgentView() {
|
|
const { data: s, error: sErr } = useAgentStatus()
|
|
const { data: modelsData } = useModels()
|
|
const { showAlert, dialogElement } = useDialog()
|
|
const qc = useQueryClient()
|
|
const error = sErr ? String(sErr) : ""
|
|
const [showBrainSelect, setShowBrainSelect] = useState(false)
|
|
|
|
const installed = modelsData?.models ?? []
|
|
|
|
// Adressen aus dem Status-Payload statt hartcodiert — die UI zeigt, was konfiguriert IST.
|
|
const port = (url?: string) => url?.match(/:(\d+)/)?.[1] ?? ""
|
|
const gwPort = port(s?.gateway_url) || "8642"
|
|
const pcUrl = s?.pc_executor_url ?? ""
|
|
const pcHost = pcUrl.replace(/^https?:\/\//, "")
|
|
const pcPort = port(pcUrl) || "7777"
|
|
|
|
// Alias setzen → roher Config-Write (model.default zeigt aufs Alias).
|
|
async function setBrainAlias(alias: string) {
|
|
try {
|
|
await api("/api/agent/brain", { method: "POST", body: JSON.stringify({ model: alias }) })
|
|
showAlert("Erledigt", `Hermes-Hirn zeigt jetzt auf das Alias '${alias}'. Der Gateway-Dienst wurde neu gestartet.`)
|
|
qc.invalidateQueries({ queryKey: qk.agentStatus })
|
|
setShowBrainSelect(false)
|
|
} catch (e: any) {
|
|
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 (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent">
|
|
Hermes Agenten-Cockpit
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Hermes ist ein autonomer Agent, der im Hintergrund der Box läuft (Port <code>:{gwPort}</code>) und seine eigene UI besitzt.
|
|
</p>
|
|
</div>
|
|
|
|
{s?.hermes_ui_url && (
|
|
<a
|
|
href={resolveExternalUrl(s.hermes_ui_url)}
|
|
target="_blank"
|
|
rel="noopener"
|
|
className={cn(
|
|
"flex h-9 px-4 items-center gap-1.5 text-xs font-semibold rounded-lg transition-all cursor-pointer shadow-md shrink-0 self-start",
|
|
s.hermes_ui_reachable
|
|
? "bg-primary text-primary-foreground hover:opacity-90 shadow-primary/10"
|
|
: "border border-border/60 text-muted-foreground bg-background/20"
|
|
)}
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
<span>Hermes-GUI öffnen</span>
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="rounded-xl border border-red-500/20 bg-red-500/5 p-4 text-xs text-red-400 font-mono">
|
|
Status nicht lesbar ({error}).
|
|
</div>
|
|
)}
|
|
|
|
{s && (
|
|
<div className="space-y-6">
|
|
{/* Agent-Fluss: Terminal → Gateway → Hirn / Verdrahtung / PC (ruhig, statt SVG-Graph) */}
|
|
<div className="mc-card p-5">
|
|
<div className="mb-4 flex items-center gap-2">
|
|
<Bot className="h-4.5 w-4.5 text-primary" />
|
|
<span className="text-xs font-bold uppercase tracking-wider text-foreground">Agent-Fluss</span>
|
|
</div>
|
|
|
|
<div className="flex flex-col lg:flex-row lg:items-stretch gap-3">
|
|
{/* Quelle: Terminal/Clients */}
|
|
<button
|
|
onClick={() => s.hermes_ui_reachable && window.open(resolveExternalUrl(s.hermes_ui_url), "_blank")}
|
|
title={s.hermes_ui_reachable ? "Hermes-GUI öffnen" : "Desktop-Gateway 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"
|
|
>
|
|
<div className="flex items-center gap-1.5">
|
|
<Activity className={cn("h-3.5 w-3.5", s.hermes_ui_reachable ? "text-emerald-400" : "text-amber-500")} />
|
|
<span className="text-xs font-semibold text-foreground">Desktop & GUI</span>
|
|
<span className={cn("ml-auto h-1.5 w-1.5 rounded-full animate-pulse", s.hermes_ui_reachable ? "bg-emerald-500" : "bg-amber-500")} />
|
|
</div>
|
|
<span className="text-[10px] text-muted-foreground font-mono mt-0.5">Hermes Desktop · Clients</span>
|
|
</button>
|
|
|
|
<div className="hidden lg:flex items-center text-muted-foreground text-lg select-none">→</div>
|
|
|
|
{/* Gateway + nachgelagerte Module */}
|
|
<div className="flex-1 flex flex-col gap-2.5">
|
|
{/* Agent Gateway */}
|
|
<div className="rounded-xl border border-primary/40 bg-primary/5 px-3.5 py-2.5 flex items-center gap-2">
|
|
<Bot className="h-3.5 w-3.5 text-primary" />
|
|
<span className="text-xs font-semibold text-primary">Agent Gateway</span>
|
|
<span className="text-[11px] font-mono text-muted-foreground">:{gwPort}</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>
|
|
|
|
{/* Drei Module: Hirn (klickbar), Verdrahtung, PC */}
|
|
<div className="grid gap-2.5 sm:grid-cols-3">
|
|
<button
|
|
onClick={() => setShowBrainSelect(true)}
|
|
className="rounded-xl border border-border/60 bg-background/30 p-3 text-left hover:border-primary/45 transition-all cursor-pointer"
|
|
>
|
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
<Cpu className="h-3.5 w-3.5 text-primary" />
|
|
<span className="text-[10px] font-bold uppercase tracking-wider">Hirn</span>
|
|
<span className="ml-auto text-[9px] text-primary/70 font-bold uppercase">wechseln →</span>
|
|
</div>
|
|
<div className="text-[11px] font-mono font-medium text-foreground truncate mt-1" title={s.brain_model}>
|
|
{s.brain_model || "chat"}
|
|
</div>
|
|
</button>
|
|
|
|
<div className="rounded-xl border border-border/60 bg-background/30 p-3">
|
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
<Wrench className="h-3.5 w-3.5 text-primary" />
|
|
<span className="text-[10px] font-bold uppercase tracking-wider">Verdrahtung</span>
|
|
</div>
|
|
<div className="text-[10px] font-mono mt-1 flex flex-wrap gap-x-2 text-muted-foreground">
|
|
<span>Config: {s.has_config ? "✓" : "—"}</span>
|
|
<span>Skills: {s.has_skills ? "✓" : "—"}</span>
|
|
<span>Memory: {s.has_memories ? "✓" : "—"}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<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")}>
|
|
:{pcPort} {s.pc_executor_reachable ? "verbunden" : "offline"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* PC Executor Status Card */}
|
|
<div className="mc-card p-6 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Monitor className="h-5 w-5 text-primary" />
|
|
<h3 className="text-sm font-bold uppercase tracking-wider text-foreground">PC Verbindung</h3>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className={cn("h-2 w-2 rounded-full", s.pc_executor_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
|
|
<span className="text-xs font-semibold text-foreground">
|
|
{s.pc_executor_reachable ? "Erreichbar" : "Nicht verbunden"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-6 md:grid-cols-2 text-xs text-muted-foreground leading-relaxed text-left">
|
|
<div className="space-y-3">
|
|
<p>
|
|
Der <strong className="text-foreground">Hermes PC Executor</strong> läuft auf <code className="text-foreground font-mono px-1 py-0.5 rounded bg-muted/40 border border-border/30">{pcHost || "deinem Windows-PC"}</code> und ermöglicht Hermes den direkten Zugriff auf deinen Windows-PC — Shell-Befehle, Screenshots, Tastatureingaben.
|
|
</p>
|
|
<p>
|
|
Hermes erreicht den PC über den MCP-Server <code className="text-foreground font-mono px-1 py-0.5 rounded bg-muted/40 border border-border/30">hermes-pc-control</code>, der automatisch beim Gateway-Start verbunden wird.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{s.pc_executor_reachable ? (
|
|
<div className="p-3.5 bg-emerald-500/5 border border-emerald-500/20 rounded-xl space-y-1">
|
|
<div className="text-emerald-400 font-semibold text-xs flex items-center gap-1.5">
|
|
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
|
PC Executor verbunden
|
|
</div>
|
|
<p className="text-[10px]">
|
|
Hermes kann jetzt per Telegram oder Terminal Befehle auf TobisNicerPC ausführen. Nutze <code className="text-foreground">pc_shell</code>, <code className="text-foreground">pc_screenshot</code> oder <code className="text-foreground">pc_open</code>.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<h4 className="text-xs font-bold text-foreground">PC Executor starten</h4>
|
|
<p>Starte <code className="text-foreground font-mono">start.bat</code> im Ordner <code className="text-foreground font-mono">client\hermes-pc\</code> auf dem Windows-PC und lasse das Fenster offen.</p>
|
|
<div className="p-3 bg-background/25 rounded-xl border border-border/30 font-mono text-[9px] text-cyan-300">
|
|
client\hermes-pc\start.bat
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Diagnostic Details if Offline */}
|
|
{!s.gateway_reachable && (
|
|
<div className="mc-card p-6 space-y-4 text-left">
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="h-5 w-5 text-amber-500" />
|
|
<h3 className="text-sm font-bold uppercase tracking-wider text-foreground">Hermes-Agent Diagnostics</h3>
|
|
</div>
|
|
|
|
<div className="text-xs text-muted-foreground space-y-3 leading-relaxed">
|
|
<p>
|
|
Der Hermes-Dienst ist auf der Box derzeit offline oder kann sich nicht mit dem lokalen llama-swap verbinden.
|
|
</p>
|
|
<p>
|
|
Am schnellsten: In der System-Schublade unter <strong>Dienste</strong> den Eintrag{" "}
|
|
<strong>Hermes-Gateway</strong> mit einem Klick neu starten (dort gibt es auch die Logs dazu).
|
|
</p>
|
|
<button
|
|
onClick={() => window.dispatchEvent(new CustomEvent("open-system-drawer", { detail: { tab: "maintenance" } }))}
|
|
className="flex h-9 items-center gap-1.5 rounded-lg border border-primary/40 bg-primary/10 px-4 text-[11px] font-bold uppercase tracking-wide text-primary transition-all hover:bg-primary/20 cursor-pointer">
|
|
<Wrench className="h-3.5 w-3.5" /> Dienste-Übersicht öffnen
|
|
</button>
|
|
<p>
|
|
Weitere Einrichtungsdetails (Brain-Konfiguration, MCP-Kopplungen, SSH-Tunneling und Such-Engines) findest du in der Dokumentationsdatei:
|
|
<code className="ml-1 px-1.5 py-0.5 rounded bg-muted/40 text-foreground border border-border/30">docs/HERMES_SETUP.md</code>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Brain Selection Modal — eine Stelle für den Hirn-Wechsel (warm-bewusst + Alias) */}
|
|
{s && showBrainSelect && (
|
|
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 overscroll-contain" role="dialog" aria-modal="true" aria-label="Hermes-Hirn konfigurieren">
|
|
<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">
|
|
<Cpu className="h-4 w-4" />
|
|
<span>Hermes-Hirn konfigurieren</span>
|
|
</h3>
|
|
<button
|
|
onClick={() => setShowBrainSelect(false)}
|
|
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Lane / Gateway-Alias */}
|
|
<div className="space-y-1.5">
|
|
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Lane / Gateway-Alias</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{ALIASES.map((a) => {
|
|
const cur = s.brain_model === a
|
|
return (
|
|
<button
|
|
key={a}
|
|
onClick={() => setBrainAlias(a)}
|
|
className={cn(
|
|
"px-3 py-1.5 rounded-lg text-xs font-mono font-semibold border transition-all cursor-pointer flex items-center gap-1.5",
|
|
cur ? "border-primary/40 bg-primary/10 text-primary" : "border-border/30 bg-background/20 text-foreground hover:bg-accent",
|
|
)}
|
|
>
|
|
{a}{cur && <Check className="h-3.5 w-3.5" />}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground/70">
|
|
Schlank & flexibel: das Hirn folgt dem Lane-Routing (z.B. <code className="text-primary">chat</code> = fast↔heavy).
|
|
</p>
|
|
</div>
|
|
|
|
{/* Installierte Modelle (warm-bewusst) */}
|
|
<div className="space-y-1.5">
|
|
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Installiertes Modell (warm gehalten)</div>
|
|
<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>
|
|
)}
|
|
|
|
{dialogElement}
|
|
</div>
|
|
)
|
|
}
|