import React, { useState, useEffect, useCallback } from 'react'; import { getEpisodes, deleteEpisode } from '../api/orchestration'; import ReactMarkdown from 'react-markdown'; const PAGE_SIZE = 20; export default function MemoryView({ onNavigate }) { const [episodes, setEpisodes] = useState([]); const [total, setTotal] = useState(0); const [offset, setOffset] = useState(0); const [search, setSearch] = useState(''); const [query, setQuery] = useState(''); // committed search term const [expanded, setExpanded] = useState(null); // episode id const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const load = useCallback(async () => { setLoading(true); setError(null); try { const data = await getEpisodes({ limit: PAGE_SIZE, offset, q: query || undefined }); setEpisodes(data.episodes); setTotal(data.total); } catch (err) { setError(err.message); } finally { setLoading(false); } }, [offset, query]); useEffect(() => { load(); }, [load]); function handleSearch(e) { e.preventDefault(); setOffset(0); // reset to page 1 on new search setQuery(search); } async function handleDelete(id) { if (!confirm('Delete this memory? This cannot be undone.')) return; await deleteEpisode(id); load(); } const totalPages = Math.ceil(total / PAGE_SIZE); const currentPage = Math.floor(offset / PAGE_SIZE) + 1; return (
{/* Header */}
Memory Viewer {total} episode{total !== 1 ? 's' : ''}
{/* Search bar */}
setSearch(e.target.value)} placeholder="Search memories…" style={{ flex: 1, padding: '8px 12px', background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', color: 'var(--text-primary)', }} /> {query && ( )}
{/* Episode list */}
{loading &&

Loading…

} {error &&

{error}

} {!loading && episodes.length === 0 && (

No memories found.

)} {episodes.map(ep => ( setExpanded(expanded === ep.id ? null : ep.id)} onDelete={() => handleDelete(ep.id)} /> ))}
{/* Pagination */} {totalPages > 1 && (
{currentPage} / {totalPages}
)}
); } function stripMarkdown(text) { return text .replace(/\*\*(.*?)\*\*/g, '$1') // bold .replace(/\*(.*?)\*/g, '$1') // italic .replace(/`([^`]+)`/g, '$1') // inline code .replace(/^#{1,6}\s+/gm, '') // headings .replace(/^\s*[-*+]\s+/gm, '') // list markers .trim(); } function EpisodeCard({ episode, expanded, onToggle, onDelete }) { const date = new Date(episode.created_at * 1000).toLocaleString(); const preview = stripMarkdown(episode.user_message).slice(0, 80) + (episode.user_message.length > 80 ? '…' : ''); return (
{/* Card header — always visible */}
{preview} {date} #{episode.id} {expanded ? '▲' : '▼'}
{/* Expanded content */} {expanded && (
{episode.token_count > 0 && (

Tokens: {episode.token_count}

)}
)}
); } function MessageBlock({ label, content, color }) { const isAI = label === 'NexusAI'; return (

{label}

{children}

, ul: ({children}) => , ol: ({children}) =>
    {children}
, li: ({children}) =>
  • {children}
  • , code: ({inline, children}) => inline ? {children} :
    {children}
    , strong: ({children}) => {children}, }} >{content}
    ); }