Wartung: MemoryView-Layout: Header-Elemente direkt in die Spalten integriert (kein versetzter Freiraum mehr)
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+56
-56
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CNiM5G2e.js"></script>
|
<script type="module" crossorigin src="/assets/index-Cp5Tizb9.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DnWvM6fs.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DMTEFdKw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+229
-193
@@ -146,214 +146,250 @@ export function MemoryView() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-6">
|
||||||
{/* Kopf / Werkzeugleiste */}
|
{/* Zweispaltiges Haupt-Layout ab Y=0 */}
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
|
<div className="flex flex-col lg:flex-row gap-6 items-start">
|
||||||
<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">
|
{/* Linke Spalte (Titel, Statistiken, Filter, Liste) */}
|
||||||
Gedächtnis-Pool
|
<div className="w-full lg:max-w-md xl:max-w-xl lg:shrink-0 space-y-5">
|
||||||
</h1>
|
{/* Titel & Beschreibung */}
|
||||||
<p className="text-sm text-muted-foreground">
|
<div>
|
||||||
Geteilte Konstitution — Hermes, IDEs & Gateway lesen und lernen hier per MCP.
|
<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">
|
||||||
</p>
|
Gedächtnis-Pool
|
||||||
</div>
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
Geteilte Konstitution — Hermes, IDEs & Gateway lesen und lernen hier per MCP.
|
||||||
<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.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vorgeschlagener Prompt */}
|
{/* Statistiken (Chips) */}
|
||||||
<div className="w-full max-w-lg text-left">
|
{!empty && (
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60">Sende das an Hermes</span>
|
<StatChip value={stats.total} label="Fakten" />
|
||||||
<button onClick={copyPrompt} className="flex items-center gap-1 text-[10px] font-semibold text-muted-foreground hover:text-foreground transition-colors">
|
<StatChip value={stats.auto} label="auto gelernt" accent="auto" />
|
||||||
{copied ? <><Check className="h-3 w-3 text-emerald-400" /> Kopiert</> : <><Copy className="h-3 w-3" /> Kopieren</>}
|
<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>
|
</button>
|
||||||
</div>
|
</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">
|
{/* Eintrag anlegen (einklappbar) */}
|
||||||
<button onClick={goTerminal}
|
{showAdd && (
|
||||||
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">
|
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 space-y-3">
|
||||||
<TerminalIcon className="h-4 w-4" /> Im Terminal starten
|
<div className="flex items-center justify-between">
|
||||||
</button>
|
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Neuen Eintrag anlegen</div>
|
||||||
<button onClick={copyPrompt}
|
<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>
|
||||||
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.
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<textarea value={content} onChange={(e) => setContent(e.target.value)} rows={2}
|
||||||
<div className="space-y-5 max-h-[calc(100vh-14rem)] overflow-y-auto pr-1 scrollbar-thin">
|
aria-label="Eintragstext"
|
||||||
{[...CATEGORIES, "__other"].map((c) => {
|
placeholder="Eine Regel, Vorliebe oder einen stabilen Fakt über dich oder das Projekt…"
|
||||||
const list = c === "__other" ? otherItems : (grouped[c] || [])
|
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" />
|
||||||
if (!list.length) return null
|
<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 conf = CAT_CONFIG[c] || DEFAULT_CAT
|
||||||
const Icon = conf.icon
|
const Icon = conf.icon
|
||||||
return (
|
return (
|
||||||
<div key={c} className="space-y-2">
|
<button key={c} onClick={() => setFilter(c)}
|
||||||
{/* Sektions-Kopf je Kategorie */}
|
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",
|
||||||
<div className="flex items-center gap-2 mb-2 px-1">
|
filter === c ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>
|
||||||
<Icon className={cn("h-3.5 w-3.5", conf.text)} />
|
<Icon className="h-3 w-3" /> {conf.label}
|
||||||
<span className={cn("text-[11px] font-bold uppercase tracking-wider", conf.text)}>{conf.label}</span>
|
</button>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Graph-Sicht */}
|
{items.length === 0 ? (
|
||||||
<div className={cn("flex-1", view === "liste" && "hidden lg:block")}>
|
<div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center bg-card/10">
|
||||||
<GraphErrorBoundary>
|
Keine Einträge für die aktuellen Filterkriterien gefunden.
|
||||||
<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>}>
|
</div>
|
||||||
<GraphView data={graphData} onDelete={del} />
|
) : (
|
||||||
</Suspense>
|
<div className="space-y-5 max-h-[calc(100vh-14rem)] overflow-y-auto pr-1 scrollbar-thin">
|
||||||
</GraphErrorBoundary>
|
{[...CATEGORIES, "__other"].map((c) => {
|
||||||
</div>
|
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>
|
</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}
|
{dialogElement}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user