gemma4ge / dashboard_html.py
mmtf's picture
Upload folder using huggingface_hub
dc64bdc verified
Raw
History Blame Contribute Delete
24 kB
"""HTML / CSS / JS blob for the 4-panel live dashboard embedded in the Gradio Space.
Kept out of ``app.py`` so the Python file stays focused on the Gradio wiring.
The panels are driven from four hidden ``gr.JSON`` components (one per panel)
whose values mutate as Python generators yield. JavaScript polls the JSON
state in the DOM via a ``MutationObserver`` and re-renders each panel from the
delta.
Every selector here is namespaced under ``#gf-dashboard`` so the paste of
``dashboard/style.css`` cannot clobber Gradio's own styling.
"""
from __future__ import annotations
DASHBOARD_HTML = r"""
<style>
#gf-dashboard {
--gf-bg: #07090c;
--gf-panel: #0d1117;
--gf-panel-2: #060a10;
--gf-border: #1c232e;
--gf-text: #d8dee9;
--gf-muted: #6b7280;
--gf-accent: #f5b400;
--gf-safe: #28a76588;
--gf-warn: #e6a23c88;
--gf-danger: #ef4444cc;
--gf-safe-solid: #28a765;
--gf-warn-solid: #e6a23c;
--gf-danger-solid: #ef4444;
background: var(--gf-bg);
color: var(--gf-text);
padding: 14px;
border-radius: 12px;
margin-top: 18px;
font-family: -apple-system, "SF Pro Text", BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
#gf-dashboard * { box-sizing: border-box; }
#gf-dashboard .gf-head {
display: flex; flex-wrap: wrap; gap: 16px;
align-items: baseline; justify-content: space-between;
padding-bottom: 10px; margin-bottom: 12px;
border-bottom: 1px solid var(--gf-border);
}
#gf-dashboard .gf-brand { display: flex; align-items: baseline; gap: 12px; }
#gf-dashboard .gf-brand h2 { margin: 0; font-size: 16px; letter-spacing: -0.01em; color: var(--gf-text); }
#gf-dashboard .gf-brand .gf-pill {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em;
color: var(--gf-accent); border: 1px solid var(--gf-accent);
padding: 2px 7px; border-radius: 999px;
}
#gf-dashboard .gf-tagline { color: var(--gf-muted); font-size: 11.5px; max-width: 64ch; margin: 0; }
#gf-dashboard .gf-status { display: flex; gap: 12px; font-size: 11px; color: var(--gf-muted); }
#gf-dashboard .gf-status .gf-dot {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: var(--gf-muted); margin-right: 6px; vertical-align: middle;
}
#gf-dashboard .gf-status .gf-dot.ok { background: var(--gf-safe-solid); box-shadow: 0 0 6px var(--gf-safe-solid); }
#gf-dashboard .gf-status .gf-dot.err { background: var(--gf-danger-solid); }
#gf-dashboard .gf-grid {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 14px;
}
@media (max-width: 1100px) {
#gf-dashboard .gf-grid { grid-template-columns: 1fr; }
}
#gf-dashboard .panel {
background: var(--gf-panel);
border: 1px solid var(--gf-border);
border-radius: 10px;
padding: 12px 14px;
display: flex; flex-direction: column;
min-height: 280px;
overflow: hidden;
}
#gf-dashboard .panel h3 {
margin: 0 0 10px; font-size: 11px;
text-transform: uppercase; letter-spacing: 0.16em;
color: var(--gf-muted); font-weight: 600;
display: flex; align-items: center; justify-content: space-between;
}
#gf-dashboard .panel h3 .sub {
color: var(--gf-muted); font-weight: 400; letter-spacing: 0;
text-transform: none; font-size: 10.5px;
}
/* Panel 1: live token stream */
#gf-dashboard #panel-stream #stream-output {
background: var(--gf-panel-2); border: 1px solid var(--gf-border);
border-radius: 6px; padding: 10px 12px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12.5px; line-height: 1.65;
white-space: pre-wrap;
flex: 1; min-height: 120px; max-height: 220px; overflow-y: auto;
}
#gf-dashboard #panel-stream #stream-output.empty {
display: flex; align-items: center; justify-content: center;
color: var(--gf-muted);
font-family: -apple-system, system-ui, sans-serif; font-size: 12px;
}
#gf-dashboard #panel-stream .tok {
padding: 1px 1px 2px; border-radius: 2px;
transition: background-color 0.08s ease-in;
}
#gf-dashboard .meter-wrap { margin-top: 8px; }
#gf-dashboard .meter {
height: 8px; border-radius: 999px;
background: linear-gradient(to right, #28a765 0%, #28a765 40%, #e6a23c 40%, #e6a23c 70%, #ef4444 70%, #ef4444 100%);
position: relative; box-shadow: inset 0 1px 2px rgba(0,0,0,0.4);
}
#gf-dashboard .meter .needle {
position: absolute; top: -3px; width: 3px; height: 14px;
background: #fff; border-radius: 2px;
transform: translateX(-1.5px);
transition: left 0.1s linear;
box-shadow: 0 0 6px rgba(255,255,255,0.5);
}
#gf-dashboard .stats {
margin-top: 8px;
display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px;
}
#gf-dashboard .stats .stat {
background: var(--gf-panel-2); border: 1px solid var(--gf-border);
border-radius: 5px; padding: 6px 8px;
}
#gf-dashboard .stats .stat .v { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
#gf-dashboard .stats .stat .v.cwe { font-size: 12px; font-family: ui-monospace, monospace; color: var(--gf-accent); }
#gf-dashboard .stats .stat .l { font-size: 9.5px; text-transform: uppercase; color: var(--gf-muted); letter-spacing: 0.08em; margin-top: 1px; }
#gf-dashboard .stats .stat.maxpool .v { color: var(--gf-accent); }
/* Panel 2: scan progress */
#gf-dashboard .scan-counters {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px;
margin-bottom: 10px;
}
#gf-dashboard .scan-counters .c {
background: var(--gf-panel-2); border: 1px solid var(--gf-border);
border-radius: 5px; padding: 6px 8px;
}
#gf-dashboard .scan-counters .c .v { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
#gf-dashboard .scan-counters .c .l { font-size: 9.5px; text-transform: uppercase; color: var(--gf-muted); letter-spacing: 0.08em; margin-top: 1px; }
#gf-dashboard #scan-feed {
flex: 1; overflow-y: auto;
background: var(--gf-panel-2); border: 1px solid var(--gf-border);
border-radius: 6px; padding: 8px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 11.5px;
min-height: 0; max-height: 280px;
}
#gf-dashboard .file-row {
margin-bottom: 8px; padding: 6px 8px; border-radius: 4px;
background: rgba(255,255,255,0.015);
}
#gf-dashboard .file-row .name {
display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px;
}
#gf-dashboard .file-row .name .p { color: var(--gf-text); word-break: break-all; }
#gf-dashboard .file-row .name .n { color: var(--gf-muted); font-size: 10.5px; white-space: nowrap; }
#gf-dashboard .file-row .strip {
display: flex; flex-wrap: wrap; gap: 2px; min-height: 8px;
}
#gf-dashboard .file-row .strip .dot {
width: 7px; height: 7px; border-radius: 2px;
background: var(--gf-safe);
}
#gf-dashboard .file-row .strip .dot.warn { background: var(--gf-warn); }
#gf-dashboard .file-row .strip .dot.danger { background: var(--gf-danger); }
#gf-dashboard .file-row .strip .dot[title] { cursor: help; }
#gf-dashboard .scan-empty {
color: var(--gf-muted); text-align: center; padding: 20px 0;
font-family: -apple-system, system-ui, sans-serif; font-size: 12px;
}
/* Panel 3: leads table */
#gf-dashboard #leads-table {
flex: 1; overflow-y: auto;
background: var(--gf-panel-2); border: 1px solid var(--gf-border);
border-radius: 6px;
min-height: 0; max-height: 280px;
}
#gf-dashboard table.leads {
width: 100%; border-collapse: collapse; font-size: 11.5px;
}
#gf-dashboard table.leads thead th {
position: sticky; top: 0;
background: var(--gf-panel-2);
color: var(--gf-muted);
font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;
font-size: 10px; text-align: left;
padding: 8px 8px; border-bottom: 1px solid var(--gf-border);
}
#gf-dashboard table.leads tbody tr {
border-bottom: 1px solid var(--gf-border);
}
#gf-dashboard table.leads tbody tr:hover { background: rgba(255,255,255,0.02); }
#gf-dashboard table.leads tbody td { padding: 7px 8px; vertical-align: top; }
#gf-dashboard table.leads tbody td.rank { color: var(--gf-muted); font-variant-numeric: tabular-nums; }
#gf-dashboard table.leads tbody td.conf .gf-confpill {
display: inline-block; padding: 2px 6px; border-radius: 4px;
font-size: 10.5px; font-weight: 700;
}
#gf-dashboard table.leads tbody td.conf .gf-confpill.danger { background: var(--gf-danger); color: #fff; }
#gf-dashboard table.leads tbody td.conf .gf-confpill.warn { background: var(--gf-warn); color: #1a1a1a; }
#gf-dashboard table.leads tbody td.conf .gf-confpill.safe { background: var(--gf-safe); color: #fff; }
#gf-dashboard table.leads tbody td.cwe { color: var(--gf-accent); font-family: ui-monospace, monospace; font-size: 11px; }
#gf-dashboard table.leads tbody td.file {
color: var(--gf-text); font-family: ui-monospace, monospace; font-size: 11px;
word-break: break-all;
}
#gf-dashboard table.leads tbody td.preview {
color: var(--gf-muted); font-family: ui-monospace, monospace; font-size: 10.5px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0;
}
/* Panel 4: pwnkit hand-off */
#gf-dashboard .pwnkit { flex: 1; display: flex; flex-direction: column; gap: 10px; min-height: 0; }
#gf-dashboard .pwnkit-pills {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
}
#gf-dashboard .pwnkit-pill {
background: var(--gf-panel-2); border: 1px solid var(--gf-border);
border-radius: 6px; padding: 8px 10px; text-align: center;
}
#gf-dashboard .pwnkit-pill .v { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
#gf-dashboard .pwnkit-pill.queued .v { color: var(--gf-accent); }
#gf-dashboard .pwnkit-pill.exploit .v { color: var(--gf-warn-solid); }
#gf-dashboard .pwnkit-pill.report .v { color: var(--gf-safe-solid); }
#gf-dashboard .pwnkit-pill .l {
font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em;
color: var(--gf-muted); margin-top: 3px;
}
#gf-dashboard .pwnkit-log {
flex: 1; background: var(--gf-panel-2); border: 1px solid var(--gf-border);
border-radius: 6px; padding: 8px 10px;
font-family: ui-monospace, monospace; font-size: 11px; line-height: 1.55;
overflow-y: auto; color: var(--gf-muted);
min-height: 0; max-height: 240px;
}
#gf-dashboard .pwnkit-log .line { padding: 2px 0; }
#gf-dashboard .pwnkit-log .line.ok { color: var(--gf-safe-solid); }
#gf-dashboard .pwnkit-log .line.warn { color: var(--gf-warn-solid); }
#gf-dashboard .pwnkit-log .line.danger { color: var(--gf-danger-solid); }
#gf-dashboard .pwnkit-log .line .ts { color: var(--gf-muted); margin-right: 8px; }
#gf-dashboard .pwnkit-note { font-size: 10.5px; color: var(--gf-muted); font-style: italic; }
</style>
<div id="gf-dashboard">
<div class="gf-head">
<div class="gf-brand">
<h2>Live pipeline</h2>
<span class="gf-pill">4 panels · real-time</span>
</div>
<p class="gf-tagline">
Streaming tokens with probe risk &middot; repo scan progress &middot; ranked leads &middot;
hand-off to pwnkit. Every panel is backed by real data from this Space.
</p>
<div class="gf-status">
<span><span class="gf-dot" id="dot-stream"></span><span id="lbl-stream">probe stream idle</span></span>
<span><span class="gf-dot" id="dot-events"></span><span id="lbl-events">scan idle</span></span>
<span><span class="gf-dot" id="dot-pwnkit"></span><span id="lbl-pwnkit">pwnkit idle</span></span>
</div>
</div>
<div class="gf-grid">
<!-- Panel 1 -->
<section class="panel" id="panel-stream">
<h3>Live token stream <span class="sub">Gemma 4 &middot; linear probe layer 17</span></h3>
<div id="stream-output" class="empty">
Type a prompt above and click <b>Generate with probe</b>.
Tokens stream here, colored by per-token probe risk
(<span style="color:#5fb96b">safe</span> &middot;
<span style="color:#e6a23c">elevated</span> &middot;
<span style="color:#ef4444">high</span>).
</div>
<div class="meter-wrap">
<div class="meter"><div class="needle" id="meter-needle" style="left: 0%"></div></div>
</div>
<div class="stats">
<div class="stat"><div class="v" id="stat-tokens">0</div><div class="l">tokens</div></div>
<div class="stat"><div class="v" id="stat-peak">0%</div><div class="l">peak</div></div>
<div class="stat"><div class="v" id="stat-avg">0%</div><div class="l">mean</div></div>
<div class="stat maxpool"><div class="v" id="stat-maxpool">0%</div><div class="l">max-pool</div></div>
<div class="stat"><div class="v cwe" id="stat-top-cwe">&mdash;</div><div class="l">top cwe</div></div>
</div>
</section>
<!-- Panel 2 -->
<section class="panel" id="panel-scan">
<h3>Repo scan <span class="sub">gemmaforge.events/v1</span></h3>
<div class="scan-counters">
<div class="c"><div class="v" id="scan-files">0</div><div class="l">files</div></div>
<div class="c"><div class="v" id="scan-chunks">0</div><div class="l">chunks</div></div>
<div class="c"><div class="v" id="scan-leads">0</div><div class="l">leads</div></div>
<div class="c"><div class="v" id="scan-elapsed">0s</div><div class="l">elapsed</div></div>
</div>
<div id="scan-feed">
<div class="scan-empty">Run a scan above &mdash; per-file progress streams here.</div>
</div>
</section>
<!-- Panel 3 -->
<section class="panel" id="panel-leads">
<h3>Top leads <span class="sub">rolling, sorted by confidence</span></h3>
<div id="leads-table">
<table class="leads">
<thead>
<tr><th>#</th><th>conf</th><th>cwe</th><th>file:lines</th><th>preview</th></tr>
</thead>
<tbody id="leads-body">
<tr><td colspan="5" style="color:#6b7280;text-align:center;padding:18px 0;font-family:system-ui;">No leads yet.</td></tr>
</tbody>
</table>
</div>
</section>
<!-- Panel 4 -->
<section class="panel" id="panel-pwnkit">
<h3>Pwnkit hand-off <span class="sub">downstream agent, live stderr</span></h3>
<div class="pwnkit">
<div class="pwnkit-pills">
<div class="pwnkit-pill queued"><div class="v" id="pk-queued">0</div><div class="l">leads queued</div></div>
<div class="pwnkit-pill exploit"><div class="v" id="pk-exploit">0</div><div class="l">tool calls</div></div>
<div class="pwnkit-pill report"><div class="v" id="pk-report">0</div><div class="l">findings</div></div>
</div>
<div class="pwnkit-log" id="pk-log">
<div class="line">Run the Tier 2 pwnkit accordion below to populate this panel.</div>
</div>
<div class="pwnkit-note">
Each line above is a real pwnkit stderr line parsed live. <code>save_finding</code> rows are green.
</div>
</div>
</section>
</div>
</div>
<script>
(function () {
if (window.__gfDashboardInit) return;
window.__gfDashboardInit = true;
const $ = (id) => document.getElementById(id);
const escapeHtml = (s) => String(s)
.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
function classifyConf(c) {
if (c >= 0.7) return "danger";
if (c >= 0.4) return "warn";
return "safe";
}
function riskColor(r) {
if (r >= 0.7) return "rgba(239,68,68,0.55)";
if (r >= 0.4) return "rgba(230,162,60,0.55)";
return "rgba(40,167,101,0.32)";
}
// ----- Panel 1: token stream ----- //
let lastStreamSeq = -1;
function renderStream(state) {
if (!state) return;
const seq = state.seq | 0;
if (state.reset) {
$("stream-output").innerHTML = "";
$("stream-output").classList.remove("empty");
$("stat-tokens").textContent = "0";
$("stat-peak").textContent = "0%";
$("stat-avg").textContent = "0%";
$("stat-maxpool").textContent = "0%";
$("stat-top-cwe").innerHTML = "&mdash;";
$("meter-needle").style.left = "0%";
lastStreamSeq = -1;
}
const tokens = state.tokens || [];
// append only new tokens
for (let i = lastStreamSeq + 1; i < tokens.length; i++) {
const t = tokens[i];
const span = document.createElement("span");
span.className = "tok";
span.style.backgroundColor = riskColor(t.risk_maxpool || t.risk || 0);
span.title = `risk ${(((t.risk_maxpool || t.risk || 0))*100).toFixed(1)}% · cwe ${t.top_cwe || '—'}`;
span.textContent = t.token;
$("stream-output").appendChild(span);
}
if (tokens.length > 0) {
lastStreamSeq = tokens.length - 1;
$("stream-output").scrollTop = $("stream-output").scrollHeight;
}
const stats = state.stats || {};
$("stat-tokens").textContent = String(stats.n || tokens.length || 0);
$("stat-peak").textContent = ((stats.peak || 0) * 100).toFixed(0) + "%";
$("stat-avg").textContent = ((stats.mean || 0) * 100).toFixed(0) + "%";
$("stat-maxpool").textContent = ((stats.maxpool || 0) * 100).toFixed(0) + "%";
$("meter-needle").style.left = ((stats.mean || 0) * 100).toFixed(1) + "%";
if (stats.top_cwe) $("stat-top-cwe").textContent = stats.top_cwe;
$("dot-stream").className = "gf-dot " + (state.active ? "ok" : "");
$("lbl-stream").textContent = state.active ? "probe stream live" : (tokens.length ? "probe stream done" : "probe stream idle");
}
// ----- Panel 2 + 3: scan + leads ----- //
function renderScan(state) {
if (!state) return;
const counters = state.counters || {};
$("scan-files").textContent = String(counters.files || 0);
$("scan-chunks").textContent = String(counters.chunks || 0);
$("scan-leads").textContent = String(counters.leads || 0);
$("scan-elapsed").textContent = (counters.elapsed || 0).toFixed(1) + "s";
const feed = $("scan-feed");
const files = state.files || {};
const order = state.file_order || Object.keys(files);
if (!order.length) {
feed.innerHTML = '<div class="scan-empty">Run a scan above &mdash; per-file progress streams here.</div>';
} else {
// Rebuild only if file count changed; otherwise update strips in place.
const existing = feed.querySelectorAll(".file-row");
if (existing.length !== order.length) {
feed.innerHTML = "";
order.forEach((f) => {
const fr = document.createElement("div");
fr.className = "file-row";
fr.dataset.file = f;
fr.innerHTML = `<div class="name"><span class="p"></span><span class="n"></span></div><div class="strip"></div>`;
fr.querySelector(".p").textContent = f;
feed.appendChild(fr);
});
}
order.forEach((f) => {
const info = files[f];
if (!info) return;
const fr = feed.querySelector(`.file-row[data-file="${CSS.escape(f)}"]`);
if (!fr) return;
const expected = info.expected || info.scores.length || 0;
fr.querySelector(".n").textContent = `${info.scores.length}/${expected} chunks`;
const strip = fr.querySelector(".strip");
// Append only the new dots.
const haveDots = strip.childElementCount;
for (let i = haveDots; i < info.scores.length; i++) {
const s = info.scores[i];
const d = document.createElement("span");
const cls = classifyConf(s.confidence || 0);
d.className = `dot ${cls}`;
d.title = `lines ${s.start_line}-${s.end_line} · conf ${(((s.confidence||0))*100).toFixed(0)}% · ${s.top_cwe || ''}`;
strip.appendChild(d);
}
});
feed.scrollTop = feed.scrollHeight;
}
const leads = state.leads || [];
const body = $("leads-body");
if (!leads.length) {
body.innerHTML = '<tr><td colspan="5" style="color:#6b7280;text-align:center;padding:18px 0;font-family:system-ui;">No leads yet.</td></tr>';
} else {
const sorted = [...leads].sort((a, b) => (b.confidence || 0) - (a.confidence || 0));
body.innerHTML = sorted.map((l, i) => {
const cls = classifyConf(l.confidence || 0);
const preview = (l.preview || "").trim();
return `<tr>
<td class="rank">${i + 1}</td>
<td class="conf"><span class="gf-confpill ${cls}">${((l.confidence||0)*100).toFixed(0)}%</span></td>
<td class="cwe">${escapeHtml(l.top_cwe || '—')}</td>
<td class="file">${escapeHtml(l.location || '')}</td>
<td class="preview">${escapeHtml(preview)}</td>
</tr>`;
}).join("");
}
$("dot-events").className = "gf-dot " + (state.active ? "ok" : (counters.files ? "" : ""));
$("lbl-events").textContent = state.active ? `scan live · ${counters.chunks||0} chunks` : (counters.files ? `scan done · ${counters.leads||0} leads` : "scan idle");
}
// ----- Panel 4: pwnkit ----- //
let lastPkLogSeq = -1;
function renderPwnkit(state) {
if (!state) return;
if (state.reset) {
$("pk-log").innerHTML = "";
lastPkLogSeq = -1;
}
const counts = state.counts || {};
$("pk-queued").textContent = String(counts.queued || 0);
$("pk-exploit").textContent = String(counts.tools || 0);
$("pk-report").textContent = String(counts.findings || 0);
const log = state.log || [];
for (let i = lastPkLogSeq + 1; i < log.length; i++) {
const e = log[i];
const div = document.createElement("div");
div.className = "line " + (e.level || "");
const ts = new Date((e.ts || Date.now() / 1000) * 1000).toLocaleTimeString([], { hour12: false });
div.innerHTML = `<span class="ts">${ts}</span>${escapeHtml(e.message || "")}`;
$("pk-log").appendChild(div);
}
if (log.length) {
lastPkLogSeq = log.length - 1;
$("pk-log").scrollTop = $("pk-log").scrollHeight;
// bounded
while ($("pk-log").childNodes.length > 200) $("pk-log").removeChild($("pk-log").firstChild);
}
$("dot-pwnkit").className = "gf-dot " + (state.active ? "ok" : "");
$("lbl-pwnkit").textContent = state.active ? `pwnkit live · ${counts.tools||0} calls` : (log.length ? `pwnkit done · ${counts.findings||0} findings` : "pwnkit idle");
}
// ----- bridge from hidden gr.JSON components ----- //
// Each gr.JSON renders as <div class="gradio-json"> containing a <pre>
// with the serialised value. We watch the four mount points by elem_id.
function readJSON(elemId) {
const root = document.getElementById(elemId);
if (!root) return null;
// gr.JSON renders into a nested element; grab the deepest .json-holder or pre.
const pre = root.querySelector("pre, .json-holder code, code");
if (!pre) return null;
const txt = (pre.innerText || pre.textContent || "").trim();
if (!txt) return null;
try { return JSON.parse(txt); }
catch (_) {
// gr.JSON may render expandable nodes — fall back to data attribute set by Python tick.
try { return JSON.parse(root.dataset.value || "null"); } catch (__) { return null; }
}
}
function tick() {
try { renderStream(readJSON("gf-state-stream")); } catch (e) { console.warn(e); }
try { renderScan(readJSON("gf-state-scan")); } catch (e) { console.warn(e); }
try { renderPwnkit(readJSON("gf-state-pwnkit")); } catch (e) { console.warn(e); }
}
// Poll every 250ms — Gradio's gr.JSON doesn't fire DOM mutation observers
// reliably across all versions, and a quarter-second tick is cheap.
setInterval(tick, 250);
// Also tick once on load (might race with Gradio mount, hence the interval too).
setTimeout(tick, 500);
})();
</script>
"""
def initial_stream_state() -> dict:
return {
"active": False, "reset": True, "seq": 0,
"tokens": [],
"stats": {"n": 0, "peak": 0.0, "mean": 0.0, "maxpool": 0.0, "top_cwe": None},
}
def initial_scan_state() -> dict:
return {
"active": False,
"counters": {"files": 0, "chunks": 0, "leads": 0, "elapsed": 0.0},
"file_order": [],
"files": {},
"leads": [],
}
def initial_pwnkit_state() -> dict:
return {
"active": False, "reset": True,
"counts": {"queued": 0, "tools": 0, "findings": 0},
"log": [],
}