LLM_Monitor / templates /dataflow.html
potato-pzy
fix: preserve query parameters during dashboard navigation to fix 404 in iframe
e07fd90
Raw
History Blame Contribute Delete
22.8 kB
<!DOCTYPE html>
<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>