SPB Git
5.5 KB · 78 lines tsx
1"use client";2import { useMemo, useState } from "react";3import useSWR from "swr";4import { Brain, Plus, Search, Download, ShieldCheck } from "lucide-react";5import { fetcher } from "@/lib/client/fetcher";6import type { MemoryDTO } from "@/lib/client/types";7import { Button, Empty, Spinner, cn } from "@/components/ui";8import { MemoryCard } from "@/components/memory/memory-card";9import { AddMemoryDialog } from "@/components/memory/add-memory-dialog";10import { CATEGORIES, MemoryToggles, categoryLabel } from "@/components/memory/memory-shared";1112interface Resp { memories: MemoryDTO[]; stats: { status: string; n: number }[] }1314export default function MemoryPage() {15  const [q, setQ] = useState(""); const [cat, setCat] = useState<string | null>(null); const [add, setAdd] = useState(false);16  const { data, isLoading, mutate } = useSWR<Resp>(`/api/memories${q ? `?q=${encodeURIComponent(q)}` : ""}`, fetcher, { keepPreviousData: true });17  const stat = (s: string) => data?.stats.find(x => x.status === s)?.n ?? 0;18  const candidates = useMemo(() => (data?.memories ?? []).filter(m => m.status === "candidate"), [data]);19  const grouped = useMemo(() => {20    const list = (data?.memories ?? []).filter(m => m.status !== "candidate" && (!cat || m.category === cat));21    const g = new Map<string, MemoryDTO[]>();22    for (const c of CATEGORIES) g.set(c.id, []);23    for (const m of list) { if (!g.has(m.category)) g.set(m.category, []); g.get(m.category)!.push(m); }24    return [...g.entries()].filter(([, v]) => v.length);25  }, [data, cat]);26  const refresh = () => void mutate();2728  return (29    <div className="min-h-0 flex-1 overflow-y-auto">30      <div className="mx-auto w-full max-w-4xl px-4 py-6 md:px-8">31        <div className="mb-5 flex flex-wrap items-start justify-between gap-3">32          <div>33            <h1 className="flex items-center gap-2 text-xl font-semibold tracking-tight"><Brain className="h-5 w-5 text-accent-2" /> Mémoire</h1>34            <p className="mt-1 text-[13px] text-muted">Faits durables réutilisés dans tes conversations. Tu vois tout, tu contrôles tout : rien n'est stocké en silence.</p>35          </div>36          <div className="flex gap-2">37            <a href="/api/memories/export?format=md" className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-border px-3 text-sm text-fg-2 hover:bg-surface-2 hover:text-fg"><Download className="h-4 w-4" /> Markdown</a>38            <a href="/api/memories/export?format=json" className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-border px-3 text-sm text-fg-2 hover:bg-surface-2 hover:text-fg"><Download className="h-4 w-4" /> JSON</a>39            <Button variant="primary" onClick={() => setAdd(true)}><Plus className="h-4 w-4" /> Ajouter</Button>40          </div>41        </div>4243        <div className="mb-4 grid grid-cols-3 gap-2">44          {[{ l: "Actifs", v: stat("active"), tone: "text-ok" }, { l: "Candidats", v: stat("candidate"), tone: "text-warn" }, { l: "Désactivés", v: stat("disabled"), tone: "text-muted" }].map(t => (45            <div key={t.l} className="rounded-xl border border-border bg-surface px-4 py-3"><div className={cn("text-2xl font-semibold tabular-nums", t.tone)}>{t.v}</div><div className="text-[12px] text-muted">{t.l}</div></div>46          ))}47        </div>48        <div className="mb-6"><MemoryToggles /></div>4950        {candidates.length > 0 && (51          <section className="mb-6">52            <h2 className="mb-2 flex items-center gap-2 text-[13px] font-semibold uppercase tracking-wider text-warn"><ShieldCheck className="h-4 w-4" /> Candidats à approuver · {candidates.length}</h2>53            <div className="space-y-2">{candidates.map(m => <MemoryCard key={m.id} m={m} candidate onChanged={refresh} />)}</div>54          </section>55        )}5657        <div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center">58          <div className="relative flex-1"><Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-muted" /><input value={q} onChange={e => setQ(e.target.value)} placeholder="Rechercher dans la mémoire…" className="input !pl-8" /></div>59          {isLoading && <Spinner />}60        </div>61        <div className="mb-4 flex flex-wrap gap-1.5">62          <button onClick={() => setCat(null)} className={cn("rounded-lg border px-2.5 py-1 text-[12px]", !cat ? "border-accent bg-accent/10 text-accent" : "border-border text-fg-2 hover:bg-surface-2")}>Toutes</button>63          {CATEGORIES.map(c => <button key={c.id} onClick={() => setCat(cat === c.id ? null : c.id)} className={cn("rounded-lg border px-2.5 py-1 text-[12px]", cat === c.id ? "border-accent bg-accent/10 text-accent" : "border-border text-fg-2 hover:bg-surface-2")}>{c.label}</button>)}64        </div>6566        {data && !grouped.length && <Empty icon={<Brain className="h-8 w-8" />} title={q ? "Aucun souvenir ne correspond" : "Aucun souvenir pour l'instant"} hint="Les souvenirs apparaissent au fil des conversations (extraction automatique) ou quand tu en ajoutes un." action={<Button onClick={() => setAdd(true)}><Plus className="h-4 w-4" /> Ajouter un souvenir</Button>} />}67        {grouped.map(([c, items]) => (68          <section key={c} className="mb-6">69            <h2 className="mb-2 text-[12px] font-semibold uppercase tracking-wider text-muted">{categoryLabel(c)} · {items.length}</h2>70            <div className="space-y-2">{items.map(m => <MemoryCard key={m.id} m={m} onChanged={refresh} />)}</div>71          </section>72        ))}73      </div>74      <AddMemoryDialog open={add} onClose={() => setAdd(false)} onAdded={refresh} />75    </div>76  );77}78
Files · laptop-build-20261101
Menu

Theme