crime / dashboard.html
printf-sourav's picture
Upload core project files (environment, server, dashboard, training)
1704969 verified
Raw
History Blame Contribute Delete
53.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>AI Crime Investigation β€” Live Dashboard</title>
<meta name="description" content="Real-time dashboard for the AI Crime Investigation World multi-agent RL environment">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ── Reset & Variables ─────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-primary: #FDFCF9;
--bg-secondary: #FFFFFF;
--bg-card: #FFFFFF;
--bg-card-hover: #F8F7F4;
--bg-panel: #FDFCF9;
--border: #000000;
--border-glow: #000000;
--text-primary: #000000;
--text-secondary: #000000;
--text-muted: #444444;
--accent-teal: #65A3BB;
--accent-yellow: #F4CC64;
--accent-orange: #F5A972;
--accent-red: #F25D68;
--accent-blue: #65A3BB;
--accent-cyan: #F4CC64;
--accent-amber: #F5A972;
--accent-green: #F4CC64;
--accent-purple: #F5A972;
--gradient-reward: linear-gradient(90deg, #F25D68, #F5A972, #65A3BB);
--shadow-glow: 4px 4px 0px #000000;
--font-mono: 'JetBrains Mono', monospace;
--font-sans: 'Inter', system-ui, sans-serif;
--radius: 0px;
--radius-sm: 0px;
}
html { font-size: 15px; }
body {
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
background-image: radial-gradient(rgba(0, 0, 0, 0.2) 2px, transparent 2px);
background-size: 20px 20px;
}
/* ── Layout ───────────────────────────────────────── */
.app { position: relative; z-index: 1; }
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 28px;
background: var(--accent-teal);
border-bottom: 3px solid #000;
position: sticky;
top: 0;
z-index: 100;
}
.topbar-left {
display: flex;
align-items: center;
gap: 14px;
}
.logo {
font-size: 1.15rem;
font-weight: 800;
color: var(--text-primary);
letter-spacing: -0.02em;
text-transform: uppercase;
}
.logo-icon { font-size: 1.3rem; }
.status-badge {
background: var(--bg-card); border: 2px solid #000; color: #000 !important; box-shadow: 2px 2px 0px #000;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: var(--bg-card);
border: 3px solid #000;
font-size: 0.72rem;
font-weight: 800;
color: var(--text-primary) !important;
text-transform: uppercase;
letter-spacing: 0.05em;
box-shadow: 2px 2px 0px #000;
}
.status-dot {
box-shadow: 0 0 8px var(--accent-green); border: 3px solid #000; border-radius: 50%;
width: 8px;
height: 8px;
border-radius: 0px;
border: 3px solid #000;
background: var(--accent-green);
}
.topbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: 3px solid #000;
background: var(--bg-card);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 0.8rem;
font-weight: 800;
border-radius: 0px;
cursor: pointer;
box-shadow: 3px 3px 0px #000;
transition: all 0.1s;
}
.btn:hover {
transform: translate(-2px, -2px);
box-shadow: 5px 5px 0px #000;
}
.btn:active {
transform: translate(2px, 2px);
box-shadow: 1px 1px 0px #000;
}
.btn-primary {
background: var(--accent-blue);
color: #000;
}
.btn-danger {
background: var(--accent-red);
color: #000;
}
/* ── Stats Strip ──────────────────────────────────── */
.stats-strip {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
padding: 16px 28px;
background: var(--bg-primary);
border-bottom: 3px solid #000;
}
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--bg-card);
border: 3px solid #000;
border-radius: 0px;
box-shadow: 4px 4px 0px #000;
transition: all 0.1s;
}
.stat-card:hover {
transform: translate(-2px, -2px);
box-shadow: 5px 5px 0px #000;
}
.stat-icon {
width: 38px;
height: 38px;
border-radius: 0px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
border: 3px solid #000;
background: var(--bg-primary);
}
.stat-icon.turns { background: var(--accent-blue); }
.stat-icon.evidence { background: var(--accent-cyan); }
.stat-icon.contradictions { background: var(--accent-amber); }
.stat-icon.reward { background: var(--accent-green); }
.stat-icon.result { background: var(--accent-teal); }
.stat-info { display: flex; flex-direction: column; }
.stat-label {
font-size: 0.72rem;
color: var(--text-primary);
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 800;
}
.stat-value {
font-family: var(--font-mono);
font-size: 1.25rem;
font-weight: 800;
color: var(--text-primary);
}
.stat-value.positive { color: #16a34a; }
.stat-value.negative { color: #dc2626; }
.stat-value.neutral { color: var(--text-primary); }
/* ── Main Grid ────────────────────────────────────── */
.main-grid {
display: grid;
grid-template-columns: 1fr 340px;
gap: 16px;
padding: 16px 28px 28px;
min-height: calc(100vh - 160px);
}
/* ── Panel ────────────────────────────────────────── */
.panel {
background: var(--bg-panel);
border: 3px solid #000;
border-radius: 0px;
box-shadow: 5px 5px 0px #000;
overflow: hidden;
display: flex;
flex-direction: column;
margin-bottom: 4px;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 3px solid #000;
background: var(--bg-card);
}
.panel-title {
font-size: 0.82rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-secondary);
}
.panel-body {
padding: 14px 18px;
background: var(--bg-panel);
flex: 1;
overflow-y: auto;
}
/* ── Conversation Log ─────────────────────────────── */
.conversation-panel {
max-height: calc(100vh - 230px);
}
.conversation-panel .panel-body {
display: flex;
flex-direction: column;
gap: 10px;
padding-bottom: 16px;
}
.msg {
display: flex;
gap: 10px;
padding: 12px 14px;
border-radius: 0px;
border: 2px solid transparent;
background: transparent;
border-bottom: 2px solid rgba(0,0,0,0.05);
animation: msg-in 0.35s ease-out;
transition: all 0.2s;
}
.msg:hover {
background: rgba(0,0,0,0.02);
}
@keyframes msg-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.msg-avatar {
width: 38px;
height: 38px;
border-radius: 0px;
border: 3px solid #000;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
flex-shrink: 0;
font-weight: 800;
color: #000;
box-shadow: 2px 2px 0px #000;
}
.msg-avatar.detective { background: var(--accent-blue); }
.msg-avatar.suspect-a { background: var(--accent-red); }
.msg-avatar.suspect-b { background: var(--accent-orange); }
.msg-avatar.witness { background: var(--accent-purple); }
.msg-avatar.system { background: #e2e8f0; }
.msg-content { flex: 1; min-width: 0; }
.msg-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 6px;
}
.msg-name {
font-weight: 800;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.msg-name.detective { color: var(--accent-blue); }
.msg-name.suspect-a { color: var(--accent-red); }
.msg-name.suspect-b { color: var(--accent-orange); }
.msg-name.witness { color: var(--accent-purple); }
.msg-name.system { color: var(--text-muted); }
.msg-turn {
font-size: 0.65rem;
color: var(--text-muted);
font-weight: 600;
}
.msg-text {
font-size: 1rem;
line-height: 1.5;
color: var(--text-secondary);
}
.msg-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 0px;
border: 2px solid #000;
font-size: 0.65rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-top: 8px;
box-shadow: 2px 2px 0px #000;
}
.msg-tag.evidence { background: var(--accent-cyan); color: #000; }
.msg-tag.contradiction { background: var(--accent-red); color: #000; }
.msg-tag.accusation { background: var(--accent-green); color: #000; }
/* ── Sidebar ──────────────────────────────────────── */
.sidebar {
display: flex;
flex-direction: column;
gap: 16px;
}
/* ── Reward Meter ─────────────────────────────────── */
.reward-meter {
padding: 18px;
}
.reward-bar-track {
width: 100%;
height: 12px;
background: rgba(0, 0, 0, 0.05);
border: 2px solid #000;
border-radius: 0px;
overflow: hidden;
margin: 12px 0 8px;
}
.reward-bar-fill {
height: 100%;
border-radius: 0px;
background: var(--gradient-reward);
width: 0%;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.reward-labels {
display: flex;
justify-content: space-between;
font-size: 0.72rem;
color: var(--text-muted);
font-weight: 700;
}
.reward-detail {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
font-size: 0.8rem;
}
.reward-detail:last-child { border-bottom: none; }
.reward-detail-label { color: var(--text-muted); font-weight: 600; }
.reward-detail-value {
font-family: var(--font-mono);
font-weight: 700;
}
/* ── Evidence Panel ───────────────────────────────── */
.evidence-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.evidence-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
background: var(--bg-card);
border: 3px solid #000;
border-radius: 0px;
box-shadow: 4px 4px 0px #000;
transition: all 0.2s;
position: relative;
}
.evidence-item::before {
border-top-left-radius: var(--radius-sm); border-bottom-left-radius: var(--radius-sm);
content: '';
position: absolute;
left: -3px; top: -3px; bottom: -3px;
width: 10px;
background: var(--text-muted);
border-right: 3px solid #000;
}
.evidence-item.revealed { background: var(--accent-yellow); border: 3px solid #000; box-shadow: 4px 4px 0px #000; color: #000; }
.evidence-item.revealed::before { background: var(--bg-primary); border-right: 3px solid #000; }
.evidence-item.locked { opacity: 0.5; background: var(--bg-card); }
.evidence-item.locked::before {
background: #94a3b8;
}
.evidence-icon {
font-size: 1.1rem;
flex-shrink: 0;
margin-top: 2px;
}
.evidence-info { flex: 1; }
.evidence-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 3px;
}
.evidence-desc {
font-size: 0.72rem;
color: var(--text-muted);
line-height: 1.4;
}
/* ── Contradiction Alerts ─────────────────────────── */
.alert-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.alert-item {
padding: 12px 14px;
background: var(--bg-card);
border: 3px solid #000;
border-left: 10px solid var(--accent-red);
border-radius: 0px;
box-shadow: 4px 4px 0px #000;
animation: alert-flash 0.5s ease-out;
}
@keyframes alert-flash {
0% { transform: translateX(10px); }
100% { transform: translateX(0); }
}
.alert-suspect {
font-size: 0.72rem;
font-weight: 700;
color: var(--accent-red);
margin-bottom: 4px;
}
.alert-detail {
font-size: 0.72rem;
color: var(--text-muted);
line-height: 1.4;
}
.alert-turns {
font-size: 0.65rem;
color: var(--text-muted);
margin-top: 4px;
font-weight: 600;
}
/* ── Reward History Chart ─────────────────────────── */
.chart-container {
height: 200px;
position: relative;
}
.training-metrics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 10px;
}
.training-metric {
border: 2px solid #000;
background: var(--bg-card);
padding: 8px;
box-shadow: 2px 2px 0px #000;
}
.training-metric-label {
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 800;
color: var(--text-muted);
}
.training-metric-value {
font-family: var(--font-mono);
font-weight: 800;
margin-top: 2px;
font-size: 0.9rem;
}
.chart-canvas {
width: 100%;
height: 100%;
}
/* ── Empty State ──────────────────────────────────── */
.empty-state {
text-align: center;
padding: 30px 16px;
color: var(--text-muted);
}
.empty-icon { font-size: 2rem; margin-bottom: 8px; }
.empty-text {
font-size: 0.78rem;
line-height: 1.5;
}
/* ── Scrollbar ────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
@keyframes live-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* ── Responsive ───────────────────────────────────── */
@media (max-width: 1100px) {
.main-grid { grid-template-columns: 1fr; }
.stats-strip { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 700px) {
.stats-strip { grid-template-columns: 1fr 1fr; }
.topbar { padding: 12px 16px; }
.main-grid { padding: 12px 16px; }
}
/* ── Outcome overlay ──────────────────────────────── */
.outcome-overlay {
position: fixed;
inset: 0;
background: rgba(253, 252, 249, 0.85);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s;
}
.outcome-overlay.visible {
opacity: 1;
pointer-events: auto;
}
.outcome-card {
background: var(--bg-card);
border: 3px solid #000;
border-radius: 0px;
padding: 40px 50px;
text-align: center;
transform: scale(0.9);
box-shadow: 8px 8px 0px #000;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
max-width: 420px;
}
.outcome-overlay.visible .outcome-card {
transform: scale(1);
}
.outcome-icon { font-size: 3.5rem; margin-bottom: 12px; }
.outcome-title {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 6px;
}
.outcome-subtitle {
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 20px;
}
.outcome-stat {
font-family: var(--font-mono);
font-size: 2rem;
font-weight: 700;
margin-bottom: 20px;
}
.outcome-stat.win { color: var(--accent-green); }
.outcome-stat.lose { color: var(--accent-red); }
</style>
</head>
<body>
<div class="app" id="app">
<!-- ── Topbar ──────────────────────────────────────── -->
<header class="topbar">
<div class="topbar-left">
<span class="logo-icon">πŸ”</span>
<span class="logo">Crime Investigation World</span>
<div class="status-badge" id="statusBadge">
<span class="status-dot"></span>
<span id="statusText">Ready</span>
</div>
</div>
<div class="topbar-right">
<span id="episodeLabel" style="font-size:0.75rem;color:var(--text-muted);font-family:var(--font-mono);">Episode β€”</span>
<button class="btn btn-primary" id="btnRunLive" onclick="runLiveAgent()" style="background: linear-gradient(135deg, #10b981, #059669);">πŸš€ Run Live Server</button>
<button class="btn btn-danger" id="btnReset" onclick="resetBoard()" style="display:none;">β†Ί Reset</button>
</div>
</header>
<!-- ── Stats Strip ─────────────────────────────────── -->
<div class="stats-strip">
<div class="stat-card">
<div class="stat-icon turns">πŸ•</div>
<div class="stat-info">
<span class="stat-label">Turn</span>
<span class="stat-value" id="statTurn">0 / 15</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon evidence">πŸ”¬</div>
<div class="stat-info">
<span class="stat-label">Evidence</span>
<span class="stat-value" id="statEvidence">0 / 3</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon contradictions">⚑</div>
<div class="stat-info">
<span class="stat-label">Contradictions</span>
<span class="stat-value" id="statContra">0</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon reward">πŸ’Ž</div>
<div class="stat-info">
<span class="stat-label">Reward</span>
<span class="stat-value" id="statReward">0.00</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon result">🎯</div>
<div class="stat-info">
<span class="stat-label">Outcome</span>
<span class="stat-value neutral" id="statOutcome">β€”</span>
</div>
</div>
</div>
<!-- ── Main Content ────────────────────────────────── -->
<div class="main-grid">
<!-- Conversation Log -->
<div class="panel conversation-panel">
<div class="panel-header">
<span class="panel-title">πŸ’¬ Interrogation Log</span>
<span id="msgCount" style="font-size:0.7rem;color:var(--text-muted);font-family:var(--font-mono);">0 messages</span>
</div>
<div class="panel-body" id="conversationLog">
<div class="empty-state">
<div class="empty-icon">πŸ•΅οΈ</div>
<div class="empty-text">Click <strong>Run Live Server</strong> to watch a full<br>investigation episode unfold live.</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- Reward Meter -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">πŸ“Š Reward Breakdown</span>
</div>
<div class="panel-body reward-meter">
<div class="reward-bar-track">
<div class="reward-bar-fill" id="rewardBar"></div>
</div>
<div class="reward-labels">
<span>-10</span>
<span>0</span>
<span>+10</span>
</div>
<div style="margin-top:12px;" id="rewardBreakdown">
<div class="reward-detail">
<span class="reward-detail-label">Base</span>
<span class="reward-detail-value" id="rwBase">0.00</span>
</div>
<div class="reward-detail">
<span class="reward-detail-label">Turn Cost</span>
<span class="reward-detail-value" id="rwTurnCost" style="color:var(--accent-red);">0.00</span>
</div>
<div class="reward-detail">
<span class="reward-detail-label">Contradiction Bonus</span>
<span class="reward-detail-value" id="rwContra" style="color:var(--accent-amber);">0.00</span>
</div>
<div class="reward-detail">
<span class="reward-detail-label">Evidence Bonus</span>
<span class="reward-detail-value" id="rwEvidence" style="color:var(--accent-cyan);">0.00</span>
</div>
<div class="reward-detail">
<span class="reward-detail-label"><strong>Total</strong></span>
<span class="reward-detail-value" id="rwTotal" style="font-size:1.05rem;">0.00</span>
</div>
</div>
</div>
</div>
<!-- Evidence Panel -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">πŸ”¬ Evidence</span>
</div>
<div class="panel-body">
<div class="evidence-list" id="evidenceList">
<div class="empty-state" id="evidenceEmptyState" style="padding:14px;">
<div class="empty-text" style="font-size:0.72rem;">No evidence revealed yet.</div>
</div>
</div>
</div>
</div>
<!-- Contradiction Alerts -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">⚑ Contradiction Alerts</span>
</div>
<div class="panel-body">
<div class="alert-list" id="alertList">
<div class="empty-state" style="padding:14px;">
<div class="empty-text" style="font-size:0.72rem;">No contradictions detected yet.</div>
</div>
</div>
</div>
</div>
<!-- Reward History -->
<div class="panel" id="trainingPanel">
<div class="panel-header">
<span class="panel-title">πŸ“ˆ Training Progress</span>
<span id="trainLiveBadge" style="display:none;font-size:0.66rem;font-weight:800;text-transform:uppercase;letter-spacing:0.06em;background:#16a34a;color:#fff;padding:3px 8px;border:2px solid #000;box-shadow:2px 2px 0px #000;animation:live-pulse 1.5s ease-in-out infinite;">● LIVE</span>
</div>
<div class="panel-body">
<div class="training-metrics">
<div class="training-metric">
<div class="training-metric-label">Episodes</div>
<div class="training-metric-value" id="trainEpisodes">-</div>
</div>
<div class="training-metric">
<div class="training-metric-label">Model</div>
<div class="training-metric-value" id="trainModel" style="font-size:0.72rem;">-</div>
</div>
<div class="training-metric">
<div class="training-metric-label">Accuracy</div>
<div class="training-metric-value" id="trainAccuracy">-</div>
</div>
<div class="training-metric">
<div class="training-metric-label">Difficulty</div>
<div class="training-metric-value" id="trainDifficulty" style="text-transform:capitalize;">-</div>
</div>
<div class="training-metric">
<div class="training-metric-label">First-50 Mean</div>
<div class="training-metric-value" id="trainFirst">-</div>
</div>
<div class="training-metric">
<div class="training-metric-label">Last-50 Mean</div>
<div class="training-metric-value" id="trainLast">-</div>
</div>
</div>
<div style="font-size:0.72rem; font-weight:800; margin-bottom:6px;" id="trainImprovement">Improvement: -</div>
<div style="font-size:0.7rem; margin-bottom:8px; color:#666;" id="trainStatus">Checking training artifacts...</div>
<div id="trainResultDots" style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:10px;min-height:16px;"></div>
<div class="chart-container">
<canvas class="chart-canvas" id="trainingRewardChart"></canvas>
</div>
</div>
</div>
<div class="panel" id="historyPanel" style="display:none;">
<div class="panel-header">
<span class="panel-title">πŸ“ˆ Reward History</span>
</div>
<div class="panel-body">
<div class="chart-container">
<canvas class="chart-canvas" id="rewardChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- ── Outcome Overlay ─────────────────────────────── -->
<div class="outcome-overlay" id="outcomeOverlay">
<div class="outcome-card">
<div class="outcome-icon" id="outcomeIcon">πŸŽ‰</div>
<div class="outcome-title" id="outcomeTitle">Case Solved!</div>
<div class="outcome-subtitle" id="outcomeSub">The detective correctly identified the criminal.</div>
<div class="outcome-stat win" id="outcomeStat">+8.50</div>
<button class="btn btn-primary" onclick="dismissOutcome()">Continue</button>
</div>
</div>
</div>
<script>
const EVIDENCE_ICONS = {
keycard_log: "πŸ“‡",
cctv_footage: "πŸ“Ή",
forensic_report: "πŸ”¬",
};
const EVIDENCE_NAMES = {
keycard_log: "Keycard Log",
cctv_footage: "CCTV Footage",
forensic_report: "Forensic Report",
};
// ── State ────────────────────────────────────────────────────────────────────
let demoRunning = false;
let currentTurn = 0;
let evidenceCount = 0;
let contradictionCount = 0;
let totalReward = 0;
let rewardHistory = [];
let messageCount = 0;
let revealedEvidenceKeys = new Set();
let trainingRewards = [];
let trainingSmoothed = [];
let lastTrainEpisodeCount = 0;
const rewardComponents = {
base: 0,
turnCost: 0,
contradictions: 0,
evidence: 0,
total: 0,
};
// ── Rendering ────────────────────────────────────────────────────────────────
function getAvatarClass(speaker) {
if (speaker === "Detective") return "detective";
if (speaker === "Suspect_A") return "suspect-a";
if (speaker === "Suspect_B") return "suspect-b";
if (speaker === "Witness_1") return "witness";
return "system";
}
function getAvatarEmoji(speaker) {
if (speaker === "Detective") return "πŸ”";
if (speaker === "Suspect_A") return "πŸ…°οΈ";
if (speaker === "Suspect_B") return "πŸ…±οΈ";
if (speaker === "Witness_1") return "πŸ‘οΈ";
return "βš™οΈ";
}
function addMessage(speaker, text, turn, tag) {
const log = document.getElementById("conversationLog");
// Remove empty state on first message
if (messageCount === 0) log.innerHTML = "";
const msgEl = document.createElement("div");
msgEl.className = "msg";
const cls = getAvatarClass(speaker);
let tagHTML = "";
if (tag) {
tagHTML = `<div class="msg-tag ${tag}">${tag}</div>`;
}
msgEl.innerHTML = `
<div class="msg-avatar ${cls}">${getAvatarEmoji(speaker)}</div>
<div class="msg-content">
<div class="msg-header">
<span class="msg-name ${cls}">${speaker}</span>
<span class="msg-turn">Turn ${turn}</span>
</div>
<div class="msg-text">${text}</div>
${tagHTML}
</div>
`;
log.appendChild(msgEl);
log.scrollTop = log.scrollHeight;
messageCount++;
document.getElementById("msgCount").textContent = `${messageCount} messages`;
}
function revealEvidence(itemKey, nameOverride, descOverride) {
let keyNorm = itemKey.toLowerCase().replace(" ", "_");
// Normalize to standard keys if string varies slightly
if (keyNorm.includes("keycard")) keyNorm = "keycard_log";
if (keyNorm.includes("cctv")) keyNorm = "cctv_footage";
if (keyNorm.includes("forensic")) keyNorm = "forensic_report";
if (revealedEvidenceKeys.has(keyNorm)) return;
revealedEvidenceKeys.add(keyNorm);
// Always use the passed-in description (from backend trace or demo script)
const name = nameOverride || EVIDENCE_NAMES[keyNorm] || itemKey;
const description = descOverride || "";
const icon = EVIDENCE_ICONS[keyNorm] || "πŸ“„";
evidenceCount++;
document.getElementById("statEvidence").textContent = `${evidenceCount} / 3`;
// Update evidence list
const list = document.getElementById("evidenceList");
const emptyState = document.getElementById("evidenceEmptyState");
if (emptyState) emptyState.remove();
const el = document.createElement("div");
el.className = "evidence-item revealed";
el.innerHTML = `
<span class="evidence-icon">${icon}</span>
<div class="evidence-info">
<div class="evidence-name">${name}</div>
<div class="evidence-desc">${description}</div>
</div>
`;
list.appendChild(el);
addMessage("System", `πŸ“‹ Evidence revealed: <strong>${name}</strong> β€” ${description}`, currentTurn, "evidence");
// Reward β€” matches reward_calculator.py evidence_request_confirms_lead = +1.0
rewardComponents.evidence += 1.0;
updateReward();
}
function addContradiction(suspect, claim1, t1, claim2, t2) {
contradictionCount++;
document.getElementById("statContra").textContent = contradictionCount;
const list = document.getElementById("alertList");
if (contradictionCount === 1) list.innerHTML = "";
const el = document.createElement("div");
el.className = "alert-item";
el.innerHTML = `
<div class="alert-suspect">⚠ ${suspect} β€” Contradiction #${contradictionCount}</div>
<div class="alert-detail">"${claim1}" ↔ "${claim2}"</div>
<div class="alert-turns">Turns ${t1} β†’ ${t2}</div>
`;
list.appendChild(el);
rewardComponents.contradictions += 2.0;
updateReward();
}
function updateReward() {
const total = rewardComponents.base + rewardComponents.turnCost + rewardComponents.contradictions + rewardComponents.evidence;
rewardComponents.total = total;
totalReward = total;
document.getElementById("statReward").textContent = total.toFixed(2);
const statEl = document.getElementById("statReward");
statEl.className = "stat-value " + (total >= 0 ? "positive" : "negative");
// Reward bar: map -10..+10 to 0..100%
const pct = Math.max(0, Math.min(100, ((total + 10) / 20) * 100));
document.getElementById("rewardBar").style.width = `${pct}%`;
document.getElementById("rwBase").textContent = rewardComponents.base.toFixed(2);
document.getElementById("rwTurnCost").textContent = rewardComponents.turnCost.toFixed(2);
document.getElementById("rwContra").textContent = `+${rewardComponents.contradictions.toFixed(2)}`;
document.getElementById("rwEvidence").textContent = `+${rewardComponents.evidence.toFixed(2)}`;
document.getElementById("rwTotal").textContent = total.toFixed(2);
document.getElementById("rwTotal").style.color = total >= 0 ? "var(--accent-green)" : "var(--accent-red)";
}
function updateTurn(t) {
currentTurn = t;
document.getElementById("statTurn").textContent = `${t} / 15`;
rewardComponents.turnCost = -(t * 0.3);
updateReward();
}
function showOutcome(correct) {
const overlay = document.getElementById("outcomeOverlay");
document.getElementById("outcomeIcon").textContent = correct ? "πŸŽ‰" : "❌";
document.getElementById("outcomeTitle").textContent = correct ? "Case Solved!" : "Wrong Accusation";
document.getElementById("outcomeSub").textContent = correct
? "The detective correctly identified the true criminal."
: "The detective accused the wrong suspect.";
document.getElementById("outcomeStat").textContent = (totalReward >= 0 ? "+" : "") + totalReward.toFixed(2);
document.getElementById("outcomeStat").className = "outcome-stat " + (correct ? "win" : "lose");
document.getElementById("statOutcome").textContent = correct ? "βœ… Correct" : "❌ Wrong";
document.getElementById("statOutcome").className = "stat-value " + (correct ? "positive" : "negative");
overlay.classList.add("visible");
}
function dismissOutcome() {
document.getElementById("outcomeOverlay").classList.remove("visible");
document.getElementById("btnReset").style.display = "inline-flex";
updateRewardChart();
}
// ── Reward Chart ─────────────────────────────────────────────────────────────
function updateRewardChart() {
rewardHistory.push(totalReward);
const panel = document.getElementById("historyPanel");
panel.style.display = "block";
const canvas = document.getElementById("rewardChart");
const ctx = canvas.getContext("2d");
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * 2;
canvas.height = rect.height * 2;
ctx.scale(2, 2);
const w = rect.width;
const h = rect.height;
const data = rewardHistory;
// Dynamic scale limits based on data
const maxVal = data.length > 0 ? Math.max(...data) : 0;
const minVal = data.length > 0 ? Math.min(...data) : 0;
const maxR = Math.max(10, Math.ceil(maxVal / 5) * 5 + 5);
const minR = Math.min(-10, Math.floor(minVal / 5) * 5 - 5);
const stepV = Math.max(5, Math.floor((maxR - minR) / 6));
const pad = { t: 15, b: 25, l: 40, r: 15 };
ctx.clearRect(0, 0, w, h);
// Grid and Labels
ctx.strokeStyle = "rgba(0,0,0,0.1)"; // Darker grid lines
ctx.lineWidth = 1;
ctx.fillStyle = "rgba(0,0,0,0.6)"; // Dark text instead of white
ctx.font = "11px 'JetBrains Mono'";
ctx.textAlign = "right";
for (let v = minR; v <= maxR; v += stepV) {
const y = pad.t + ((maxR - v) / (maxR - minR)) * (h - pad.t - pad.b);
ctx.beginPath();
ctx.moveTo(pad.l, y);
ctx.lineTo(w - pad.r, y);
ctx.stroke();
ctx.fillText(v.toString(), pad.l - 8, y + 4);
}
// Zero line
const zeroY = pad.t + ((maxR - 0) / (maxR - minR)) * (h - pad.t - pad.b);
ctx.strokeStyle = "rgba(0,0,0,0.2)"; // Darker zero line
ctx.beginPath();
ctx.moveTo(pad.l, zeroY);
ctx.lineTo(w - pad.r, zeroY);
ctx.stroke();
if (data.length < 2) return;
// Line
const stepX = (w - pad.l - pad.r) / (Math.max(data.length - 1, 1));
ctx.beginPath();
data.forEach((v, i) => {
const x = pad.l + i * stepX;
const y = pad.t + ((maxR - v) / (maxR - minR)) * (h - pad.t - pad.b);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.strokeStyle = "#3b82f6";
ctx.lineWidth = 2;
ctx.stroke();
// Gradient fill
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, "rgba(59, 130, 246, 0.2)");
grad.addColorStop(1, "rgba(59, 130, 246, 0)");
ctx.lineTo(pad.l + (data.length - 1) * stepX, h - pad.b);
ctx.lineTo(pad.l, h - pad.b);
ctx.closePath();
ctx.fillStyle = grad;
ctx.fill();
// Dots
data.forEach((v, i) => {
const x = pad.l + i * stepX;
const y = pad.t + ((maxR - v) / (maxR - minR)) * (h - pad.t - pad.b);
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fillStyle = v >= 0 ? "#10b981" : "#ef4444";
ctx.fill();
ctx.strokeStyle = "#ffffff"; // White border inside the dot
ctx.lineWidth = 2;
ctx.stroke();
});
// Episode labels
ctx.fillStyle = "rgba(0,0,0,0.5)"; // Darker episode text
ctx.font = "9px 'JetBrains Mono'";
ctx.textAlign = "center";
data.forEach((_, i) => {
ctx.fillText(`E${i + 1}`, pad.l + i * stepX, h - 4);
});
}
function drawTrainingChart(rewards, smoothed) {
const canvas = document.getElementById("trainingRewardChart");
const ctx = canvas.getContext("2d");
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * 2;
canvas.height = rect.height * 2;
ctx.scale(2, 2);
const w = rect.width;
const h = rect.height;
const pad = { t: 15, b: 25, l: 40, r: 10 };
const data = rewards || [];
const avg = smoothed || [];
ctx.clearRect(0, 0, w, h);
if (data.length === 0) {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.font = "12px 'JetBrains Mono'";
ctx.fillText("No training data found yet.", 12, 24);
return;
}
const maxVal = Math.max(...data, ...(avg.length ? avg : data), 10);
const minVal = Math.min(...data, ...(avg.length ? avg : data), -10);
const span = Math.max(1, maxVal - minVal);
ctx.strokeStyle = "rgba(0,0,0,0.12)";
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {
const y = pad.t + (i / 5) * (h - pad.t - pad.b);
ctx.beginPath();
ctx.moveTo(pad.l, y);
ctx.lineTo(w - pad.r, y);
ctx.stroke();
}
const valueToY = (v) => pad.t + ((maxVal - v) / span) * (h - pad.t - pad.b);
const xStep = (w - pad.l - pad.r) / Math.max(1, data.length - 1);
ctx.beginPath();
data.forEach((v, i) => {
const x = pad.l + i * xStep;
const y = valueToY(v);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.strokeStyle = "#65A3BB";
ctx.lineWidth = 2;
ctx.stroke();
if (avg.length === data.length && avg.length > 1) {
ctx.beginPath();
avg.forEach((v, i) => {
const x = pad.l + i * xStep;
const y = valueToY(v);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.strokeStyle = "#F25D68";
ctx.lineWidth = 2.5;
ctx.stroke();
}
}
async function loadTrainingProgress() {
try {
const res = await fetch("/api/reward_curve");
if (!res.ok) return;
const data = await res.json();
trainingRewards = data.rewards || [];
trainingSmoothed = data.smoothed || [];
document.getElementById("trainEpisodes").textContent = String(data.num_episodes || trainingRewards.length || 0);
document.getElementById("trainModel").textContent = (data.model || "-").replace("Qwen/", "");
document.getElementById("trainFirst").textContent = Number(data.mean_first_50 || 0).toFixed(2);
document.getElementById("trainLast").textContent = Number(data.mean_last_50 || 0).toFixed(2);
const improvement = Number(data.improvement || 0);
const sign = improvement >= 0 ? "+" : "";
const improveEl = document.getElementById("trainImprovement");
improveEl.textContent = `Improvement: ${sign}${improvement.toFixed(2)}`;
improveEl.style.color = improvement >= 0 ? "#16a34a" : "#dc2626";
const statusEl = document.getElementById("trainStatus");
if (data.has_data) {
statusEl.textContent = "Live: training metrics loaded.";
statusEl.style.color = "#16a34a";
} else {
statusEl.textContent = data.message || "No training data found yet.";
statusEl.style.color = "#666";
}
drawTrainingChart(trainingRewards, trainingSmoothed);
} catch (err) {
const statusEl = document.getElementById("trainStatus");
statusEl.textContent = "Could not load training metrics from server.";
statusEl.style.color = "#dc2626";
}
}
function resetBoard() {
currentTurn = 0;
evidenceCount = 0;
contradictionCount = 0;
totalReward = 0;
messageCount = 0;
revealedEvidenceKeys.clear();
if (window.revealedContradictionKeys) window.revealedContradictionKeys.clear();
Object.keys(rewardComponents).forEach(k => rewardComponents[k] = 0);
document.getElementById("statTurn").textContent = "0 / 15";
document.getElementById("statEvidence").textContent = "0 / 3";
document.getElementById("statContra").textContent = "0";
document.getElementById("statReward").textContent = "0.00";
document.getElementById("statReward").className = "stat-value";
document.getElementById("statOutcome").textContent = "β€”";
document.getElementById("statOutcome").className = "stat-value neutral";
document.getElementById("rewardBar").style.width = "50%";
document.getElementById("msgCount").textContent = "0 messages";
document.getElementById("conversationLog").innerHTML = `
<div class="empty-state">
<div class="empty-icon">πŸ•΅οΈ</div>
<div class="empty-text">Click <strong>Run Live Server</strong> to watch a full<br>investigation episode unfold live.</div>
</div>`;
document.getElementById("evidenceList").innerHTML = `
<div class="empty-state" id="evidenceEmptyState" style="padding:14px;">
<div class="empty-text" style="font-size:0.72rem;">No evidence revealed yet.</div>
</div>`;
document.getElementById("alertList").innerHTML = `
<div class="empty-state" style="padding:14px;">
<div class="empty-text" style="font-size:0.72rem;">No contradictions detected yet.</div>
</div>`;
document.getElementById("btnReset").style.display = "none";
document.getElementById("outcomeOverlay").classList.remove("visible");
updateReward();
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// ── Live Backend Runner ──────────────────────────────────────────────────────
async function runLiveAgent() {
if (demoRunning) return;
demoRunning = true;
resetBoard();
document.getElementById("btnRunLive").disabled = true;
document.getElementById("btnRunLive").textContent = "⏳ Waiting for AI...";
document.getElementById("statusText").textContent = "Live Investigating";
document.getElementById("statusBadge").style.borderColor = "rgba(16, 185, 129, 0.4)";
document.getElementById("statusBadge").style.background = "rgba(16, 185, 129, 0.1)";
document.getElementById("statusBadge").querySelector(".status-dot").style.background = "var(--accent-green)";
document.getElementById("statusBadge").querySelector("span:last-child").style.color = "var(--accent-green)";
const epNum = rewardHistory.length + 1;
document.getElementById("episodeLabel").textContent = `Episode ${epNum}`;
const DELAY_SHORT = 600;
const DELAY_LONG = 1200;
try {
const response = await fetch("/api/run_episode");
if (!response.ok) throw new Error("Server not responding");
const data = await response.json();
if (data.status !== "ok") {
throw new Error("Simulation failed");
}
const script = data.trace;
document.getElementById("btnRunLive").textContent = "βš™οΈ Executing Trace...";
for (const event of script) {
if (!demoRunning) break;
if (event.type === "detective") {
updateTurn(event.turn);
const actionDesc = event.action.includes("ask")
? `Asking ${event.target}: "${event.text}"`
: event.action.includes("evid")
? `Requesting evidence: ${event.item}`
: `Accusing ${event.target}`;
addMessage("Detective", actionDesc, event.turn);
// turnCost is tracked by updateTurn() already (-0.3 * turn)
// Don't touch rewardComponents.base here β€” we derive it at the end
await sleep(DELAY_LONG);
} else if (event.type === "response") {
addMessage(event.speaker, event.text, event.turn);
await sleep(DELAY_SHORT);
} else if (event.type === "contradiction") {
addContradiction(event.suspect, event.claim1 || "Previous claim", event.claim1Turn || "?", event.claim2 || ("New claim: " + event.detail), event.turn);
// addContradiction() already adds +2.0 to rewardComponents.contradictions
// No extra shifting needed since we derive base at the end
await sleep(DELAY_SHORT);
} else if (event.type === "evidence") {
revealEvidence(event.item, event.item, event.desc);
// revealEvidence() already adds +1.0 to rewardComponents.evidence
// No extra shifting needed since we derive base at the end
await sleep(DELAY_SHORT);
} else if (event.type === "outcome") {
// Now derive the base component from the true server total
// base = serverTotal - turnCost - contradictions - evidence
let serverTotal = data.rewards.detective;
rewardComponents.base = serverTotal - rewardComponents.turnCost - rewardComponents.contradictions - rewardComponents.evidence;
totalReward = serverTotal;
updateReward();
await sleep(800);
showOutcome(event.correct || false);
}
}
// Final safeguard: force-set everything to server truth
totalReward = data.rewards.detective;
rewardComponents.base = totalReward - rewardComponents.turnCost - rewardComponents.contradictions - rewardComponents.evidence;
rewardComponents.total = totalReward;
updateReward();
document.getElementById("statReward").textContent = totalReward.toFixed(2);
} catch (e) {
addMessage("System", `⚠️ Error connecting to live server: ${e.message}. Note: you must run 'uvicorn server.app:app' and open the app from localhost to use Live Mode!`, currentTurn || 0);
}
document.getElementById("statusText").textContent = "Complete";
document.getElementById("statusBadge").style.borderColor = "rgba(16, 185, 129, 0.25)";
document.getElementById("btnRunLive").disabled = false;
document.getElementById("btnRunLive").textContent = "πŸš€ Run Live Server";
demoRunning = false;
}
window.addEventListener("load", () => {
loadTrainingProgress();
// Auto-refresh metrics so new training artifacts show up without reloading.
setInterval(loadTrainingProgress, 15000);
});
</script>
</body>
</html>