feat: lower memory dedupe threshold for more aggressive cleaning
This commit is contained in:
+90
-90
@@ -1,90 +1,90 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Activity, Code, Bookmark, Wrench, ArrowLeft, type LucideIcon } from "lucide-react"
|
||||
import { ConnectView } from "@/views/ConnectView"
|
||||
import { MemoryView } from "@/views/MemoryView"
|
||||
import { SystemDrawer } from "@/components/SystemDrawer"
|
||||
import { useHealth } from "@/lib/queries"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Mc3Start } from "./Mc3Start"
|
||||
import { Mc3Maschinenraum } from "./Mc3Maschinenraum"
|
||||
|
||||
// MC3-Prototyp-Schale. Läuft nicht-destruktiv unter #mc3, das produktive MC2 bleibt
|
||||
// unangetastet. Vier Türen; Innereien sind bewusst die bestehenden, bewährten Views.
|
||||
type Door = "start" | "vibe" | "konstitution" | "maschinenraum"
|
||||
|
||||
const DOORS: { id: Door; label: string; icon: LucideIcon }[] = [
|
||||
{ id: "start", label: "Start", icon: Activity },
|
||||
{ id: "vibe", label: "Vibe-Coding", icon: Code },
|
||||
{ id: "konstitution", label: "Konstitution", icon: Bookmark },
|
||||
{ id: "maschinenraum", label: "Maschinenraum", icon: Wrench },
|
||||
]
|
||||
|
||||
export function Mc3App() {
|
||||
const [door, setDoor] = useState<Door>("start")
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance")
|
||||
const { data: health } = useHealth()
|
||||
|
||||
useEffect(() => {
|
||||
const onOpen = (e: Event) => {
|
||||
setDrawerTab(((e as CustomEvent).detail?.tab as "maintenance" | "logs") || "maintenance")
|
||||
setDrawerOpen(true)
|
||||
}
|
||||
window.addEventListener("open-system-drawer", onOpen)
|
||||
return () => window.removeEventListener("open-system-drawer", onOpen)
|
||||
}, [])
|
||||
|
||||
const boxOk = health && health.engine_reachable && (health.brain ? health.brain.ready : true)
|
||||
|
||||
return (
|
||||
<div className="flex h-full relative">
|
||||
<div className="fixed inset-0 -z-50 pointer-events-none overflow-hidden bg-[hsl(224,30%,6%)]">
|
||||
<div className="absolute inset-0 opacity-30 animate-aurora bg-gradient-to-tr from-teal-500/15 via-indigo-500/10 to-purple-500/15 blur-[130px]" />
|
||||
</div>
|
||||
|
||||
<SystemDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} defaultTab={drawerTab} />
|
||||
|
||||
{/* Sidebar — 4 Türen */}
|
||||
<aside className="flex w-56 shrink-0 flex-col border-r border-border/40 bg-card/45 backdrop-blur-md">
|
||||
<div className="flex items-center gap-2 px-5 py-4 border-b border-border/40">
|
||||
<div className="h-7 w-7 rounded-lg bg-primary shadow-md shadow-primary/20" />
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold tracking-wide font-space">Mission Control</div>
|
||||
<div className="text-[10px] text-muted-foreground">3 · Prototyp</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 px-3 py-4">
|
||||
{DOORS.map((d) => (
|
||||
<button key={d.id} onClick={() => setDoor(d.id)}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-all cursor-pointer",
|
||||
door === d.id ? "bg-primary/15 text-primary" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
)}>
|
||||
<d.icon className="h-4.5 w-4.5 shrink-0" /> {d.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="px-3 py-3 border-t border-border/40 space-y-2">
|
||||
<div className="flex items-center gap-2 px-2 text-xs text-muted-foreground">
|
||||
<span className={cn("h-2 w-2 rounded-full", boxOk ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
|
||||
{boxOk ? "Box läuft" : "prüfen"}
|
||||
</div>
|
||||
<a href="#dashboard"
|
||||
className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors">
|
||||
<ArrowLeft className="h-3.5 w-3.5" /> zurück zu MC2
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main */}
|
||||
<main className="flex-1 min-w-0 overflow-y-auto p-6 scrollbar-thin">
|
||||
{door === "start" && <Mc3Start />}
|
||||
{door === "vibe" && <ConnectView />}
|
||||
{door === "konstitution" && <MemoryView />}
|
||||
{door === "maschinenraum" && <Mc3Maschinenraum />}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { useEffect, useState } from "react"
|
||||
import { Activity, Code, Bookmark, Wrench, ArrowLeft, type LucideIcon } from "lucide-react"
|
||||
import { ConnectView } from "@/views/ConnectView"
|
||||
import { MemoryView } from "@/views/MemoryView"
|
||||
import { SystemDrawer } from "@/components/SystemDrawer"
|
||||
import { useHealth } from "@/lib/queries"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Mc3Start } from "./Mc3Start"
|
||||
import { Mc3Maschinenraum } from "./Mc3Maschinenraum"
|
||||
|
||||
// MC3-Prototyp-Schale. Läuft nicht-destruktiv unter #mc3, das produktive MC2 bleibt
|
||||
// unangetastet. Vier Türen; Innereien sind bewusst die bestehenden, bewährten Views.
|
||||
type Door = "start" | "vibe" | "konstitution" | "maschinenraum"
|
||||
|
||||
const DOORS: { id: Door; label: string; icon: LucideIcon }[] = [
|
||||
{ id: "start", label: "Start", icon: Activity },
|
||||
{ id: "vibe", label: "Vibe-Coding", icon: Code },
|
||||
{ id: "konstitution", label: "Konstitution", icon: Bookmark },
|
||||
{ id: "maschinenraum", label: "Maschinenraum", icon: Wrench },
|
||||
]
|
||||
|
||||
export function Mc3App() {
|
||||
const [door, setDoor] = useState<Door>("start")
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance")
|
||||
const { data: health } = useHealth()
|
||||
|
||||
useEffect(() => {
|
||||
const onOpen = (e: Event) => {
|
||||
setDrawerTab(((e as CustomEvent).detail?.tab as "maintenance" | "logs") || "maintenance")
|
||||
setDrawerOpen(true)
|
||||
}
|
||||
window.addEventListener("open-system-drawer", onOpen)
|
||||
return () => window.removeEventListener("open-system-drawer", onOpen)
|
||||
}, [])
|
||||
|
||||
const boxOk = health && health.engine_reachable && (health.brain ? health.brain.ready : true)
|
||||
|
||||
return (
|
||||
<div className="flex h-full relative">
|
||||
<div className="fixed inset-0 -z-50 pointer-events-none overflow-hidden bg-[hsl(224,30%,6%)]">
|
||||
<div className="absolute inset-0 opacity-30 animate-aurora bg-gradient-to-tr from-teal-500/15 via-indigo-500/10 to-purple-500/15 blur-[130px]" />
|
||||
</div>
|
||||
|
||||
<SystemDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} defaultTab={drawerTab} />
|
||||
|
||||
{/* Sidebar — 4 Türen */}
|
||||
<aside className="flex w-56 shrink-0 flex-col border-r border-border/40 bg-card/45 backdrop-blur-md">
|
||||
<div className="flex items-center gap-2 px-5 py-4 border-b border-border/40">
|
||||
<div className="h-7 w-7 rounded-lg bg-primary shadow-md shadow-primary/20" />
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold tracking-wide font-space">Mission Control</div>
|
||||
<div className="text-[10px] text-muted-foreground">3 · Prototyp</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 px-3 py-4">
|
||||
{DOORS.map((d) => (
|
||||
<button key={d.id} onClick={() => setDoor(d.id)}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-all cursor-pointer",
|
||||
door === d.id ? "bg-primary/15 text-primary" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
)}>
|
||||
<d.icon className="h-4.5 w-4.5 shrink-0" /> {d.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="px-3 py-3 border-t border-border/40 space-y-2">
|
||||
<div className="flex items-center gap-2 px-2 text-xs text-muted-foreground">
|
||||
<span className={cn("h-2 w-2 rounded-full", boxOk ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
|
||||
{boxOk ? "Box läuft" : "prüfen"}
|
||||
</div>
|
||||
<a href="#dashboard"
|
||||
className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors">
|
||||
<ArrowLeft className="h-3.5 w-3.5" /> zurück zu MC2
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main */}
|
||||
<main className="flex-1 min-w-0 overflow-y-auto p-6 scrollbar-thin">
|
||||
{door === "start" && <Mc3Start />}
|
||||
{door === "vibe" && <ConnectView />}
|
||||
{door === "konstitution" && <MemoryView />}
|
||||
{door === "maschinenraum" && <Mc3Maschinenraum />}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import { useState } from "react"
|
||||
import { Cpu, ShieldCheck, Bot, ChevronRight, ArrowLeft } from "lucide-react"
|
||||
import { Cockpit } from "@/views/models/Cockpit"
|
||||
import { AgentView } from "@/views/AgentView"
|
||||
import { ExpertToggle } from "@/components/ExpertToggle"
|
||||
|
||||
// Maschinenraum = die eine Tür für alles Technische. Faltet Modelle/Wartung/Hermes
|
||||
// zusammen; jede Sektion rendert die BESTEHENDE, bewährte View wieder.
|
||||
function SectionCard({ icon: Icon, title, sub, onClick }: {
|
||||
icon: any; title: string; sub: string; onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
className="w-full flex items-center gap-4 rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 text-left transition-all hover:border-primary/40 cursor-pointer">
|
||||
<Icon className="h-6 w-6 text-muted-foreground shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-foreground">{title}</div>
|
||||
<div className="text-xs text-muted-foreground">{sub}</div>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground shrink-0" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function Mc3Maschinenraum() {
|
||||
const [section, setSection] = useState<null | "modelle" | "hermes">(null)
|
||||
|
||||
if (section) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<button onClick={() => setSection(null)}
|
||||
className="flex items-center gap-1.5 text-xs font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
<ArrowLeft className="h-4 w-4" /> Maschinenraum
|
||||
</button>
|
||||
{section === "modelle" ? <Cockpit /> : <AgentView />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<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">
|
||||
Maschinenraum
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Nur nötig, wenn du wirklich was ändern willst.</p>
|
||||
</div>
|
||||
<ExpertToggle />
|
||||
</div>
|
||||
|
||||
<SectionCard icon={Cpu} title="Modelle" sub="Hirne, Coder, Immer-bereit-Set, Bibliothek"
|
||||
onClick={() => setSection("modelle")} />
|
||||
<SectionCard icon={ShieldCheck} title="Wartung und Logs" sub="Updates, Backup, Dienste, Logs, Zugangsdaten"
|
||||
onClick={() => window.dispatchEvent(new CustomEvent("open-system-drawer", { detail: { tab: "maintenance" } }))} />
|
||||
<SectionCard icon={Bot} title="Hermes und Verdrahtung" sub="Agent, Gehirn-Modell, PC-Verbindung"
|
||||
onClick={() => setSection("hermes")} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { useState } from "react"
|
||||
import { Cpu, ShieldCheck, Bot, ChevronRight, ArrowLeft } from "lucide-react"
|
||||
import { Cockpit } from "@/views/models/Cockpit"
|
||||
import { AgentView } from "@/views/AgentView"
|
||||
import { ExpertToggle } from "@/components/ExpertToggle"
|
||||
|
||||
// Maschinenraum = die eine Tür für alles Technische. Faltet Modelle/Wartung/Hermes
|
||||
// zusammen; jede Sektion rendert die BESTEHENDE, bewährte View wieder.
|
||||
function SectionCard({ icon: Icon, title, sub, onClick }: {
|
||||
icon: any; title: string; sub: string; onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
className="w-full flex items-center gap-4 rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 text-left transition-all hover:border-primary/40 cursor-pointer">
|
||||
<Icon className="h-6 w-6 text-muted-foreground shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-foreground">{title}</div>
|
||||
<div className="text-xs text-muted-foreground">{sub}</div>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground shrink-0" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function Mc3Maschinenraum() {
|
||||
const [section, setSection] = useState<null | "modelle" | "hermes">(null)
|
||||
|
||||
if (section) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<button onClick={() => setSection(null)}
|
||||
className="flex items-center gap-1.5 text-xs font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
<ArrowLeft className="h-4 w-4" /> Maschinenraum
|
||||
</button>
|
||||
{section === "modelle" ? <Cockpit /> : <AgentView />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<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">
|
||||
Maschinenraum
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Nur nötig, wenn du wirklich was ändern willst.</p>
|
||||
</div>
|
||||
<ExpertToggle />
|
||||
</div>
|
||||
|
||||
<SectionCard icon={Cpu} title="Modelle" sub="Hirne, Coder, Immer-bereit-Set, Bibliothek"
|
||||
onClick={() => setSection("modelle")} />
|
||||
<SectionCard icon={ShieldCheck} title="Wartung und Logs" sub="Updates, Backup, Dienste, Logs, Zugangsdaten"
|
||||
onClick={() => window.dispatchEvent(new CustomEvent("open-system-drawer", { detail: { tab: "maintenance" } }))} />
|
||||
<SectionCard icon={Bot} title="Hermes und Verdrahtung" sub="Agent, Gehirn-Modell, PC-Verbindung"
|
||||
onClick={() => setSection("hermes")} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
import { ArrowUpCircle, ChevronRight, Check, Cpu, Code } from "lucide-react"
|
||||
import { LucyHealthCard } from "@/components/dashboard/LucyHealthCard"
|
||||
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
||||
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
||||
import { useUpdates, useModels } from "@/lib/queries"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// MC3-Startseite = der eine „Basisstation"-Blick: Ampel (wiederverwendet) + Updates
|
||||
// (klickbar) + was gerade läuft + die geschätzte Telemetrie (System/Token/Sprach-Latenz).
|
||||
// Bewusst KEINE Wärme/Platte/Speicher-Kacheln — die stehen schon im System-Status.
|
||||
export function Mc3Start() {
|
||||
const { data: updates } = useUpdates(30_000)
|
||||
const { data: modelsResp } = useModels()
|
||||
|
||||
const upCount = (updates?.os || 0) + (updates?.engine || 0) + (updates?.swap || 0) + (updates?.models || 0)
|
||||
const openUpdates = () => window.dispatchEvent(new CustomEvent("open-system-drawer", { detail: { tab: "maintenance" } }))
|
||||
|
||||
const models = modelsResp?.models ?? []
|
||||
const running = modelsResp?.running ?? []
|
||||
const brain = models.find((m) => m.role === "hermes")
|
||||
const coder = models.find((m) => m.role === "coder-lite") || models.find((m) => m.role === "coder")
|
||||
const isWarm = (name?: string) => (name ? running.includes(name) : false)
|
||||
const shortName = (n?: string) => n?.split("/").pop()?.replace(/\.gguf$/i, "") || "—"
|
||||
|
||||
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">
|
||||
Basisstation
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Geht's der Box gut, gibt's Updates, was läuft gerade.</p>
|
||||
</div>
|
||||
|
||||
{/* Gesamt-Ampel + Fähigkeiten + Speicher (voll wiederverwendet, Box-Rahmen) */}
|
||||
<LucyHealthCard frame="box" />
|
||||
|
||||
{/* Updates — klickbar, führt direkt zur Liste, was aktualisiert wird */}
|
||||
<button
|
||||
onClick={openUpdates}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-3 rounded-2xl border p-4 text-left transition-all cursor-pointer",
|
||||
upCount > 0
|
||||
? "border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/10"
|
||||
: "border-border/60 bg-card/45 hover:border-primary/40",
|
||||
)}
|
||||
>
|
||||
{upCount > 0
|
||||
? <ArrowUpCircle className="h-6 w-6 shrink-0 text-amber-400" />
|
||||
: <Check className="h-6 w-6 shrink-0 text-emerald-400" />}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={cn("text-sm font-semibold", upCount > 0 ? "text-amber-400" : "text-foreground")}>
|
||||
{upCount > 0 ? `${upCount} Update${upCount === 1 ? "" : "s"} verfügbar` : "Alles aktuell"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{upCount > 0 ? "Antippen, um zu sehen, was genau aktualisiert wird." : "Keine ausstehenden Updates."}
|
||||
</div>
|
||||
</div>
|
||||
{upCount > 0 && (
|
||||
<span className="shrink-0 flex items-center gap-1 text-xs font-semibold text-amber-400">
|
||||
Ansehen <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Was gerade läuft — beide Lanes */}
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">Was gerade läuft</div>
|
||||
<div className="rounded-2xl border border-border/60 overflow-hidden">
|
||||
{[
|
||||
{ icon: Cpu, label: "Lucys Hirn", model: brain?.name, warm: isWarm(brain?.name) },
|
||||
{ icon: Code, label: "Coder (Vibe-Coding)", model: coder?.name, warm: isWarm(coder?.name) },
|
||||
].map((r) => (
|
||||
<div key={r.label} className="flex items-center gap-3 px-4 py-3 border-b border-border/40 last:border-0">
|
||||
<span className={cn("h-2 w-2 shrink-0 rounded-full", r.warm ? "bg-emerald-500 animate-pulse" : "bg-muted-foreground/40")} />
|
||||
<r.icon className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm text-foreground w-40 shrink-0">{r.label}</span>
|
||||
<span className="text-xs text-muted-foreground flex-1 truncate font-mono">{shortName(r.model)}</span>
|
||||
<span className={cn("text-xs shrink-0", r.warm ? "text-emerald-400" : "text-muted-foreground")}>
|
||||
{r.model ? (r.warm ? "warm" : "bereit") : "nicht gesetzt"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Geschätzte Telemetrie — 1:1 aus MC2 übernommen */}
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<SystemStatusCard />
|
||||
<TokenPerformanceCard />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { ArrowUpCircle, ChevronRight, Check, Cpu, Code } from "lucide-react"
|
||||
import { LucyHealthCard } from "@/components/dashboard/LucyHealthCard"
|
||||
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
||||
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
||||
import { useUpdates, useModels } from "@/lib/queries"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// MC3-Startseite = der eine „Basisstation"-Blick: Ampel (wiederverwendet) + Updates
|
||||
// (klickbar) + was gerade läuft + die geschätzte Telemetrie (System/Token/Sprach-Latenz).
|
||||
// Bewusst KEINE Wärme/Platte/Speicher-Kacheln — die stehen schon im System-Status.
|
||||
export function Mc3Start() {
|
||||
const { data: updates } = useUpdates(30_000)
|
||||
const { data: modelsResp } = useModels()
|
||||
|
||||
const upCount = (updates?.os || 0) + (updates?.engine || 0) + (updates?.swap || 0) + (updates?.models || 0)
|
||||
const openUpdates = () => window.dispatchEvent(new CustomEvent("open-system-drawer", { detail: { tab: "maintenance" } }))
|
||||
|
||||
const models = modelsResp?.models ?? []
|
||||
const running = modelsResp?.running ?? []
|
||||
const brain = models.find((m) => m.role === "hermes")
|
||||
const coder = models.find((m) => m.role === "coder-lite") || models.find((m) => m.role === "coder")
|
||||
const isWarm = (name?: string) => (name ? running.includes(name) : false)
|
||||
const shortName = (n?: string) => n?.split("/").pop()?.replace(/\.gguf$/i, "") || "—"
|
||||
|
||||
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">
|
||||
Basisstation
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Geht's der Box gut, gibt's Updates, was läuft gerade.</p>
|
||||
</div>
|
||||
|
||||
{/* Gesamt-Ampel + Fähigkeiten + Speicher (voll wiederverwendet, Box-Rahmen) */}
|
||||
<LucyHealthCard frame="box" />
|
||||
|
||||
{/* Updates — klickbar, führt direkt zur Liste, was aktualisiert wird */}
|
||||
<button
|
||||
onClick={openUpdates}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-3 rounded-2xl border p-4 text-left transition-all cursor-pointer",
|
||||
upCount > 0
|
||||
? "border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/10"
|
||||
: "border-border/60 bg-card/45 hover:border-primary/40",
|
||||
)}
|
||||
>
|
||||
{upCount > 0
|
||||
? <ArrowUpCircle className="h-6 w-6 shrink-0 text-amber-400" />
|
||||
: <Check className="h-6 w-6 shrink-0 text-emerald-400" />}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={cn("text-sm font-semibold", upCount > 0 ? "text-amber-400" : "text-foreground")}>
|
||||
{upCount > 0 ? `${upCount} Update${upCount === 1 ? "" : "s"} verfügbar` : "Alles aktuell"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{upCount > 0 ? "Antippen, um zu sehen, was genau aktualisiert wird." : "Keine ausstehenden Updates."}
|
||||
</div>
|
||||
</div>
|
||||
{upCount > 0 && (
|
||||
<span className="shrink-0 flex items-center gap-1 text-xs font-semibold text-amber-400">
|
||||
Ansehen <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Was gerade läuft — beide Lanes */}
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">Was gerade läuft</div>
|
||||
<div className="rounded-2xl border border-border/60 overflow-hidden">
|
||||
{[
|
||||
{ icon: Cpu, label: "Lucys Hirn", model: brain?.name, warm: isWarm(brain?.name) },
|
||||
{ icon: Code, label: "Coder (Vibe-Coding)", model: coder?.name, warm: isWarm(coder?.name) },
|
||||
].map((r) => (
|
||||
<div key={r.label} className="flex items-center gap-3 px-4 py-3 border-b border-border/40 last:border-0">
|
||||
<span className={cn("h-2 w-2 shrink-0 rounded-full", r.warm ? "bg-emerald-500 animate-pulse" : "bg-muted-foreground/40")} />
|
||||
<r.icon className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm text-foreground w-40 shrink-0">{r.label}</span>
|
||||
<span className="text-xs text-muted-foreground flex-1 truncate font-mono">{shortName(r.model)}</span>
|
||||
<span className={cn("text-xs shrink-0", r.warm ? "text-emerald-400" : "text-muted-foreground")}>
|
||||
{r.model ? (r.warm ? "warm" : "bereit") : "nicht gesetzt"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Geschätzte Telemetrie — 1:1 aus MC2 übernommen */}
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<SystemStatusCard />
|
||||
<TokenPerformanceCard />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user