CounterFeint / server /static /investigate_hq.html
QuantumTransformer's picture
Upload folder using huggingface_hub
7c6b9e9 verified
Raw
History Blame Contribute Delete
82.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CounterFeint — Multi-Agent Ad Fraud Arena</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0a0d14;
--surface: rgba(255,255,255,0.035);
--surface-hover: rgba(255,255,255,0.07);
--border: rgba(255,255,255,0.07);
--text: #e2e8f0;
--text-dim: #8b95a5;
--text-muted: #5a6377;
--indigo: #6366f1;
--indigo-glow: rgba(99,102,241,0.25);
--red: #ef4444;
--red-glow: rgba(239,68,68,0.2);
--green: #22c55e;
--green-glow: rgba(34,197,94,0.2);
--amber: #f59e0b;
--amber-glow: rgba(245,158,11,0.2);
--cyan: #06b6d4;
--cyan-glow: rgba(6,182,212,0.15);
--radius: 14px;
--radius-sm: 8px;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
width: 500px; height: 500px;
background: radial-gradient(circle, rgba(99,102,241,0.12) 0%, transparent 70%);
top: -180px; left: -80px;
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
.container { max-width: 1360px; margin: 0 auto; padding: 20px 24px; position: relative; z-index: 1; }
/* ── Header ── */
header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; flex-wrap: wrap; gap: 16px; }
.logo { display: flex; align-items: center; gap: 14px; }
.logo-icon {
width: 44px; height: 44px;
background: linear-gradient(135deg, var(--indigo), var(--cyan));
border-radius: 12px;
display: grid; place-items: center;
font-size: 20px;
box-shadow: 0 4px 20px var(--indigo-glow);
}
.logo h1 { font-size: 1.2rem; font-weight: 800; letter-spacing: -0.3px; }
.logo span { display: block; font-size: 0.68rem; color: var(--text-dim); margin-top: 1px; }
.header-badges { display: flex; gap: 8px; }
.badge {
padding: 5px 12px; border-radius: 999px; font-size: 0.62rem;
font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase;
}
.badge-accent { background: var(--indigo-glow); color: #a5b4fc; border: 1px solid rgba(99,102,241,0.25); }
.badge-green { background: var(--green-glow); color: #86efac; border: 1px solid rgba(34,197,94,0.25); display: flex; align-items: center; gap: 5px; }
.pulse { width: 6px; height: 6px; background: var(--green); border-radius: 50%; animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100%{box-shadow:0 0 0 0 rgba(34,197,94,0.3)} 50%{box-shadow:0 0 0 6px transparent} }
/* ── Tabs ── */
.tab-bar {
display: flex; gap: 6px; margin-bottom: 24px;
background: var(--surface); border-radius: var(--radius-sm);
padding: 5px 6px; width: fit-content;
}
.tab-btn {
padding: 9px 22px; border: none; border-radius: 6px;
font-family: inherit; font-size: 0.78rem; font-weight: 600;
color: var(--text-dim); background: transparent; cursor: pointer;
transition: all 0.2s;
position: relative;
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active { background: var(--indigo); color: #fff; box-shadow: 0 2px 12px var(--indigo-glow); }
/* Tab tooltips */
.tab-btn[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
padding: 10px 16px;
background: rgba(15,23,42,0.96);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 10px;
font-size: 0.68rem;
font-weight: 400;
color: var(--text-dim);
white-space: normal;
width: 240px;
text-align: center;
line-height: 1.5;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 100;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.tab-btn[data-tooltip]:hover::after { opacity: 1; }
.tab-content { display: none; }
.tab-content.active { display: block; }
/* ── Shared components ── */
.panel {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); overflow: hidden; margin-bottom: 16px;
}
.panel-hdr {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 18px; border-bottom: 1px solid var(--border);
}
.panel-title { font-size: 0.82rem; font-weight: 700; }
.panel-body { padding: 16px 18px; }
.btn {
display: inline-flex; align-items: center; gap: 7px;
padding: 10px 20px; border: none; border-radius: var(--radius-sm);
font-family: inherit; font-size: 0.78rem; font-weight: 600;
cursor: pointer; transition: all 0.2s;
}
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary { background: linear-gradient(135deg, var(--indigo), #818cf8); color: #fff; box-shadow: 0 3px 14px var(--indigo-glow); }
.btn-red { background: linear-gradient(135deg, #dc2626, var(--red)); color: #fff; box-shadow: 0 3px 14px var(--red-glow); }
.btn-green { background: linear-gradient(135deg, #059669, var(--green)); color: #fff; box-shadow: 0 3px 14px var(--green-glow); }
.btn-amber { background: linear-gradient(135deg, #d97706, var(--amber)); color: #fff; box-shadow: 0 3px 14px var(--amber-glow); }
.btn-ghost { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
.stat-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 14px 16px;
}
.stat-label { font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: var(--text-dim); margin-bottom: 4px; }
.stat-value { font-size: 1.5rem; font-weight: 800; }
/* Guidance banners */
.guide-banner {
margin-bottom: 18px; padding: 14px 20px; border-radius: var(--radius);
font-size: 0.78rem; line-height: 1.6; color: var(--text-dim);
}
.guide-banner strong { color: var(--text); }
.guide-banner code { color: var(--cyan); font-size: 0.74rem; }
.guide-banner.arena { background: linear-gradient(135deg,rgba(239,68,68,0.04),rgba(99,102,241,0.05)); border: 1px solid rgba(99,102,241,0.15); }
.guide-banner.playground { background: linear-gradient(135deg,rgba(99,102,241,0.06),rgba(6,182,212,0.04)); border: 1px solid rgba(99,102,241,0.2); }
.guide-banner.results { background: linear-gradient(135deg,rgba(34,197,94,0.06),rgba(6,182,212,0.04)); border: 1px solid rgba(34,197,94,0.2); }
/* ── Toast ── */
.toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 1000; display: flex; flex-direction: column; gap: 6px; }
.toast {
padding: 12px 18px; border-radius: var(--radius-sm); font-size: 0.78rem;
max-width: 340px; animation: slideIn 0.3s ease;
}
.toast.success { background: rgba(34,197,94,0.15); border: 1px solid rgba(34,197,94,0.3); color: #86efac; }
.toast.error { background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); color: #fca5a5; }
.toast.info { background: rgba(6,182,212,0.12); border: 1px solid rgba(6,182,212,0.3); color: #67e8f9; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
/* ════════════════════════ ARENA TAB ════════════════════════ */
.agent-icon {
width: 48px; height: 48px; border-radius: 14px;
display: grid; place-items: center; font-size: 22px;
flex-shrink: 0;
}
.agent-icon.fraudster { background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(239,68,68,0.08)); border: 1px solid rgba(239,68,68,0.3); }
.agent-icon.investigator { background: linear-gradient(135deg, rgba(99,102,241,0.2), rgba(99,102,241,0.08)); border: 1px solid rgba(99,102,241,0.3); }
.agent-icon.auditor { background: linear-gradient(135deg, rgba(245,158,11,0.2), rgba(245,158,11,0.08)); border: 1px solid rgba(245,158,11,0.3); }
.agent-label { font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; }
.agent-label.fraudster { color: var(--red); }
.agent-label.investigator { color: var(--indigo); }
.agent-label.auditor { color: var(--amber); }
.phase-bar {
display: flex; align-items: center; gap: 0; margin-bottom: 20px;
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 10px 16px; overflow-x: auto;
}
.phase-step {
display: flex; align-items: center; gap: 8px;
padding: 6px 14px; border-radius: 6px; font-size: 0.72rem;
font-weight: 600; color: var(--text-muted); white-space: nowrap;
transition: all 0.3s;
}
.phase-step.active { color: #fff; }
.phase-step.active.f { background: var(--red-glow); color: var(--red); }
.phase-step.active.i { background: var(--indigo-glow); color: #a5b4fc; }
.phase-step.active.a { background: var(--amber-glow); color: var(--amber); }
.phase-step.active.d { background: var(--green-glow); color: var(--green); }
.phase-step.done-phase { color: var(--text-dim); }
.phase-arrow { color: var(--text-muted); font-size: 0.7rem; margin: 0 4px; }
.arena-controls {
display: flex; gap: 10px; margin-bottom: 18px; flex-wrap: wrap; align-items: center;
}
.arena-controls select {
padding: 10px 14px; border-radius: var(--radius-sm); font-size: 0.78rem;
font-family: inherit; background: #0f172a; color: #f1f5f9;
border: 1px solid rgba(148,163,184,0.35);
}
.arena-controls select option { background: #0f172a; color: #f1f5f9; }
.arena-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-bottom: 18px; }
.arena-main { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; margin-bottom: 16px; }
@media (max-width: 1024px) { .arena-main { grid-template-columns: 1fr; } }
.agent-hdr { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--border); }
.agent-hdr-info { flex: 1; }
.agent-name { font-size: 0.85rem; font-weight: 700; }
.agent-desc { font-size: 0.65rem; color: var(--text-dim); margin-top: 2px; }
.reward-badge {
padding: 4px 10px; border-radius: 999px; font-size: 0.68rem;
font-weight: 700; font-variant-numeric: tabular-nums;
}
.reward-badge.pos { background: var(--green-glow); color: var(--green); }
.reward-badge.neg { background: var(--red-glow); color: var(--red); }
.reward-badge.zero { background: var(--surface); color: var(--text-dim); border: 1px solid var(--border); }
.trace-timeline {
max-height: 320px; overflow-y: auto; padding: 12px 16px;
font-size: 0.72rem;
}
.trace-entry {
display: flex; align-items: flex-start; gap: 10px;
padding: 8px 0; border-bottom: 1px solid var(--border);
animation: fadeIn 0.3s ease;
}
.trace-entry:last-child { border-bottom: none; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.trace-dot {
width: 8px; height: 8px; border-radius: 50%; margin-top: 4px; flex-shrink: 0;
}
.trace-dot.fraudster { background: var(--red); }
.trace-dot.investigator { background: var(--indigo); }
.trace-dot.auditor { background: var(--amber); }
.trace-text { flex: 1; color: var(--text-dim); line-height: 1.4; }
.trace-text strong { color: var(--text); }
.trace-reward { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.trace-reward.pos { color: var(--green); }
.trace-reward.neg { color: var(--red); }
.chart-container {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 16px 20px; margin-bottom: 16px;
}
.chart-title { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: var(--text-dim); margin-bottom: 10px; }
.chart-legend { display: flex; gap: 18px; margin-top: 10px; }
.chart-legend-item { display: flex; align-items: center; gap: 6px; font-size: 0.65rem; color: var(--text-dim); }
.chart-legend-dot { width: 8px; height: 8px; border-radius: 2px; }
.queue-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.q-chip {
padding: 6px 12px; border-radius: 6px; font-size: 0.7rem; font-weight: 600;
border: 1px solid var(--border); color: var(--text-dim);
display: inline-flex; align-items: center; gap: 5px;
transition: all 0.2s;
}
.q-chip.pending { border-color: var(--text-muted); }
.q-chip.approved { border-color: var(--green); color: var(--green); }
.q-chip.rejected { border-color: var(--red); color: var(--red); }
.q-chip.escalated { border-color: var(--cyan); color: var(--cyan); }
.q-chip.proposed { border-color: var(--amber); color: var(--amber); }
.q-dot { width: 6px; height: 6px; border-radius: 50%; }
.audit-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 768px) { .audit-grid { grid-template-columns: 1fr; } }
.flag-entry {
padding: 8px 12px; border-radius: 6px;
border: 1px solid var(--border); font-size: 0.72rem;
background: rgba(0,0,0,0.15);
}
.flag-entry .flag-type { font-weight: 700; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.5px; }
.flag-entry.track-a .flag-type { color: var(--indigo); }
.flag-entry.track-b .flag-type { color: var(--amber); }
.severity-bar {
height: 3px; border-radius: 2px; margin-top: 4px;
background: rgba(255,255,255,0.06);
}
.severity-fill { height: 100%; border-radius: 2px; }
.severity-fill.low { background: var(--green); }
.severity-fill.med { background: var(--amber); }
.severity-fill.high { background: var(--red); }
/* Arena agent action scrollable area */
.agent-actions-scroll {
max-height: 400px; overflow-y: auto; scroll-behavior: smooth;
scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.15) transparent;
}
.agent-actions-scroll::-webkit-scrollbar { width: 5px; }
.agent-actions-scroll::-webkit-scrollbar-track { background: transparent; }
.agent-actions-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
/* ════════════════════════ PLAYGROUND TAB ════════════════════════ */
.pg-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-bottom: 16px; }
.pg-grid { display: grid; grid-template-columns: 1fr 360px; gap: 16px; }
@media (max-width: 1024px) { .pg-grid { grid-template-columns: 1fr; } }
.inv-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
@media (max-width: 768px) { .inv-grid { grid-template-columns: repeat(2, 1fr); } }
.inv-card {
border: 1px solid var(--border); border-radius: var(--radius-sm);
padding: 10px 12px; min-height: 76px; position: relative;
overflow: hidden; background: rgba(0,0,0,0.12);
}
.inv-card.revealed { border-color: rgba(99,102,241,0.4); }
.inv-card.locked .inv-inner { filter: blur(4px); opacity: 0.15; }
.inv-card.locked::after {
content: ''; position: absolute; inset: 0;
background: repeating-linear-gradient(-45deg, transparent, transparent 5px, rgba(255,255,255,0.03) 5px, rgba(255,255,255,0.03) 10px);
pointer-events: none;
}
.inv-label { font-size: 0.58rem; text-transform: uppercase; letter-spacing: 0.7px; font-weight: 700; color: var(--indigo); margin-bottom: 4px; }
.inv-card.locked .inv-label { color: var(--text-muted); }
.inv-content { font-size: 0.68rem; line-height: 1.35; color: var(--text-dim); }
.lock-icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 1.1rem; z-index: 2; }
.form-group label {
display: block; font-size: 0.62rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.7px; color: var(--text-dim); margin-bottom: 5px;
}
.form-group select, .form-group input, .form-group textarea {
width: 100%; padding: 9px 12px; background: rgba(15,23,42,0.95);
border: 1px solid var(--border); border-radius: var(--radius-sm);
color: #f1f5f9; font-family: inherit; font-size: 0.78rem; outline: none;
}
.form-group select option { background: #0f172a; color: #f1f5f9; }
.form-group textarea { min-height: 80px; resize: vertical; line-height: 1.4; }
.form-group select:focus, .form-group input:focus, .form-group textarea:focus {
border-color: var(--indigo); box-shadow: 0 0 0 2px var(--indigo-glow);
}
.action-form { display: flex; flex-direction: column; gap: 12px; }
.verdict-row {
display: flex; justify-content: space-between; align-items: center;
padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px;
margin-bottom: 4px; font-size: 0.75rem;
}
.v-badge { padding: 2px 8px; border-radius: 999px; font-size: 0.58rem; font-weight: 700; text-transform: uppercase; }
.v-badge.approve { background: var(--green-glow); color: var(--green); }
.v-badge.reject { background: var(--red-glow); color: var(--red); }
.v-badge.escalate { background: var(--indigo-glow); color: #a5b4fc; }
.ad-queue { display: flex; flex-wrap: wrap; gap: 8px; }
.ad-chip {
padding: 7px 12px; border-radius: var(--radius-sm); border: 1px solid var(--border);
font-size: 0.75rem; font-weight: 600; color: var(--text-dim);
display: inline-flex; align-items: center; gap: 6px;
}
.ad-chip.focus { border-color: var(--amber); color: var(--cyan); }
.ad-chip.approved { border-color: var(--green); color: var(--green); }
.ad-chip.rejected { border-color: var(--red); color: var(--red); }
.ad-copy-block {
background: rgba(0,0,0,0.2); border-left: 3px solid var(--cyan);
padding: 10px 14px; border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
font-style: italic; color: var(--text-dim); line-height: 1.45; font-size: 0.8rem;
}
.hidden { display: none !important; }
.log-area { max-height: 180px; overflow-y: auto; font-family: ui-monospace, monospace; font-size: 0.65rem; }
.log-entry { padding: 6px 8px; border-radius: 4px; margin-bottom: 3px; background: rgba(0,0,0,0.15); color: var(--text-dim); }
.log-entry.ok { color: var(--green); }
.log-entry.bad { color: var(--red); }
/* ════════════════════════ RESULTS TAB ════════════════════════ */
.results-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 768px) { .results-grid { grid-template-columns: 1fr; } }
.results-table {
width: 100%; border-collapse: collapse; font-size: 0.75rem;
}
.results-table th {
text-align: left; padding: 10px 12px; font-size: 0.62rem;
font-weight: 700; text-transform: uppercase; letter-spacing: 0.7px;
color: var(--text-dim); border-bottom: 1px solid var(--border);
}
.results-table td {
padding: 10px 12px; border-bottom: 1px solid var(--border);
font-variant-numeric: tabular-nums;
}
.results-table tr:last-child td { border-bottom: none; }
.score-pill {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-weight: 700; font-size: 0.72rem;
}
.score-pill.good { background: var(--green-glow); color: var(--green); }
.score-pill.mid { background: var(--amber-glow); color: var(--amber); }
.score-pill.low { background: var(--red-glow); color: var(--red); }
.arch-diagram {
display: flex; align-items: center; justify-content: center; gap: 24px;
padding: 28px 20px; flex-wrap: wrap;
}
.arch-node {
display: flex; flex-direction: column; align-items: center; gap: 8px;
padding: 18px 20px; border-radius: var(--radius); border: 1px solid var(--border);
background: var(--surface); min-width: 140px; text-align: center;
transition: transform 0.2s;
}
.arch-node:hover { transform: translateY(-3px); }
.arch-arrow { font-size: 1.4rem; color: var(--text-muted); }
.arch-node-name { font-size: 0.75rem; font-weight: 700; }
.arch-node-desc { font-size: 0.6rem; color: var(--text-dim); line-height: 1.3; }
footer {
margin-top: 28px; text-align: center; padding: 16px;
font-size: 0.65rem; color: var(--text-muted); border-top: 1px solid var(--border);
}
footer a { color: var(--indigo); text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">
<div class="logo-icon">&#x1f575;</div>
<div>
<h1>CounterFeint</h1>
<span>Multi-Agent Ad Fraud Arena &middot; OpenEnv</span>
</div>
</div>
<div class="header-badges">
<span class="badge badge-accent">GRPO</span>
<span class="badge badge-accent">OpenEnv</span>
<span class="badge badge-green"><span class="pulse"></span> Live</span>
</div>
</header>
<div class="tab-bar">
<button class="tab-btn active" data-tab="arena" data-tooltip="Watch agents compete in real-time. Fraudster vs Investigator with live reward tracking and step-by-step replay.">&#9876; Arena</button>
<button class="tab-btn" data-tab="playground" data-tooltip="Hands-on single-agent mode. Investigate ads yourself, render verdicts, and earn rewards under an action budget.">&#128269; Playground</button>
<button class="tab-btn" data-tab="results" data-tooltip="Training overview, baseline scores, reward design details, and animated model comparison reward curves.">&#128200; Results</button>
</div>
<!-- ════════════════════════ ARENA TAB ════════════════════════ -->
<div class="tab-content active" id="tab-arena">
<div class="guide-banner arena">
<strong>Welcome to the CounterFeint Arena!</strong>
Click <strong>Run Auto Match</strong> to watch a live simulation using <strong>deterministic scripted agents</strong> &mdash; a reactive Fraudster, a heuristic Investigator, and a rule-based Auditor.
Watch reward curves evolve in real-time as agents compete step by step. For <strong>LLM-based agent</strong> results and trained model comparisons, check the <strong>Results</strong> tab.
</div>
<!-- Architecture overview — Auditor oversees from above -->
<div class="panel" style="margin-bottom:18px">
<div class="panel-body">
<div style="display:flex;flex-direction:column;align-items:center;padding:24px 20px;gap:0;">
<!-- Auditor (top tier — oversight) -->
<div class="arch-node" style="border-color:rgba(245,158,11,0.35);box-shadow:0 0 24px var(--amber-glow);z-index:1;">
<div class="agent-icon auditor">
<svg viewBox="0 0 40 40" width="26" height="26"><rect x="6" y="12" width="28" height="22" rx="5" fill="none" stroke="#f59e0b" stroke-width="2.5"/><circle cx="15" cy="22" r="3" fill="#f59e0b"/><circle cx="25" cy="22" r="3" fill="#f59e0b"/><line x1="14" y1="30" x2="26" y2="30" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/><line x1="20" y1="5" x2="20" y2="12" stroke="#f59e0b" stroke-width="2"/><line x1="14" y1="7" x2="26" y2="7" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round"/><circle cx="14" cy="9" r="2" fill="#f59e0b" opacity="0.6"/><circle cx="26" cy="9" r="2" fill="#f59e0b" opacity="0.6"/></svg>
</div>
<span class="agent-label auditor">Auditor</span>
<span class="arch-node-desc">Post-hoc reasoning &amp;<br/>plausibility auditor</span>
</div>
<!-- Hierarchy arrows -->
<svg width="340" height="40" style="display:block;overflow:visible;flex-shrink:0;" viewBox="0 0 340 40">
<line x1="170" y1="0" x2="60" y2="36" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="5,3" opacity="0.5"/>
<polygon points="56,32 60,40 66,34" fill="#f59e0b" opacity="0.5"/>
<line x1="170" y1="0" x2="280" y2="36" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="5,3" opacity="0.5"/>
<polygon points="274,34 280,40 284,32" fill="#f59e0b" opacity="0.5"/>
<text x="170" y="32" text-anchor="middle" fill="#f59e0b" font-size="9" font-family="Inter,sans-serif" font-weight="600" letter-spacing="1" opacity="0.7">AUDITS BOTH AGENTS</text>
</svg>
<!-- Bottom tier: Fraudster → Queue → Investigator -->
<div style="display:flex;align-items:center;justify-content:center;gap:24px;flex-wrap:wrap;">
<div class="arch-node" style="border-color:rgba(239,68,68,0.3);">
<div class="agent-icon fraudster">
<svg viewBox="0 0 40 40" width="26" height="26"><rect x="6" y="12" width="28" height="22" rx="5" fill="none" stroke="#ef4444" stroke-width="2.5"/><circle cx="15" cy="22" r="3" fill="#ef4444"/><circle cx="25" cy="22" r="3" fill="#ef4444"/><path d="M14 30 Q20 35 26 30" fill="none" stroke="#ef4444" stroke-width="2"/><line x1="12" y1="12" x2="8" y2="4" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/><line x1="28" y1="12" x2="32" y2="4" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/></svg>
</div>
<span class="agent-label fraudster">Fraudster</span>
<span class="arch-node-desc">Proposes &amp; modifies<br/>deceptive ads</span>
</div>
<span class="arch-arrow">&#x27A1;</span>
<div class="arch-node" style="border-color:rgba(6,182,212,0.3);min-width:160px;">
<div style="font-size:22px">&#128220;</div>
<span class="arch-node-name" style="color:var(--cyan)">Shared Ad Queue</span>
<span class="arch-node-desc">Ads accumulate here.<br/>Both agents see it.</span>
</div>
<span class="arch-arrow">&#x27A1;</span>
<div class="arch-node" style="border-color:rgba(99,102,241,0.3);">
<div class="agent-icon investigator">
<svg viewBox="0 0 40 40" width="26" height="26"><rect x="6" y="12" width="28" height="22" rx="5" fill="none" stroke="#6366f1" stroke-width="2.5"/><circle cx="15" cy="22" r="3" fill="#6366f1"/><circle cx="25" cy="22" r="3" fill="#6366f1"/><line x1="14" y1="30" x2="26" y2="30" stroke="#6366f1" stroke-width="2" stroke-linecap="round"/><circle cx="20" cy="8" r="3" fill="#6366f1" opacity="0.6"/><line x1="20" y1="11" x2="20" y2="12" stroke="#6366f1" stroke-width="2"/></svg>
</div>
<span class="agent-label investigator">Investigator</span>
<span class="arch-node-desc">Investigates ads &amp;<br/>renders verdicts</span>
</div>
</div>
</div>
</div>
</div>
<!-- Arena controls -->
<div class="arena-controls">
<select id="arena-task">
<option value="task_1">Task 1 — Basic triage (5 ads)</option>
<option value="task_2">Task 2 — Sophisticated fraud (12 ads)</option>
<option value="task_3">Task 3 — Fraud networks (20 ads)</option>
</select>
<input type="number" id="arena-seed" value="77" min="0" style="width:80px;padding:10px;border-radius:var(--radius-sm);background:#0f172a;color:#f1f5f9;border:1px solid rgba(148,163,184,0.35);font-family:inherit;font-size:0.78rem;" placeholder="Seed" />
<button class="btn btn-primary" id="btn-auto">&#9654; Run Auto Match</button>
<span id="arena-status" style="font-size:0.72rem;color:var(--text-dim);margin-left:8px;">Ready</span>
</div>
<!-- Phase bar -->
<div class="phase-bar" id="phase-bar">
<div class="phase-step f" id="ph-fraudster">&#x1f916; Fraudster Turn</div>
<span class="phase-arrow">&#8594;</span>
<div class="phase-step i" id="ph-investigator">&#x1f50d; Investigator Turn</div>
<span class="phase-arrow">&#8594;</span>
<div class="phase-step a" id="ph-auditor">&#x2696; Audit Phase</div>
<span class="phase-arrow">&#8594;</span>
<div class="phase-step d" id="ph-done">&#x2714; Done</div>
</div>
<!-- Arena stats -->
<div class="arena-stats">
<div class="stat-card"><div class="stat-label">Round</div><div class="stat-value" id="ar-round" style="color:var(--cyan)">-</div></div>
<div class="stat-card"><div class="stat-label">Total Steps</div><div class="stat-value" id="ar-steps" style="color:var(--text)">-</div></div>
<div class="stat-card"><div class="stat-label">Proposals Used</div><div class="stat-value" id="ar-proposals" style="color:var(--amber)">-</div></div>
<div class="stat-card"><div class="stat-label">Grader Score</div><div class="stat-value" id="ar-grader" style="color:var(--green)">-</div></div>
<div class="stat-card"><div class="stat-label">End Reason</div><div class="stat-value" id="ar-reason" style="font-size:0.85rem;color:var(--text-dim)">-</div></div>
</div>
<!-- Reward chart (Fraudster + Investigator only) -->
<div class="chart-container">
<div class="chart-title">Agent Reward Trajectories</div>
<div id="arena-chart" style="width:100%;height:220px;"></div>
<div class="chart-legend">
<div class="chart-legend-item"><div class="chart-legend-dot" style="background:var(--red)"></div>Fraudster</div>
<div class="chart-legend-item"><div class="chart-legend-dot" style="background:var(--indigo)"></div>Investigator</div>
</div>
</div>
<!-- Main 3-panel grid -->
<div class="arena-main">
<!-- Fraudster panel -->
<div class="panel">
<div class="agent-hdr">
<div class="agent-icon fraudster">
<svg viewBox="0 0 40 40" width="24" height="24"><rect x="6" y="12" width="28" height="22" rx="5" fill="none" stroke="#ef4444" stroke-width="2.5"/><circle cx="15" cy="22" r="3" fill="#ef4444"/><circle cx="25" cy="22" r="3" fill="#ef4444"/><path d="M14 30 Q20 35 26 30" fill="none" stroke="#ef4444" stroke-width="2"/><line x1="12" y1="12" x2="8" y2="4" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/><line x1="28" y1="12" x2="32" y2="4" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/></svg>
</div>
<div class="agent-hdr-info">
<div class="agent-name" style="color:var(--red)">Fraudster</div>
<div class="agent-desc">Adversarial ad proposer</div>
</div>
<div class="reward-badge zero" id="ar-f-reward">0.00</div>
</div>
<div class="panel-body">
<div class="agent-actions-scroll" id="ar-f-actions"><span class="ph-text" style="color:var(--text-dim);font-size:0.72rem">Run a match to see fraudster actions.</span></div>
</div>
</div>
<!-- Queue panel -->
<div class="panel">
<div class="panel-hdr">
<span class="panel-title" style="color:var(--cyan)">&#128220; Ad Queue</span>
<span style="font-size:0.65rem;color:var(--text-dim)" id="ar-q-count">0 ads</span>
</div>
<div class="panel-body">
<div class="queue-grid" id="ar-queue">
<span style="color:var(--text-dim);font-size:0.75rem">No ads yet</span>
</div>
</div>
</div>
<!-- Investigator panel -->
<div class="panel">
<div class="agent-hdr">
<div class="agent-icon investigator">
<svg viewBox="0 0 40 40" width="24" height="24"><rect x="6" y="12" width="28" height="22" rx="5" fill="none" stroke="#6366f1" stroke-width="2.5"/><circle cx="15" cy="22" r="3" fill="#6366f1"/><circle cx="25" cy="22" r="3" fill="#6366f1"/><line x1="14" y1="30" x2="26" y2="30" stroke="#6366f1" stroke-width="2" stroke-linecap="round"/><circle cx="20" cy="8" r="3" fill="#6366f1" opacity="0.6"/><line x1="20" y1="11" x2="20" y2="12" stroke="#6366f1" stroke-width="2"/></svg>
</div>
<div class="agent-hdr-info">
<div class="agent-name" style="color:var(--indigo)">Investigator</div>
<div class="agent-desc">Evidence-based reviewer</div>
</div>
<div class="reward-badge zero" id="ar-i-reward">0.00</div>
</div>
<div class="panel-body">
<div class="agent-actions-scroll" id="ar-i-actions"><span class="ph-text" style="color:var(--text-dim);font-size:0.72rem">Run a match to see investigator actions.</span></div>
</div>
</div>
</div>
<!-- Auditor panel -->
<div class="panel" id="ar-auditor-panel">
<div class="agent-hdr">
<div class="agent-icon auditor">
<svg viewBox="0 0 40 40" width="24" height="24"><rect x="6" y="12" width="28" height="22" rx="5" fill="none" stroke="#f59e0b" stroke-width="2.5"/><circle cx="15" cy="22" r="3" fill="#f59e0b"/><circle cx="25" cy="22" r="3" fill="#f59e0b"/><line x1="14" y1="30" x2="26" y2="30" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/><line x1="20" y1="5" x2="20" y2="12" stroke="#f59e0b" stroke-width="2"/><line x1="14" y1="7" x2="26" y2="7" stroke="#f59e0b" stroke-width="2.5" stroke-linecap="round"/><circle cx="14" cy="9" r="2" fill="#f59e0b" opacity="0.6"/><circle cx="26" cy="9" r="2" fill="#f59e0b" opacity="0.6"/></svg>
</div>
<div class="agent-hdr-info">
<div class="agent-name" style="color:var(--amber)">Auditor</div>
<div class="agent-desc">Post-hoc reasoning &amp; plausibility auditor</div>
</div>
<div class="reward-badge zero" id="ar-a-reward">0.00</div>
</div>
<div class="panel-body" id="ar-audit-body">
<div style="font-size:0.72rem;color:var(--text-dim)">Auditor acts after the match concludes. Run a match to see audit results.</div>
</div>
</div>
<!-- Full trace timeline -->
<div class="panel">
<div class="panel-hdr">
<span class="panel-title">&#128337; Match Timeline</span>
<span style="font-size:0.65rem;color:var(--text-dim)" id="ar-trace-count">0 events</span>
</div>
<div class="trace-timeline" id="ar-timeline"></div>
</div>
</div>
<!-- ════════════════════════ PLAYGROUND TAB ════════════════════════ -->
<div class="tab-content" id="tab-playground">
<div class="guide-banner playground">
<strong>Step into the Investigator's shoes!</strong>
Select a task difficulty below, click <strong>Reset environment</strong> to begin, then investigate ads by examining advertiser histories, landing pages, and payment methods.
Render your verdict (approve, reject, or escalate) on each ad. Every investigation costs budget &mdash; balance thoroughness against efficiency to maximize your cumulative reward.
</div>
<div class="pg-stats">
<div class="stat-card"><div class="stat-label">Total ads</div><div class="stat-value" id="st-total">-</div></div>
<div class="stat-card"><div class="stat-label">Reviewed</div><div class="stat-value" id="st-reviewed" style="color:var(--green)">-</div></div>
<div class="stat-card"><div class="stat-label">Budget left</div><div class="stat-value" id="st-budget" style="color:#f472b6">-</div></div>
<div class="stat-card"><div class="stat-label">Step</div><div class="stat-value" id="st-step" style="color:var(--amber)">-</div></div>
<div class="stat-card"><div class="stat-label">Env score</div><div class="stat-value" id="st-score" style="color:var(--cyan)">-</div></div>
<div class="stat-card"><div class="stat-label">Cum. reward</div><div class="stat-value" id="st-cum">-</div></div>
</div>
<div class="chart-container" style="margin-bottom:16px;width:55%;min-width:380px;">
<div class="chart-title">Cumulative Reward</div>
<div id="pg-chart" style="width:100%;height:300px;"></div>
</div>
<div style="display:flex;gap:10px;margin-bottom:18px;flex-wrap:wrap;align-items:center">
<select id="task-select" style="padding:10px 14px;border-radius:var(--radius-sm);font-size:0.78rem;font-family:inherit;background:#0f172a;color:#f1f5f9;border:1px solid rgba(148,163,184,0.35);">
<option value="task_1">Task 1 — Basic triage</option>
<option value="task_2">Task 2 — Sophisticated fraud</option>
<option value="task_3">Task 3 — Fraud networks</option>
</select>
<button class="btn btn-primary" id="btn-reset">Reset environment</button>
<button class="btn btn-green" id="btn-step" disabled>Execute action</button>
<button class="btn btn-amber" id="btn-score">Get grader score</button>
<button class="btn btn-ghost" onclick="window.open('/docs','_blank')">API docs</button>
</div>
<div style="padding:10px 14px;border-radius:var(--radius-sm);border:1px solid var(--border);margin-bottom:16px;font-size:0.78rem;color:var(--text-dim)" id="feedback">Select a task and reset to begin.</div>
<div class="pg-grid">
<div>
<div class="panel">
<div class="panel-hdr"><span class="panel-title">Ad queue</span></div>
<div class="panel-body"><div class="ad-queue" id="ad-queue"></div></div>
</div>
<div class="panel">
<div class="panel-hdr"><span class="panel-title">Subject profile</span></div>
<div class="panel-body" id="profile-body"></div>
</div>
<div class="panel">
<div class="panel-hdr"><span class="panel-title">Investigation findings</span></div>
<div class="panel-body"><div class="inv-grid" id="findings-grid"></div></div>
</div>
<div class="panel">
<div class="panel-hdr">
<span class="panel-title">RL intelligence log</span>
<button class="btn btn-ghost" style="padding:4px 10px;font-size:0.62rem;" id="btn-clear-log">Clear</button>
</div>
<div class="panel-body"><div class="log-area" id="log-area"></div></div>
</div>
</div>
<div>
<div class="panel">
<div class="panel-hdr"><span class="panel-title">Take action</span></div>
<div class="panel-body">
<div class="action-form">
<div class="form-group">
<label>Action type</label>
<select id="act-type">
<option value="investigate">Investigate</option>
<option value="verdict">Verdict</option>
<option value="link_accounts">Link accounts</option>
</select>
</div>
<div class="form-group"><label>Ad ID</label><select id="act-ad"></select></div>
<div class="form-group" id="grp-target">
<label>Investigation target</label>
<select id="act-target">
<option value="advertiser_history">advertiser_history</option>
<option value="landing_page">landing_page</option>
<option value="payment_method">payment_method</option>
<option value="targeting_overlap">targeting_overlap</option>
<option value="campaign_structure">campaign_structure</option>
<option value="policy_classifier">policy_classifier</option>
</select>
</div>
<div class="form-group hidden" id="grp-verdict">
<label>Verdict</label>
<select id="act-verdict"><option value="approve">approve</option><option value="reject">reject</option><option value="escalate">escalate</option></select>
</div>
<div class="form-group hidden" id="grp-conf">
<label>Confidence (0-1)</label>
<input type="number" id="act-conf" min="0" max="1" step="0.05" value="0.85" />
</div>
<div class="form-group hidden" id="grp-link"><label>Linked ad ID</label><select id="act-linked"></select></div>
<div class="form-group hidden" id="grp-reason">
<label>Link reason</label>
<textarea id="act-reason" placeholder="Why are these ads connected?"></textarea>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-hdr"><span class="panel-title">Verdict history</span></div>
<div class="panel-body" id="verdict-list"></div>
</div>
</div>
</div>
</div>
<!-- ════════════════════════ RESULTS TAB ════════════════════════ -->
<div class="tab-content" id="tab-results">
<div class="guide-banner results">
<strong>Training overview &amp; model comparison.</strong>
Explore baseline scores, reward design, and the GRPO training pipeline below.
Use <strong>Run Demo Match</strong> to see a live animated simulation. Model comparison curves will be updated as new models are trained and evaluated.
</div>
<div class="results-grid">
<div class="panel">
<div class="panel-hdr"><span class="panel-title">GRPO Training Curves — Qwen3-0.6B</span></div>
<div class="panel-body" style="padding:12px;text-align:center;">
<img src="/static/loss-reward-kl-curve.png" alt="GRPO training — Loss, Mean Reward, KL Divergence" style="width:100%;max-width:720px;border-radius:8px;border:1px solid var(--border);" />
<div style="margin-top:10px;font-size:0.68rem;color:var(--text-muted);line-height:1.4">
GRPO loss converges, mean reward trends upward, and KL divergence stays controlled — indicating stable policy improvement over 25 training steps.
</div>
</div>
</div>
<div class="panel">
<div class="panel-hdr"><span class="panel-title">Reward Design</span></div>
<div class="panel-body">
<table class="results-table">
<thead><tr><th>Action</th><th>Reward</th><th>Rationale</th></tr></thead>
<tbody>
<tr><td>Investigation</td><td style="color:var(--amber)">-0.02</td><td style="color:var(--text-dim)">Time/latency cost</td></tr>
<tr><td>Correct rejection</td><td style="color:var(--green)">+0.30 to +0.40</td><td style="color:var(--text-dim)">Scaled by severity</td></tr>
<tr><td>Correct approval</td><td style="color:var(--green)">+0.10</td><td style="color:var(--text-dim)">Revenue preserved</td></tr>
<tr><td>False positive</td><td style="color:var(--red)">-0.35</td><td style="color:var(--text-dim)">Lost advertiser revenue</td></tr>
<tr><td>False negative</td><td style="color:var(--red)">-0.50</td><td style="color:var(--text-dim)">Fraud goes live</td></tr>
<tr><td>Correct link</td><td style="color:var(--green)">+0.40</td><td style="color:var(--text-dim)">Ring detection</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Multi-agent reward diagram -->
<div class="panel" style="margin-top:16px">
<div class="panel-hdr"><span class="panel-title">Multi-Agent Reward Functions</span></div>
<div class="panel-body">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px">
<div style="padding:14px;border-radius:var(--radius-sm);border:1px solid rgba(239,68,68,0.2);background:rgba(239,68,68,0.04)">
<div style="font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:0.7px;color:var(--red);margin-bottom:8px">Fraudster Reward</div>
<div style="font-size:0.72rem;color:var(--text-dim);line-height:1.5">
<code style="color:var(--text);font-size:0.68rem">&sum; severity &times; plausibility</code> for fraud ads <strong style="color:var(--green)">not rejected</strong>, minus penalty per <strong style="color:var(--red)">rejected</strong> ad. Higher plausibility = more reward for evasion.
</div>
</div>
<div style="padding:14px;border-radius:var(--radius-sm);border:1px solid rgba(99,102,241,0.2);background:rgba(99,102,241,0.04)">
<div style="font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:0.7px;color:var(--indigo);margin-bottom:8px">Investigator Reward</div>
<div style="font-size:0.72rem;color:var(--text-dim);line-height:1.5">
Base grader score + plausibility-weighted <strong style="color:var(--green)">clean rationale bonus</strong> &minus; capped inconsistency penalty. Track A flags strip the bonus.
</div>
</div>
<div style="padding:14px;border-radius:var(--radius-sm);border:1px solid rgba(245,158,11,0.2);background:rgba(245,158,11,0.04)">
<div style="font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:0.7px;color:var(--amber);margin-bottom:8px">Auditor Reward</div>
<div style="font-size:0.72rem;color:var(--text-dim);line-height:1.5">
Reward for <strong style="color:var(--green)">true-positive</strong> flags vs ground truth, minus false-positive penalty. Deterministic rule-based scorecards.
</div>
</div>
</div>
</div>
</div>
<!-- Training pipeline -->
<div class="panel" style="margin-top:16px">
<div class="panel-hdr"><span class="panel-title">Training Pipeline — GRPO Self-Play</span></div>
<div class="panel-body">
<div class="arch-diagram" style="padding:20px 16px">
<div class="arch-node" style="border-color:rgba(239,68,68,0.3)">
<div style="font-size:18px">&#129302;</div>
<span class="arch-node-name" style="color:var(--red)">Frozen Fraudster</span>
<span class="arch-node-desc">llama3.1:8b via Ollama<br/>(8B params, frozen)</span>
</div>
<span class="arch-arrow">&#x1f4a5;</span>
<div class="arch-node" style="border-color:rgba(99,102,241,0.4);box-shadow:0 0 20px var(--indigo-glow)">
<div style="font-size:18px">&#129302;</div>
<span class="arch-node-name" style="color:var(--indigo)">Trainable Investigator</span>
<span class="arch-node-desc">Qwen3-0.6B + QLoRA<br/>(GRPO training)</span>
</div>
<span class="arch-arrow">&#x1f4cb;</span>
<div class="arch-node" style="border-color:rgba(245,158,11,0.3)">
<div style="font-size:18px">&#9878;</div>
<span class="arch-node-name" style="color:var(--amber)">Deterministic Auditor</span>
<span class="arch-node-desc">Rule-based scorecards<br/>(reward source)</span>
</div>
</div>
<div style="text-align:center;font-size:0.7rem;color:var(--text-muted);margin-top:4px">
Sequential self-play: train one agent at a time against frozen opponents (AlphaGo paradigm)
</div>
</div>
</div>
<!-- Live demo match (animated like arena) -->
<div class="panel" style="margin-top:16px">
<div class="panel-hdr">
<span class="panel-title">&#128200; Live Match Reward Curves</span>
<button class="btn btn-primary" id="btn-results-demo" style="padding:6px 14px;font-size:0.68rem">Run Demo Match</button>
</div>
<div class="panel-body">
<div id="results-chart" style="width:100%;height:220px;margin-bottom:12px">
<div style="color:var(--text-dim);font-size:0.78rem;text-align:center;padding:50px 20px">Click &ldquo;Run Demo Match&rdquo; to generate animated reward curves from a live simulation.</div>
</div>
<div class="chart-legend" id="results-legend" style="display:none">
<div class="chart-legend-item"><div class="chart-legend-dot" style="background:var(--red)"></div>Fraudster</div>
<div class="chart-legend-item"><div class="chart-legend-dot" style="background:var(--indigo)"></div>Investigator</div>
</div>
<div id="results-summary" style="margin-top:14px"></div>
</div>
</div>
<!-- Model comparison (placeholder — data to be added) -->
<div class="panel" style="margin-top:16px">
<div class="panel-hdr">
<span class="panel-title">&#129504; Model Comparison — Investigator Reward Curves</span>
<button class="btn btn-green" id="btn-model-compare" style="padding:6px 14px;font-size:0.68rem" disabled>Play Comparison</button>
</div>
<div class="panel-body">
<div id="model-chart" style="width:100%;height:220px;margin-bottom:12px">
<div style="color:var(--text-dim);font-size:0.78rem;text-align:center;padding:50px 20px;line-height:1.7">
Model comparison curves will appear here as training progresses.<br/>
<span style="font-size:0.68rem;color:var(--text-muted)">Planned models: Untrained Qwen3.5-0.8B, Fine-tuned Qwen3.5-0.8B, and more.</span>
</div>
</div>
<div class="chart-legend" id="model-legend" style="display:none">
<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#94a3b8"></div>Untrained Qwen3.5-0.8B</div>
<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#22c55e"></div>Fine-tuned Qwen3.5-0.8B</div>
</div>
</div>
</div>
</div>
<footer>
CounterFeint &mdash; Multi-Agent Ad Fraud Arena &middot;
<a href="/docs">API Docs</a> &middot;
<a href="/tasks">Tasks</a> &middot;
<a href="/grader">Grader</a> &middot;
<a href="/schema">Schema</a> &middot;
<a href="/matches">Matches</a>
</footer>
</div>
<div class="toast-container" id="toasts"></div>
<script>
/* ═══════════════════ Shared helpers ═══════════════════ */
const API = '';
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function toast(msg, type) {
const c = document.getElementById('toasts');
const t = document.createElement('div');
t.className = 'toast ' + (type || 'info');
t.textContent = msg;
c.appendChild(t);
setTimeout(() => t.remove(), 3200);
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
/* ═══════════════════ Tab switching ═══════════════════ */
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
/* ═══════════════════ SVG Chart renderer ═══════════════════ */
function renderMultiChart(container, datasets, options = {}) {
const el = typeof container === 'string' ? document.getElementById(container) : container;
const w = options.width || el.clientWidth || 600;
const h = options.height || el.clientHeight || 160;
const pad = { t: 20, r: 52, b: 28, l: 52 };
const cw = w - pad.l - pad.r;
const ch = h - pad.t - pad.b;
let allVals = [];
datasets.forEach(ds => allVals.push(...ds.data));
if (!allVals.length) { el.innerHTML = '<div style="color:var(--text-dim);font-size:0.75rem;text-align:center;padding:30px">No data</div>'; return; }
let mn = Math.min(0, ...allVals), mx = Math.max(0, ...allVals);
if (mn === mx) { mn -= 0.1; mx += 0.1; }
const range = mx - mn;
mn -= range * 0.05;
mx += range * 0.05;
const maxLen = options.totalSteps || Math.max(...datasets.map(ds => ds.data.length));
function sx(i) { return pad.l + (maxLen <= 1 ? cw / 2 : (i / (maxLen - 1)) * cw); }
function sy(v) { return pad.t + ch - ((v - mn) / (mx - mn)) * ch; }
let svg = `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid meet" style="display:block;width:100%;height:100%">`;
svg += `<rect width="${w}" height="${h}" fill="rgba(0,0,0,0.2)" rx="8"/>`;
const gridLines = 5;
for (let g = 0; g <= gridLines; g++) {
const yy = pad.t + (g / gridLines) * ch;
const val = mx - (g / gridLines) * (mx - mn);
svg += `<line x1="${pad.l}" y1="${yy}" x2="${w - pad.r}" y2="${yy}" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>`;
svg += `<text x="${pad.l - 8}" y="${yy + 4}" fill="rgba(255,255,255,0.55)" font-size="11" text-anchor="end" font-family="Inter,sans-serif" font-weight="500">${val.toFixed(2)}</text>`;
}
const zeroY = sy(0);
if (zeroY > pad.t + 2 && zeroY < h - pad.b - 2) {
svg += `<line x1="${pad.l}" y1="${zeroY}" x2="${w - pad.r}" y2="${zeroY}" stroke="rgba(255,255,255,0.15)" stroke-width="1" stroke-dasharray="4,3"/>`;
svg += `<text x="${pad.l - 8}" y="${zeroY + 4}" fill="rgba(255,255,255,0.5)" font-size="10" text-anchor="end" font-family="Inter,sans-serif" font-weight="600">0</text>`;
}
datasets.forEach(ds => {
if (!ds.data.length) return;
const pts = ds.data.map((v, i) => `${sx(i)},${sy(v)}`).join(' ');
const gradId = 'g' + Math.random().toString(36).slice(2, 8);
const lastX = sx(ds.data.length - 1);
const lastY = sy(ds.data[ds.data.length - 1]);
svg += `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="${ds.color}" stop-opacity="0.25"/><stop offset="100%" stop-color="${ds.color}" stop-opacity="0"/></linearGradient></defs>`;
const areaPath = `M${sx(0)},${sy(ds.data[0])} ` + ds.data.map((v, i) => `L${sx(i)},${sy(v)}`).join(' ') + ` L${lastX},${h - pad.b} L${sx(0)},${h - pad.b} Z`;
svg += `<path d="${areaPath}" fill="url(#${gradId})"/>`;
svg += `<polyline fill="none" stroke="${ds.color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" points="${pts}"/>`;
svg += `<circle cx="${lastX}" cy="${lastY}" r="4" fill="${ds.color}" stroke="rgba(0,0,0,0.3)" stroke-width="1"/>`;
const labelX = lastX + 8;
const labelVal = ds.data[ds.data.length - 1].toFixed(2);
svg += `<text x="${labelX}" y="${lastY + 4}" fill="${ds.color}" font-size="12" font-weight="700" font-family="Inter,sans-serif">${labelVal}</text>`;
});
const stepLabels = [0, Math.floor(maxLen / 2), maxLen - 1];
stepLabels.forEach(i => {
if (i < 0 || maxLen <= 0) return;
svg += `<text x="${sx(i)}" y="${h - 6}" fill="rgba(255,255,255,0.4)" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">${i}</text>`;
});
svg += `<text x="${w / 2}" y="${h - 6}" fill="rgba(255,255,255,0.25)" font-size="9" text-anchor="middle" font-family="Inter,sans-serif"></text>`;
svg += '</svg>';
el.innerHTML = svg;
}
/* ═══════════════════ ARENA TAB ═══════════════════ */
let arenaAnimating = false;
let arenaAbort = false;
function updatePhaseBar(phase) {
['ph-fraudster','ph-investigator','ph-auditor','ph-done'].forEach(id => {
document.getElementById(id).classList.remove('active','done-phase');
});
const map = { fraudster_turn: 'ph-fraudster', investigator_turn: 'ph-investigator', audit_phase: 'ph-auditor', done: 'ph-done' };
const order = ['fraudster_turn','investigator_turn','audit_phase','done'];
const idx = order.indexOf(phase);
for (let i = 0; i < idx; i++) {
document.getElementById(map[order[i]]).classList.add('done-phase');
}
if (map[phase]) document.getElementById(map[phase]).classList.add('active');
}
function updateRewardBadge(id, value) {
const el = document.getElementById(id);
if (!el) return;
const cls = value > 0.001 ? 'pos' : value < -0.001 ? 'neg' : 'zero';
el.className = 'reward-badge ' + cls;
el.textContent = (value >= 0 ? '+' : '') + value.toFixed(2);
}
function appendActionEntry(containerId, entry, color) {
const el = document.getElementById(containerId);
const ph = el.querySelector('.ph-text');
if (ph) ph.remove();
const div = document.createElement('div');
div.style.cssText = 'padding:6px 0;border-bottom:1px solid var(--border);font-size:0.72rem;animation:fadeIn 0.3s ease';
const rStyle = entry.reward > 0 ? 'color:var(--green)' : entry.reward < 0 ? 'color:var(--red)' : 'color:var(--text-muted)';
div.innerHTML = `<span style="color:${color};font-weight:600">${esc(entry.action_type)}</span> <span style="color:var(--text-dim)">${esc(entry.detail)}</span> <span style="${rStyle};font-weight:600;float:right">${entry.reward >= 0 ? '+' : ''}${entry.reward.toFixed(3)}</span>`;
el.appendChild(div);
el.scrollTop = el.scrollHeight;
}
function appendTimelineEntry(entry) {
const timeline = document.getElementById('ar-timeline');
const isAuditor = entry.role === 'auditor';
const rCls = isAuditor ? '' : (entry.reward > 0 ? 'pos' : entry.reward < 0 ? 'neg' : '');
const rewardText = isAuditor ? '-' : `${entry.reward >= 0 ? '+' : ''}${entry.reward.toFixed(3)}`;
const div = document.createElement('div');
div.className = 'trace-entry';
div.innerHTML = `<div class="trace-dot ${entry.role}"></div><div class="trace-text"><strong>${entry.role}</strong> ${esc(entry.detail)}</div><div class="trace-reward ${rCls}">${rewardText}</div>`;
timeline.appendChild(div);
timeline.scrollTop = timeline.scrollHeight;
document.getElementById('ar-trace-count').textContent = timeline.children.length + ' events';
}
function updateArenaQueue(trace, upToIdx) {
const verdictMap = {};
const proposedAds = new Set();
for (let i = 0; i <= upToIdx; i++) {
const t = trace[i];
if (t.role === 'investigator' && t.action_type === 'verdict') {
const m = t.detail.match(/Verdict on (ad_\d+): (\w+)/);
if (m) verdictMap[m[1]] = m[2];
}
if (t.role === 'fraudster' && t.action_type === 'propose_ad') {
const m = (t.feedback || '').match(/ad_id=(ad_\d+)/);
if (m) proposedAds.add(m[1]);
}
}
const allAds = [...new Set([...Object.keys(verdictMap), ...proposedAds])].sort();
const qEl = document.getElementById('ar-queue');
if (allAds.length) {
qEl.innerHTML = allAds.map(id => {
const v = verdictMap[id] || 'pending';
const isProp = proposedAds.has(id);
const cls = v === 'approve' ? 'approved' : v === 'reject' ? 'rejected' : v === 'escalate' ? 'escalated' : isProp ? 'proposed' : 'pending';
const dotColor = cls === 'approved' ? 'var(--green)' : cls === 'rejected' ? 'var(--red)' : cls === 'escalated' ? 'var(--cyan)' : cls === 'proposed' ? 'var(--amber)' : 'var(--text-muted)';
return `<div class="q-chip ${cls}"><span class="q-dot" style="background:${dotColor}"></span>${id}${isProp ? ' \u2605' : ''}</div>`;
}).join('');
document.getElementById('ar-q-count').textContent = allAds.length + ' ads';
}
}
function renderAuditorPanel(data) {
const fs = data.final_state;
updateRewardBadge('ar-a-reward', data.final_rewards.auditor);
const auditBody = document.getElementById('ar-audit-body');
if (fs.audit_report) {
const ar = fs.audit_report;
const ta = ar.track_a_flags || [];
const tb = ar.track_b_flags || [];
let html = '<div class="audit-grid"><div>';
html += `<div style="font-size:0.65rem;font-weight:700;text-transform:uppercase;color:var(--indigo);margin-bottom:8px">Track A \u2014 Investigator Audit (score: ${(ar.investigator_audit_score || 0).toFixed(2)})</div>`;
if (ta.length) {
ta.forEach(f => {
const sev = f.severity || 0.5;
const sevCls = sev >= 0.7 ? 'high' : sev >= 0.4 ? 'med' : 'low';
html += `<div class="flag-entry track-a"><div class="flag-type">${esc(f.flag_type || 'flag')} ${f.target_ad_id ? '(' + esc(f.target_ad_id) + ')' : ''}</div><div style="font-size:0.65rem;color:var(--text-dim);margin-top:2px">${esc((f.note || '').slice(0, 100))}</div><div class="severity-bar"><div class="severity-fill ${sevCls}" style="width:${sev * 100}%"></div></div></div>`;
});
} else { html += '<div style="font-size:0.72rem;color:var(--text-dim)">No Track A flags.</div>'; }
html += '</div><div>';
html += `<div style="font-size:0.65rem;font-weight:700;text-transform:uppercase;color:var(--amber);margin-bottom:8px">Track B \u2014 Fraudster Plausibility (score: ${(ar.fraudster_plausibility_score || 0).toFixed(2)})</div>`;
if (tb.length) {
tb.forEach(f => {
const sev = f.severity || 0.5;
const sevCls = sev >= 0.7 ? 'high' : sev >= 0.4 ? 'med' : 'low';
html += `<div class="flag-entry track-b"><div class="flag-type">${esc(f.flag_type || 'flag')} ${f.target_ad_id ? '(' + esc(f.target_ad_id) + ')' : ''}</div><div style="font-size:0.65rem;color:var(--text-dim);margin-top:2px">${esc((f.note || '').slice(0, 100))}</div><div class="severity-bar"><div class="severity-fill ${sevCls}" style="width:${sev * 100}%"></div></div></div>`;
});
} else { html += '<div style="font-size:0.72rem;color:var(--text-dim)">No Track B flags.</div>'; }
html += '</div></div>';
auditBody.innerHTML = html;
}
}
function resetArenaUI() {
document.getElementById('ar-f-actions').innerHTML = '<span class="ph-text" style="color:var(--text-dim);font-size:0.72rem">Waiting...</span>';
document.getElementById('ar-i-actions').innerHTML = '<span class="ph-text" style="color:var(--text-dim);font-size:0.72rem">Waiting...</span>';
document.getElementById('ar-timeline').innerHTML = '';
document.getElementById('ar-queue').innerHTML = '<span style="color:var(--text-dim);font-size:0.75rem">Waiting for match...</span>';
document.getElementById('ar-q-count').textContent = '0 ads';
document.getElementById('ar-trace-count').textContent = '0 events';
updateRewardBadge('ar-f-reward', 0);
updateRewardBadge('ar-i-reward', 0);
updateRewardBadge('ar-a-reward', 0);
document.getElementById('ar-round').textContent = '-';
document.getElementById('ar-steps').textContent = '0';
document.getElementById('ar-proposals').textContent = '0';
document.getElementById('ar-grader').textContent = '-';
document.getElementById('ar-reason').textContent = 'Running...';
document.getElementById('ar-audit-body').innerHTML = '<div style="font-size:0.72rem;color:var(--text-dim)">Auditor will act after match concludes...</div>';
document.getElementById('arena-chart').innerHTML = '';
updatePhaseBar('');
}
async function animateArenaMatch(data) {
const trace = data.trace;
const fs = data.final_state;
const totalSteps = trace.length;
let cumF = 0, cumI = 0;
const chartDataF = [], chartDataI = [];
trace.forEach(t => {
if (t.role === 'fraudster') cumF += t.reward;
else if (t.role === 'investigator') cumI += t.reward;
chartDataF.push(cumF);
chartDataI.push(cumI);
});
for (let i = 0; i < totalSteps; i++) {
if (arenaAbort) return;
const entry = trace[i];
if (entry.role === 'fraudster') updatePhaseBar('fraudster_turn');
else if (entry.role === 'investigator') updatePhaseBar('investigator_turn');
else if (entry.role === 'auditor') updatePhaseBar('audit_phase');
updateRewardBadge('ar-f-reward', chartDataF[i]);
updateRewardBadge('ar-i-reward', chartDataI[i]);
if (entry.role === 'fraudster') appendActionEntry('ar-f-actions', entry, 'var(--red)');
else if (entry.role === 'investigator') appendActionEntry('ar-i-actions', entry, 'var(--indigo)');
updateArenaQueue(trace, i);
appendTimelineEntry(entry);
document.getElementById('ar-steps').textContent = i + 1;
renderMultiChart('arena-chart', [
{ data: chartDataF.slice(0, i + 1), color: '#ef4444', label: 'Fraudster' },
{ data: chartDataI.slice(0, i + 1), color: '#6366f1', label: 'Investigator' },
], { height: 220, totalSteps: totalSteps });
await sleep(600);
}
updatePhaseBar('done');
document.getElementById('ar-round').textContent = fs.round_number || '-';
document.getElementById('ar-proposals').textContent = fs.proposals_used || 0;
document.getElementById('ar-grader').textContent = fs.grader_score != null ? fs.grader_score.toFixed(3) : '-';
document.getElementById('ar-reason').textContent = fs.end_reason || '-';
await sleep(400);
renderAuditorPanel(data);
}
document.getElementById('btn-auto').onclick = async () => {
if (arenaAnimating) {
arenaAbort = true;
return;
}
const btn = document.getElementById('btn-auto');
const status = document.getElementById('arena-status');
btn.innerHTML = '&#9632; Stop';
btn.className = 'btn btn-red';
btn.disabled = false;
arenaAnimating = true;
arenaAbort = false;
status.textContent = 'Fetching match data...';
status.style.color = 'var(--amber)';
try {
const task = document.getElementById('arena-task').value;
const seed = parseInt(document.getElementById('arena-seed').value) || 42;
const res = await fetch(API + '/arena/api/auto', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: task, seed: seed })
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || res.statusText);
resetArenaUI();
status.textContent = `Animating match (${data.total_steps} steps)...`;
await animateArenaMatch(data);
if (!arenaAbort) {
status.textContent = `Match complete \u2014 ${data.total_steps} steps`;
status.style.color = 'var(--green)';
toast('Match completed successfully', 'success');
} else {
status.textContent = 'Stopped';
status.style.color = 'var(--text-dim)';
}
} catch (e) {
status.textContent = 'Error: ' + e.message;
status.style.color = 'var(--red)';
toast(e.message, 'error');
} finally {
arenaAnimating = false;
arenaAbort = false;
btn.innerHTML = '&#9654; Run Auto Match';
btn.className = 'btn btn-primary';
}
};
/* ═══════════════════ RESULTS TAB ═══════════════════ */
/* Live Demo */
let resultsDemoRunning = false;
document.getElementById('btn-results-demo').onclick = async () => {
if (resultsDemoRunning) return;
const btn = document.getElementById('btn-results-demo');
btn.disabled = true;
btn.textContent = 'Running...';
resultsDemoRunning = true;
try {
const res = await fetch(API + '/arena/api/auto', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: 'task_2', seed: 123 })
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || res.statusText);
const trace = data.trace;
let cumF = 0, cumI = 0;
const cF = [], cI = [];
trace.forEach(t => {
if (t.role === 'fraudster') cumF += t.reward;
else if (t.role === 'investigator') cumI += t.reward;
cF.push(cumF);
cI.push(cumI);
});
document.getElementById('results-legend').style.display = 'flex';
for (let i = 0; i < trace.length; i++) {
renderMultiChart('results-chart', [
{ data: cF.slice(0, i + 1), color: '#ef4444' },
{ data: cI.slice(0, i + 1), color: '#6366f1' },
], { height: 220, totalSteps: trace.length });
await sleep(600);
}
const fs = data.final_state;
document.getElementById('results-summary').innerHTML = `
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px">
<div class="stat-card"><div class="stat-label">Grader Score</div><div class="stat-value" style="color:var(--green)">${fs.grader_score != null ? fs.grader_score.toFixed(3) : '-'}</div></div>
<div class="stat-card"><div class="stat-label">Fraudster Reward</div><div class="stat-value" style="color:var(--red)">${data.final_rewards.fraudster.toFixed(3)}</div></div>
<div class="stat-card"><div class="stat-label">Investigator Reward</div><div class="stat-value" style="color:var(--indigo)">${data.final_rewards.investigator.toFixed(3)}</div></div>
<div class="stat-card"><div class="stat-label">Total Steps</div><div class="stat-value">${data.total_steps}</div></div>
<div class="stat-card"><div class="stat-label">End Reason</div><div class="stat-value" style="font-size:0.85rem;color:var(--text-dim)">${fs.end_reason || '-'}</div></div>
</div>`;
toast('Demo match rendered', 'success');
} catch (e) {
toast(e.message, 'error');
} finally {
resultsDemoRunning = false;
btn.disabled = false;
btn.textContent = 'Run Demo Match';
}
};
/*
* Model comparison data placeholder.
* To add model curves, populate the `data` arrays with cumulative reward
* values (one per match step) and set `ready: true`.
*/
const MODEL_CURVES = [
{ name: 'Untrained Qwen3.5-0.8B', color: '#94a3b8', data: [], ready: false },
{ name: 'Fine-tuned Qwen3.5-0.8B', color: '#22c55e', data: [], ready: false },
];
document.getElementById('btn-model-compare').onclick = async () => {
const ready = MODEL_CURVES.filter(m => m.ready && m.data.length);
if (!ready.length) { toast('No model data available yet', 'info'); return; }
const btn = document.getElementById('btn-model-compare');
btn.disabled = true;
btn.textContent = 'Playing...';
document.getElementById('model-legend').style.display = 'flex';
const totalSteps = Math.max(...ready.map(m => m.data.length));
for (let i = 0; i < totalSteps; i++) {
renderMultiChart('model-chart',
ready.map(m => ({ data: m.data.slice(0, i + 1), color: m.color })),
{ height: 220, totalSteps: totalSteps }
);
await sleep(600);
}
btn.disabled = false;
btn.textContent = 'Play Comparison';
};
/* ═══════════════════ PLAYGROUND TAB ═══════════════════ */
const TARGETS = ['advertiser_history','landing_page','payment_method','targeting_overlap','campaign_structure','policy_classifier'];
const TARGET_LABELS = { advertiser_history:'ADVERTISER', landing_page:'LANDING PAGE', payment_method:'PAYMENT', targeting_overlap:'TARGETING', campaign_structure:'CAMPAIGN', policy_classifier:'POLICY (LLAMA GUARD)' };
const FINDING_RE = /^\[(ad_\d+)\s*\/\s*([a-z_]+)\]/;
let lastObs = null, pgVerdicts = {}, pgCumReward = 0, pgCumHistory = [], pgMaxBudget = 0, pgStep = 0, pgDone = false;
let allParsedFindings = {};
function logLine(msg, cls) {
const a = document.getElementById('log-area');
const d = document.createElement('div');
d.className = 'log-entry ' + (cls || '');
d.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
a.appendChild(d);
a.scrollTop = a.scrollHeight;
}
function parseFindings(raw) {
const out = {};
if (!raw) return out;
let curAd = null, curTgt = null, lines = [];
raw.split('\n').forEach(line => {
const m = line.trim().match(FINDING_RE);
if (m) {
if (curAd && curTgt) { if (!out[curAd]) out[curAd] = {}; out[curAd][curTgt] = lines.join('\n').trim(); }
curAd = m[1]; curTgt = m[2]; lines = [];
} else lines.push(line);
});
if (curAd && curTgt) { if (!out[curAd]) out[curAd] = {}; out[curAd][curTgt] = lines.join('\n').trim(); }
return out;
}
function focusedFromInfo(info) { const m = info && info.match(/Ad in Focus:\s*(ad_\d+)/); return m ? m[1] : null; }
function renderPgStats(obs) {
const qs = obs.queue_status || {};
document.getElementById('st-total').textContent = qs.total_ads ?? '-';
document.getElementById('st-reviewed').textContent = qs.reviewed ?? '-';
document.getElementById('st-budget').textContent = qs.investigation_budget ?? qs.steps_remaining ?? '-';
document.getElementById('st-step').textContent = pgMaxBudget ? (pgStep + ' / ' + pgMaxBudget) : String(pgStep);
document.getElementById('st-score').textContent = '-';
const el = document.getElementById('st-cum');
el.textContent = (pgCumReward >= 0 ? '+' : '') + pgCumReward.toFixed(2);
el.style.color = pgCumReward >= 0 ? 'var(--green)' : 'var(--red)';
}
function renderPgChart() {
if (!pgCumHistory.length) {
document.getElementById('pg-chart').innerHTML = '<div style="color:var(--text-dim);font-size:0.75rem;text-align:center;padding:20px">No steps yet.</div>';
return;
}
const col = pgCumHistory[pgCumHistory.length - 1] >= 0 ? '#22c55e' : '#ef4444';
renderMultiChart('pg-chart', [{ data: pgCumHistory, color: col }], { height: 300 });
}
function renderPgQueue(obs) {
const ads = obs.available_ads || [];
const focused = focusedFromInfo(obs.current_ad_info || '');
const ids = [...new Set([...ads, ...Object.keys(pgVerdicts)])].sort();
const el = document.getElementById('ad-queue');
el.innerHTML = '';
ids.forEach(id => {
const d = document.createElement('div');
let cls = 'ad-chip';
if (id === focused) cls += ' focus';
else if (pgVerdicts[id]) cls += ' ' + (pgVerdicts[id].verdict || '');
d.className = cls;
d.innerHTML = id + ' <span class="q-dot" style="background:' + (id === focused ? 'var(--amber)' : pgVerdicts[id] ? 'var(--green)' : 'var(--text-dim)') + '"></span>';
el.appendChild(d);
});
if (!ids.length) el.innerHTML = '<span style="color:var(--text-dim)">Reset to load queue.</span>';
}
function renderPgProfile(obs) {
const info = obs.current_ad_info || '';
const body = document.getElementById('profile-body');
if (!info) { body.innerHTML = '<p style="color:var(--text-dim)">No ad in focus.</p>'; return; }
const fid = focusedFromInfo(info);
const cat = (info.match(/Category:\s*(.+)/) || [])[1] || '';
const copy = (info.match(/Ad copy:\s*(.+)/) || [])[1] || '';
body.innerHTML =
'<div style="font-size:1.2rem;font-weight:800;margin-bottom:10px">' + (fid || '') + '</div>' +
'<div style="font-size:0.68rem;color:var(--text-dim);margin-bottom:8px">Category: <strong style="color:var(--text)">' + esc(cat) + '</strong></div>' +
(copy ? '<div class="ad-copy-block">' + esc(copy) + '</div>' : '');
}
function mergeFindings(obs) {
const raw = obs.investigation_findings || '';
const inv = parseFindings(raw);
for (const adId in inv) {
if (!allParsedFindings[adId]) allParsedFindings[adId] = {};
for (const target in inv[adId]) {
allParsedFindings[adId][target] = inv[adId][target];
}
}
}
function renderPgFindings(adId) {
adId = adId || document.getElementById('act-ad').value || focusedFromInfo((lastObs && lastObs.current_ad_info) || '');
const adInv = (adId && allParsedFindings[adId]) || {};
const grid = document.getElementById('findings-grid');
grid.innerHTML = '';
TARGETS.forEach(t => {
const card = document.createElement('div');
card.className = 'inv-card' + (adInv[t] ? ' revealed' : ' locked');
const label = TARGET_LABELS[t] || t;
card.innerHTML = adInv[t]
? '<div class="inv-inner"><div class="inv-label">' + esc(label) + '</div><div class="inv-content">' + esc(adInv[t].slice(0, 180)) + (adInv[t].length > 180 ? '...' : '') + '</div></div>'
: '<div class="inv-inner"><div class="inv-label">' + esc(label) + '</div><div class="inv-content">Classified</div></div><div class="lock-icon">&#128274;</div>';
grid.appendChild(card);
});
}
function fillAdSelects(obs) {
const ads = obs.available_ads || [];
['act-ad','act-linked'].forEach(id => {
const sel = document.getElementById(id);
const prev = sel.value;
sel.innerHTML = '';
ads.forEach(a => { const o = document.createElement('option'); o.value = a; o.textContent = a; sel.appendChild(o); });
if (prev && ads.includes(prev)) sel.value = prev;
});
}
function renderPgVerdicts() {
const el = document.getElementById('verdict-list');
const keys = Object.keys(pgVerdicts);
if (!keys.length) { el.innerHTML = '<p style="color:var(--text-dim);font-size:0.75rem;">None yet.</p>'; return; }
el.innerHTML = '';
keys.forEach(aid => {
const v = pgVerdicts[aid];
const row = document.createElement('div');
row.className = 'verdict-row';
row.innerHTML = '<span>' + esc(aid) + '</span><span style="color:var(--text-dim)">' + ((v.confidence * 100) | 0) + '%</span><span class="v-badge ' + esc(v.verdict) + '">' + esc(v.verdict) + '</span>';
el.appendChild(row);
});
}
function applyPgObs(data) {
const obs = data.observation || {};
lastObs = obs;
mergeFindings(obs);
renderPgStats(obs); renderPgQueue(obs); renderPgProfile(obs);
renderPgFindings(); fillAdSelects(obs); renderPgChart(); renderPgVerdicts();
}
function toggleActionFields() {
const t = document.getElementById('act-type').value;
document.getElementById('grp-target').classList.toggle('hidden', t !== 'investigate');
document.getElementById('grp-verdict').classList.toggle('hidden', t !== 'verdict');
document.getElementById('grp-conf').classList.toggle('hidden', t !== 'verdict');
document.getElementById('grp-link').classList.toggle('hidden', t !== 'link_accounts');
document.getElementById('grp-reason').classList.toggle('hidden', t !== 'link_accounts');
}
document.getElementById('act-type').addEventListener('change', toggleActionFields);
document.getElementById('act-ad').addEventListener('change', () => { renderPgFindings(); });
document.getElementById('btn-reset').onclick = async () => {
try {
const task = document.getElementById('task-select').value;
const res = await fetch(API + '/investigate/api/reset', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: task, seed: 42 })
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || res.statusText);
pgVerdicts = {}; pgCumReward = 0; pgCumHistory = []; pgStep = 0; pgDone = false;
allParsedFindings = {};
pgMaxBudget = (data.observation && data.observation.queue_status && data.observation.queue_status.investigation_budget) || 25;
applyPgObs(data);
document.getElementById('btn-step').disabled = false;
document.getElementById('feedback').textContent = 'Episode started. Budget: ' + pgMaxBudget + ' actions.';
logLine('Reset OK (' + task + ')', 'ok');
toast('Environment reset', 'success');
} catch (e) { toast(e.message, 'error'); logLine('Reset failed: ' + e.message, 'bad'); }
};
document.getElementById('btn-step').onclick = async () => {
if (pgDone) { toast('Episode finished \u2014 reset first', 'error'); return; }
const t = document.getElementById('act-type').value;
const ad = document.getElementById('act-ad').value;
const body = { action_type: t, ad_id: ad };
if (t === 'investigate') body.investigation_target = document.getElementById('act-target').value;
else if (t === 'verdict') {
body.verdict = document.getElementById('act-verdict').value;
body.confidence = parseFloat(document.getElementById('act-conf').value) || 0.5;
pgVerdicts[ad] = { verdict: body.verdict, confidence: body.confidence };
} else if (t === 'link_accounts') {
body.linked_ad_id = document.getElementById('act-linked').value;
body.link_reason = document.getElementById('act-reason').value.trim() || '\u2014';
}
try {
const res = await fetch(API + '/investigate/api/step', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (!res.ok) throw new Error(typeof data.detail === 'string' ? data.detail : JSON.stringify(data.detail));
const r = data.reward != null ? data.reward : 0;
pgCumReward += r; pgCumHistory.push(pgCumReward); pgStep += 1;
pgDone = !!data.done;
applyPgObs(data);
document.getElementById('feedback').textContent = (data.observation && data.observation.feedback) || ('Reward ' + r);
logLine('Step ' + pgStep + ' reward ' + r + ' cum ' + pgCumReward.toFixed(2), r < 0 ? 'bad' : 'ok');
if (data.done) { document.getElementById('btn-step').disabled = true; toast('Episode complete', 'success'); }
} catch (e) { toast(e.message, 'error'); logLine('Step error: ' + e.message, 'bad'); }
};
document.getElementById('btn-score').onclick = async () => {
try {
const res = await fetch(API + '/grader');
const g = await res.json();
if (g.grader_score != null) {
document.getElementById('st-score').textContent = Number(g.grader_score).toFixed(3);
toast('Grader score: ' + g.grader_score.toFixed(3), 'success');
} else toast(g.error || 'No grader yet', 'info');
} catch (e) { toast(e.message, 'error'); }
};
document.getElementById('btn-clear-log').onclick = () => { document.getElementById('log-area').innerHTML = ''; };
toggleActionFields();
</script>
</body>
</html>