| <!DOCTYPE html> |
| <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; } |
| |
| |
| .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"> |
| |
| |
| <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"> |
| |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
| |
| 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; |
| } |
| } |
| |
| |
| 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'); |
| |
| |
| 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 }); |
| |
| |
| const reader = response.body.getReader(); |
| const decoder = new TextDecoder(); |
| |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| |
| |
| 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') { |
| |
| status.textContent = "⏳ " + data.message; |
| } else if (data.status === 'success') { |
| |
| 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') { |
| |
| 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"; |
| } |
| }); |
| |
| |
| 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 => { |
| |
| 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"; |
| |
| |
| 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> |
| `; |
| |
| |
| 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; |
| |
| |
| const footer = document.createElement('div'); |
| footer.className = "flex flex-wrap gap-2 mt-1"; |
| |
| |
| 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); |
| }); |
| } |
| |
| |
| if (item.labels && item.labels.length > 0) { |
| item.labels.forEach(lbl => { |
| |
| 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> |