/** * Left pane of MemoryWorkspace — search box, slim heatmap header, and four * collapsible lens sections (recent / sources / people / topics). * * Selections are NOT mutually exclusive: multiple selected items intersect * the result list filter. Sections may be collapsed independently. */ import { useEffect, useMemo, useState } from 'react'; import { useT } from '../../lib/i18n/I18nContext'; import type { Chunk, EntityRef, Source } from '../../utils/tauriCommands'; import { MemoryHeatmap } from './MemoryHeatmap'; export interface NavigatorSelection { sourceIds: string[]; entityIds: string[]; } interface MemoryNavigatorProps { chunks: Chunk[]; sources: Source[]; topPeople: EntityRef[]; topTopics: EntityRef[]; selection: NavigatorSelection; onSelectionChange: (next: NavigatorSelection) => void; searchQuery: string; onSearchChange: (next: string) => void; } const HOUR_MS = 60 * 60 * 1000; const DAY_MS = 24 * HOUR_MS; function dotClassFor(status: string | undefined): string { switch (status) { case 'admitted': return 'mw-dot dot-admitted'; case 'pending_extraction': return 'mw-dot dot-pending'; case 'buffered': return 'mw-dot dot-buffered'; case 'dropped': return 'mw-dot dot-dropped'; default: return 'mw-dot'; } } interface SectionProps { label: string; defaultOpen?: boolean; countSummary?: string; children: React.ReactNode; } function NavSection({ label, defaultOpen = true, countSummary, children }: SectionProps) { const [open, setOpen] = useState(defaultOpen); return (
{open && children}
); } export function MemoryNavigator({ chunks, sources, topPeople, topTopics, selection, onSelectionChange, searchQuery, onSearchChange, }: MemoryNavigatorProps) { const { t } = useT(); const heatmapTimestamps = useMemo( () => chunks.map(c => Math.floor(c.timestamp_ms / 1000)), [chunks] ); // Wall-clock-derived counts. Computed in an effect to keep render pure // (the `react-hooks/components-and-hooks-must-be-pure` rule rejects a // raw `Date.now()` call inside a `useMemo` body, since two equivalent // renders could produce different values). const [recentCounts, setRecentCounts] = useState<{ today: number; week: number }>({ today: 0, week: 0, }); useEffect(() => { const now = Date.now(); const startOfDay = new Date(now); startOfDay.setHours(0, 0, 0, 0); const startOfDayMs = startOfDay.getTime(); const startOfWeekMs = now - 7 * DAY_MS; let today = 0; let week = 0; for (const c of chunks) { if (c.timestamp_ms >= startOfDayMs) today++; if (c.timestamp_ms >= startOfWeekMs) week++; } setRecentCounts({ today, week }); }, [chunks]); const { today: todayCount, week: weekCount } = recentCounts; const toggleSource = (id: string) => { const has = selection.sourceIds.includes(id); onSelectionChange({ ...selection, sourceIds: has ? selection.sourceIds.filter(s => s !== id) : [...selection.sourceIds, id], }); }; const toggleEntity = (id: string) => { const has = selection.entityIds.includes(id); const next = has ? selection.entityIds.filter(s => s !== id) : [...selection.entityIds, id]; console.debug( '[ui-flow][memory-navigator] toggleEntity id=%s wasActive=%o next=%o', id, has, next ); onSelectionChange({ ...selection, entityIds: next }); }; const renderEntityList = (refs: EntityRef[]) => ( ); return ( ); }