memvid / templates /index.html
broadfield-dev's picture
Update templates/index.html
5a64e8d verified
<!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; }
/* 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>