import { useEffect, useState, useRef, useCallback } from "react"
import { Bot, ExternalLink, Activity, Cpu, Wrench, Shield, Check, X } from "lucide-react"
import { api, type AgentStatus } from "@/lib/api"
import { cn, resolveExternalUrl } from "@/lib/utils"
import { CustomDialog } from "@/components/CustomDialog"
function Tile({ label, ok, detail, icon: Icon, onClick }: { label: string; ok: boolean; detail?: string; icon: any; onClick?: () => void }) {
return (
{label}
{ok ? "Bereit / Online" : "Offline / Inaktiv"}
{detail &&
{detail}
}
{onClick && (
{
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"
>
Gehirn wechseln
)}
)
}
export function AgentView() {
const [s, setS] = useState(null)
const [error, setError] = useState("")
// Graph UI state
const [hoveredNode, setHoveredNode] = useState(null)
const [showBrainSelect, setShowBrainSelect] = useState(false)
const [availableModels, setAvailableModels] = useState([])
// Custom Dialog State
const [dialog, setDialog] = useState<{
type: "alert" | "confirm"
title: string
message: string
onConfirm?: () => void
onCancel?: () => void
} | null>(null)
function showAlert(title: string, message: string, onConfirm?: () => void) {
setDialog({ type: "alert", title, message, onConfirm })
}
// Canvas pixel tracking for pixel-perfect connection graph without non-uniform scaling
const [dimensions, setDimensions] = useState({ width: 800, height: 360 })
const observerRef = useRef(null)
const containerRef = useCallback((node: HTMLDivElement | null) => {
if (observerRef.current) {
observerRef.current.disconnect()
observerRef.current = null
}
if (node) {
const observer = new ResizeObserver((entries) => {
if (!entries || entries.length === 0) return
const rect = entries[0].contentRect
setDimensions({ width: rect.width, height: rect.height })
})
observer.observe(node)
observerRef.current = observer
}
}, [])
const w = dimensions.width
const h = dimensions.height
// Helper to generate curve between any two points
const getCurvePath = (startX: number, startY: number, endX: number, endY: number) => {
const midX = (startX + endX) / 2
return `M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`
}
function loadData() {
api("/api/agent/status").then(setS).catch((e) => setError(String(e)))
}
function loadModels() {
api<{ models: { name: string }[] }>("/api/models")
.then((res) => {
const names = res.models.map(m => m.name.split("/").pop()?.replace(".gguf", "") || m.name)
setAvailableModels(["auto", "fast", "heavy", ...names])
})
.catch((e) => console.error("Error loading models", e))
}
async function changeBrainModel(model: string) {
try {
await api("/api/agent/brain", {
method: "POST",
body: JSON.stringify({ model })
})
showAlert("Erfolgreich", `Hermes-Gehirn wurde auf '${model}' geändert. Der Gateway-Dienst wurde neu gestartet.`)
loadData()
setShowBrainSelect(false)
} catch (e: any) {
showAlert("Fehler", `Fehler beim Wechseln des Gehirns: ${e.message}`)
}
}
useEffect(() => {
loadData()
loadModels()
const t = setInterval(loadData, 5000)
return () => clearInterval(t)
}, [])
return (
{/* Styles inside AgentView for dash flow animations */}
{/* Header */}
Hermes Agenten-Cockpit
Hermes ist ein autonomer Agent, der im Hintergrund der Box läuft (Port :8642) und seine eigene UI besitzt.
{s?.webui_url && (
Hermes WebUI öffnen
)}
{error && (
Status nicht lesbar ({error}).
)}
{s && (
{/* Tile Grid */}
setShowBrainSelect(true)}
/>
{/* Interactive Gateway Graph */}
Interactive Agent Flow Graph
Visualisiert das Zusammenspiel und die Verbindungspfade zwischen den Hermes-Systemkomponenten.
{/* The Graph Canvas Area */}
{/* Connection: WebUI to Gateway */}
{(hoveredNode === "webui" || s.webui_reachable) && (
)}
{/* Connection: Gateway to Gehirn */}
{(hoveredNode === "gateway" || hoveredNode === "brain" || s.gateway_reachable) && (
)}
{/* Connection: Gateway to Verdrahtung */}
{(hoveredNode === "gateway" || hoveredNode === "wiring" || s.gateway_reachable) && (
)}
{/* COLUMN 1: Interface */}
setHoveredNode("webui")}
onMouseLeave={() => setHoveredNode(null)}
onClick={() => s.webui_reachable && window.open(resolveExternalUrl(s.webui_url), "_blank")}
title={s.webui_reachable ? "Klicken um Chat-WebUI zu öffnen" : "WebUI Offline"}
>
Agent WebUI
{/* COLUMN 2: Gateway */}
setHoveredNode("gateway")}
onMouseLeave={() => setHoveredNode(null)}
>
Agent Gateway
Port :8642
{s.gateway_reachable ? "Online" : "Offline"}
{/* COLUMN 3: Brain & Wiring */}
setHoveredNode("brain")}
onMouseLeave={() => setHoveredNode(null)}
onClick={() => setShowBrainSelect(true)}
>
Aktives Gehirn
{s.gateway_reachable &&
}
{s.brain_model || "auto"}
setHoveredNode("wiring")}
onMouseLeave={() => setHoveredNode(null)}
>
Verdrahtung
{s.has_config &&
}
Config: {s.has_config ? "✓" : "—"}
Skills: {s.has_skills ? "✓" : "—"}
Memory: {s.has_memories ? "✓" : "—"}
{/* Legend */}
Cyan-Fluss: Client-Anfrage an Agenten
Grüner Puls: Verbindung hergestellt / Modul online
{/* SSH / Sandbox-Hilfe Card */}
SSH-Kopplung & Sandbox-Umgehung
1. Warum verweigert Hermes Aktionen auf dem PC?
Hermes ist standardmäßig so ausgerichtet, dass er sich als virtueller Assistent ohne Systemzugriff versteht.
Um dies zu umgehen, musst du Hermes' **System-Prompt** in der Chat-WebUI anpassen.
Beispiel System-Prompt Erweiterung:
"Du hast über das terminal-Tool vollen SSH-Zugriff auf den Windows-PC des Users (TobisPC). Nutze SSH-Befehle um Aktionen dort auszuführen, wenn danach gefragt wird. Behaupte nie, du könntest das nicht!"
2. SSH-Verbindung zum lokalen Windows-PC einrichten
Damit der Agent Befehle auf deinem PC ausführen kann, muss OpenSSH auf Windows aktiv und mit einem Key gekoppelt sein:
OpenSSH Server auf Windows starten: In PowerShell als Admin ausführen: Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
SSH-Key auf der Box erzeugen: ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_hermes_agent
Key autorisieren: Kopiere den Inhalt von ~/.ssh/id_ed25519_hermes_agent.pub in deine Windows-Datei C:\Users\TobisPC\.ssh\authorized_keys
{/* Diagnostic Details if Offline */}
{!s.gateway_reachable && (
Hermes-Agent Diagnostics
Der Hermes-Dienst ist auf der Box derzeit offline oder kann sich nicht mit dem lokalen llama-swap verbinden.
Stelle sicher, dass die systemd-Dienste auf der Box laufen. Du kannst den Status im OS & Updates Drawer prüfen und die Dienste bei Bedarf neu starten.
# Dienste manuell auf der Box prüfen:
systemctl --user status hermes-gateway
systemctl --user status hermes-webui
Weitere Einrichtungsdetails (Brain-Konfiguration, MCP-Kopplungen, SSH-Tunneling und Such-Engines) findest du in der Dokumentationsdatei:
docs/HERMES_SETUP.md.
)}
)}
{/* Brain Selection Modal */}
{s && showBrainSelect && (
Hermes-Gehirn konfigurieren
setShowBrainSelect(false)}
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
>
Das „Gehirn" ist das primäre LLM für Hermes. Wähle ein Modell aus deiner Bibliothek oder nutze ein Gateway-Alias (auto / fast / heavy):
{availableModels.map((m) => {
const isAlias = ["auto", "fast", "heavy"].includes(m);
return (
changeBrainModel(m)}
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",
s.brain_model === m || (!s.brain_model && m === "auto")
? "text-primary font-bold bg-primary/10 border-primary/30"
: "text-foreground bg-background/20"
)}
>
{m}
{isAlias ? "Gateway Routing Alias" : "Installiertes GGUF Modell"}
{(s.brain_model === m || (!s.brain_model && m === "auto")) && (
)}
);
})}
)}
{dialog && (
dialog.onConfirm && dialog.onConfirm()}
onCancel={dialog.onCancel}
/>
)}
)
}