feat: complete UI/UX Rework into Sleek Glassmorphic AI OS (June 2026)
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Bot, Cpu, ExternalLink, Brain, Layers, Plus } from "lucide-react"
|
||||
import { api, type SystemStatus, type AgentStatus, type ModelInfo, type Memory } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function gb(b: number) {
|
||||
return (b / 1024 ** 3).toFixed(1)
|
||||
}
|
||||
|
||||
function RadialGauge({ value, label, detail }: { value: number; label: string; detail?: string }) {
|
||||
const radius = 24
|
||||
const circ = 2 * Math.PI * radius
|
||||
const offset = circ - (Math.min(value, 100) / 100) * circ
|
||||
|
||||
// Verfärbung bei hoher Last
|
||||
const strokeColor = value > 90
|
||||
? "stroke-red-500"
|
||||
: value > 75
|
||||
? "stroke-amber-500"
|
||||
: "stroke-primary"
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1.5 p-2 bg-background/20 rounded-xl border border-border/40">
|
||||
<div className="relative flex h-16 w-16 items-center justify-center">
|
||||
<svg className="absolute inset-0 h-full w-full -rotate-90">
|
||||
<circle cx="32" cy="32" r={radius} className="stroke-muted fill-none" strokeWidth="4.5" />
|
||||
<circle cx="32" cy="32" r={radius} className={cn("fill-none transition-all duration-700 ease-out", strokeColor)} strokeWidth="4.5" strokeDasharray={circ} strokeDashoffset={offset} strokeLinecap="round" />
|
||||
</svg>
|
||||
<span className="text-xs font-mono font-bold tracking-tight text-foreground">{Math.round(value)}%</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">{label}</span>
|
||||
{detail && <span className="text-[10px] font-mono text-muted-foreground/80">{detail}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DashboardView() {
|
||||
const [sys, setSys] = useState<SystemStatus | null>(null)
|
||||
const [agent, setAgent] = useState<AgentStatus | null>(null)
|
||||
const [models, setModels] = useState<ModelInfo[]>([])
|
||||
const [running, setRunning] = useState<string[]>([])
|
||||
const [memories, setMemories] = useState<Memory[]>([])
|
||||
|
||||
// Quick Memory Form State
|
||||
const [memContent, setMemContent] = useState("")
|
||||
const [memCat, setMemCat] = useState("stable")
|
||||
const [savingMem, setSavingMem] = useState(false)
|
||||
|
||||
function loadData() {
|
||||
api<SystemStatus>("/api/system/status").then(setSys).catch(() => {})
|
||||
api<AgentStatus>("/api/agent/status").then(setAgent).catch(() => {})
|
||||
api<{ models: ModelInfo[]; running?: string[] }>("/api/models")
|
||||
.then((d) => {
|
||||
setModels(d.models)
|
||||
setRunning(d.running || [])
|
||||
})
|
||||
.catch(() => {})
|
||||
api<Memory[]>("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
const t = setInterval(loadData, 3000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
|
||||
async function saveQuickMemory() {
|
||||
if (!memContent.trim() || savingMem) return
|
||||
setSavingMem(true)
|
||||
try {
|
||||
await api("/api/memory", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: memContent, category: memCat, source: "dashboard" }),
|
||||
})
|
||||
setMemContent("")
|
||||
// Liste sofort aktualisieren
|
||||
api<Memory[]>("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setSavingMem(false)
|
||||
}
|
||||
}
|
||||
|
||||
const activeModels = models.filter((m) => m.role)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<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">
|
||||
Zentrale
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{/* Card 1: System Status */}
|
||||
<div className="flex flex-col justify-between rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Cpu className="h-4.5 w-4.5 text-primary" />
|
||||
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">System-Status</h2>
|
||||
</div>
|
||||
{sys ? (
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<RadialGauge value={sys.cpu.percent} label="CPU" detail={`${sys.cpu.cores} Cores`} />
|
||||
<RadialGauge value={sys.ram.percent} label="RAM" detail={`${gb(sys.ram.used)} / ${gb(sys.ram.total)} GB`} />
|
||||
{sys.gpu && sys.gpu.busy_percent != null && (
|
||||
<RadialGauge
|
||||
value={sys.gpu.busy_percent}
|
||||
label="GPU"
|
||||
detail={sys.gpu.gtt_used != null && sys.gpu.gtt_total != null ? `${gb(sys.gpu.gtt_used)} / ${gb(sys.gpu.gtt_total)} GB` : undefined}
|
||||
/>
|
||||
)}
|
||||
{sys.disk && (
|
||||
<RadialGauge value={sys.disk.percent} label="Disk" detail={`${gb(sys.disk.used)} / ${gb(sys.disk.total)} GB`} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-28 flex items-center justify-center text-xs text-muted-foreground">Lade Systemdaten…</div>
|
||||
)}
|
||||
</div>
|
||||
{sys?.temp && (sys.temp.cpu || sys.temp.gpu) && (
|
||||
<div className="mt-4 flex gap-4 text-xs font-mono text-muted-foreground/80 border-t border-border/30 pt-3">
|
||||
{sys.temp.cpu != null && <span>CPU Temp: {sys.temp.cpu} °C</span>}
|
||||
{sys.temp.gpu != null && <span>GPU Temp: {sys.temp.gpu} °C</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Card 2: Hermes Agent Status */}
|
||||
<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 justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="h-4.5 w-4.5 text-primary" />
|
||||
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Hermes Agent</h2>
|
||||
</div>
|
||||
{agent?.webui_url && (
|
||||
<a
|
||||
href={agent.webui_url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-3 py-1 rounded-lg text-xs font-medium transition-all",
|
||||
agent.webui_reachable
|
||||
? "bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-primary/20"
|
||||
: "border border-border text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" /> Hermes öffnen
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{agent ? (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
|
||||
<div className="text-[10px] text-muted-foreground uppercase font-semibold">Gateway</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className={cn("h-2 w-2 rounded-full", agent.gateway_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
|
||||
<span className="text-xs font-medium">{agent.gateway_reachable ? "Online" : "Offline"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
|
||||
<div className="text-[10px] text-muted-foreground uppercase font-semibold">WebUI</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className={cn("h-2 w-2 rounded-full", agent.webui_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
|
||||
<span className="text-xs font-medium">{agent.webui_reachable ? "Online" : "Offline"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
|
||||
<div className="text-[10px] text-muted-foreground uppercase font-semibold">Aktives Gehirn</div>
|
||||
<div className="text-xs font-medium mt-1 font-mono text-primary flex items-center gap-1.5">
|
||||
<Layers className="h-3.5 w-3.5" />
|
||||
{agent.brain_model ? `model: ${agent.brain_model}` : "model: auto"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-28 flex items-center justify-center text-xs text-muted-foreground">Lade Agenten-Status…</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3">
|
||||
Gedächtnis & Stack-Tools via MCP gekoppelt.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{/* Card 3: Active Model Roles */}
|
||||
<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">Aktive Rollen</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{activeModels.length === 0 ? (
|
||||
<div className="text-xs text-muted-foreground py-6 text-center">Keine Modelle als Rollen zugewiesen.</div>
|
||||
) : (
|
||||
activeModels.map((m) => {
|
||||
const isRunning = running.includes(m.name)
|
||||
return (
|
||||
<div
|
||||
key={m.name}
|
||||
className={cn(
|
||||
"flex items-center justify-between p-3 rounded-xl border transition-all duration-300",
|
||||
isRunning
|
||||
? "border-primary/50 bg-primary/5 shadow-md shadow-primary/5"
|
||||
: "border-border/40 bg-background/20"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("text-xs font-semibold uppercase px-1.5 py-0.5 rounded",
|
||||
m.role === "fast" ? "bg-cyan-500/15 text-cyan-400" :
|
||||
m.role === "heavy" ? "bg-amber-500/15 text-amber-400" :
|
||||
m.role === "coder" ? "bg-violet-500/15 text-violet-400" :
|
||||
m.role === "vision" ? "bg-pink-500/15 text-pink-400" : "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
{m.role}
|
||||
</span>
|
||||
{isRunning && (
|
||||
<span className="flex h-2 w-2 relative">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs font-medium mt-1.5 truncate max-w-[200px] sm:max-w-[280px]" title={m.name}>
|
||||
{m.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-muted-foreground">
|
||||
{isRunning ? "Warm / Aktiv" : "Bereit"}
|
||||
</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>
|
||||
|
||||
{/* Card 4: Quick Memory Input */}
|
||||
<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">
|
||||
<Brain className="h-4.5 w-4.5 text-primary" />
|
||||
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Gedächtnis-Schnellform</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<textarea
|
||||
value={memContent}
|
||||
onChange={(e) => setMemContent(e.target.value)}
|
||||
placeholder="Fakt / Regel auf der Box speichern..."
|
||||
rows={2}
|
||||
className="flex-1 resize-none rounded-xl border border-border/50 bg-background/30 px-3 py-2 text-xs outline-none focus:ring-1.5 focus:ring-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<select
|
||||
value={memCat}
|
||||
onChange={(e) => setMemCat(e.target.value)}
|
||||
className="rounded-lg border border-border/50 bg-background/50 px-2 py-1 text-xs outline-none"
|
||||
>
|
||||
<option value="stable">🔵 Fakt</option>
|
||||
<option value="instruction">📋 Regel</option>
|
||||
<option value="user">👤 User</option>
|
||||
<option value="versioned">🟡 Version</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={saveQuickMemory}
|
||||
disabled={!memContent.trim() || savingMem}
|
||||
className="flex items-center gap-1 rounded-lg bg-primary px-3 py-1 text-xs font-semibold text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-all cursor-pointer"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" /> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-[10px] text-muted-foreground uppercase font-semibold tracking-wider">Zuletzt gespeichert:</div>
|
||||
{memories.length === 0 ? (
|
||||
<div className="text-xs text-muted-foreground/75 py-2">Keine Einträge vorhanden.</div>
|
||||
) : (
|
||||
memories.map((m) => (
|
||||
<div key={m.id} className="text-xs bg-background/10 border border-border/30 rounded-lg p-2 flex items-start gap-2">
|
||||
<span className="shrink-0 text-[10px] font-mono text-muted-foreground/80 px-1 py-0.5 rounded bg-muted/20">
|
||||
{m.category}
|
||||
</span>
|
||||
<span className="truncate flex-1 text-muted-foreground hover:text-foreground transition-colors" title={m.content}>
|
||||
{m.content}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3">
|
||||
Steht allen Clients (IDEs, Hermes) per MCP zur Verfügung.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user