RasoSpeak / memory.html
Sourabh-Kumar04
Fix all code review findings — remove dual-write, use public APIs
594ed02
<!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">
<!-- Skip to main content -->
<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>
<!-- Navigation -->
<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 Content -->
<main id="main-content" class="max-w-3xl mx-auto px-6 py-8">
<!-- Memory Search -->
<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>
<!-- Memory Results -->
<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>
<!-- Memory Stats -->
<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>
<!-- Add to Memory -->
<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>
<!-- Recent Facts -->
<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>
<!-- Toast -->
<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>
<!-- Scripts -->
<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>
// Toast notification
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);
}
// Load memory stats (both systems)
async function loadMemoryStats() {
try {
// Get Second Brain stats (primary)
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;
}
// localStorage fallback for stats only
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);
// Show demo values
document.getElementById('mem-conversations').textContent = localStorage.getItem('rs_conversations') || 0;
document.getElementById('mem-facts').textContent = localStorage.getItem('rs_facts_count') || 0;
}
}
// Search memory (Second Brain primary)
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 {
// Try Second Brain first (primary)
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);
}
}
// No results in Second Brain — use Raso as last resort
return queryRasoForMemory(query);
} catch (err) {
console.error('Memory search failed:', err);
return queryRasoForMemory(query);
}
}
// Display Second Brain results
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>
`;
}
// Query Raso for memory
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' }
});
// If that fails, just show the query in a message
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>
`;
}
}
// Display search results
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>
`;
}
// Quick search
function quickSearch(query) {
document.getElementById('memory-search').value = query;
searchMemory();
}
// Add fact to memory (Second Brain only — no legacy dual-write)
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 {
// Save to Second Brain via /brain/memory
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) {
// Save to localStorage as offline fallback
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();
}
}
// Load recent facts (Second Brain → localStorage fallback)
async function loadRecentFacts() {
const container = document.getElementById('recent-facts');
try {
// Second Brain primary
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);
}
// localStorage fallback only (no deprecated /memory/* endpoint)
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>';
}
}
// Delete fact from Second Brain
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');
}
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Initialize
window.addEventListener('load', () => {
loadMemoryStats();
loadRecentFacts();
});
</script>
</body>
</html>