Feat: Add Hermes brain change buttons and skills.sh guide source
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react"
|
||||
import { Bot, ExternalLink, Activity, Cpu, Wrench, Shield } from "lucide-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 }: { label: string; ok: boolean; detail?: string; icon: any }) {
|
||||
|
||||
function Tile({ label, ok, detail, icon: Icon, onClick }: { label: string; ok: boolean; detail?: string; icon: any; onClick?: () => void }) {
|
||||
return (
|
||||
<div 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"
|
||||
)}>
|
||||
<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")} />
|
||||
@@ -26,6 +32,18 @@ function Tile({ label, ok, detail, icon: Icon }: { label: string; ok: boolean; d
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -36,6 +54,21 @@ export function AgentView() {
|
||||
|
||||
// Graph UI state
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
|
||||
const [showBrainSelect, setShowBrainSelect] = useState(false)
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([])
|
||||
|
||||
// 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 })
|
||||
@@ -70,8 +103,32 @@ export function AgentView() {
|
||||
api<AgentStatus>("/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)
|
||||
}, [])
|
||||
@@ -147,6 +204,7 @@ export function AgentView() {
|
||||
ok={s.gateway_reachable}
|
||||
detail={s.brain_model ? `Model: ${s.brain_model}` : "Model: auto"}
|
||||
icon={Cpu}
|
||||
onClick={() => setShowBrainSelect(true)}
|
||||
/>
|
||||
<Tile
|
||||
label="Verdrahtung"
|
||||
@@ -236,12 +294,13 @@ export function AgentView() {
|
||||
{/* COLUMN 3: Brain & Wiring */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute z-10 w-44 py-1.5 px-3 -translate-y-1/2 -translate-x-1/2 rounded-xl border flex flex-col justify-center hover:border-primary/50 transition-all text-left shadow select-none",
|
||||
"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",
|
||||
s.gateway_reachable ? "border-emerald-500/50 bg-emerald-500/5 shadow-emerald-500/5" : "border-border/60 bg-card/75"
|
||||
)}
|
||||
style={{ left: "85%", top: "30%" }}
|
||||
onMouseEnter={() => setHoveredNode("brain")}
|
||||
onMouseLeave={() => setHoveredNode(null)}
|
||||
onClick={() => setShowBrainSelect(true)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
@@ -250,8 +309,13 @@ export function AgentView() {
|
||||
</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 className="flex items-center justify-between mt-1">
|
||||
<span className="text-[10px] font-mono font-medium truncate text-foreground font-space max-w-[100px]" title={s.brain_model}>
|
||||
{s.brain_model || "auto"}
|
||||
</span>
|
||||
<span className="text-[8px] bg-primary/20 hover:bg-primary/30 border border-primary/30 text-primary px-1.5 py-0.5 rounded uppercase font-bold tracking-wider transition-colors">
|
||||
Ändern
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -358,6 +422,69 @@ export function AgentView() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Brain Selection Modal */}
|
||||
{s && showBrainSelect && (
|
||||
<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">
|
||||
<Cpu className="h-4 w-4" />
|
||||
<span>Hermes-Gehirn 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>
|
||||
<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>):
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5 max-h-60 overflow-y-auto pr-1">
|
||||
{availableModels.map((m) => {
|
||||
const isAlias = ["auto", "fast", "heavy"].includes(m);
|
||||
return (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => 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"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="font-semibold truncate max-w-[280px]">
|
||||
{m}
|
||||
</span>
|
||||
<span className="text-[9px] text-muted-foreground mt-0.5">
|
||||
{isAlias ? "Gateway Routing Alias" : "Installiertes GGUF Modell"}
|
||||
</span>
|
||||
</div>
|
||||
{(s.brain_model === m || (!s.brain_model && m === "auto")) && (
|
||||
<Check className="h-4 w-4 shrink-0 text-primary" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
onConfirm={() => dialog.onConfirm && dialog.onConfirm()}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user