Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Memvid AI Memory Layer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| body { background-color: #0f172a; color: #e2e8f0; } | |
| .glass { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); } | |
| .accent { color: #38bdf8; } | |
| /* Ensures the raw snippet text wraps and preserves newlines */ | |
| .snippet-text { | |
| white-space: pre-wrap; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen p-6 font-sans"> | |
| <div class="max-w-4xl mx-auto space-y-8"> | |
| <!-- Header --> | |
| <div class="text-center space-y-2"> | |
| <h1 class="text-4xl font-bold tracking-tight text-white">Memvid <span class="accent">Live Demo</span></h1> | |
| <p class="text-slate-400">Single-file memory layer for AI agents.</p> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <!-- Left: Add Memory --> | |
| <div class="glass p-6 rounded-xl shadow-lg"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center gap-2"> | |
| <span>📥</span> Append Memory | |
| </h2> | |
| <form id="addForm" class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-300 mb-1">Content</label> | |
| <textarea id="content" name="content" rows="4" class="w-full bg-slate-800 border border-slate-700 rounded-lg p-3 text-sm focus:ring-2 focus:ring-sky-500 outline-none" placeholder="Paste meeting notes, facts, or context here..."></textarea> | |
| </div> | |
| <button type="submit" class="w-full bg-sky-600 hover:bg-sky-500 text-white font-medium py-2 rounded-lg transition"> | |
| Commit to Memory | |
| </button> | |
| </form> | |
| <div id="addStatus" class="mt-4 text-sm text-center hidden"></div> | |
| </div> | |
| <!-- Right: Retrieval --> | |
| <div class="glass p-6 rounded-xl shadow-lg flex flex-col"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center gap-2"> | |
| <span>🔍</span> Neural Retrieval | |
| </h2> | |
| <form id="searchForm" class="space-y-4 mb-4"> | |
| <div class="flex gap-2"> | |
| <input type="text" id="query" name="query" class="flex-1 bg-slate-800 border border-slate-700 rounded-lg p-3 text-sm focus:ring-2 focus:ring-sky-500 outline-none" placeholder="Search your memory..."> | |
| <button type="submit" class="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-lg transition">Search</button> | |
| </div> | |
| </form> | |
| <!-- Results Area --> | |
| <div id="resultsArea" class="flex-1 overflow-y-auto space-y-3 max-h-[400px] pr-2"> | |
| <p class="text-slate-500 text-sm text-center italic mt-10">Waiting for query...</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <div class="text-center text-xs text-slate-500 pt-8 border-t border-slate-800"> | |
| Video Memory File: <a href="https://huggingface.co/datasets/broadfield-dev/memvid-storage" class="text-sky-400 hover:underline">https://huggingface.co/datasets/.../knowledge.mv2</a> | |
| </div> | |
| <div class="text-center text-xs text-slate-500 pt-8 border-t border-slate-800"> | |
| Powered by <a href="https://github.com/memvid/memvid" class="text-sky-400 hover:underline">Memvid</a> & Flask | |
| </div> | |
| </div> | |
| <script> | |
| // Helper to format date nicely | |
| function formatDate(dateString) { | |
| if (!dateString) return ''; | |
| try { | |
| const date = new Date(dateString); | |
| return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); | |
| } catch (e) { | |
| return dateString; | |
| } | |
| } | |
| // Handle Add Memory (Streaming Version) | |
| document.getElementById('addForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const formData = new FormData(e.target); | |
| const status = document.getElementById('addStatus'); | |
| const btn = e.target.querySelector('button'); | |
| // UI Reset | |
| btn.disabled = true; | |
| status.classList.remove('hidden'); | |
| status.textContent = "Starting stream..."; | |
| status.className = "mt-4 text-sm text-center text-indigo-400 animate-pulse"; | |
| try { | |
| const response = await fetch('/add', { method: 'POST', body: formData }); | |
| // Reader to handle the stream | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| // Decode chunk and split by newline (NDJSON) | |
| const chunk = decoder.decode(value, { stream: true }); | |
| const lines = chunk.split('\n').filter(line => line.trim() !== ''); | |
| for (const line of lines) { | |
| try { | |
| const data = JSON.parse(line); | |
| if (data.status === 'processing') { | |
| // Show progress | |
| status.textContent = "⏳ " + data.message; | |
| } else if (data.status === 'success') { | |
| // Success state | |
| status.textContent = "✅ " + data.message; | |
| status.className = "mt-4 text-sm text-center text-green-400"; | |
| e.target.reset(); | |
| setTimeout(() => status.classList.add('hidden'), 4000); | |
| } else if (data.status === 'error') { | |
| // Error state | |
| throw new Error(data.message); | |
| } | |
| } catch (err) { | |
| console.error("Stream parse error:", err); | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| status.textContent = "❌ " + err.message; | |
| status.className = "mt-4 text-sm text-center text-red-400"; | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = "Commit to Memory"; | |
| } | |
| }); | |
| // Handle Search | |
| document.getElementById('searchForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const formData = new FormData(e.target); | |
| const resultsArea = document.getElementById('resultsArea'); | |
| resultsArea.innerHTML = '<p class="text-slate-500 text-center animate-pulse">Scanning Memory Timeline...</p>'; | |
| try { | |
| const res = await fetch('/search', { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| resultsArea.innerHTML = ''; | |
| if (data.success && data.results.length > 0) { | |
| data.results.forEach(item => { | |
| // Create Card | |
| const div = document.createElement('div'); | |
| div.className = "bg-slate-800/50 p-4 rounded-lg border border-slate-700 hover:border-indigo-500/50 transition flex flex-col gap-3"; | |
| // 1. Header: Title, Date, Score | |
| const header = document.createElement('div'); | |
| header.className = "flex justify-between items-start"; | |
| header.innerHTML = ` | |
| <div> | |
| <h3 class="font-semibold text-slate-200 text-sm">${item.title}</h3> | |
| <span class="text-xs text-slate-500">${formatDate(item.date)}</span> | |
| </div> | |
| <span class="bg-indigo-900/50 text-indigo-300 text-xs px-2 py-1 rounded font-mono border border-indigo-500/20"> | |
| ${item.score} | |
| </span> | |
| `; | |
| // 2. Body: Clean Text | |
| const body = document.createElement('div'); | |
| body.className = "text-sm text-slate-300 snippet-text pl-2 border-l-2 border-slate-600"; | |
| body.textContent = item.text; | |
| // 3. Footer: Tags & Labels | |
| const footer = document.createElement('div'); | |
| footer.className = "flex flex-wrap gap-2 mt-1"; | |
| // Render Tags (Green) | |
| if (item.tags && item.tags.length > 0) { | |
| item.tags.forEach(tag => { | |
| const span = document.createElement('span'); | |
| span.className = "px-2 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider bg-emerald-900/40 text-emerald-400 border border-emerald-700/50"; | |
| span.textContent = tag; | |
| footer.appendChild(span); | |
| }); | |
| } | |
| // Render Labels (Blue) | |
| if (item.labels && item.labels.length > 0) { | |
| item.labels.forEach(lbl => { | |
| // Filter out generic 'text' label to reduce noise | |
| if(lbl === 'text') return; | |
| const span = document.createElement('span'); | |
| span.className = "px-2 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider bg-sky-900/40 text-sky-400 border border-sky-700/50"; | |
| span.textContent = lbl; | |
| footer.appendChild(span); | |
| }); | |
| } | |
| div.appendChild(header); | |
| div.appendChild(body); | |
| if (footer.hasChildNodes()) div.appendChild(footer); | |
| resultsArea.appendChild(div); | |
| }); | |
| } else if (data.success) { | |
| resultsArea.innerHTML = '<p class="text-slate-500 text-sm text-center">No high-confidence memories found.</p>'; | |
| } else { | |
| resultsArea.innerHTML = `<p class="text-red-400 text-sm text-center">Error: ${data.error}</p>`; | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| resultsArea.innerHTML = `<p class="text-red-400 text-sm text-center">Connection Error</p>`; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |