| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"/> |
| <meta content="width=device-width, initial-scale=1.0" name="viewport"/> |
| <title>RasoSpeak — Memory</title> |
| <link rel="icon" type="image/png" href="logo.png"/> |
| <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/> |
| <style> |
| * { font-family: 'Inter', system-ui, sans-serif; } |
| body { background: #FAFAFA; } |
| .nav-link { @apply px-4 py-2 rounded-lg text-sm font-medium transition-colors; } |
| .nav-link:hover { @apply bg-gray-100; } |
| .nav-link.active { @apply bg-emerald-50 text-emerald-700; } |
| </style> |
| </head> |
| <body class="min-h-screen"> |
|
|
| |
| <a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-emerald-500 focus:text-white focus:rounded-lg focus:font-medium">Skip to main content</a> |
|
|
| |
| <nav class="bg-white border-b border-gray-200 sticky top-0 z-50" aria-label="Main navigation"> |
| <div class="max-w-5xl mx-auto px-6"> |
| <div class="flex items-center justify-between h-16"> |
| <a href="index.html" class="flex items-center gap-3"> |
| <img src="logo.png" alt="RasoSpeak" class="w-8 h-8"> |
| <span class="font-bold text-lg">RasoSpeak</span> |
| </a> |
| <div class="flex items-center gap-1"> |
| <a href="index.html" class="nav-link" aria-label="Home">Home</a> |
| <a href="chat.html" class="nav-link" aria-label="Chat">Chat</a> |
| <a href="memory.html" class="nav-link active" aria-label="Memory">Memory</a> |
| <a href="coach.html" class="nav-link" aria-label="Coach">Coach</a> |
| <a href="docs.html" class="nav-link" aria-label="Docs">Docs</a> |
| <a href="settings.html" class="nav-link" aria-label="Settings">Settings</a> |
| </div> |
| </div> |
| </div> |
| </nav> |
|
|
| |
| <main id="main-content" class="max-w-3xl mx-auto px-6 py-8"> |
|
|
| |
| <div class="bg-white rounded-lg border border-gray-200 p-6 mb-6"> |
| <h2 class="text-lg font-semibold mb-2">Query Your Memory</h2> |
| <p class="text-sm text-gray-500 mb-4">Search through all your conversations, documents, and knowledge</p> |
| <div class="flex gap-3"> |
| <input type="text" id="memory-search" placeholder="Try: What did I say about AI?" |
| class="flex-1 px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500" |
| aria-label="Memory search input" |
| onkeydown="if(event.key==='Enter')searchMemory()"> |
| <button onclick="searchMemory()" class="px-6 py-3 bg-emerald-500 text-white rounded-lg font-medium hover:bg-emerald-600 transition-colors" aria-label="Search memory"> |
| Search |
| </button> |
| </div> |
| <div class="mt-3 flex flex-wrap gap-2"> |
| <span class="text-xs text-gray-400">Try:</span> |
| <button onclick="quickSearch('What did I say about AI?')" class="text-xs text-emerald-600 hover:underline">AI questions</button> |
| <button onclick="quickSearch('My personal facts')" class="text-xs text-emerald-600 hover:underline">personal facts</button> |
| <button onclick="quickSearch('Recent conversations')" class="text-xs text-emerald-600 hover:underline">recent</button> |
| </div> |
| </div> |
|
|
| |
| <div id="memory-results" class="bg-white rounded-lg border border-gray-200 p-6 mb-6 min-h-48"> |
| <div id="results-content" class="text-center text-gray-400 py-8"> |
| <p>Your memories will appear here</p> |
| <p class="text-sm mt-1">Ask Raso questions or import documents to build your memory</p> |
| </div> |
| </div> |
|
|
| |
| <div class="grid grid-cols-4 gap-4 mb-6"> |
| <div class="bg-white rounded-lg border border-gray-200 p-4 text-center"> |
| <div id="mem-conversations" class="text-2xl font-bold text-emerald-600">0</div> |
| <div class="text-xs text-gray-500 mt-1">Conversations</div> |
| </div> |
| <div class="bg-white rounded-lg border border-gray-200 p-4 text-center"> |
| <div id="mem-facts" class="text-2xl font-bold text-purple-600">0</div> |
| <div class="text-xs text-gray-500 mt-1">Facts Stored</div> |
| </div> |
| <div class="bg-white rounded-lg border border-gray-200 p-4 text-center"> |
| <div id="mem-documents" class="text-2xl font-bold text-blue-600">0</div> |
| <div class="text-xs text-gray-500 mt-1">Documents</div> |
| </div> |
| <div class="bg-white rounded-lg border border-gray-200 p-4 text-center"> |
| <div id="mem-sessions" class="text-2xl font-bold text-orange-600">0</div> |
| <div class="text-xs text-gray-500 mt-1">Sessions</div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-lg border border-gray-200 p-6 mb-6"> |
| <h3 class="font-semibold mb-4">Quick Add to Memory</h3> |
| <textarea id="quick-fact-input" placeholder="Type a fact or note to remember... (e.g., 'My name is John and I work at Company X')" |
| class="w-full h-24 px-4 py-3 border border-gray-200 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-emerald-500 mb-3"></textarea> |
| <div class="flex gap-3"> |
| <select id="fact-category" class="px-3 py-2 border border-gray-200 rounded-lg text-sm"> |
| <option value="general">General</option> |
| <option value="work">Work</option> |
| <option value="personal">Personal</option> |
| <option value="learning">Learning</option> |
| <option value="health">Health</option> |
| <option value="finance">Finance</option> |
| </select> |
| <button onclick="addFactToMemory()" class="flex-1 py-2 bg-emerald-500 text-white rounded-lg font-medium hover:bg-emerald-600 transition-colors" aria-label="Save fact to memory"> |
| Save to Memory |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-lg border border-gray-200 p-6"> |
| <div class="flex items-center justify-between mb-4"> |
| <h3 class="font-semibold">Recent Facts</h3> |
| <button onclick="loadRecentFacts()" class="text-sm text-emerald-600 hover:underline" aria-label="Refresh recent facts">Refresh</button> |
| </div> |
| <div id="recent-facts" class="space-y-2"> |
| <p class="text-gray-400 text-center py-4">No facts saved yet</p> |
| </div> |
| </div> |
|
|
| </main> |
|
|
| |
| <div id="toast" class="fixed bottom-6 left-1/2 -translate-x-1/2 bg-gray-900 text-white px-6 py-3 rounded-full text-sm font-medium shadow-xl transition-all duration-300 opacity-0 pointer-events-none translate-y-4 z-50" role="status" aria-live="polite" aria-atomic="true"></div> |
|
|
| |
| <script src="state.js"></script> |
| <script src="nlp.js"></script> |
| <script src="speech.js"></script> |
| <script src="ui.js"></script> |
| <script src="app.js"></script> |
|
|
| <script> |
| |
| function showToast(msg) { |
| const t = document.getElementById('toast'); |
| t.textContent = msg; |
| t.classList.remove('opacity-0', 'translate-y-4', 'pointer-events-none'); |
| setTimeout(() => t.classList.add('opacity-0', 'translate-y-4', 'pointer-events-none'), 3000); |
| } |
| |
| |
| async function loadMemoryStats() { |
| try { |
| |
| const brainResp = await fetch('/brain/stats'); |
| if (brainResp.ok) { |
| const brainData = await brainResp.json(); |
| document.getElementById('mem-conversations').textContent = brainData.type_counts?.conversation || brainData.conversations || 0; |
| document.getElementById('mem-facts').textContent = brainData.type_counts?.semantic || brainData.semantic || 0; |
| document.getElementById('mem-documents').textContent = brainData.type_counts?.document || brainData.documents || 0; |
| document.getElementById('mem-sessions').textContent = brainData.type_counts?.episodic || brainData.episodes || 0; |
| return; |
| } |
| |
| |
| try { |
| document.getElementById('mem-conversations').textContent = localStorage.getItem('rs_chat_count') || 0; |
| document.getElementById('mem-facts').textContent = localStorage.getItem('rs_facts_count') || 0; |
| document.getElementById('mem-documents').textContent = 0; |
| document.getElementById('mem-sessions').textContent = localStorage.getItem('rs_total_sessions') || 0; |
| } catch (e) { |
| console.warn('Stats fallback failed:', e); |
| } |
| |
| } catch (err) { |
| console.error('Failed to load memory stats:', err); |
| |
| document.getElementById('mem-conversations').textContent = localStorage.getItem('rs_conversations') || 0; |
| document.getElementById('mem-facts').textContent = localStorage.getItem('rs_facts_count') || 0; |
| } |
| } |
| |
| |
| async function searchMemory() { |
| const query = document.getElementById('memory-search').value.trim(); |
| const resultsContent = document.getElementById('results-content'); |
| |
| if (!query) { |
| showToast('Please enter a search query'); |
| return; |
| } |
| |
| resultsContent.innerHTML = '<div class="text-center text-gray-400 py-8"><p>Searching...</p></div>'; |
| |
| try { |
| |
| const brainResp = await fetch('/brain/recall', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| query: query, |
| limit: 10, |
| time_range: 'all' |
| }) |
| }); |
| |
| if (brainResp.ok) { |
| const brainData = await brainResp.json(); |
| const memories = brainData.results || []; |
| |
| if (memories.length > 0) { |
| return displayBrainResults(memories, query); |
| } |
| } |
| |
| |
| return queryRasoForMemory(query); |
| } catch (err) { |
| console.error('Memory search failed:', err); |
| return queryRasoForMemory(query); |
| } |
| } |
| |
| |
| function displayBrainResults(memories, query) { |
| const resultsContent = document.getElementById('results-content'); |
| |
| resultsContent.innerHTML = ` |
| <div class="space-y-3"> |
| <p class="text-sm text-gray-500 mb-4">Found ${memories.length} results in your Second Brain for "${escapeHtml(query)}"</p> |
| ${memories.map(mem => { |
| const node = mem.node || mem; |
| const content = node.content; |
| const contentStr = typeof content === 'object' ? JSON.stringify(content) : content; |
| return ` |
| <div class="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors"> |
| <p class="text-gray-800">${escapeHtml(String(contentStr).substring(0, 500))}</p> |
| <div class="flex flex-wrap gap-2 mt-2"> |
| ${node.type ? `<span class="px-2 py-0.5 bg-emerald-100 rounded text-xs text-emerald-700">${node.type}</span>` : ''} |
| ${node.tier ? `<span class="px-2 py-0.5 bg-purple-100 rounded text-xs text-purple-700">${node.tier}</span>` : ''} |
| ${mem.score ? `<span class="px-2 py-0.5 bg-blue-100 rounded text-xs text-blue-600">Score: ${(mem.score * 100).toFixed(0)}%</span>` : ''} |
| </div> |
| ${node.created_at ? `<p class="text-xs text-gray-400 mt-2">Created: ${new Date(node.created_at).toLocaleString()}</p>` : ''} |
| </div> |
| `}).join('')} |
| <div class="mt-4 p-4 bg-emerald-50 rounded-lg"> |
| <p class="text-sm text-emerald-700"> |
| <strong>Powered by Second Brain Memory</strong> — Graph-based, with entity extraction, temporal indexing, and active forgetting. |
| </p> |
| </div> |
| </div> |
| `; |
| } |
| |
| |
| async function queryRasoForMemory(query) { |
| const resultsContent = document.getElementById('results-content'); |
| resultsContent.innerHTML = '<div class="text-center text-gray-400 py-8"><p>Searching memory...</p></div>'; |
| |
| try { |
| const resp = await fetch('/raso/query', { |
| method: 'GET', |
| headers: { 'Content-Type': 'application/json' } |
| }); |
| |
| |
| resultsContent.innerHTML = ` |
| <div class="space-y-3"> |
| <div class="bg-emerald-50 rounded-lg p-4"> |
| <p class="font-medium text-emerald-700">Your Query:</p> |
| <p class="text-gray-700 mt-1">${escapeHtml(query)}</p> |
| </div> |
| <p class="text-gray-500 text-center py-4">No matching memories found. Try importing more documents or adding facts.</p> |
| </div> |
| `; |
| } catch (err) { |
| resultsContent.innerHTML = ` |
| <div class="bg-gray-50 rounded-lg p-4"> |
| <p class="font-medium">Query:</p> |
| <p class="text-gray-700 mt-1">${escapeHtml(query)}</p> |
| </div> |
| <p class="text-gray-400 text-center py-4">Memory search unavailable</p> |
| `; |
| } |
| } |
| |
| |
| function displayResults(memories, query) { |
| const resultsContent = document.getElementById('results-content'); |
| |
| if (!memories || memories.length === 0) { |
| resultsContent.innerHTML = `<p class="text-gray-400 text-center py-4">No results for "${escapeHtml(query)}"</p>`; |
| return; |
| } |
| |
| resultsContent.innerHTML = ` |
| <div class="space-y-3"> |
| <p class="text-sm text-gray-500 mb-4">Found ${memories.length} results for "${escapeHtml(query)}"</p> |
| ${memories.map((mem, i) => { |
| const content = mem.content || mem.value || mem.text; |
| const display = content |
| ? escapeHtml(String(content).substring(0, 300)) |
| : `<span class="text-gray-400 italic">Node ID: ${escapeHtml(mem.node_id || mem.id || 'unknown')}</span>`; |
| return ` |
| <div class="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors"> |
| <p class="text-gray-800">${display}</p> |
| ${mem.memory_type || mem.category ? `<span class="inline-block mt-2 px-2 py-0.5 bg-gray-200 rounded text-xs text-gray-600">${mem.memory_type || mem.category}</span>` : ''} |
| ${mem.relevance ? `<span class="inline-block mt-2 ml-2 px-2 py-0.5 bg-blue-100 rounded text-xs text-blue-600">${(mem.relevance * 100).toFixed(0)}%</span>` : ''} |
| </div>`; |
| }).join('')} |
| </div> |
| `; |
| } |
| |
| |
| function quickSearch(query) { |
| document.getElementById('memory-search').value = query; |
| searchMemory(); |
| } |
| |
| |
| async function addFactToMemory() { |
| const input = document.getElementById('quick-fact-input'); |
| const catSelect = document.getElementById('fact-category'); |
| const fact = input.value.trim(); |
| const category = catSelect.value; |
| |
| if (!fact) { |
| showToast('Please enter a fact to remember'); |
| return; |
| } |
| |
| try { |
| |
| const resp = await fetch('/brain/memory', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| content: fact, |
| memory_type: 'semantic', |
| tier: 'long_term', |
| importance: 3, |
| tags: [category], |
| }) |
| }); |
| |
| if (resp.ok) { |
| showToast('Fact saved to Second Brain!'); |
| } else { |
| throw new Error(`Server returned ${resp.status}`); |
| } |
| |
| input.value = ''; |
| loadMemoryStats(); |
| loadRecentFacts(); |
| } catch (err) { |
| |
| console.warn('Second Brain unavailable, saving locally:', err.message); |
| const facts = JSON.parse(localStorage.getItem('rs_facts') || '[]'); |
| facts.unshift({ fact, category, timestamp: new Date().toISOString() }); |
| localStorage.setItem('rs_facts', JSON.stringify(facts.slice(0, 100))); |
| localStorage.setItem('rs_facts_count', (parseInt(localStorage.getItem('rs_facts_count') || '0') + 1).toString()); |
| showToast('Fact saved (local mode)'); |
| input.value = ''; |
| loadMemoryStats(); |
| loadRecentFacts(); |
| } |
| } |
| |
| |
| async function loadRecentFacts() { |
| const container = document.getElementById('recent-facts'); |
| |
| try { |
| |
| const brainResp = await fetch('/brain/recall', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ memory_type: 'semantic', limit: 20, time_range: 'all' }) |
| }); |
| |
| if (brainResp.ok) { |
| const brainData = await brainResp.json(); |
| const facts = (brainData.results || []).map(r => r.content || r.value || r); |
| if (facts.length > 0) { |
| container.innerHTML = facts.slice(0, 20).map((content, i) => { |
| const text = typeof content === 'object' ? (content.fact || content.content || JSON.stringify(content)) : String(content); |
| return `<div class="flex items-start justify-between p-3 bg-gray-50 rounded-lg"> |
| <p class="text-gray-800">${escapeHtml(text.substring(0, 200))}</p> |
| </div>`; |
| }).join(''); |
| return; |
| } |
| } |
| } catch (err) { |
| console.warn('Second Brain recall failed:', err.message); |
| } |
| |
| |
| try { |
| const facts = JSON.parse(localStorage.getItem('rs_facts') || '[]'); |
| if (facts.length === 0) { |
| container.innerHTML = '<p class="text-gray-400 text-center py-4">No facts saved yet</p>'; |
| return; |
| } |
| container.innerHTML = facts.slice(0, 20).map(f => ` |
| <div class="flex items-start justify-between p-3 bg-gray-50 rounded-lg"> |
| <p class="text-gray-800">${escapeHtml(String(f.fact || '').substring(0, 200))}</p> |
| </div>`).join(''); |
| } catch (err) { |
| console.error('localStorage parse failed:', err); |
| showToast('Failed to load facts'); |
| container.innerHTML = '<p class="text-red-400 text-center py-4">Error loading facts</p>'; |
| } |
| } |
| |
| |
| async function deleteFact(nodeId) { |
| if (!nodeId) return; |
| try { |
| const resp = await fetch(`/brain/memory/${encodeURIComponent(nodeId)}`, { method: 'DELETE' }); |
| if (resp.ok) { |
| showToast('Fact deleted'); |
| } else { |
| throw new Error(`Server returned ${resp.status}`); |
| } |
| loadRecentFacts(); |
| loadMemoryStats(); |
| } catch (err) { |
| console.error('Delete failed:', err); |
| showToast('Could not delete fact'); |
| } |
| } |
| |
| |
| function escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
| |
| |
| window.addEventListener('load', () => { |
| loadMemoryStats(); |
| loadRecentFacts(); |
| }); |
| </script> |
|
|
| </body> |
| </html> |