Download static/flow.html from 0xarchit/UrbanLens: direct link, hf CLI and curl.
- Browser
- Download file 42.5 kB
-
https://huggingface.co/spaces/0xarchit/UrbanLens/resolve/main/static/flow.html
- Command line
-
hf download hf://spaces/0xarchit/UrbanLens/static/flow.html
-
curl -L -o flow.html https://huggingface.co/spaces/0xarchit/UrbanLens/resolve/main/static/flow.html
42.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Agent Mission Control | UrbanLens</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Fira+Sans:wght@400;500;600;700&display=swap" | |
| rel="stylesheet"> | |
| <!-- Lucide Icons --> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['Fira Sans', 'sans-serif'], | |
| mono: ['Fira Code', 'monospace'], | |
| }, | |
| colors: { | |
| urban: { | |
| bg: '#0F172A', | |
| card: '#1E293B', | |
| primary: '#3B82F6', | |
| success: '#10B981', | |
| warning: '#F59E0B', | |
| error: '#EF4444', | |
| text: '#F1F5F9', | |
| muted: '#64748B', | |
| } | |
| }, | |
| animation: { | |
| 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| body { | |
| background-color: #0F172A; | |
| color: #F1F5F9; | |
| } | |
| .glass-panel { | |
| background: rgba(30, 41, 59, 0.7); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(148, 163, 184, 0.1); | |
| } | |
| .step-connector { | |
| position: absolute; | |
| left: 24px; | |
| top: 40px; | |
| bottom: -24px; | |
| width: 2px; | |
| background: #334155; | |
| z-index: 0; | |
| } | |
| .step-connector.active { | |
| background: #3B82F6; | |
| box-shadow: 0 0 8px rgba(59, 130, 246, 0.5); | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #334155; | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #475569; | |
| } | |
| /* Modal Transition */ | |
| .modal-enter { | |
| opacity: 0; | |
| transform: scale(0.95); | |
| } | |
| .modal-enter-active { | |
| opacity: 1; | |
| transform: scale(1); | |
| transition: opacity 200ms, transform 200ms; | |
| } | |
| .modal-exit { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| .modal-exit-active { | |
| opacity: 0; | |
| transform: scale(0.95); | |
| transition: opacity 200ms, transform 200ms; | |
| } | |
| </style> | |
| </head> | |
| <body class="h-screen flex flex-col overflow-hidden selection:bg-blue-500/30"> | |
| <!-- Header --> | |
| <header | |
| class="h-16 border-b border-slate-800 flex items-center justify-between px-6 bg-slate-900/50 backdrop-blur-md z-50"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center shadow-lg shadow-blue-500/20"> | |
| <i data-lucide="cpu" class="w-5 h-5 text-white"></i> | |
| </div> | |
| <div> | |
| <h1 class="font-bold text-lg leading-tight tracking-tight">UrbanLens <span | |
| class="text-blue-500">Core</span></h1> | |
| <p class="text-[10px] text-slate-400 font-mono tracking-wider uppercase">Agent Orchestration Node</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <div class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-slate-800 border border-slate-700"> | |
| <div id="connection-status" class="w-2 h-2 rounded-full bg-slate-500"></div> | |
| <span id="status-text" class="text-xs font-mono text-slate-400">DISCONNECTED</span> | |
| </div> | |
| </div> | |
| </header> | |
| <main class="flex-1 flex overflow-hidden"> | |
| <!-- Left Sidebar: Controls & History --> | |
| <aside class="w-80 border-r border-slate-800 flex flex-col bg-slate-900/30"> | |
| <div class="p-6 border-b border-slate-800"> | |
| <h2 class="text-xs font-bold text-slate-500 uppercase tracking-widest mb-4">Input Parameters</h2> | |
| <form id="issue-form" onsubmit="handleSubmit(event)" class="space-y-4"> | |
| <!-- Drag & Drop Upload --> | |
| <div class="relative group cursor-pointer" onclick="document.getElementById('file').click()"> | |
| <input type="file" id="file" name="images" class="hidden" accept="image/*" | |
| onchange="handleFileSelect(this)"> | |
| <div id="drop-zone" | |
| class="h-32 rounded-xl border-2 border-dashed border-slate-700 bg-slate-800/50 hover:bg-slate-800 hover:border-blue-500/50 transition-all flex flex-col items-center justify-center gap-2 p-4 text-center"> | |
| <i data-lucide="upload-cloud" | |
| class="w-6 h-6 text-slate-500 group-hover:text-blue-400 transition-colors"></i> | |
| <span id="file-label" class="text-xs text-slate-400 font-medium">Upload Evidence</span> | |
| </div> | |
| <!-- Preview Overlay --> | |
| <img id="upload-preview" | |
| class="absolute inset-0 w-full h-full object-cover rounded-xl hidden opacity-50 hover:opacity-100 transition-opacity" /> | |
| </div> | |
| <div class="space-y-1"> | |
| <label class="text-[10px] uppercase font-bold text-slate-500">Description</label> | |
| <textarea name="description" id="desc-input" rows="2" | |
| class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all placeholder:text-slate-600" | |
| placeholder="Describe the issue..."></textarea> | |
| </div> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <div class="space-y-1"> | |
| <label class="text-[10px] uppercase font-bold text-slate-500">Lat</label> | |
| <input type="number" step="any" name="latitude" id="lat-input" value="28.6304" | |
| class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono focus:border-blue-500 outline-none"> | |
| </div> | |
| <div class="space-y-1"> | |
| <label class="text-[10px] uppercase font-bold text-slate-500">Lng</label> | |
| <input type="number" step="any" name="longitude" id="lng-input" value="77.2177" | |
| class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono focus:border-blue-500 outline-none"> | |
| </div> | |
| </div> | |
| <!-- Hidden fields --> | |
| <input type="hidden" name="platform" value="web-console"> | |
| <input type="hidden" name="device_model" value="AdminStation"> | |
| <button type="submit" id="submit-btn" | |
| class="w-full bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 rounded-xl shadow-lg shadow-blue-600/20 text-sm transition-all flex items-center justify-center gap-2"> | |
| <i data-lucide="play" class="w-4 h-4 text-white/50"></i> EXECUTE PIPELINE | |
| </button> | |
| <button type="button" id="reset-btn" onclick="resetUI()" | |
| class="hidden w-full bg-slate-700 hover:bg-slate-600 text-slate-300 font-bold py-3 rounded-xl text-sm transition-all"> | |
| RESET CONSOLE | |
| </button> | |
| <div id="replay-banner" | |
| class="hidden text-center p-2 bg-amber-500/10 border border-amber-500/30 rounded-lg text-amber-500 text-xs font-bold uppercase animate-pulse"> | |
| Historial Replay Mode | |
| </div> | |
| </form> | |
| </div> | |
| <div class="flex-1 overflow-y-auto p-4 flex flex-col"> | |
| <div class="flex justify-between items-center mb-3"> | |
| <h2 class="text-[10px] font-bold text-slate-600 uppercase tracking-widest">Recent Runs</h2> | |
| <button onclick="clearHistory()" | |
| class="text-[10px] text-red-400 hover:text-red-300 uppercase font-bold tracking-wider hover:underline" | |
| id="clear-history-btn">Clear All</button> | |
| </div> | |
| <div id="history-list" class="space-y-2 flex-1"> | |
| <!-- Populated by JS --> | |
| <div class="text-center py-8 text-slate-600 text-xs italic">No execution history</div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Center: Pipeline Visualization --> | |
| <section class="flex-1 flex flex-col bg-[#0B1120] relative"> | |
| <div | |
| class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 pointer-events-none"> | |
| </div> | |
| <div class="flex-1 p-8 overflow-y-auto" id="pipeline-stage"> | |
| <div id="empty-stage" class="h-full flex flex-col items-center justify-center text-slate-600 space-y-4"> | |
| <div | |
| class="w-24 h-24 rounded-full bg-slate-800/50 border border-slate-700 flex items-center justify-center animate-pulse-slow"> | |
| <i data-lucide="network" class="w-10 h-10 text-slate-500 opacity-50"></i> | |
| </div> | |
| <p class="text-sm font-medium">System Idle. Awaiting Task Injection.</p> | |
| </div> | |
| <div id="active-pipeline" class="hidden max-w-3xl mx-auto space-y-0 relative pb-20"> | |
| <!-- Steps Injected Here --> | |
| </div> | |
| </div> | |
| <!-- Bottom: Log Stream --> | |
| <div class="h-48 border-t border-slate-800 bg-slate-900/80 backdrop-blur flex flex-col"> | |
| <div class="px-4 py-2 border-b border-slate-800 flex justify-between items-center bg-slate-900"> | |
| <div class="flex items-center gap-2"> | |
| <i data-lucide="terminal" class="w-3 h-3 text-emerald-500"></i> | |
| <span class="text-xs font-bold text-slate-400 uppercase tracking-wide">System Event | |
| Stream</span> | |
| </div> | |
| <span id="log-count" class="text-[10px] text-slate-600 font-mono">0 events</span> | |
| </div> | |
| <div id="console-logs" class="flex-1 overflow-y-auto p-4 font-mono text-xs text-slate-400 space-y-1"> | |
| <!-- Logs --> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Right: Inspector Panel --> | |
| <aside | |
| class="w-96 border-l border-slate-800 bg-slate-900/50 backdrop-blur-sm flex flex-col transition-transform duration-300 transform translate-x-0" | |
| id="inspector-panel"> | |
| <div class="p-6 border-b border-slate-800 flex justify-between items-center"> | |
| <h2 class="text-sm font-bold text-slate-200 flex items-center gap-2"> | |
| <i data-lucide="scan-eye" class="w-4 h-4 text-blue-500"></i> | |
| Vision Inspector | |
| </h2> | |
| <span | |
| class="text-[10px] bg-blue-500/10 text-blue-400 px-2 py-1 rounded border border-blue-500/20 font-mono">LIVE</span> | |
| </div> | |
| <div class="p-6 space-y-6 flex-1 overflow-y-auto"> | |
| <!-- Vision Comparison --> | |
| <div class="space-y-3"> | |
| <div class="flex justify-between text-[10px] font-bold text-slate-500 uppercase"> | |
| <span>Original Input</span> | |
| <span>AI Analysis</span> | |
| </div> | |
| <div class="grid grid-cols-2 gap-2 h-32"> | |
| <div class="bg-slate-800 rounded-lg overflow-hidden border border-slate-700 relative group cursor-zoom-in" | |
| onclick="openZoomModal(document.getElementById('inspector-original').src, 'Original Evidence')"> | |
| <img id="inspector-original" class="w-full h-full object-cover opacity-50" /> | |
| <div class="absolute inset-0 flex items-center justify-center text-[10px] text-slate-500" | |
| id="inspector-original-placeholder">NO DATA</div> | |
| <div | |
| class="absolute inset-0 bg-black/50 hidden group-hover:flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <i data-lucide="maximize-2" class="w-4 h-4 text-white"></i> | |
| </div> | |
| </div> | |
| <div class="bg-slate-800 rounded-lg overflow-hidden border border-slate-700 relative group cursor-zoom-in" | |
| onclick="openZoomModal(document.getElementById('inspector-annotated').src, 'AI Vision Analysis')"> | |
| <img id="inspector-annotated" class="w-full h-full object-cover" /> | |
| <div class="absolute inset-0 flex items-center justify-center text-[10px] text-slate-500" | |
| id="inspector-annotated-placeholder">PENDING</div> | |
| <!-- Overlay Badge --> | |
| <div id="vision-badge" | |
| class="hidden absolute bottom-2 right-2 bg-black/70 text-emerald-400 text-[8px] font-bold px-1.5 py-0.5 rounded border border-emerald-500/30"> | |
| DETECTED | |
| </div> | |
| <div | |
| class="absolute inset-0 bg-black/50 hidden group-hover:flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <i data-lucide="maximize-2" class="w-4 h-4 text-white"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step Details --> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider">Selected Step Data</h3> | |
| <div class="bg-slate-950 rounded-lg border border-slate-800 p-3 overflow-hidden"> | |
| <pre id="json-viewer" | |
| class="text-[10px] text-emerald-400 font-mono whitespace-pre-wrap break-all">Select a pipeline step to inspect output...</pre> | |
| </div> | |
| </div> | |
| <!-- Global State --> | |
| <div class="space-y-2"> | |
| <h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider">Metrics</h3> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <div class="bg-slate-800 p-2 rounded border border-slate-700"> | |
| <div class="text-[10px] text-slate-500">Latency</div> | |
| <div class="text-sm font-mono font-bold text-slate-200" id="metric-latency">-- ms</div> | |
| </div> | |
| <div class="bg-slate-800 p-2 rounded border border-slate-700"> | |
| <div class="text-[10px] text-slate-500">Confidence</div> | |
| <div class="text-sm font-mono font-bold text-slate-200" id="metric-confidence">--%</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| </main> | |
| <!-- Zoom Modal --> | |
| <div id="zoom-modal" | |
| class="fixed inset-0 z-[100] bg-black/90 hidden flex items-center justify-center opacity-0 transition-opacity duration-300" | |
| onclick="closeZoomModal()"> | |
| <div class="relative max-w-7xl max-h-[90vh] p-4 group" onclick="event.stopPropagation()"> | |
| <button onclick="closeZoomModal()" | |
| class="absolute -top-10 right-0 text-white hover:text-blue-400 transition-colors"> | |
| <i data-lucide="x" class="w-8 h-8"></i> | |
| </button> | |
| <h3 id="modal-title" | |
| class="text-slate-400 text-sm font-mono absolute -top-8 left-0 uppercase tracking-wider">Image Preview | |
| </h3> | |
| <img id="modal-img" src="" class="max-w-full max-h-[85vh] rounded-lg shadow-2xl border border-slate-800" /> | |
| </div> | |
| </div> | |
| <script> | |
| lucide.createIcons(); | |
| const API_BASE = window.location.origin; | |
| let eventSource = null; | |
| let logCounter = 0; | |
| let startTime = 0; | |
| // Runtime State Capture | |
| let currentRunId = null; | |
| let currentRunLogs = []; | |
| let currentRunSteps = []; | |
| let currentOriginalImage = null; // Base64 | |
| let currentAnnotatedImage = null; // URI | |
| // --- IndexedDB Setup --- | |
| const DB_NAME = 'UrbanLensDB'; | |
| const DB_VERSION = 1; | |
| let db; | |
| const initDB = () => { | |
| return new Promise((resolve, reject) => { | |
| const request = indexedDB.open(DB_NAME, DB_VERSION); | |
| request.onerror = (event) => console.error("DB Error", event); | |
| request.onupgradeneeded = (event) => { | |
| const db = event.target.result; | |
| if (!db.objectStoreNames.contains('runs')) { | |
| db.createObjectStore('runs', { keyPath: 'id' }); | |
| } | |
| }; | |
| request.onsuccess = (event) => { | |
| db = event.target.result; | |
| loadHistory(); | |
| resolve(db); | |
| }; | |
| }); | |
| }; | |
| const storeRun = async (runData) => { | |
| if (!db) await initDB(); | |
| const tx = db.transaction(['runs'], 'readwrite'); | |
| const store = tx.objectStore('runs'); | |
| store.put(runData); | |
| }; | |
| const getRun = (id) => { | |
| return new Promise((resolve, reject) => { | |
| const tx = db.transaction(['runs'], 'readonly'); | |
| const store = tx.objectStore('runs'); | |
| const request = store.get(id); | |
| request.onsuccess = () => resolve(request.result); | |
| request.onerror = () => reject(request.error); | |
| }); | |
| }; | |
| const getAllRuns = () => { | |
| return new Promise((resolve, reject) => { | |
| if (!db) return resolve([]); | |
| const tx = db.transaction(['runs'], 'readonly'); | |
| const store = tx.objectStore('runs'); | |
| const request = store.getAll(); | |
| request.onsuccess = () => resolve(request.result); | |
| request.onerror = () => reject(request.error); | |
| }); | |
| }; | |
| function deleteRun(id) { | |
| const tx = db.transaction(['runs'], 'readwrite'); | |
| const store = tx.objectStore('runs'); | |
| store.delete(id); | |
| // Redundant cleanup for safety as requested | |
| localStorage.removeItem(id); | |
| tx.oncomplete = () => loadHistory(); | |
| } | |
| function clearAllRuns() { | |
| const tx = db.transaction(['runs'], 'readwrite'); | |
| const store = tx.objectStore('runs'); | |
| store.clear(); | |
| // Full storage wipe as requested | |
| localStorage.clear(); | |
| tx.oncomplete = () => loadHistory(); | |
| } | |
| // --- UI Interactions --- | |
| function handleFileSelect(input) { | |
| if (input.files && input.files[0]) { | |
| const reader = new FileReader(); | |
| reader.onload = function (e) { | |
| const base64 = e.target.result; | |
| currentOriginalImage = base64; // STORE FOR IDB | |
| const preview = document.getElementById('upload-preview'); | |
| const smallPreview = document.getElementById('inspector-original'); | |
| preview.src = base64; | |
| preview.classList.remove('hidden'); | |
| smallPreview.src = base64; | |
| smallPreview.classList.remove('opacity-50'); | |
| document.getElementById('inspector-original-placeholder').style.display = 'none'; | |
| document.getElementById('file-label').innerText = input.files[0].name; | |
| } | |
| reader.readAsDataURL(input.files[0]); | |
| } | |
| } | |
| function resetUI(isReplay = false) { | |
| if (!isReplay) { | |
| document.getElementById('issue-form').reset(); | |
| document.getElementById('upload-preview').classList.add('hidden'); | |
| document.getElementById('file-label').innerText = "Upload Evidence"; | |
| currentOriginalImage = null; | |
| document.getElementById('replay-banner').classList.add('hidden'); | |
| document.getElementById('submit-btn').classList.remove('hidden'); // Show submit | |
| document.getElementById('submit-btn').disabled = false; | |
| } else { | |
| document.getElementById('replay-banner').classList.remove('hidden'); | |
| document.getElementById('submit-btn').classList.add('hidden'); // Hide submit | |
| } | |
| document.getElementById('active-pipeline').innerHTML = ''; | |
| document.getElementById('active-pipeline').classList.add('hidden'); | |
| document.getElementById('empty-stage').classList.remove('hidden'); | |
| document.getElementById('reset-btn').classList.add('hidden'); | |
| document.getElementById('console-logs').innerHTML = ''; | |
| document.getElementById('inspector-original').src = ''; | |
| document.getElementById('inspector-original').classList.add('opacity-50'); | |
| document.getElementById('inspector-original-placeholder').style.display = 'flex'; | |
| document.getElementById('inspector-annotated').src = ''; | |
| document.getElementById('inspector-annotated-placeholder').style.display = 'flex'; | |
| document.getElementById('vision-badge').classList.add('hidden'); | |
| document.getElementById('metric-latency').innerText = '-- ms'; | |
| document.getElementById('metric-confidence').innerText = '--%'; | |
| document.getElementById('json-viewer').innerText = 'Select a pipeline step to inspect output...'; | |
| setStatus('DISCONNECTED', 'text-slate-400', 'bg-slate-500'); | |
| logCounter = 0; | |
| currentRunId = null; | |
| currentRunLogs = []; | |
| currentRunSteps = []; | |
| currentAnnotatedImage = null; | |
| if (eventSource) eventSource.close(); | |
| } | |
| function setStatus(text, textColor, indicatorColor) { | |
| document.getElementById('status-text').innerText = text; | |
| document.getElementById('status-text').className = `text-xs font-mono ${textColor}`; | |
| document.getElementById('connection-status').className = `w-2 h-2 rounded-full ${indicatorColor} animate-pulse`; | |
| } | |
| function logConsole(source, message) { | |
| // Save state | |
| currentRunLogs.push({ source, message, time: Date.now() }); | |
| const div = document.createElement('div'); | |
| const time = new Date().toLocaleTimeString().split(' ')[0]; | |
| div.innerHTML = `<span class="text-slate-600">[${time}]</span> <span class="text-blue-500 font-bold">${source}:</span> <span class="text-slate-300">${message}</span>`; | |
| const container = document.getElementById('console-logs'); | |
| container.insertBefore(div, container.firstChild); | |
| logCounter++; | |
| document.getElementById('log-count').innerText = `${logCounter} events`; | |
| // Persist Update | |
| if (currentRunId) persistCurrentState(); | |
| } | |
| async function loadHistory() { | |
| const hist = await getAllRuns(); | |
| // Sor by time desc | |
| hist.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); | |
| const container = document.getElementById('history-list'); | |
| container.innerHTML = ''; | |
| if (hist.length === 0) { | |
| container.innerHTML = '<div class="text-center py-8 text-slate-600 text-xs italic">No execution history</div>'; | |
| document.getElementById('clear-history-btn').style.display = 'none'; | |
| return; | |
| } | |
| document.getElementById('clear-history-btn').style.display = 'block'; | |
| hist.forEach(item => { | |
| const div = document.createElement('div'); | |
| div.className = "bg-slate-800 p-2 rounded border border-slate-700 hover:border-blue-500 flex justify-between items-center group transition-colors"; | |
| div.innerHTML = ` | |
| <div class="flex flex-col cursor-pointer flex-1" onclick="replayRun('${item.id}')"> | |
| <span class="text-[10px] font-mono text-slate-300">#${item.id.slice(0, 8)}</span> | |
| <span class="text-[8px] text-slate-500">${new Date(item.timestamp).toLocaleTimeString()} • ${item.description ? item.description.slice(0, 15) + '...' : 'No Desc'}</span> | |
| </div> | |
| <button onclick="deleteRun('${item.id}')" class="text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity p-1"> | |
| <i data-lucide="trash-2" class="w-3 h-3"></i> | |
| </button> | |
| `; | |
| container.appendChild(div); | |
| }); | |
| lucide.createIcons(); | |
| } | |
| async function replayRun(id) { | |
| const run = await getRun(id); | |
| if (!run) return; | |
| resetUI(true); // Is Replay Mode | |
| // Restore Form | |
| document.getElementById('desc-input').value = run.description || ''; | |
| document.getElementById('lat-input').value = run.formData?.latitude || ''; | |
| document.getElementById('lng-input').value = run.formData?.longitude || ''; | |
| // Restore Images | |
| if (run.originalImage) { | |
| const preview = document.getElementById('upload-preview'); | |
| const inspectorOrig = document.getElementById('inspector-original'); | |
| preview.src = run.originalImage; | |
| preview.classList.remove('hidden'); | |
| inspectorOrig.src = run.originalImage; | |
| inspectorOrig.classList.remove('opacity-50'); | |
| document.getElementById('inspector-original-placeholder').style.display = 'none'; | |
| } | |
| // Restore Logs | |
| run.logs.forEach(l => { | |
| const div = document.createElement('div'); | |
| const time = new Date(l.time).toLocaleTimeString().split(' ')[0]; | |
| div.innerHTML = `<span class="text-slate-600">[${time}]</span> <span class="text-blue-500 font-bold">${l.source}:</span> <span class="text-slate-300">${l.message}</span>`; | |
| const container = document.getElementById('console-logs'); | |
| container.insertBefore(div, container.firstChild); | |
| }); | |
| document.getElementById('log-count').innerText = `${run.logs.length} events`; | |
| // Setup Visual Environment | |
| document.getElementById('empty-stage').classList.add('hidden'); | |
| document.getElementById('active-pipeline').innerHTML = ''; // Ensure clear | |
| document.getElementById('active-pipeline').classList.remove('hidden'); | |
| document.getElementById('reset-btn').classList.remove('hidden'); | |
| // Restore Steps | |
| if (run.steps && run.steps.length > 0) { | |
| run.steps.forEach(step => { | |
| renderStep(step.agent_name, 'replaying'); // Render Container | |
| updateStep(step.agent_name, step.data); // Fill Data | |
| // If Vision step, restore annotated image | |
| if (step.agent_name === 'VisionAgent' && step.data.result.annotated_urls) { | |
| const img = document.getElementById('inspector-annotated'); | |
| img.src = step.data.result.annotated_urls[0]; | |
| document.getElementById('inspector-annotated-placeholder').style.display = 'none'; | |
| document.getElementById('vision-badge').classList.remove('hidden'); | |
| if (step.data.result.primary_confidence) { | |
| document.getElementById('metric-confidence').innerText = `${(step.data.result.primary_confidence * 100).toFixed(1)}%`; | |
| } | |
| } | |
| }); | |
| setStatus('REPLAY LOADED', 'text-amber-400', 'bg-amber-500'); | |
| } else { | |
| setStatus('ARCHIVED DATA', 'text-slate-400', 'bg-slate-500'); | |
| } | |
| } | |
| function clearHistory() { | |
| if (confirm('Delete all execution history from database?')) { | |
| clearAllRuns(); | |
| } | |
| } | |
| // --- Utilities --- | |
| function dataURItoBlob(dataURI) { | |
| const byteString = atob(dataURI.split(',')[1]); | |
| const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; | |
| const ab = new ArrayBuffer(byteString.length); | |
| const ia = new Uint8Array(ab); | |
| for (let i = 0; i < byteString.length; i++) { | |
| ia[i] = byteString.charCodeAt(i); | |
| } | |
| return new Blob([ab], { type: mimeString }); | |
| } | |
| // --- Pipeline Logic --- | |
| async function handleSubmit(event) { | |
| event.preventDefault(); | |
| // Client-side Validation | |
| const fileInput = document.getElementById('file'); | |
| if (fileInput.files.length === 0 && !currentOriginalImage) { | |
| alert("Please upload an image evidence first."); | |
| return; | |
| } | |
| // 1. Capture Data | |
| const form = document.getElementById('issue-form'); | |
| const formData = new FormData(form); | |
| // Handle Replay: Inject stored image if input is empty | |
| if (fileInput.files.length === 0 && currentOriginalImage) { | |
| if (currentOriginalImage.startsWith('data:')) { | |
| const blob = dataURItoBlob(currentOriginalImage); | |
| formData.append('images', blob, "replayed_evidence_base64.jpg"); | |
| } else { | |
| // It's a server URL - fetch it | |
| try { | |
| const resp = await fetch(currentOriginalImage); | |
| const blob = await resp.blob(); | |
| formData.append('images', blob, "replayed_evidence_url.jpg"); | |
| } catch (e) { | |
| console.error("Failed to fetch replayed image", e); | |
| alert("Could not load original image for replay. Source unreachable."); | |
| return; | |
| } | |
| } | |
| } | |
| const formObj = Object.fromEntries(formData.entries()); | |
| // Capture image for storage BEFORE reset clears the global | |
| // We use local (Base64) initially, but will overwrite with Server URL on success | |
| let imgToSave = currentOriginalImage; | |
| // 2. Reset UI (Visuals) | |
| resetUI(false); | |
| const btn = document.getElementById('submit-btn'); | |
| btn.classList.add('hidden'); | |
| document.getElementById('reset-btn').classList.remove('hidden'); | |
| document.getElementById('empty-stage').classList.add('hidden'); | |
| document.getElementById('active-pipeline').classList.remove('hidden'); | |
| setStatus('INITIALIZING...', 'text-blue-400', 'bg-blue-500'); | |
| startTime = Date.now(); | |
| try { | |
| // Initial Log with current Image | |
| logConsole('System', 'Initializing agent pipeline...'); | |
| const res = await fetch(`${API_BASE}/issues/stream`, { method: 'POST', body: formData }); | |
| if (!res.ok) throw new Error('API Error'); | |
| const data = await res.json(); | |
| // Initialize Run Record | |
| currentRunId = data.issue_id; | |
| // Use Server URL if available | |
| if (data.image_urls && data.image_urls.length > 0) { | |
| imgToSave = data.image_urls[0]; | |
| } | |
| // Restore the global explicitly with the SERVER URL now | |
| currentOriginalImage = imgToSave; | |
| await storeRun({ | |
| id: currentRunId, | |
| timestamp: new Date().toISOString(), | |
| description: formObj.description, | |
| formData: formObj, | |
| originalImage: imgToSave, | |
| logs: [], | |
| steps: [] | |
| }); | |
| logConsole('API', `Issue Created: ${data.issue_id}`); | |
| connectStream(data.stream_url); | |
| } catch (err) { | |
| logConsole('Error', err.message); | |
| setStatus('ERROR', 'text-red-500', 'bg-red-500'); | |
| } | |
| } | |
| function connectStream(url) { | |
| eventSource = new EventSource(url); | |
| setStatus('CONNECTED', 'text-emerald-400', 'bg-emerald-500'); | |
| eventSource.onopen = () => logConsole('Stream', 'Connection established'); | |
| eventSource.onmessage = (e) => { | |
| const payload = JSON.parse(e.data); | |
| handleStreamEvent(payload); | |
| }; | |
| eventSource.onerror = (e) => { | |
| logConsole('Stream', 'Connection closed'); | |
| setStatus('COMPLETED', 'text-slate-400', 'bg-slate-600'); | |
| eventSource.close(); | |
| // Final save | |
| persistCurrentState(); | |
| loadHistory(); // Refresh Sidebar | |
| }; | |
| } | |
| function handleStreamEvent(packet) { | |
| const { type, data } = packet; | |
| const content = data || packet; | |
| if (type === 'step_started') { | |
| logConsole('Pipeline', `Started: ${content.agent_name}`); | |
| renderStep(content.agent_name, 'running'); | |
| } | |
| else if (type === 'step_completed') { | |
| logConsole('Pipeline', `Completed: ${content.agent_name}`); | |
| // Save Step to state | |
| currentRunSteps.push({ agent_name: content.agent_name, data: content }); | |
| persistCurrentState(); | |
| updateStep(content.agent_name, content); | |
| // Visualizers | |
| if (content.agent_name === 'VisionAgent' && content.result.annotated_urls) { | |
| updateVisionInspector(content.result); | |
| } | |
| const elapsed = Date.now() - startTime; | |
| document.getElementById('metric-latency').innerText = `${elapsed}ms`; | |
| } | |
| } | |
| async function persistCurrentState() { | |
| if (!currentRunId) return; | |
| const run = await getRun(currentRunId); | |
| if (run) { | |
| run.logs = currentRunLogs; | |
| run.steps = currentRunSteps; | |
| run.originalImage = currentOriginalImage || run.originalImage; | |
| await storeRun(run); | |
| } | |
| } | |
| // --- Rendering Components --- | |
| function renderStep(agentName, status) { | |
| const container = document.getElementById('active-pipeline'); | |
| const id = `step-${agentName.replace(/\s+/g, '-')}`; | |
| if (document.getElementById(id)) return; | |
| const div = document.createElement('div'); | |
| div.id = id; | |
| div.className = "relative pl-12 py-4 animate-in slide-in-from-left-4 fade-in duration-300"; | |
| const isFirst = container.children.length === 0; | |
| div.innerHTML = ` | |
| ${!isFirst ? '<div class="step-connector"></div>' : ''} | |
| <div class="absolute left-0 top-6 w-10 h-10 rounded-xl bg-slate-800 border border-slate-700 flex items-center justify-center z-10 shadow-lg transition-colors duration-500" id="${id}-icon"> | |
| <i data-lucide="loader-2" class="w-5 h-5 text-blue-500 animate-spin"></i> | |
| </div> | |
| <div class="glass-panel rounded-xl p-5 border-l-4 border-l-transparent transition-all duration-300 hover:border-l-blue-500 cursor-pointer group" onclick="inspectStep('${id}')"> | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="font-bold text-slate-200 text-sm tracking-wide">${agentName}</h3> | |
| <span class="text-[10px] bg-blue-500/10 text-blue-400 px-2 py-0.5 rounded font-mono animate-pulse" id="${id}-badge">RUNNING</span> | |
| </div> | |
| <div class="text-xs text-slate-500 font-mono" id="${id}-body">Processing...</div> | |
| </div> | |
| <script type="application/json" id="${id}-data">{}<\/script> | |
| `; | |
| container.appendChild(div); | |
| lucide.createIcons(); | |
| } | |
| function updateStep(agentName, data) { | |
| const id = `step-${agentName.replace(/\s+/g, '-')}`; | |
| const el = document.getElementById(id); | |
| if (!el) return; | |
| const store = document.createElement('script'); | |
| store.id = `${id}-data`; | |
| store.type = 'application/json'; | |
| store.text = JSON.stringify(data.result || data, null, 2); | |
| const oldStore = document.getElementById(`${id}-data`); | |
| if (oldStore) oldStore.remove(); | |
| el.appendChild(store); | |
| const icon = document.getElementById(`${id}-icon`); | |
| const badge = document.getElementById(`${id}-badge`); | |
| const body = document.getElementById(`${id}-body`); | |
| const connector = el.querySelector('.step-connector'); | |
| icon.innerHTML = `<i data-lucide="check" class="w-5 h-5 text-white"></i>`; | |
| icon.className = "absolute left-0 top-6 w-10 h-10 rounded-xl bg-emerald-500 shadow-lg shadow-emerald-500/30 flex items-center justify-center z-10 scale-110 transition-transform"; | |
| badge.className = "text-[10px] bg-emerald-500/10 text-emerald-400 px-2 py-0.5 rounded font-mono"; | |
| badge.innerText = "COMPLETED"; | |
| body.innerText = data.decision || data.reasoning || "Process completed successfully"; | |
| body.className = "text-xs text-slate-400"; | |
| if (connector) connector.classList.add('active'); | |
| lucide.createIcons(); | |
| // If live flow, verify if inspected | |
| // For replay flow, auto-inspection might be distracting, so we skip auto-inspect here | |
| } | |
| function inspectStep(id) { | |
| const dataEl = document.getElementById(`${id}-data`); | |
| if (!dataEl) return; | |
| const raw = JSON.parse(dataEl.text); | |
| const viewer = document.getElementById('json-viewer'); | |
| viewer.innerText = JSON.stringify(raw, null, 2); | |
| document.querySelectorAll('.glass-panel').forEach(p => p.classList.remove('ring-1', 'ring-blue-500')); | |
| document.querySelector(`#${id} .glass-panel`)?.classList.add('ring-1', 'ring-blue-500'); | |
| } | |
| function updateVisionInspector(result) { | |
| // Update Annotated Image | |
| if (result.annotated_urls && result.annotated_urls.length > 0) { | |
| const img = document.getElementById('inspector-annotated'); | |
| img.src = result.annotated_urls[0]; | |
| document.getElementById('inspector-annotated-placeholder').style.display = 'none'; | |
| document.getElementById('vision-badge').classList.remove('hidden'); | |
| } | |
| // Update Original Image from Global State (URL or Base64) | |
| if (currentOriginalImage) { | |
| const origImg = document.getElementById('inspector-original'); | |
| origImg.src = currentOriginalImage; | |
| origImg.classList.remove('opacity-50'); | |
| document.getElementById('inspector-original-placeholder').style.display = 'none'; | |
| } | |
| if (result.primary_confidence) { | |
| document.getElementById('metric-confidence').innerText = `${(result.primary_confidence * 100).toFixed(1)}%`; | |
| } | |
| } | |
| // --- Modal Logic --- | |
| function openZoomModal(src, title) { | |
| if (!src) return; | |
| const modal = document.getElementById('zoom-modal'); | |
| const img = document.getElementById('modal-img'); | |
| const titleEl = document.getElementById('modal-title'); | |
| img.src = src; | |
| titleEl.innerText = title || 'Evidence Preview'; | |
| modal.classList.remove('hidden'); | |
| setTimeout(() => modal.classList.remove('opacity-0'), 10); | |
| } | |
| function closeZoomModal() { | |
| const modal = document.getElementById('zoom-modal'); | |
| modal.classList.add('opacity-0'); | |
| setTimeout(() => modal.classList.add('hidden'), 300); | |
| } | |
| // Initialize DB on Boot | |
| initDB(); | |
| </script> | |
| </body> | |
| </html> |