| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Agent Mission Control | CityTrack</title> |
| |
| <script src="https://cdn.tailwindcss.com"></script> |
| |
| <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"> |
| |
| <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); |
| } |
| |
| |
| ::-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-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 |
| 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">CityTrack <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"> |
|
|
| |
| <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"> |
|
|
| |
| <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> |
| |
| <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> |
|
|
| |
| <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"> |
| |
| <div class="text-center py-8 text-slate-600 text-xs italic">No execution history</div> |
| </div> |
| </div> |
| </aside> |
|
|
| |
| <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"> |
| |
| </div> |
|
|
| </div> |
|
|
| |
| <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"> |
| |
| </div> |
| </div> |
| </section> |
|
|
| |
| <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"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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; |
| |
| |
| let currentRunId = null; |
| let currentRunLogs = []; |
| let currentRunSteps = []; |
| let currentOriginalImage = null; |
| let currentAnnotatedImage = null; |
| |
| |
| const DB_NAME = 'CityTrackDB'; |
| 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); |
| |
| localStorage.removeItem(id); |
| tx.oncomplete = () => loadHistory(); |
| } |
| |
| function clearAllRuns() { |
| const tx = db.transaction(['runs'], 'readwrite'); |
| const store = tx.objectStore('runs'); |
| store.clear(); |
| |
| localStorage.clear(); |
| tx.oncomplete = () => loadHistory(); |
| } |
| |
| |
| |
| |
| function handleFileSelect(input) { |
| if (input.files && input.files[0]) { |
| const reader = new FileReader(); |
| reader.onload = function (e) { |
| const base64 = e.target.result; |
| currentOriginalImage = base64; |
| |
| 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'); |
| document.getElementById('submit-btn').disabled = false; |
| } else { |
| document.getElementById('replay-banner').classList.remove('hidden'); |
| document.getElementById('submit-btn').classList.add('hidden'); |
| } |
| |
| 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) { |
| |
| 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`; |
| |
| |
| if (currentRunId) persistCurrentState(); |
| } |
| |
| async function loadHistory() { |
| const hist = await getAllRuns(); |
| |
| 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); |
| |
| |
| document.getElementById('desc-input').value = run.description || ''; |
| document.getElementById('lat-input').value = run.formData?.latitude || ''; |
| document.getElementById('lng-input').value = run.formData?.longitude || ''; |
| |
| |
| 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'; |
| } |
| |
| |
| 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`; |
| |
| |
| document.getElementById('empty-stage').classList.add('hidden'); |
| document.getElementById('active-pipeline').innerHTML = ''; |
| document.getElementById('active-pipeline').classList.remove('hidden'); |
| document.getElementById('reset-btn').classList.remove('hidden'); |
| |
| |
| if (run.steps && run.steps.length > 0) { |
| run.steps.forEach(step => { |
| renderStep(step.agent_name, 'replaying'); |
| updateStep(step.agent_name, step.data); |
| |
| |
| 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(); |
| } |
| } |
| |
| |
| |
| |
| 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 }); |
| } |
| |
| |
| |
| async function handleSubmit(event) { |
| event.preventDefault(); |
| |
| |
| const fileInput = document.getElementById('file'); |
| if (fileInput.files.length === 0 && !currentOriginalImage) { |
| alert("Please upload an image evidence first."); |
| return; |
| } |
| |
| |
| const form = document.getElementById('issue-form'); |
| const formData = new FormData(form); |
| |
| |
| if (fileInput.files.length === 0 && currentOriginalImage) { |
| if (currentOriginalImage.startsWith('data:')) { |
| const blob = dataURItoBlob(currentOriginalImage); |
| formData.append('images', blob, "replayed_evidence_base64.jpg"); |
| } else { |
| |
| 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()); |
| |
| |
| |
| let imgToSave = currentOriginalImage; |
| |
| |
| 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 { |
| |
| 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(); |
| |
| |
| currentRunId = data.issue_id; |
| |
| |
| if (data.image_urls && data.image_urls.length > 0) { |
| imgToSave = data.image_urls[0]; |
| } |
| |
| |
| 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(); |
| |
| persistCurrentState(); |
| loadHistory(); |
| }; |
| } |
| |
| 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}`); |
| |
| |
| currentRunSteps.push({ agent_name: content.agent_name, data: content }); |
| persistCurrentState(); |
| |
| updateStep(content.agent_name, content); |
| |
| |
| 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); |
| } |
| } |
| |
| |
| |
| 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(); |
| |
| |
| |
| } |
| |
| 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) { |
| |
| 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'); |
| } |
| |
| 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)}%`; |
| } |
| } |
| |
| |
| |
| 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); |
| } |
| |
| |
| initDB(); |
| |
| </script> |
| </body> |
|
|
| </html> |