feat: complete UI/UX Rework into Sleek Glassmorphic AI OS (June 2026)

This commit is contained in:
Hitonabi
2026-06-25 22:18:51 +02:00
parent e1da5c797d
commit 6a8e55cc43
18 changed files with 2362 additions and 787 deletions
+317
View File
@@ -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>
)
}