diff --git a/src/components/ops/memory-panel.tsx b/src/components/ops/memory-panel.tsx index 70a259c..4b19d6b 100644 --- a/src/components/ops/memory-panel.tsx +++ b/src/components/ops/memory-panel.tsx @@ -1,13 +1,14 @@ "use client"; import * as React from "react"; -import { Loader2, Plus, Trash2 } from "lucide-react"; +import { ChevronLeft, ChevronRight, Loader2, Plus, Search, Trash2, X } from "lucide-react"; import { api } from "@/lib/api"; import { useAsync } from "@/hooks/use-async"; import { relativeTime } from "@/lib/utils"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Select } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { ConfirmModal } from "@/components/ui/confirm-modal"; @@ -15,6 +16,11 @@ import { toast } from "sonner"; import { MEMORY_CATEGORIES } from "@/lib/types"; import { IconButton, PanelFrame, RefreshButton, SectionTitle } from "./shared"; +/** Rows per page. The route caps a page at 500; 50 keeps one screen scannable. */ +const PAGE_SIZE = 50; + +/** Content longer than this gets a "Show more" toggle instead of a silent clamp. */ +const CLAMP_CHARS = 180; /** Keys the server generates when the caller supplied none. */ function isGeneratedKey(key: string): boolean { @@ -27,32 +33,66 @@ function previewOf(content: string): string { return flat.length > 48 ? `${flat.slice(0, 48)}…` : flat; } +function isClampable(content: string): boolean { + return content.length > CLAMP_CHARS || content.split("\n").length > 3; +} + export function MemoryPanel() { - const { data, loading, error, refresh } = useAsync(() => api.memory(100), []); + const [search, setSearch] = React.useState(""); + const [query, setQuery] = React.useState(""); + const [filter, setFilter] = React.useState(""); + const [offset, setOffset] = React.useState(0); + const [content, setContent] = React.useState(""); + const [name, setName] = React.useState(""); const [category, setCategory] = React.useState("core"); const [busy, setBusy] = React.useState(false); const [working, setWorking] = React.useState(null); + const [expanded, setExpanded] = React.useState>(new Set()); const [pendingForget, setPendingForget] = React.useState<{ key: string; content: string } | null>( null, ); - const add = async () => { + // Typing shouldn't fire a request per keystroke. + React.useEffect(() => { + const t = setTimeout(() => setQuery(search), 250); + return () => clearTimeout(t); + }, [search]); + + // A narrower result set makes the current page number meaningless. + React.useEffect(() => { + setOffset(0); + }, [query, filter]); + + const { data, loading, error, refresh } = useAsync( + () => api.memory(PAGE_SIZE, offset, { q: query, category: filter }), + [offset, query, filter], + ); + + const total = data?.total ?? 0; + const first = total === 0 ? 0 : offset + 1; + const last = offset + (data?.count ?? 0); + const narrowed = !!query.trim() || !!filter; + + const remember = async () => { if (!content.trim()) return; setBusy(true); try { - const stored = await api.addMemory({ content: content.trim(), category }); - // The server generates a key when none is given; showing it is what makes - // the entry addressable afterwards. + const stored = await api.addMemory({ + content: content.trim(), + category, + ...(name.trim() ? { key: name.trim() } : {}), + }); toast.success( stored.notes?.length - ? `Stored as ${stored.key} — ${stored.notes.join("; ")}` - : `Stored as ${stored.key}`, + ? `Remembered as ${stored.key} — ${stored.notes.join("; ")}` + : `Remembered as ${stored.key}`, ); setContent(""); + setName(""); refresh(); } catch (e) { - toast.error(`Store failed: ${e instanceof Error ? e.message : e}`); + toast.error(`Could not remember that: ${e instanceof Error ? e.message : e}`); } finally { setBusy(false); } @@ -69,38 +109,45 @@ export function MemoryPanel() { } }; - const del = async () => { + const forget = async () => { const key = pendingForget?.key; if (!key) return; setWorking(key); try { await api.deleteMemory(key); - toast.success("Fact forgotten"); + toast.success("Forgotten"); setPendingForget(null); refresh(); } catch (e) { - toast.error(`Delete failed: ${e instanceof Error ? e.message : e}`); + toast.error(`Could not forget that: ${e instanceof Error ? e.message : e}`); } finally { setWorking(null); } }; + const toggleExpanded = (key: string) => + setExpanded((prev) => { + const next = new Set(prev); + if (!next.delete(key)) next.add(key); + return next; + }); + return (
}> - Memory entries + Memories {data && ( {" · "} - {data.count} - {data.total > data.count ? ` of ${data.total}` : ""} + {total === 0 ? "none" : `${first}–${last} of ${total}`} + {narrowed ? " matching" : ""} )}
- Store a fact + Remember something