Wartung: UI/UX-Politur fuer MC2.0 (Sidebar-Gruppierung, Kachel-Livedaten, Memory-Splitscreen und Mobile-Touch)

This commit is contained in:
Hitonabi
2026-07-09 11:58:16 +02:00
parent 3572667e08
commit eb3caf6476
10 changed files with 417 additions and 279 deletions
+77 -71
View File
@@ -174,7 +174,7 @@ export function MemoryView() {
className="h-9 px-3 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center gap-1.5 cursor-pointer shadow-md shadow-primary/10">
<Plus className="h-4 w-4" /> Eintrag
</button>
<div className="flex items-center gap-1 p-1 bg-card/30 border border-border/40 rounded-lg">
<div className="flex items-center gap-1 p-1 bg-card/30 border border-border/40 rounded-lg lg:hidden">
{([["liste", List, "Liste"], ["graph", Share2, "Graph"]] as const).map(([v, Icon, lbl]) => (
<button key={v} onClick={() => setView(v)}
className={cn("flex h-7 px-2.5 items-center gap-1.5 text-[11px] font-semibold rounded-md transition-all cursor-pointer",
@@ -270,83 +270,89 @@ export function MemoryView() {
<button onClick={() => setShowAdd(true)} className="underline hover:text-foreground">oder lieber manuell anlegen</button>.
</p>
</div>
) : view === "graph" ? (
<GraphErrorBoundary>
<Suspense fallback={<div className="h-[480px] rounded-2xl border border-border/60 bg-card/30 flex items-center justify-center text-xs text-muted-foreground">Graph wird geladen</div>}>
<GraphView data={graphData} onDelete={del} />
</Suspense>
</GraphErrorBoundary>
) : (
<>
{/* Kategorie-Filter */}
<div className="flex flex-wrap items-center gap-1.5 p-1 bg-card/30 border border-border/40 rounded-xl w-fit">
<button onClick={() => setFilter("")}
className={cn("h-7 px-3 rounded-lg text-[10px] font-semibold uppercase tracking-wider transition-all cursor-pointer",
!filter ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>Alle</button>
{CATEGORIES.map((c) => {
const conf = CAT_CONFIG[c] || DEFAULT_CAT
const Icon = conf.icon
return (
<button key={c} onClick={() => setFilter(c)}
className={cn("h-7 px-2.5 rounded-lg text-[10px] font-semibold uppercase tracking-wider transition-all cursor-pointer flex items-center gap-1",
filter === c ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>
<Icon className="h-3 w-3" /> {conf.label}
</button>
)
})}
</div>
{items.length === 0 ? (
<div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center">
Keine Einträge für die aktuellen Filterkriterien gefunden.
</div>
) : (
<div className="space-y-5">
{[...CATEGORIES, "__other"].map((c) => {
const list = c === "__other" ? otherItems : (grouped[c] || [])
if (!list.length) return null
<div className="flex flex-col lg:flex-row gap-6 items-stretch">
{/* Liste-Sicht */}
<div className={cn("flex-1 space-y-5 lg:max-w-md xl:max-w-xl lg:shrink-0", view === "graph" && "hidden lg:block")}>
{/* Kategorie-Filter */}
<div className="flex flex-wrap items-center gap-1.5 p-1 bg-card/30 border border-border/40 rounded-xl w-fit">
<button onClick={() => setFilter("")}
className={cn("h-7 px-3 rounded-lg text-[10px] font-semibold uppercase tracking-wider transition-all cursor-pointer",
!filter ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>Alle</button>
{CATEGORIES.map((c) => {
const conf = CAT_CONFIG[c] || DEFAULT_CAT
const Icon = conf.icon
return (
<div key={c}>
{/* Sektions-Kopf je Kategorie */}
<div className="flex items-center gap-2 mb-2 px-1">
<Icon className={cn("h-3.5 w-3.5", conf.text)} />
<span className={cn("text-[11px] font-bold uppercase tracking-wider", conf.text)}>{conf.label}</span>
<span className="text-[10px] font-mono text-muted-foreground/60 bg-card/50 rounded px-1.5 py-0.5">{list.length}</span>
<div className="ml-1 h-px flex-1 bg-border/30" />
</div>
<div className="space-y-2">
{list.map((m) => {
const isAuto = AUTO_SOURCES.has(m.source)
return (
<div key={m.id} className={cn("flex items-start justify-between gap-4 p-3.5 rounded-xl border border-l-4 bg-card/45 backdrop-blur-md border-border/60 hover:border-primary/20 transition-all group",
BORDER_CLASSES[m.category] || "border-l-muted")}>
<span className="text-xs text-foreground leading-relaxed break-words whitespace-pre-wrap flex-1 min-w-0">{m.content}</span>
<div className="flex items-center gap-2 shrink-0">
{typeof m.score === "number" && (
<span className="text-[9px] font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded" title="Relevanz der semantischen Suche">{Math.round(m.score * 100)}%</span>
)}
<span className={cn("flex items-center gap-1 text-[9px] font-mono px-1.5 py-0.5 rounded",
isAuto ? "text-emerald-400 bg-emerald-500/10" : "text-muted-foreground/60 bg-background/20")}
title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}>
{isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source}
</span>
<button onClick={() => del(m.id)} aria-label="Eintrag löschen" title="Eintrag löschen"
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all opacity-0 group-hover:opacity-100">
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</div>
</div>
)
})}
</div>
</div>
<button key={c} onClick={() => setFilter(c)}
className={cn("h-7 px-2.5 rounded-lg text-[10px] font-semibold uppercase tracking-wider transition-all cursor-pointer flex items-center gap-1",
filter === c ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>
<Icon className="h-3 w-3" /> {conf.label}
</button>
)
})}
</div>
)}
</>
{items.length === 0 ? (
<div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center bg-card/10">
Keine Einträge für die aktuellen Filterkriterien gefunden.
</div>
) : (
<div className="space-y-5 max-h-[calc(100vh-14rem)] overflow-y-auto pr-1 scrollbar-thin">
{[...CATEGORIES, "__other"].map((c) => {
const list = c === "__other" ? otherItems : (grouped[c] || [])
if (!list.length) return null
const conf = CAT_CONFIG[c] || DEFAULT_CAT
const Icon = conf.icon
return (
<div key={c} className="space-y-2">
{/* Sektions-Kopf je Kategorie */}
<div className="flex items-center gap-2 mb-2 px-1">
<Icon className={cn("h-3.5 w-3.5", conf.text)} />
<span className={cn("text-[11px] font-bold uppercase tracking-wider", conf.text)}>{conf.label}</span>
<span className="text-[10px] font-mono text-muted-foreground/60 bg-card/50 rounded px-1.5 py-0.5">{list.length}</span>
<div className="ml-1 h-px flex-1 bg-border/30" />
</div>
<div className="space-y-2">
{list.map((m) => {
const isAuto = AUTO_SOURCES.has(m.source)
return (
<div key={m.id} className={cn("flex items-start justify-between gap-4 p-3.5 rounded-xl border border-l-4 bg-card/45 backdrop-blur-md border-border/60 hover:border-primary/20 transition-all group",
BORDER_CLASSES[m.category] || "border-l-muted")}>
<span className="text-xs text-foreground leading-relaxed break-words whitespace-pre-wrap flex-1 min-w-0">{m.content}</span>
<div className="flex items-center gap-2 shrink-0">
{typeof m.score === "number" && (
<span className="text-[9px] font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded" title="Relevanz der semantischen Suche">{Math.round(m.score * 100)}%</span>
)}
<span className={cn("flex items-center gap-1 text-[9px] font-mono px-1.5 py-0.5 rounded",
isAuto ? "text-emerald-400 bg-emerald-500/10" : "text-muted-foreground/60 bg-background/20")}
title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}>
{isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source}
</span>
<button onClick={() => del(m.id)} aria-label="Eintrag löschen" title="Eintrag löschen"
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all opacity-0 group-hover:opacity-100">
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</div>
</div>
)
})}
</div>
</div>
)
})}
</div>
)}
</div>
{/* Graph-Sicht */}
<div className={cn("flex-1", view === "liste" && "hidden lg:block")}>
<GraphErrorBoundary>
<Suspense fallback={<div className="h-[480px] rounded-2xl border border-border/60 bg-card/30 flex items-center justify-center text-xs text-muted-foreground">Graph wird geladen</div>}>
<GraphView data={graphData} onDelete={del} />
</Suspense>
</GraphErrorBoundary>
</div>
</div>
)}
{dialogElement}
+31 -11
View File
@@ -75,6 +75,20 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
const connectOk = connect?.gateway.ok && connect?.memory.ok
const openAuftraege = auftraege?.available ? auftraege.open_count : 0
// Zusätzliche reaktive Livedaten für die Kacheln
const activeBrain = models?.models?.find((m: any) => m.role === "hermes")
const brainName = activeBrain?.name ? activeBrain.name.split("/").pop()?.replace(/\.gguf$/i, "") : ""
const autoNotes = memory ? memory.filter((m: any) => ["auto", "agent", "hermes"].includes(m.source)).length : 0
const failedSvc = svc?.services.filter((s: any) => !s.ok).map((s: any) => s.name).join(", ")
const upParts = updates ? [
updates.os > 0 && "OS",
updates.engine > 0 && "Engine",
updates.swap > 0 && "Swap",
updates.models > 0 && "Modelle"
].filter(Boolean) : []
const updateSummary = upParts.length > 0 ? upParts.join(" + ") : ""
return (
<div className="space-y-7">
{/* Kopf */}
@@ -109,38 +123,44 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
icon={Inbox} title="Auftragsbuch" value={auftraege?.available ? openAuftraege : "—"}
unit={openAuftraege === 1 ? "Vorschlag" : "Vorschläge"}
tone={!auftraege ? "loading" : openAuftraege > 0 ? "warn" : "ok"}
hint="Annehmen oder ablehnen"
hint={openAuftraege > 0 ? `${openAuftraege} offene Patches prüfen` : "Keine ausstehenden Vorschläge"}
onClick={() => onNavigate("auftraege")}
/>
<CockpitTile
icon={Boxes} title="Modelle" value={running} unit="warm"
tone={running > 0 ? "ok" : "muted"} hint="Speicher, laden & Rollen"
tone={running > 0 ? "ok" : "muted"}
hint={brainName ? `Hirn: ${brainName}` : "Kein Hermes-Modell geladen"}
onClick={() => onNavigate("models")}
/>
<CockpitTile
icon={Brain} title="Gedächtnis" value={memory ? notes : "…"} unit="Notizen"
tone={memory ? "ok" : "loading"} hint="Lucys geteiltes Wissen"
tone={memory ? "ok" : "loading"}
hint={memory ? `${notes} Fakten (${autoNotes} auto gelernt)` : "Gedächtnis-Pool laden..."}
onClick={() => onNavigate("memory")}
/>
<CockpitTile
icon={Server} title="Dienste" value={svc ? `${svcOk}/${svcTotal}` : "…"} unit="laufen"
tone={!svc ? "loading" : svcErrors > 0 ? "warn" : "ok"} hint="Status & Neustart"
tone={!svc ? "loading" : svcErrors > 0 ? "warn" : "ok"}
hint={failedSvc ? `Ausfall: ${failedSvc}` : "Alle Dienste online"}
onClick={() => openDrawer("maintenance")}
/>
<CockpitTile
icon={ShieldAlert} title="Updates" value={updates ? (pendingCount > 0 ? pendingCount : "0") : "…"}
unit={pendingCount > 0 ? "bereit" : "aktuell"}
tone={!updates ? "loading" : pendingCount > 0 ? "warn" : "ok"} hint="Prüfen & einspielen"
tone={!updates ? "loading" : pendingCount > 0 ? "warn" : "ok"}
hint={pendingCount > 0 ? `Verfügbar: ${updateSummary}` : "Alles auf dem neuesten Stand"}
onClick={() => openDrawer("maintenance")}
/>
<CockpitTile
icon={Plug} title="Verbinden" value="Zed" unit={connectOk ? "· bereit" : ""}
tone={!connect ? "loading" : connectOk ? "ok" : "warn"} hint="IDE- & Agent-Configs"
tone={!connect ? "loading" : connectOk ? "ok" : "warn"}
hint={agent?.pc_executor_reachable ? "IDE & PC online (:7777)" : "IDE-Kopplung (PC offline)"}
onClick={() => onNavigate("connect")}
/>
<CockpitTile
icon={ScrollText} title="Logs" value={svc ? svcErrors : "…"} unit={svcErrors === 1 ? "Fehler" : "Fehler"}
tone={!svc ? "loading" : svcErrors > 0 ? "alert" : "ok"} hint="Verlauf & Diagnose"
tone={!svc ? "loading" : svcErrors > 0 ? "alert" : "ok"}
hint={svcErrors > 0 ? `${svcErrors} Dienst-Fehler gefunden` : "Keine Log-Warnungen"}
onClick={() => openDrawer("logs")}
/>
</div>
@@ -288,12 +308,12 @@ function NeedsYou({
) : (
<div className="space-y-2">
{problems.map((c) => (
<div key={c.id} className="flex items-center justify-between gap-3 rounded-2xl border border-red-500/25 bg-red-500/5 p-4">
<div className="flex min-w-0 items-center gap-3">
<div key={c.id} className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 rounded-2xl border border-red-500/25 bg-red-500/5 p-4">
<div className="flex min-w-0 items-center gap-3 w-full">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-red-500/30 bg-background/30 text-red-300">
<c.icon className="h-4.5 w-4.5" />
</span>
<div className="min-w-0">
<div className="min-w-0 flex-1">
<p className="text-sm font-bold text-foreground">{c.label}</p>
<p className="truncate text-xs text-muted-foreground">{c.detail}</p>
</div>
@@ -302,7 +322,7 @@ function NeedsYou({
<button
onClick={() => c.repair && onRepair(c.id, c.repair)}
disabled={!!busy[c.id]}
className="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border border-red-500/40 bg-red-500/10 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300 transition-all hover:bg-red-500/20 cursor-pointer disabled:opacity-60"
className="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border border-red-500/40 bg-red-500/10 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300 transition-all hover:bg-red-500/20 cursor-pointer disabled:opacity-60 w-full sm:w-auto justify-center"
>
{busy[c.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Wrench className="h-3.5 w-3.5" />}
{c.repair.label}