Spaces:
Running
Running
Komalpreet Kaur
feat: implement memory consolidation service with sleep cycle, add frontend visualization components, and integrate backend database orchestration.
2e9dd8e unverified | import { useState, useEffect, useCallback, useMemo } from 'react'; | |
| import { apiFetch } from '../api'; | |
| import './MemoryExplorer.css'; | |
| function MemoryExplorer() { | |
| const [search, setSearch] = useState(''); | |
| const [memories, setMemories] = useState([]); | |
| const [graphData, setGraphData] = useState({ nodes: [], edges: [] }); | |
| const [selectedId, setSelectedId] = useState(null); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [isSearching, setIsSearching] = useState(false); | |
| const [isDeleting, setIsDeleting] = useState(false); | |
| const [activeFilter, setActiveFilter] = useState('all'); | |
| const [insightEntity, setInsightEntity] = useState(null); | |
| const fetchData = useCallback(async (query = '') => { | |
| const isSearch = query.trim().length > 0; | |
| if (isSearch) setIsSearching(true); | |
| else setIsLoading(true); | |
| try { | |
| const memoryUrl = isSearch | |
| ? `/api/v1/memory/search?q=${encodeURIComponent(query)}` | |
| : '/api/v1/memory/sensory'; | |
| const [memRes, graphRes] = await Promise.all([ | |
| apiFetch(memoryUrl), | |
| apiFetch('/api/v1/graph') | |
| ]); | |
| if (memRes.ok) { | |
| const data = await memRes.json(); | |
| const results = data.memories || []; | |
| setMemories(results); | |
| if (results.length > 0) { | |
| if (!selectedId || !results.find(m => m.id === selectedId)) { | |
| setSelectedId(results[0].id); | |
| } | |
| } else { | |
| setSelectedId(null); | |
| } | |
| } | |
| if (graphRes.ok) { | |
| const gData = await graphRes.json(); | |
| setGraphData(gData); | |
| } | |
| } catch (error) { | |
| console.error('Neural synchronization failure', error); | |
| } finally { | |
| setIsLoading(false); | |
| setIsSearching(false); | |
| } | |
| }, [selectedId]); | |
| const handlePurge = async (id) => { | |
| if (!window.confirm("Are you sure you want to purge this memory chunk from the neural cortex? This action is irreversible.")) return; | |
| setIsDeleting(true); | |
| try { | |
| const res = await apiFetch(`/api/v1/memory/${id}`, { method: 'DELETE' }); | |
| if (res.ok) { | |
| setMemories(prev => prev.filter(m => m.id !== id)); | |
| setSelectedId(null); | |
| fetchData(search); | |
| } | |
| } catch (error) { | |
| console.error('Purge failed', error); | |
| } finally { | |
| setIsDeleting(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchData(); | |
| }, []); | |
| useEffect(() => { | |
| const timer = setTimeout(() => { | |
| fetchData(search); | |
| }, 500); | |
| return () => clearTimeout(timer); | |
| }, [search]); | |
| const filteredMemories = useMemo(() => { | |
| if (activeFilter === 'all') return memories; | |
| return memories.filter(m => { | |
| const type = m.metadata?.type; | |
| if (activeFilter === 'episodic') return type === 'conversation' || type === 'chat_exchange'; | |
| if (activeFilter === 'semantic') return type === 'concept' || type === 'note'; | |
| if (activeFilter === 'sleep') return type === 'sleep_summary'; | |
| return true; | |
| }); | |
| }, [memories, activeFilter]); | |
| const selectedMemory = useMemo(() => | |
| memories.find(m => m.id === selectedId) || memories[0], | |
| [memories, selectedId] | |
| ); | |
| const linkedEntities = useMemo(() => { | |
| if (!selectedMemory || !graphData.nodes) return []; | |
| const content = selectedMemory.content.toLowerCase(); | |
| return graphData.nodes.filter(node => | |
| content.includes(node.id.toLowerCase()) || | |
| (node.label && content.includes(node.label.toLowerCase())) | |
| ); | |
| }, [selectedMemory, graphData]); | |
| const getTitle = (content) => { | |
| if (!content) return 'Untitled Memory Chunks'; | |
| const lines = content.split('\n'); | |
| const firstLine = lines[0].trim(); | |
| if (firstLine.length > 40) return firstLine.substring(0, 37) + '...'; | |
| return firstLine || 'Untitled Memory Chunks'; | |
| }; | |
| const getMemoryTheme = (type) => { | |
| switch (type) { | |
| case 'conversation': return { color: '#3b82f6', label: 'Episodic' }; | |
| case 'chat_exchange': return { color: '#8b5cf6', label: 'Social' }; | |
| case 'concept': return { color: '#ff6b35', label: 'Semantic' }; | |
| case 'note': return { color: '#10b981', label: 'Explicit' }; | |
| case 'sleep_summary': return { color: '#f59e0b', label: 'Consolidated' }; | |
| default: return { color: '#94a3b8', label: 'Raw Sensory' }; | |
| } | |
| }; | |
| return ( | |
| <div className="memory-view-grid fade-in"> | |
| {/* ── SIDEBAR: Memory Feed ── */} | |
| <section className="memory-surface list-surface"> | |
| <div className="memory-toolbar"> | |
| <div className="memory-search-wrap"> | |
| <span className="material-icons">{isSearching ? 'sync' : 'search'}</span> | |
| <input | |
| type="text" | |
| value={search} | |
| onChange={(e) => setSearch(e.target.value)} | |
| placeholder="Query sensory cortex..." | |
| /> | |
| </div> | |
| <div className="filter-tabs"> | |
| {['all', 'episodic', 'semantic', 'sleep'].map(f => ( | |
| <button | |
| key={f} | |
| className={`filter-tab ${activeFilter === f ? 'active' : ''}`} | |
| onClick={() => setActiveFilter(f)} | |
| > | |
| {f} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="density-stats"> | |
| <span className="material-icons" style={{fontSize: '14px'}}>dns</span> | |
| <span>{filteredMemories.length} Active Nodes</span> | |
| </div> | |
| </div> | |
| <div className="memory-list"> | |
| {isLoading ? ( | |
| <div className="empty-detail"> | |
| <span className="material-icons pulse">settings_input_component</span> | |
| <p style={{fontSize: '0.7rem'}}>Calibrating neural paths...</p> | |
| </div> | |
| ) : ( | |
| filteredMemories.map((memory) => { | |
| const theme = getMemoryTheme(memory.metadata?.type); | |
| return ( | |
| <button | |
| key={memory.id} | |
| className={`memory-row ${memory.id === selectedId ? 'active' : ''}`} | |
| onClick={() => setSelectedId(memory.id)} | |
| > | |
| <div | |
| className="memory-dot" | |
| style={{ backgroundColor: theme.color }} | |
| /> | |
| <div className="memory-row-copy"> | |
| <strong>{getTitle(memory.content)}</strong> | |
| <div className="memory-row-meta"> | |
| <span style={{ color: theme.color }}>{theme.label}</span> | |
| <span>•</span> | |
| <span>{memory.id.substring(0, 8)}</span> | |
| </div> | |
| </div> | |
| </button> | |
| ); | |
| }) | |
| )} | |
| </div> | |
| </section> | |
| {/* ── MAIN: Diagnostic Detail ── */} | |
| <section className="memory-surface detail-surface" style={{position: 'relative'}}> | |
| {selectedMemory ? ( | |
| <div className="fade-in"> | |
| <div className="memory-header-meta"> | |
| <div className="memory-type-pill" style={{ | |
| color: getMemoryTheme(selectedMemory.metadata?.type).color, | |
| background: `${getMemoryTheme(selectedMemory.metadata?.type).color}15` | |
| }}> | |
| Layer: {getMemoryTheme(selectedMemory.metadata?.type).label} | |
| </div> | |
| <div className="memory-id-badge" style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> | |
| <span>UUID: {selectedMemory.id}</span> | |
| <button | |
| className="purge-btn" | |
| onClick={() => handlePurge(selectedMemory.id)} | |
| disabled={isDeleting} | |
| > | |
| <span className="material-icons" style={{fontSize: '14px'}}>{isDeleting ? 'sync' : 'delete_forever'}</span> | |
| Purge | |
| </button> | |
| </div> | |
| </div> | |
| <h1 className="memory-detail-title">{getTitle(selectedMemory.content)}</h1> | |
| <p className="memory-detail-date"> | |
| <span className="material-icons" style={{fontSize: '14px'}}>event</span> | |
| Encoded: {selectedMemory.metadata?.timestamp ? new Date(selectedMemory.metadata.timestamp).toLocaleString() : 'System Boot Sequence'} | |
| </p> | |
| <div className="memory-content-box"> | |
| {selectedMemory.content} | |
| </div> | |
| {/* Instrument Metrics */} | |
| <div className="instrument-grid"> | |
| <div className="instrument-card"> | |
| <h4>Retrieval Salience</h4> | |
| <div className="meter-track"> | |
| <div className="meter-fill" style={{ | |
| width: `${Math.round((selectedMemory.similarity || 0.85) * 100)}%`, | |
| backgroundColor: getMemoryTheme(selectedMemory.metadata?.type).color | |
| }} /> | |
| </div> | |
| <div className="metric-value">{(selectedMemory.similarity || 0.85).toFixed(4)}</div> | |
| </div> | |
| <div className="instrument-card"> | |
| <h4>Graph Associations</h4> | |
| <div className="meter-track"> | |
| <div className="meter-fill" style={{ | |
| width: `${Math.min(linkedEntities.length * 20, 100)}%`, | |
| backgroundColor: '#ff6b35' | |
| }} /> | |
| </div> | |
| <div className="metric-value">{linkedEntities.length} Links</div> | |
| </div> | |
| <div className="instrument-card"> | |
| <h4>Entropy Score</h4> | |
| <div className="meter-track"> | |
| <div className="meter-fill" style={{ | |
| width: '42%', | |
| backgroundColor: '#10b981' | |
| }} /> | |
| </div> | |
| <div className="metric-value">0.4281</div> | |
| </div> | |
| </div> | |
| {/* Knowledge Links */} | |
| <div className="neighborhood-section"> | |
| <div className="section-label"> | |
| <span className="material-icons" style={{fontSize: '16px'}}>account_tree</span> | |
| Knowledge Graph Intersections (Click for Insights) | |
| </div> | |
| <div className="neighborhood-grid"> | |
| {linkedEntities.map(node => ( | |
| <button | |
| key={node.id} | |
| className="neighbor-card" | |
| onClick={() => setInsightEntity(node)} | |
| > | |
| <strong>{node.id}</strong> | |
| <span>Connections: {node.connections || 0}</span> | |
| </button> | |
| ))} | |
| {linkedEntities.length === 0 && ( | |
| <p style={{fontSize: '0.75rem', color: '#999', fontStyle: 'italic'}}>No direct semantic associations detected in current graph state.</p> | |
| )} | |
| </div> | |
| </div> | |
| {/* Raw Metadata */} | |
| <div className="neighborhood-section"> | |
| <div className="section-label"> | |
| <span className="material-icons" style={{fontSize: '16px'}}>terminal</span> | |
| Raw Metadata Explorer | |
| </div> | |
| <div className="metadata-grid"> | |
| {Object.entries(selectedMemory.metadata || {}).map(([key, val]) => ( | |
| <div key={key} className="metadata-tag"> | |
| <span>{key}</span> | |
| <span>{String(val)}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Neural Insight Overlay */} | |
| {insightEntity && ( | |
| <div className="neural-insight-overlay fade-in"> | |
| <div className="neural-insight-modal"> | |
| <div className="insight-header"> | |
| <span className="material-icons">psychology</span> | |
| <h3>Neural Insight: {insightEntity.id}</h3> | |
| <button className="insight-close" onClick={() => setInsightEntity(null)}> | |
| <span className="material-icons">close</span> | |
| </button> | |
| </div> | |
| <div className="insight-body"> | |
| <div className="insight-stat"> | |
| <label>Semantic Weight</label> | |
| <strong>{insightEntity.connections * 12.5} nm</strong> | |
| </div> | |
| <div className="insight-stat"> | |
| <label>Active Connections</label> | |
| <strong>{insightEntity.connections} Nodes</strong> | |
| </div> | |
| <p>This entity is a core semantic node within your current knowledge base. It facilitates retrieval of related episodic contexts and strengthens the reasoning cortex.</p> | |
| </div> | |
| <button className="insight-btn" onClick={() => setInsightEntity(null)}>Close Diagnostic</button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="empty-detail"> | |
| <span className="material-icons pulse">sensors</span> | |
| <h3>No Active Focus</h3> | |
| <p style={{maxWidth: '280px', margin: '0 auto', fontSize: '0.85rem'}}>Select a sensory chunk from the feed to begin deep-layer diagnostic analysis.</p> | |
| </div> | |
| )} | |
| </section> | |
| </div> | |
| ); | |
| } | |
| export default MemoryExplorer; | |