Spaces:
Sleeping
Sleeping
| <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 ; 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) ; | |
| 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); } | |
| /* ββ 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> | |
| </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">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:8px;" id="trainImprovement">Improvement: -</div> | |
| <div style="font-size:0.7rem; margin-bottom:8px; color:#666;" id="trainStatus">Checking training artifacts...</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 = []; | |
| 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> | |