Wartung: MemoryView-Layout: Header-Elemente direkt in die Spalten integriert (kein versetzter Freiraum mehr)

This commit is contained in:
Hitonabi
2026-07-09 12:12:45 +02:00
parent 5c279ccc38
commit c20c070a6b
6 changed files with 289 additions and 253 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-CNiM5G2e.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DnWvM6fs.css">
<script type="module" crossorigin src="/assets/index-Cp5Tizb9.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DMTEFdKw.css">
</head>
<body>
<div id="root"></div>
+228 -192
View File
@@ -146,214 +146,250 @@ export function MemoryView() {
)
return (
<div className="space-y-5">
{/* Kopf / Werkzeugleiste */}
<div className="flex flex-col lg:flex-row lg:items-center 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">
Gedächtnis-Pool
</h1>
<p className="text-sm text-muted-foreground">
Geteilte Konstitution Hermes, IDEs &amp; Gateway lesen und lernen hier per MCP.
</p>
</div>
<div className="space-y-6">
{/* Zweispaltiges Haupt-Layout ab Y=0 */}
<div className="flex flex-col lg:flex-row gap-6 items-start">
<div className="flex items-center gap-2 flex-wrap">
<div className="relative">
<input
value={q}
onChange={(e) => setQ(e.target.value)}
aria-label="Gedächtnis durchsuchen"
type="search"
placeholder="Semantisch suchen…"
className="w-52 h-9 pl-8 pr-3 rounded-lg border border-border/60 bg-card/45 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
/>
<Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
</div>
<button onClick={() => setShowAdd((s) => !s)}
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 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",
view === v ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>
<Icon className="h-3.5 w-3.5" /> {lbl}
</button>
))}
</div>
<button onClick={cleanup} disabled={deduping}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-card hover:border-primary/50 transition-all cursor-pointer shadow-md disabled:opacity-50"
aria-label="Duplikate bereinigen" title="Deduplizieren">
<Sparkles className="h-4 w-4 text-primary" aria-hidden="true" />
</button>
</div>
</div>
{/* Statuszeile */}
{!empty && (
<div className="flex flex-wrap items-center gap-2">
<StatChip value={stats.total} label="Fakten" />
<StatChip value={stats.auto} label="auto gelernt" accent="auto" />
<StatChip value={stats.manual} label="manuell" />
<StatChip value={stats.cats} label="Kategorien" />
<span className="ml-auto text-[11px] text-muted-foreground/70 flex items-center gap-1.5">
<Sparkles className="h-3 w-3 text-emerald-400" /> lernt automatisch aus Hermes-Gesprächen
</span>
</div>
)}
{/* Eintrag anlegen (einklappbar) */}
{showAdd && (
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Neuen Eintrag anlegen</div>
<button onClick={() => setShowAdd(false)} aria-label="Schließen" className="text-muted-foreground hover:text-foreground"><X className="h-4 w-4" aria-hidden="true" /></button>
</div>
<textarea value={content} onChange={(e) => setContent(e.target.value)} rows={2}
aria-label="Eintragstext"
placeholder="Eine Regel, Vorliebe oder einen stabilen Fakt über dich oder das Projekt…"
className="w-full resize-none rounded-xl border border-border/60 bg-background/30 p-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground leading-relaxed" />
<div className="flex flex-wrap items-center justify-between gap-3">
<select value={category} onChange={(e) => setCategory(e.target.value)}
aria-label="Kategorie"
className="h-8 rounded-lg border border-border/60 bg-background/50 px-2 text-xs outline-none font-semibold text-foreground cursor-pointer">
{CATEGORIES.map((c) => <option key={c} value={c} className="bg-popover text-foreground">{CAT_CONFIG[c]?.label || c}</option>)}
</select>
<button onClick={add} className="h-9 px-4 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center gap-1.5 cursor-pointer">
<Plus className="h-4 w-4" /> Speichern
</button>
</div>
</div>
)}
{error && <div className="rounded-xl border border-red-500/20 bg-red-500/5 p-4 text-xs text-red-400">Fehler: {error}</div>}
{/* Inhalt: Empty State · Graph · Liste */}
{empty ? (
<div className="rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 sm:p-10 flex flex-col items-center text-center gap-5">
<div className="h-14 w-14 rounded-2xl bg-primary/10 flex items-center justify-center">
<MessagesSquare className="h-7 w-7 text-primary" />
</div>
<div className="space-y-1.5 max-w-lg">
<h3 className="text-base font-semibold text-foreground">Lass Hermes dich kennenlernen</h3>
<p className="text-xs text-muted-foreground leading-relaxed">
Statt Fakten einzutippen: führ ein kurzes Onboarding-Gespräch mit Hermes. Was ihr besprecht,
merkt sich das Gedächtnis automatisch du musst nichts manuell pflegen.
{/* Linke Spalte (Titel, Statistiken, Filter, Liste) */}
<div className="w-full lg:max-w-md xl:max-w-xl lg:shrink-0 space-y-5">
{/* Titel & Beschreibung */}
<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">
Gedächtnis-Pool
</h1>
<p className="text-sm text-muted-foreground">
Geteilte Konstitution Hermes, IDEs &amp; Gateway lesen und lernen hier per MCP.
</p>
</div>
{/* Vorgeschlagener Prompt */}
<div className="w-full max-w-lg text-left">
<div className="flex items-center justify-between mb-1.5">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60">Sende das an Hermes</span>
<button onClick={copyPrompt} className="flex items-center gap-1 text-[10px] font-semibold text-muted-foreground hover:text-foreground transition-colors">
{copied ? <><Check className="h-3 w-3 text-emerald-400" /> Kopiert</> : <><Copy className="h-3 w-3" /> Kopieren</>}
{/* Statistiken (Chips) */}
{!empty && (
<div className="flex flex-wrap items-center gap-2">
<StatChip value={stats.total} label="Fakten" />
<StatChip value={stats.auto} label="auto gelernt" accent="auto" />
<StatChip value={stats.manual} label="manuell" />
<StatChip value={stats.cats} label="Kategorien" />
</div>
)}
{/* Mobile-Steuerung (nur sichtbar unter lg) */}
{!empty && (
<div className="flex flex-wrap items-center gap-2 lg:hidden">
<div className="relative flex-1 min-w-[140px]">
<input
value={q}
onChange={(e) => setQ(e.target.value)}
aria-label="Gedächtnis durchsuchen"
type="search"
placeholder="Suchen…"
className="w-full h-9 pl-8 pr-3 rounded-lg border border-border/60 bg-card/45 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
/>
<Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
</div>
<button onClick={() => setShowAdd((s) => !s)}
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" />
</button>
<div className="flex items-center gap-1 p-1 bg-card/30 border border-border/40 rounded-lg">
{([["liste", List], ["graph", Share2]] as const).map(([v, Icon]) => (
<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",
view === v ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>
<Icon className="h-3.5 w-3.5" />
</button>
))}
</div>
<button onClick={cleanup} disabled={deduping}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-card hover:border-primary/50 transition-all cursor-pointer shadow-md disabled:opacity-50">
<Sparkles className="h-4 w-4 text-primary" />
</button>
</div>
<pre className="w-full rounded-xl border border-border/60 bg-background/40 p-3.5 text-[11px] leading-relaxed text-foreground/90 whitespace-pre-wrap font-mono">{ONBOARDING_PROMPT}</pre>
</div>
)}
<div className="flex flex-wrap items-center justify-center gap-2">
<button onClick={goTerminal}
className="h-9 px-4 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center gap-1.5 cursor-pointer">
<TerminalIcon className="h-4 w-4" /> Im Terminal starten
</button>
<button onClick={copyPrompt}
className="h-9 px-4 rounded-lg border border-border/60 bg-card text-xs font-semibold hover:border-primary/50 transition-all flex items-center gap-1.5 cursor-pointer">
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />} Prompt kopieren
</button>
</div>
<p className="text-[11px] text-muted-foreground/70 flex items-center gap-1.5">
<Send className="h-3 w-3" /> Funktioniert genauso über Telegram {" "}
<button onClick={() => setShowAdd(true)} className="underline hover:text-foreground">oder lieber manuell anlegen</button>.
</p>
</div>
) : (
<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 (
<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.
{/* Eintrag anlegen (einklappbar) */}
{showAdd && (
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Neuen Eintrag anlegen</div>
<button onClick={() => setShowAdd(false)} aria-label="Schließen" className="text-muted-foreground hover:text-foreground"><X className="h-4 w-4" aria-hidden="true" /></button>
</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
<textarea value={content} onChange={(e) => setContent(e.target.value)} rows={2}
aria-label="Eintragstext"
placeholder="Eine Regel, Vorliebe oder einen stabilen Fakt über dich oder das Projekt…"
className="w-full resize-none rounded-xl border border-border/60 bg-background/30 p-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground leading-relaxed" />
<div className="flex flex-wrap items-center justify-between gap-3">
<select value={category} onChange={(e) => setCategory(e.target.value)}
aria-label="Kategorie"
className="h-8 rounded-lg border border-border/60 bg-background/50 px-2 text-xs outline-none font-semibold text-foreground cursor-pointer">
{CATEGORIES.map((c) => <option key={c} value={c} className="bg-popover text-foreground">{CAT_CONFIG[c]?.label || c}</option>)}
</select>
<button onClick={add} className="h-9 px-4 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center gap-1.5 cursor-pointer">
<Plus className="h-4 w-4" /> Speichern
</button>
</div>
</div>
)}
{error && <div className="rounded-xl border border-red-500/20 bg-red-500/5 p-4 text-xs text-red-400">Fehler: {error}</div>}
{/* Empty-State oder Listen-Inhalte */}
{empty ? (
<div className="rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 sm:p-10 flex flex-col items-center text-center gap-5">
<div className="h-14 w-14 rounded-2xl bg-primary/10 flex items-center justify-center">
<MessagesSquare className="h-7 w-7 text-primary" />
</div>
<div className="space-y-1.5 max-w-lg">
<h3 className="text-base font-semibold text-foreground">Lass Hermes dich kennenlernen</h3>
<p className="text-xs text-muted-foreground leading-relaxed">
Statt Fakten einzutippen: führ ein kurzes Onboarding-Gespräch mit Hermes. Was ihr besprecht,
merkt sich das Gedächtnis automatisch du musst nichts manuell pflegen.
</p>
</div>
{/* Vorgeschlagener Prompt */}
<div className="w-full max-w-lg text-left">
<div className="flex items-center justify-between mb-1.5">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60">Sende das an Hermes</span>
<button onClick={copyPrompt} className="flex items-center gap-1 text-[10px] font-semibold text-muted-foreground hover:text-foreground transition-colors">
{copied ? <><Check className="h-3 w-3 text-emerald-400" /> Kopiert</> : <><Copy className="h-3 w-3" /> Kopieren</>}
</button>
</div>
<pre className="w-full rounded-xl border border-border/60 bg-background/40 p-3.5 text-[11px] leading-relaxed text-foreground/90 whitespace-pre-wrap font-mono">{ONBOARDING_PROMPT}</pre>
</div>
<div className="flex flex-wrap items-center justify-center gap-2">
<button onClick={goTerminal}
className="h-9 px-4 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center gap-1.5 cursor-pointer">
<TerminalIcon className="h-4 w-4" /> Im Terminal starten
</button>
<button onClick={copyPrompt}
className="h-9 px-4 rounded-lg border border-border/60 bg-card text-xs font-semibold hover:border-primary/50 transition-all flex items-center gap-1.5 cursor-pointer">
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />} Prompt kopieren
</button>
</div>
<p className="text-[11px] text-muted-foreground/70 flex items-center gap-1.5">
<Send className="h-3 w-3" /> Funktioniert genauso über Telegram {" "}
<button onClick={() => setShowAdd(true)} className="underline hover:text-foreground">oder lieber manuell anlegen</button>.
</p>
</div>
) : (
<div className={cn("space-y-5", 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} 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>
<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>
)}
</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>
{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>
)}
</div>
)}
{/* Rechte Spalte (Steuerungszeile & Graph-Bereich) */}
{!empty && (
<div className="flex-1 w-full space-y-5">
{/* Desktop-Steuerungszeile (nur sichtbar ab lg) */}
<div className="hidden lg:flex items-center justify-between gap-3 h-12">
<span className="text-[11px] text-muted-foreground/70 flex items-center gap-1.5">
<Sparkles className="h-3 w-3 text-emerald-400" /> lernt automatisch aus Hermes-Gesprächen
</span>
<div className="flex items-center gap-2">
<div className="relative">
<input
value={q}
onChange={(e) => setQ(e.target.value)}
aria-label="Gedächtnis durchsuchen"
type="search"
placeholder="Semantisch suchen…"
className="w-56 h-9 pl-8 pr-3 rounded-lg border border-border/60 bg-card/45 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
/>
<Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
</div>
<button onClick={() => setShowAdd((s) => !s)}
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>
<button onClick={cleanup} disabled={deduping}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-card hover:border-primary/50 transition-all cursor-pointer shadow-md disabled:opacity-50"
title="Deduplizieren">
<Sparkles className="h-4 w-4 text-primary" />
</button>
</div>
</div>
{/* Graph-Sicht */}
<div className={cn("w-full", 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>
)}
</div>
{dialogElement}
</div>