Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>GenAI Shield β Data Flow</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --bg:#07070f;--s1:#0f0f1a;--s2:#161625;--b:#1f1f35; | |
| --acc:#6c63ff;--grn:#00d4aa;--red:#ff4757;--ylw:#ffa502; | |
| --txt:#e0e0f0;--muted:#5a5a7a; | |
| --mono:'JetBrains Mono',monospace;--sans:'Inter',sans-serif; | |
| } | |
| body{font-family:var(--sans);background:var(--bg);color:var(--txt);height:100vh;display:flex;flex-direction:column;overflow:hidden} | |
| /* Header */ | |
| .hdr{padding:.75rem 1.5rem;border-bottom:1px solid var(--b);background:var(--s1);display:flex;align-items:center;justify-content:space-between;flex-shrink:0} | |
| .hdr-l{display:flex;align-items:center;gap:.75rem} | |
| .hdr-icon{width:34px;height:34px;border-radius:8px;background:linear-gradient(135deg,var(--acc),var(--grn));display:flex;align-items:center;justify-content:center;font-size:1rem} | |
| .hdr h1{font-size:.95rem;font-weight:700} | |
| .hdr-sub{font-family:var(--mono);font-size:.58rem;color:var(--muted);text-transform:uppercase} | |
| .hdr-r{display:flex;align-items:center;gap:.75rem} | |
| .dot{width:8px;height:8px;border-radius:50%;background:var(--grn);box-shadow:0 0 6px var(--grn);animation:pulse 2s ease-in-out infinite} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}} | |
| .nav-link{font-family:var(--mono);font-size:.6rem;padding:4px 10px;border:1px solid var(--b);border-radius:4px;color:var(--muted);text-decoration:none;transition:color .2s,border-color .2s} | |
| .nav-link:hover{color:var(--acc);border-color:var(--acc)} | |
| /* Layout */ | |
| .layout{flex:1;display:grid;grid-template-columns:1fr 320px;overflow:hidden} | |
| /* Pipeline canvas area */ | |
| .canvas-wrap{padding:1.5rem;display:flex;flex-direction:column;gap:1rem;overflow:hidden} | |
| .section-title{font-family:var(--mono);font-size:.6rem;text-transform:uppercase;color:var(--muted);letter-spacing:.08em;margin-bottom:.5rem} | |
| /* Pipeline diagram */ | |
| .pipeline{background:var(--s1);border:1px solid var(--b);border-radius:12px;padding:1.5rem;flex-shrink:0} | |
| .nodes{display:flex;align-items:center;gap:0;position:relative} | |
| .node{display:flex;flex-direction:column;align-items:center;gap:.5rem;flex:1;position:relative;z-index:2} | |
| .node-box{width:80px;height:80px;border-radius:12px;border:2px solid var(--b);background:var(--s2);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.25rem;cursor:default;transition:border-color .3s,box-shadow .3s;position:relative} | |
| .node-icon{font-size:1.4rem} | |
| .node-lbl{font-family:var(--mono);font-size:.55rem;text-align:center;color:var(--muted);text-transform:uppercase;line-height:1.3} | |
| .node-timer{font-family:var(--mono);font-size:.65rem;font-weight:700;color:var(--grn);min-height:1em;text-align:center} | |
| /* Node state classes */ | |
| .node-box.active{border-color:var(--acc);box-shadow:0 0 16px rgba(108,99,255,.4)} | |
| .node-box.done{border-color:var(--grn);box-shadow:0 0 12px rgba(0,212,170,.25)} | |
| .node-box.blocked{border-color:var(--red);box-shadow:0 0 16px rgba(255,71,87,.4)} | |
| .node-box.warn{border-color:var(--ylw)} | |
| /* Connectors */ | |
| .connector{flex:1;height:2px;background:var(--b);position:relative;z-index:1;align-self:center;margin-top:-40px} | |
| .connector-inner{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--grn));transition:width .4s ease} | |
| .connector.active .connector-inner{width:100%} | |
| .connector.blocked .connector-inner{background:var(--red)} | |
| /* Packet animation */ | |
| .packet{position:absolute;top:50%;transform:translateY(-50%);width:10px;height:10px;border-radius:50%;background:var(--acc);box-shadow:0 0 8px var(--acc);animation:travel .6s ease forwards;opacity:0} | |
| @keyframes travel{0%{left:0;opacity:1}100%{left:100%;opacity:0}} | |
| .packet.red{background:var(--red);box-shadow:0 0 8px var(--red)} | |
| .packet.grn{background:var(--grn);box-shadow:0 0 8px var(--grn)} | |
| /* Parallel row label */ | |
| .parallel-note{font-family:var(--mono);font-size:.58rem;color:var(--acc);text-align:center;margin-top:.75rem} | |
| /* Metrics grid */ | |
| .metrics-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem} | |
| .metric-card{background:var(--s1);border:1px solid var(--b);border-radius:10px;padding:.875rem;display:flex;flex-direction:column;gap:.25rem} | |
| .metric-card.acc{border-color:rgba(108,99,255,.4)} | |
| .metric-card.grn{border-color:rgba(0,212,170,.4)} | |
| .metric-card.red{border-color:rgba(255,71,87,.4)} | |
| .metric-card.ylw{border-color:rgba(255,165,2,.4)} | |
| .metric-lbl{font-family:var(--mono);font-size:.55rem;text-transform:uppercase;color:var(--muted)} | |
| .metric-val{font-family:var(--mono);font-size:1.5rem;font-weight:700;line-height:1} | |
| .metric-sub{font-family:var(--mono);font-size:.58rem;color:var(--muted)} | |
| .metric-card.acc .metric-val{color:var(--acc)} | |
| .metric-card.grn .metric-val{color:var(--grn)} | |
| .metric-card.red .metric-val{color:var(--red)} | |
| .metric-card.ylw .metric-val{color:var(--ylw)} | |
| /* Charts row */ | |
| .charts-row{display:grid;grid-template-columns:1fr 1fr;gap:.75rem;flex:1;min-height:0} | |
| .chart-card{background:var(--s1);border:1px solid var(--b);border-radius:10px;padding:1rem;display:flex;flex-direction:column;min-height:0} | |
| .chart-card canvas{flex:1;min-height:0} | |
| /* Right panel */ | |
| .right-panel{border-left:1px solid var(--b);display:flex;flex-direction:column;overflow:hidden} | |
| .rp-header{padding:.875rem 1rem;border-bottom:1px solid var(--b);display:flex;align-items:center;justify-content:space-between;flex-shrink:0} | |
| .rp-title{font-family:var(--mono);font-size:.6rem;text-transform:uppercase;color:var(--muted)} | |
| .rp-count{font-family:var(--mono);font-size:.6rem;color:var(--acc)} | |
| .event-log{flex:1;overflow-y:auto;padding:.5rem} | |
| .event-log::-webkit-scrollbar{width:3px} | |
| .event-log::-webkit-scrollbar-thumb{background:var(--b);border-radius:2px} | |
| .evt{padding:.5rem .75rem;border-left:2px solid var(--b);margin-bottom:.375rem;border-radius:0 6px 6px 0;background:var(--s2);cursor:pointer;transition:border-color .2s} | |
| .evt:hover{border-left-color:var(--acc)} | |
| .evt-stage{font-family:var(--mono);font-size:.58rem;font-weight:700;text-transform:uppercase} | |
| .evt-meta{font-family:var(--mono);font-size:.55rem;color:var(--muted);margin-top:2px} | |
| .evt-detail{font-size:.7rem;color:var(--txt);margin-top:3px;word-break:break-all} | |
| .evt.REQUEST_IN {border-left-color:#6c63ff} .evt.REQUEST_IN .evt-stage{color:#6c63ff} | |
| .evt.GUARD_START {border-left-color:#a78bfa} .evt.GUARD_START .evt-stage{color:#a78bfa} | |
| .evt.LLM_START {border-left-color:#38bdf8} .evt.LLM_START .evt-stage{color:#38bdf8} | |
| .evt.GUARD_PASS {border-left-color:#00d4aa} .evt.GUARD_PASS .evt-stage{color:#00d4aa} | |
| .evt.GUARD_BLOCK {border-left-color:#ff4757} .evt.GUARD_BLOCK .evt-stage{color:#ff4757} | |
| .evt.FIRST_TOKEN {border-left-color:#00d4aa} .evt.FIRST_TOKEN .evt-stage{color:#00d4aa} | |
| .evt.TOKEN_FLOW {border-left-color:#22d3ee} .evt.TOKEN_FLOW .evt-stage{color:#22d3ee} | |
| .evt.SENTENCE_READY{border-left-color:#a3e635} .evt.SENTENCE_READY .evt-stage{color:#a3e635} | |
| .evt.MONITOR_START {border-left-color:#fbbf24} .evt.MONITOR_START .evt-stage{color:#fbbf24} | |
| .evt.MONITOR_RESULT{border-left-color:#fb923c} .evt.MONITOR_RESULT .evt-stage{color:#fb923c} | |
| .evt.STREAM_DONE {border-left-color:#00d4aa} .evt.STREAM_DONE .evt-stage{color:#00d4aa} | |
| .evt.REQUEST_DONE {border-left-color:#6c63ff} .evt.REQUEST_DONE .evt-stage{color:#6c63ff} | |
| /* Threat badge */ | |
| .tbadge{font-family:var(--mono);font-size:.52rem;font-weight:700;padding:1px 5px;border-radius:2px;display:inline-block} | |
| .clean{background:rgba(0,212,170,.12);color:var(--grn);border:1px solid rgba(0,212,170,.3)} | |
| .medium{background:rgba(255,165,2,.12);color:var(--ylw);border:1px solid rgba(255,165,2,.3)} | |
| .high{background:rgba(255,71,87,.12);color:var(--red);border:1px solid rgba(255,71,87,.3)} | |
| /* Empty state */ | |
| .empty-state{padding:2rem;text-align:center;color:var(--muted);font-family:var(--mono);font-size:.7rem} | |
| </style> | |
| </head> | |
| <body> | |
| <header class="hdr"> | |
| <div class="hdr-l"> | |
| <div class="hdr-icon">π</div> | |
| <div> | |
| <h1>Data Flow <span style="color:var(--acc)">Monitor</span></h1> | |
| <div class="hdr-sub">Real-time pipeline telemetry Β· Sidecar v2</div> | |
| </div> | |
| </div> | |
| <div class="hdr-r"> | |
| <a class="nav-link" href="#" onclick="event.preventDefault(); window.location.href='/sidecar'+window.location.search;">β Chat</a> | |
| <a class="nav-link" href="#" onclick="event.preventDefault(); window.location.href='/genai-monitoring'+window.location.search;">SIEM</a> | |
| <div class="dot" id="conn-dot" title="Connecting..."></div> | |
| </div> | |
| </header> | |
| <div class="layout"> | |
| <!-- Left: pipeline + metrics --> | |
| <div class="canvas-wrap"> | |
| <!-- Pipeline diagram --> | |
| <div class="pipeline"> | |
| <div class="section-title">Request Pipeline Β· Live</div> | |
| <div class="nodes" id="nodes"> | |
| <!-- Client --> | |
| <div class="node"> | |
| <div class="node-box" id="n-client"> | |
| <div class="node-icon">π€</div> | |
| <div class="node-lbl">Client</div> | |
| </div> | |
| <div class="node-timer" id="t-client">β</div> | |
| </div> | |
| <div class="connector" id="c-gate"><div class="connector-inner"></div></div> | |
| <!-- Shield Gate --> | |
| <div class="node"> | |
| <div class="node-box" id="n-gate"> | |
| <div class="node-icon">πͺ</div> | |
| <div class="node-lbl">Shield Gate</div> | |
| </div> | |
| <div class="node-timer" id="t-gate">β</div> | |
| </div> | |
| <!-- Fork: guard + LLM (shown as two sub-connectors) --> | |
| <div style="display:flex;flex-direction:column;gap:.25rem;flex:1.4"> | |
| <div style="display:flex;align-items:center;gap:0"> | |
| <div class="connector" id="c-guard" style="flex:1"><div class="connector-inner"></div></div> | |
| <div class="node"> | |
| <div class="node-box" id="n-guard" style="width:68px;height:68px"> | |
| <div class="node-icon" style="font-size:1.1rem">π‘οΈ</div> | |
| <div class="node-lbl">Prompt Guard</div> | |
| </div> | |
| <div class="node-timer" id="t-guard">β</div> | |
| </div> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:0"> | |
| <div class="connector" id="c-llm" style="flex:1"><div class="connector-inner"></div></div> | |
| <div class="node"> | |
| <div class="node-box" id="n-llm" style="width:68px;height:68px"> | |
| <div class="node-icon" style="font-size:1.1rem">π€</div> | |
| <div class="node-lbl">LLM API</div> | |
| </div> | |
| <div class="node-timer" id="t-llm">β</div> | |
| </div> | |
| </div> | |
| <div class="parallel-note">β‘ parallel</div> | |
| </div> | |
| <div class="connector" id="c-monitor"><div class="connector-inner"></div></div> | |
| <!-- Stream Monitor --> | |
| <div class="node"> | |
| <div class="node-box" id="n-monitor"> | |
| <div class="node-icon">π</div> | |
| <div class="node-lbl">Monitor</div> | |
| </div> | |
| <div class="node-timer" id="t-monitor">β</div> | |
| </div> | |
| <div class="connector" id="c-out"><div class="connector-inner"></div></div> | |
| <!-- Output --> | |
| <div class="node"> | |
| <div class="node-box" id="n-out"> | |
| <div class="node-icon">β </div> | |
| <div class="node-lbl">Output</div> | |
| </div> | |
| <div class="node-timer" id="t-out">β</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Metrics --> | |
| <div> | |
| <div class="section-title">Session Metrics</div> | |
| <div class="metrics-grid"> | |
| <div class="metric-card acc"> | |
| <div class="metric-lbl">Total Requests</div> | |
| <div class="metric-val" id="m-total">0</div> | |
| <div class="metric-sub">since page load</div> | |
| </div> | |
| <div class="metric-card grn"> | |
| <div class="metric-lbl">Avg Guard ms</div> | |
| <div class="metric-val" id="m-guard-avg">β</div> | |
| <div class="metric-sub">Prompt Guard latency</div> | |
| </div> | |
| <div class="metric-card ylw"> | |
| <div class="metric-lbl">Avg TTFT ms</div> | |
| <div class="metric-val" id="m-ttft-avg">β</div> | |
| <div class="metric-sub">time to first token</div> | |
| </div> | |
| <div class="metric-card red"> | |
| <div class="metric-lbl">Blocked</div> | |
| <div class="metric-val" id="m-blocked">0</div> | |
| <div class="metric-sub">pre/post inference</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Charts --> | |
| <div class="charts-row"> | |
| <div class="chart-card"> | |
| <div class="section-title">Guard Latency (ms) Β· last 20</div> | |
| <canvas id="chart-guard"></canvas> | |
| </div> | |
| <div class="chart-card"> | |
| <div class="section-title">Total Request Time (ms) Β· last 20</div> | |
| <canvas id="chart-total"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right: event log --> | |
| <div class="right-panel"> | |
| <div class="rp-header"> | |
| <span class="rp-title">Pipeline Event Log</span> | |
| <span class="rp-count" id="evt-count">0 events</span> | |
| </div> | |
| <div class="event-log" id="event-log"> | |
| <div class="empty-state" id="empty-msg">Waiting for requestsβ¦</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const state = { | |
| total: 0, blocked: 0, | |
| guardTimes: [], ttftTimes: [], totalTimes: [], | |
| reqStart: {}, guardStart: {}, llmStart: {}, | |
| }; | |
| // ββ Chart setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const chartOpts = (color) => ({ | |
| type: 'line', | |
| data: { | |
| labels: Array(20).fill(''), | |
| datasets: [{ | |
| data: Array(20).fill(null), | |
| borderColor: color, | |
| backgroundColor: color + '18', | |
| borderWidth: 1.5, | |
| fill: true, | |
| tension: 0.4, | |
| pointRadius: 2, | |
| pointBackgroundColor: color, | |
| }] | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, animation: false, | |
| scales: { | |
| x: { display: false }, | |
| y: { min: 0, grid: { color: '#1f1f35' }, ticks: { color: '#5a5a7a', font: { family: 'JetBrains Mono', size: 9 } } } | |
| }, | |
| plugins: { legend: { display: false } } | |
| } | |
| }); | |
| const cGuard = new Chart(document.getElementById('chart-guard').getContext('2d'), chartOpts('#6c63ff')); | |
| const cTotal = new Chart(document.getElementById('chart-total').getContext('2d'), chartOpts('#00d4aa')); | |
| function pushChart(chart, val) { | |
| chart.data.datasets[0].data.push(val); | |
| chart.data.datasets[0].data.shift(); | |
| chart.data.labels.push(''); | |
| chart.data.labels.shift(); | |
| chart.update('none'); | |
| } | |
| // ββ Node helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function nodeState(id, cls) { | |
| const el = document.getElementById('n-' + id); | |
| if (!el) return; | |
| el.className = 'node-box ' + (cls || ''); | |
| } | |
| function nodeTime(id, ms) { | |
| const el = document.getElementById('t-' + id); | |
| if (el) el.textContent = ms != null ? ms + 'ms' : 'β'; | |
| } | |
| function connActive(id, cls) { | |
| const el = document.getElementById('c-' + id); | |
| if (el) el.className = 'connector active ' + (cls || ''); | |
| } | |
| function firePacket(connId, cls) { | |
| const conn = document.getElementById('c-' + connId); | |
| if (!conn) return; | |
| const p = document.createElement('div'); | |
| p.className = 'packet ' + (cls || ''); | |
| conn.style.position = 'relative'; | |
| conn.appendChild(p); | |
| setTimeout(() => p.remove(), 700); | |
| } | |
| function resetPipeline() { | |
| ['client','gate','guard','llm','monitor','out'].forEach(id => nodeState(id, '')); | |
| ['client','gate','guard','llm','monitor','out'].forEach(id => nodeTime(id, null)); | |
| ['gate','guard','llm','monitor','out'].forEach(id => { | |
| const el = document.getElementById('c-' + id); | |
| if (el) el.className = 'connector'; | |
| }); | |
| document.getElementById('n-out').querySelector('.node-icon').textContent = 'β '; | |
| } | |
| // ββ Metrics update βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateMetrics() { | |
| document.getElementById('m-total').textContent = state.total; | |
| document.getElementById('m-blocked').textContent = state.blocked; | |
| const avg = arr => arr.length ? Math.round(arr.slice(-20).reduce((a,b)=>a+b,0)/Math.min(arr.length,20)) : 'β'; | |
| document.getElementById('m-guard-avg').textContent = avg(state.guardTimes); | |
| document.getElementById('m-ttft-avg').textContent = avg(state.ttftTimes); | |
| } | |
| // ββ Event log ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let evtCount = 0; | |
| function logEvent(ev) { | |
| const log = document.getElementById('event-log'); | |
| const empty = document.getElementById('empty-msg'); | |
| if (empty) empty.remove(); | |
| evtCount++; | |
| document.getElementById('evt-count').textContent = evtCount + ' events'; | |
| const d = document.createElement('div'); | |
| d.className = 'evt ' + ev.stage; | |
| let detail = ''; | |
| if (ev.prompt_preview) detail = `"${ev.prompt_preview.slice(0,50)}"`; | |
| else if (ev.guard_ms) detail = `Guard: ${ev.guard_ms}ms Β· PG: ${(ev.pg_score||0).toFixed(3)}`; | |
| else if (ev.ttft_ms) detail = `TTFT: ${ev.ttft_ms}ms`; | |
| else if (ev.stream_ms) detail = `Stream: ${ev.stream_ms}ms Β· ${ev.total_tokens||0} tokens`; | |
| else if (ev.total_ms) detail = `Total: ${ev.total_ms}ms`; | |
| else if (ev.sentence_preview) detail = `"${ev.sentence_preview.slice(0,40)}"`; | |
| else if (ev.token_count) detail = `${ev.token_count} tokens`; | |
| const score = ev.threat_score; | |
| const badge = score >= 60 ? `<span class="tbadge high">HIGH ${score}</span>` | |
| : score >= 30 ? `<span class="tbadge medium">WARN ${score}</span>` | |
| : score > 0 ? `<span class="tbadge clean">CLEAN ${score}</span>` : ''; | |
| d.innerHTML = `<div class="evt-stage">${ev.stage.replace(/_/g,' ')}</div> | |
| <div class="evt-meta">${ev.wall} Β· req ${ev.request_id} Β· +${ev.elapsed_ms}ms ${badge}</div> | |
| ${detail ? `<div class="evt-detail">${detail}</div>` : ''}`; | |
| log.prepend(d); | |
| if (log.children.length > 80) log.lastChild.remove(); | |
| } | |
| // ββ Pipeline stage handler βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function handleStage(ev) { | |
| logEvent(ev); | |
| const rid = ev.request_id; | |
| switch(ev.stage) { | |
| case 'REQUEST_IN': | |
| resetPipeline(); | |
| state.reqStart[rid] = ev.elapsed_ms; | |
| nodeState('client', 'active'); | |
| connActive('gate'); | |
| firePacket('gate'); | |
| nodeState('gate', 'active'); | |
| break; | |
| case 'GUARD_START': | |
| state.guardStart[rid] = ev.elapsed_ms; | |
| nodeState('guard', 'active'); | |
| connActive('guard'); | |
| firePacket('guard'); | |
| break; | |
| case 'LLM_START': | |
| state.llmStart[rid] = ev.elapsed_ms; | |
| nodeState('llm', 'active'); | |
| connActive('llm'); | |
| firePacket('llm'); | |
| break; | |
| case 'GUARD_PASS': | |
| nodeState('guard', 'done'); | |
| nodeTime('guard', ev.guard_ms); | |
| state.guardTimes.push(ev.guard_ms); | |
| pushChart(cGuard, ev.guard_ms); | |
| updateMetrics(); | |
| break; | |
| case 'GUARD_BLOCK': | |
| state.total++; | |
| state.blocked++; | |
| nodeState('guard', 'blocked'); | |
| nodeTime('guard', ev.guard_ms); | |
| nodeState('gate', 'blocked'); | |
| nodeState('llm', 'blocked'); | |
| nodeState('out', 'blocked'); | |
| document.getElementById('n-out').querySelector('.node-icon').textContent = 'π«'; | |
| state.guardTimes.push(ev.guard_ms); | |
| pushChart(cGuard, ev.guard_ms); | |
| updateMetrics(); | |
| break; | |
| case 'FIRST_TOKEN': | |
| nodeState('llm', 'done'); | |
| nodeTime('llm', ev.ttft_ms); | |
| state.ttftTimes.push(ev.ttft_ms); | |
| updateMetrics(); | |
| break; | |
| case 'TOKEN_FLOW': | |
| firePacket('monitor', 'grn'); | |
| break; | |
| case 'SENTENCE_READY': | |
| connActive('monitor'); | |
| firePacket('monitor', 'grn'); | |
| nodeState('monitor', 'active'); | |
| break; | |
| case 'MONITOR_START': | |
| nodeState('monitor', 'active'); | |
| break; | |
| case 'MONITOR_RESULT': | |
| if (ev.blocked) { | |
| nodeState('monitor', 'blocked'); | |
| nodeState('out', 'blocked'); | |
| document.getElementById('n-out').querySelector('.node-icon').textContent = 'π«'; | |
| } else { | |
| nodeState('monitor', 'done'); | |
| } | |
| break; | |
| case 'STREAM_DONE': | |
| nodeState('llm', 'done'); | |
| nodeTime('llm', ev.stream_ms); | |
| break; | |
| case 'REQUEST_DONE': | |
| state.total++; | |
| if (ev.blocked) state.blocked++; | |
| nodeState('client', 'done'); | |
| nodeState('gate', 'done'); | |
| if (!ev.blocked) { | |
| nodeState('monitor', 'done'); | |
| nodeState('out', 'done'); | |
| connActive('out', 'grn'); | |
| firePacket('out', 'grn'); | |
| } | |
| nodeTime('out', ev.total_ms); | |
| nodeTime('client', ev.total_ms); | |
| state.totalTimes.push(ev.total_ms); | |
| pushChart(cTotal, ev.total_ms); | |
| updateMetrics(); | |
| break; | |
| } | |
| } | |
| // ββ SSE connection βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function connect() { | |
| const dot = document.getElementById('conn-dot'); | |
| dot.style.background = 'var(--ylw)'; | |
| dot.style.boxShadow = '0 0 6px var(--ylw)'; | |
| // Try sidecar direct first, fall back to current host | |
| const url = '/v1/pipeline-stream'; | |
| const es = new EventSource(url); | |
| es.onopen = () => { | |
| dot.style.background = 'var(--grn)'; | |
| dot.style.boxShadow = '0 0 6px var(--grn)'; | |
| dot.title = 'Connected'; | |
| }; | |
| es.onmessage = e => { | |
| if (!e.data || e.data.startsWith(':')) return; | |
| try { handleStage(JSON.parse(e.data)); } catch {} | |
| }; | |
| es.onerror = () => { | |
| dot.style.background = 'var(--red)'; | |
| dot.style.boxShadow = '0 0 6px var(--red)'; | |
| dot.title = 'Disconnected β retrying'; | |
| es.close(); | |
| setTimeout(connect, 3000); | |
| }; | |
| } | |
| connect(); | |
| </script> | |
| </body> | |
| </html> | |