import { useEffect, useMemo, useState } from 'react'; import { ArrowLeft, BrainCircuit, CalendarDays, CloudMoon } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { api } from '@/api/client'; import type { MemoryEntry, DailyMemoryEntry, DreamEntry } from '@/api/types'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { useSidebar } from '@/hooks/useSidebar'; import { cn } from '@/lib/utils'; // ─── Independent section data fetcher (same pattern as Analytics.tsx) ──────── function useFetch(fetcher: () => Promise): { data: T | null; loading: boolean; error: string | null; retry: () => void; } { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); function load() { setLoading(true); setError(null); fetcher() .then(setData) .catch((err: unknown) => { setError(err instanceof Error ? err.message : 'failed to load data'); }) .finally(() => setLoading(false)); } useEffect(() => { load(); }, []); // eslint-disable-line react-hooks/exhaustive-deps return { data, loading, error, retry: load }; } // ─── Skeleton pulse placeholder ───────────────────────────────────────────── function SkeletonBar({ className }: { className?: string }) { return
; } // ─── Formatters ───────────────────────────────────────────────────────────── function formatDate(iso: string | null | undefined): string { if (!iso) return '—'; return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } function formatDateShort(iso: string | null | undefined): string { if (!iso) return '—'; return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', }); } function truncate(str: string, max: number): string { if (str.length <= max) return str; return str.slice(0, max) + '…'; } function relTime(iso: string | null | undefined): string { if (!iso) return '—'; const diff = Date.now() - new Date(iso).getTime(); const seconds = Math.floor(diff / 1000); if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 30) return `${days}d ago`; return formatDate(iso); } // ─── Empty state ──────────────────────────────────────────────────────────── function EmptyState({ message }: { message: string }) { return

{message}

; } // ─── Tab bar (same pattern as Results.tsx) ────────────────────────────────── type TabId = 'all' | 'daily' | 'dreams'; function TabBar({ active, onChange }: { active: TabId; onChange: (t: TabId) => void }) { return (
{[ { id: 'all' as TabId, label: 'All Memory', icon: BrainCircuit }, { id: 'daily' as TabId, label: 'Daily Log', icon: CalendarDays }, { id: 'dreams' as TabId, label: 'Dreams', icon: CloudMoon }, ].map((tab) => ( ))}
); } // ─── All Memory Tab ───────────────────────────────────────────────────────── function AllMemoryTab({ projectId }: { projectId: string }) { const { data, loading, error, retry } = useFetch(() => api.memory.list(projectId).then((r) => r.entries)); const [expanded, setExpanded] = useState(null); if (loading) { return (
{[0, 1, 2].map((i) => (
))}
); } if (error) { return (
{error}
); } if (!data || data.length === 0) { return ; } return (
{data.map((entry: MemoryEntry) => ( {expanded === entry.id && (
{entry.content}
)}
))}
); } // ─── Daily Log Tab ────────────────────────────────────────────────────────── function DailyLogTab({ projectId }: { projectId: string }) { const { data, loading, error, retry } = useFetch(() => api.memory.daily(projectId).then((r) => r.entries)); const [expanded, setExpanded] = useState(null); const grouped = useMemo(() => { if (!data) return []; const groups: Record = {}; for (const entry of data) { const g = groups[entry.date]; if (g) { g.push(entry); } else { groups[entry.date] = [entry]; } } return Object.entries(groups).sort((a, b) => b[0].localeCompare(a[0])); }, [data]); if (loading) { return (
{[0, 1].map((day) => (
{[0, 1].map((e) => ( ))}
))}
); } if (error) { return (
{error}
); } if (!data || data.length === 0) { return ; } return (
{grouped.map(([date, entries]) => (

{formatDateShort(date)}

{entries.map((entry: DailyMemoryEntry) => ( {expanded === entry.id && (
{entry.content}
)}
))}
))}
); } // ─── Dreams Tab ───────────────────────────────────────────────────────────── function DreamsTab({ projectId }: { projectId: string }) { const { data, loading, error, retry } = useFetch(() => api.memory.dreams(projectId).then((r) => r.entries)); if (loading) { return (
{[0, 1, 2].map((i) => ( ))}
); } if (error) { return (
{error}
); } if (!data || data.length === 0) { return ; } return (
{data.map((entry: DreamEntry, i: number) => (

{formatDateShort(entry.date)}

              {entry.content}
            
))}
); } // ─── Main Page ────────────────────────────────────────────────────────────── export function Memory() { const navigate = useNavigate(); const { data: sidebar, activeSession } = useSidebar(); const [tab, setTab] = useState('all'); const [projectId, setProjectId] = useState(null); // Derive default project from active session or first project. const projects = useMemo(() => { return sidebar?.projects?.map((p: { id: string; name: string }) => p) ?? []; }, [sidebar]); useEffect(() => { if (!projectId && projects.length > 0) { // Prefer active session's project, else first project. const defaultId = activeSession?.project_id ?? projects[0]!.id; setProjectId(defaultId); } }, [projects, activeSession, projectId]); function handleBack() { if (window.history.length > 1) { navigate(-1); } else { navigate('/'); } } return (
{/* Header */}

Memory Browser

Topic-based memories, daily logs, and dream consolidation diaries.

{/* Tab bar */} {/* Tab content */} {!projectId ? ( ) : tab === 'all' ? ( ) : tab === 'daily' ? ( ) : ( )}
); }