From 2c921a911c8381bcd49348d1868daa97c7a7a4e1 Mon Sep 17 00:00:00 2001 From: jules Date: Fri, 10 Jul 2026 10:10:18 +1000 Subject: [PATCH] =?UTF-8?q?feat(kb-ui):=20MemoryReviewQueue=20=E2=80=94=20?= =?UTF-8?q?free-text=20note=20curation=20(spec=20=C2=A79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The curation base for a per-person gated `memory` collection of FREE-TEXT notes (kind: note), distinct from ClaimsReviewQueue's subject·predicate·value triples. Groups proposed (specialist-noted, awaiting confirmation) vs remembered (active), with confirm/forget callbacks, source attribution, and tags — matching the KB UI house style. Collection-agnostic (props in, callbacks out): the same base powers the KB `memory` collection and a consumer's /memory page. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components-memory.tsx | 243 ++++++++++++++++++++++++++++++++++++++ src/index.tsx | 1 + 2 files changed, 244 insertions(+) create mode 100644 src/components-memory.tsx diff --git a/src/components-memory.tsx b/src/components-memory.tsx new file mode 100644 index 0000000..98e2549 --- /dev/null +++ b/src/components-memory.tsx @@ -0,0 +1,243 @@ +// PURPOSE: Memory review queue — the curation surface for a per-person, gated +// `memory` collection of FREE-TEXT notes (kind: note), distinct from the +// claim triples of ClaimsReviewQueue. The owner reviews what the agent +// and its delegated specialists remembered: confirms a specialist's +// `proposed` note (trust-is-human) or forgets anything wrong. Collection- +// agnostic — props in, callbacks out; the same base powers the KB +// `memory` collection and a consumer's /memory page. +// =========================================================================== +import { type FC, type ReactNode } from "react"; +import { BrainCircuit, Check, Sparkles, Trash2, User } from "lucide-react"; +import { Badge, cn, formatRelative } from "./_internal"; + +export type MemoryStatus = "active" | "proposed"; + +export interface MemoryItem { + id: string; + /** The remembered statement (free text). */ + content: string; + /** Short head shown above the content; optional. */ + subject?: string | null; + /** fact | preference | event | note — a free label, surfaced as a tag. */ + kind?: string | null; + tags?: string[]; + status: MemoryStatus; + /** "user" | "assistant" | "app:" — attribution, rendered via `sourceLabel`. */ + source?: string | null; + updated_at?: string | null; +} + +export interface MemoryActions { + /** Confirm a proposed memory (→ active). */ + onConfirm?: (id: string) => void; + /** Forget a memory (archive / soft-delete). */ + onForget?: (id: string) => void; +} + +/** Default attribution copy; override via `MemoryReviewQueueProps.sourceLabel`. */ +export function defaultSourceLabel(source?: string | null): string { + if (source === "user") return "You added this"; + if (source === "assistant") return "Your assistant noted this"; + if (source && source.startsWith("app:")) { + const slug = source.slice(4); + return `Suggested by ${slug.charAt(0).toUpperCase()}${slug.slice(1)}`; + } + return "Remembered"; +} + +export interface MemoryCardProps extends MemoryActions { + item: MemoryItem; + busy?: boolean; + sourceLabel?: (source?: string | null) => string; + className?: string; +} + +export const MemoryCard: FC = ({ + item, + busy, + sourceLabel = defaultSourceLabel, + className, + onConfirm, + onForget, +}) => { + const proposed = item.status === "proposed"; + return ( +
+
+
+ {item.subject && ( +

{item.subject}

+ )} +

{item.content}

+
+ {proposed && ( + }> + Proposed + + )} +
+ +
+ + {sourceLabel(item.source)} + + {item.kind && item.kind !== "note" && ( + {item.kind} + )} + {(item.tags ?? []).map((t) => ( + + {t} + + ))} + {item.updated_at && ( + {formatRelative(item.updated_at)} + )} +
+ +
+ {proposed && onConfirm && ( + onConfirm(item.id)} + busy={busy} + icon={} + > + Confirm + + )} + {onForget && ( + onForget(item.id)} + busy={busy} + icon={} + > + Forget + + )} +
+
+ ); +}; + +export interface MemoryReviewQueueProps extends MemoryActions { + items: MemoryItem[]; + busyId?: string; + sourceLabel?: (source?: string | null) => string; + emptyState?: ReactNode; + className?: string; +} + +/** Groups: Proposed (awaiting confirmation) → Remembered (active). */ +export const MemoryReviewQueue: FC = ({ + items, + busyId, + sourceLabel, + emptyState, + className, + onConfirm, + onForget, +}) => { + if (items.length === 0) return <>{emptyState ?? }; + + const proposed = items.filter((m) => m.status === "proposed"); + const active = items.filter((m) => m.status !== "proposed"); + + return ( +
+
+ {proposed.map((m) => ( + + ))} +
+ +
+ {active.map((m) => ( + + ))} +
+
+ ); +}; + +// ---- local helpers -------------------------------------------------------- + +function sourceIcon(source?: string | null): ReactNode { + if (source === "user") return ; + if (source && source.startsWith("app:")) return ; + return ; +} + +const Section: FC<{ title: string; count: number; hint: string; children: ReactNode }> = ({ + title, + count, + hint, + children, +}) => { + if (count === 0) return null; + return ( +
+
+

{title}

+ ({count}) + — {hint} +
+
{children}
+
+ ); +}; + +const Empty: FC = () => ( +
+ +

Nothing remembered yet

+

+ Things your assistant learns about you will show up here. +

+
+); + +const ActionBtn: FC<{ + action: string; + onClick: () => void; + icon: ReactNode; + children: ReactNode; + primary?: boolean; + busy?: boolean; +}> = ({ action, onClick, icon, children, primary, busy }) => ( + +); diff --git a/src/index.tsx b/src/index.tsx index f56b011..887f17f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -34,5 +34,6 @@ export { export * from "./components-collections"; export * from "./components-object"; export * from "./components-claims"; +export * from "./components-memory"; export * from "./components-export"; export * from "./components-search";