TheAllanB commited on
Commit
2c80dff
Β·
1 Parent(s): 973485c

Add interactive frontend: radar demo + live API + results dashboard

Browse files
Files changed (4) hide show
  1. Dockerfile +1 -0
  2. requirements.txt +1 -0
  3. src/server.py +11 -0
  4. static/index.html +1285 -0
Dockerfile CHANGED
@@ -7,6 +7,7 @@ RUN pip install --no-cache-dir -r requirements.txt
7
 
8
  COPY src/ src/
9
  COPY tests/ tests/
 
10
 
11
  EXPOSE 7860
12
 
 
7
 
8
  COPY src/ src/
9
  COPY tests/ tests/
10
+ COPY static/ static/
11
 
12
  EXPOSE 7860
13
 
requirements.txt CHANGED
@@ -3,3 +3,4 @@ uvicorn[standard]>=0.23.0
3
  httpx>=0.25.0
4
  pytest>=7.4.0
5
  matplotlib>=3.7.0
 
 
3
  httpx>=0.25.0
4
  pytest>=7.4.0
5
  matplotlib>=3.7.0
6
+ aiofiles>=23.0.0
src/server.py CHANGED
@@ -1,8 +1,11 @@
1
  from __future__ import annotations
2
  from dataclasses import asdict
 
3
  from typing import Optional
4
 
5
  from fastapi import FastAPI, HTTPException
 
 
6
  from pydantic import BaseModel
7
 
8
  from .environment import TicketmeltEnv
@@ -11,6 +14,14 @@ from .models import Action, Observation
11
  app = FastAPI(title="TicketMelt", version="0.1.0")
12
  _env = TicketmeltEnv()
13
 
 
 
 
 
 
 
 
 
14
 
15
  class ResetRequest(BaseModel):
16
  seed: Optional[int] = None
 
1
  from __future__ import annotations
2
  from dataclasses import asdict
3
+ from pathlib import Path
4
  from typing import Optional
5
 
6
  from fastapi import FastAPI, HTTPException
7
+ from fastapi.staticfiles import StaticFiles
8
+ from fastapi.responses import FileResponse
9
  from pydantic import BaseModel
10
 
11
  from .environment import TicketmeltEnv
 
14
  app = FastAPI(title="TicketMelt", version="0.1.0")
15
  _env = TicketmeltEnv()
16
 
17
+ _static_dir = Path(__file__).parent.parent / "static"
18
+ app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
19
+
20
+
21
+ @app.get("/")
22
+ async def root():
23
+ return FileResponse(str(_static_dir / "index.html"))
24
+
25
 
26
  class ResetRequest(BaseModel):
27
  seed: Optional[int] = None
static/index.html ADDED
@@ -0,0 +1,1285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TICKETMELT β€” Multi-Agent Coordination Training</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
8
+ <style>
9
+ *{margin:0;padding:0;box-sizing:border-box}
10
+ :root{
11
+ --bg:#020a06;--green:#00ff7f;--amber:#ffb300;--red:#ff4444;--blue:#4fc3f7;
12
+ --pink:#f48fb1;--dim:#1a2e1f;--border:rgba(0,255,127,0.18);
13
+ --glow:0 0 8px #00ff7f,0 0 24px rgba(0,255,127,0.4);
14
+ --glow-sm:0 0 6px rgba(0,255,127,0.6);
15
+ }
16
+ html{scroll-behavior:smooth}
17
+ body{
18
+ background:var(--bg);color:var(--green);
19
+ font-family:'Share Tech Mono',monospace;font-size:14px;
20
+ overflow-x:hidden;
21
+ }
22
+ body::before{
23
+ content:'';position:fixed;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:9998;
24
+ background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,255,127,0.025) 2px,rgba(0,255,127,0.025) 4px);
25
+ }
26
+
27
+ /* NAV */
28
+ nav{
29
+ position:fixed;top:0;left:0;right:0;z-index:1000;
30
+ background:rgba(2,10,6,0.92);border-bottom:1px solid var(--border);
31
+ display:flex;align-items:center;justify-content:space-between;
32
+ padding:0 32px;height:52px;backdrop-filter:blur(8px);
33
+ }
34
+ .nav-logo{font-family:'Orbitron',sans-serif;font-weight:900;font-size:16px;
35
+ color:var(--green);text-shadow:var(--glow-sm);letter-spacing:3px;}
36
+ .nav-links{display:flex;gap:24px}
37
+ .nav-links a{color:rgba(0,255,127,0.7);text-decoration:none;font-size:12px;
38
+ letter-spacing:2px;transition:all .2s;}
39
+ .nav-links a:hover{color:var(--green);text-shadow:var(--glow-sm);}
40
+
41
+ /* SECTIONS */
42
+ section{padding:80px 0;max-width:1200px;margin:0 auto;padding-left:32px;padding-right:32px;}
43
+ .section-title{
44
+ font-family:'Orbitron',sans-serif;font-size:28px;font-weight:700;
45
+ color:var(--green);text-shadow:var(--glow);margin-bottom:8px;letter-spacing:2px;
46
+ }
47
+ .section-sub{color:rgba(0,255,127,0.5);font-size:12px;letter-spacing:3px;margin-bottom:40px;}
48
+
49
+ /* HERO */
50
+ #hero{
51
+ min-height:100vh;display:flex;flex-direction:column;justify-content:center;
52
+ align-items:center;text-align:center;position:relative;overflow:hidden;
53
+ padding-top:52px;
54
+ }
55
+ #hero-grid{
56
+ position:absolute;inset:0;pointer-events:none;
57
+ background-image:linear-gradient(rgba(0,255,127,0.07) 1px,transparent 1px),
58
+ linear-gradient(90deg,rgba(0,255,127,0.07) 1px,transparent 1px);
59
+ background-size:60px 60px;
60
+ animation:grid-pulse 4s ease-in-out infinite;
61
+ }
62
+ @keyframes grid-pulse{0%,100%{opacity:.6}50%{opacity:1}}
63
+ .hero-badge{
64
+ font-size:11px;letter-spacing:4px;color:var(--amber);
65
+ border:1px solid var(--amber);padding:4px 16px;margin-bottom:24px;
66
+ text-shadow:0 0 8px var(--amber);
67
+ }
68
+ h1{
69
+ font-family:'Orbitron',sans-serif;font-size:clamp(48px,8vw,96px);font-weight:900;
70
+ color:var(--green);text-shadow:var(--glow);line-height:1;margin-bottom:24px;
71
+ letter-spacing:4px;
72
+ }
73
+ .hero-subtitle{
74
+ font-size:12px;letter-spacing:3px;color:rgba(0,255,127,0.6);
75
+ margin-bottom:40px;max-width:700px;line-height:2;
76
+ }
77
+ .hero-stats{
78
+ display:flex;gap:48px;margin-bottom:48px;
79
+ }
80
+ .stat{text-align:center}
81
+ .stat-val{font-family:'Orbitron',sans-serif;font-size:36px;font-weight:900;
82
+ color:var(--amber);text-shadow:0 0 12px var(--amber);}
83
+ .stat-label{font-size:10px;letter-spacing:2px;color:rgba(0,255,127,0.5);margin-top:4px;}
84
+ .hero-btns{display:flex;gap:16px}
85
+ .btn{
86
+ font-family:'Share Tech Mono',monospace;font-size:13px;letter-spacing:2px;
87
+ padding:12px 28px;cursor:pointer;border:1px solid var(--green);
88
+ background:transparent;color:var(--green);transition:all .25s;text-decoration:none;
89
+ display:inline-block;
90
+ }
91
+ .btn:hover{background:rgba(0,255,127,0.12);text-shadow:var(--glow-sm);box-shadow:var(--glow-sm);}
92
+ .btn-amber{border-color:var(--amber);color:var(--amber);}
93
+ .btn-amber:hover{background:rgba(255,179,0,0.1);text-shadow:0 0 8px var(--amber);}
94
+
95
+ /* CARDS */
96
+ .cards-2x2{display:grid;grid-template-columns:1fr 1fr;gap:20px}
97
+ .card{
98
+ border:1px solid var(--border);padding:28px;position:relative;overflow:hidden;
99
+ background:rgba(0,255,127,0.02);transition:border-color .25s,background .25s;
100
+ }
101
+ .card:hover{border-color:rgba(0,255,127,0.4);background:rgba(0,255,127,0.04);}
102
+ .card::before{
103
+ content:'';position:absolute;top:0;left:0;right:0;height:2px;
104
+ background:var(--green);opacity:.5;
105
+ }
106
+ .card-num{
107
+ font-family:'Orbitron',sans-serif;font-size:28px;font-weight:900;
108
+ color:var(--amber);text-shadow:0 0 12px var(--amber);margin-bottom:8px;
109
+ }
110
+ .card-title{font-size:13px;letter-spacing:2px;color:var(--green);margin-bottom:12px;}
111
+ .card-body{font-size:12px;color:rgba(0,255,127,0.6);line-height:1.8;}
112
+
113
+ /* DEMO SECTION */
114
+ #demo-wrapper{
115
+ display:grid;grid-template-columns:420px 1fr;gap:24px;align-items:start;
116
+ }
117
+ #radar-wrap{position:relative;}
118
+ #radar{border:1px solid var(--border);background:#000d05;display:block;}
119
+ .radar-label{
120
+ font-size:10px;letter-spacing:2px;color:rgba(0,255,127,0.4);
121
+ margin-top:8px;text-align:center;
122
+ }
123
+
124
+ /* CONTROL PANEL */
125
+ #ctrl-panel{display:flex;flex-direction:column;gap:16px;}
126
+ .mode-toggle{display:flex;gap:0;}
127
+ .mode-btn{
128
+ flex:1;padding:10px;font-family:'Share Tech Mono',monospace;font-size:12px;
129
+ letter-spacing:2px;cursor:pointer;border:1px solid var(--border);
130
+ background:transparent;color:rgba(0,255,127,0.4);transition:all .2s;
131
+ }
132
+ .mode-btn.active{background:rgba(0,255,127,0.12);color:var(--green);
133
+ border-color:var(--green);text-shadow:var(--glow-sm);}
134
+ .panel-title{font-size:10px;letter-spacing:3px;color:rgba(0,255,127,0.4);margin-bottom:8px;}
135
+
136
+ /* FLIGHT STRIPS */
137
+ #strips-container{display:flex;flex-direction:column;gap:8px;}
138
+ .strip{
139
+ display:grid;grid-template-columns:90px 1fr 80px 60px;align-items:center;
140
+ gap:10px;padding:10px 14px;border:1px solid var(--border);
141
+ background:rgba(0,255,127,0.02);transition:all .3s;position:relative;overflow:hidden;
142
+ }
143
+ .strip.rwy-a{border-left:3px solid var(--blue);}
144
+ .strip.rwy-b{border-left:3px solid var(--amber);}
145
+ .strip.collision{border-left:3px solid var(--red);animation:flash-red .4s ease 3;}
146
+ .strip.landed{border-left:3px solid rgba(0,255,127,0.3);opacity:.5;}
147
+ .strip.crashed{border-left:3px solid var(--red);opacity:.4;}
148
+ @keyframes flash-red{0%,100%{background:transparent}50%{background:rgba(255,68,68,0.15)}}
149
+ .strip-call{font-size:12px;color:var(--green);font-weight:bold;}
150
+ .strip-call.priority{color:var(--red);animation:blink .8s step-end infinite;}
151
+ @keyframes blink{0%,100%{opacity:1}50%{opacity:.2}}
152
+ .strip-fuel-wrap{height:6px;background:rgba(0,255,127,0.1);border-radius:3px;overflow:hidden;}
153
+ .strip-fuel{height:100%;background:var(--green);transition:width .5s ease;border-radius:3px;}
154
+ .strip-fuel.low{background:var(--amber);}
155
+ .strip-fuel.critical{background:var(--red);}
156
+ .strip-tag{
157
+ font-size:10px;letter-spacing:1px;padding:3px 6px;text-align:center;
158
+ border:1px solid;
159
+ }
160
+ .tag-a{color:var(--blue);border-color:var(--blue);}
161
+ .tag-b{color:var(--amber);border-color:var(--amber);}
162
+ .tag-hold{color:rgba(0,255,127,0.4);border-color:rgba(0,255,127,0.2);}
163
+ .tag-land{color:var(--green);border-color:rgba(0,255,127,0.4);}
164
+ .tag-crash{color:var(--red);border-color:var(--red);}
165
+ .strip-round{font-size:10px;color:rgba(0,255,127,0.4);text-align:right;}
166
+
167
+ /* COMMS LOG */
168
+ #comms-log{
169
+ height:120px;overflow-y:auto;border:1px solid var(--border);
170
+ padding:10px;background:rgba(0,0,0,0.4);font-size:11px;
171
+ }
172
+ #comms-log::-webkit-scrollbar{width:4px;}
173
+ #comms-log::-webkit-scrollbar-track{background:transparent;}
174
+ #comms-log::-webkit-scrollbar-thumb{background:rgba(0,255,127,0.3);}
175
+ .log-line{padding:2px 0;color:rgba(0,255,127,0.7);line-height:1.5;}
176
+ .log-line .ts{color:rgba(0,255,127,0.35);margin-right:6px;}
177
+ .log-line .call{color:var(--green);margin-right:6px;}
178
+ .log-line.priority .call{color:var(--red);}
179
+ .log-line.collision-line{color:var(--red);}
180
+ .log-line.landed-line{color:var(--amber);}
181
+
182
+ /* METRICS BAR */
183
+ .metrics-bar{
184
+ display:grid;grid-template-columns:repeat(3,1fr);gap:12px;
185
+ }
186
+ .metric-box{border:1px solid var(--border);padding:12px;text-align:center;}
187
+ .metric-box .val{
188
+ font-family:'Orbitron',sans-serif;font-size:22px;font-weight:700;color:var(--amber);}
189
+ .metric-box .lbl{font-size:9px;letter-spacing:2px;color:rgba(0,255,127,0.4);margin-top:4px;}
190
+
191
+ /* CTRL BUTTONS */
192
+ .ctrl-btns{display:flex;gap:8px;}
193
+ .ctrl-btn{
194
+ flex:1;padding:10px;font-family:'Share Tech Mono',monospace;font-size:12px;
195
+ letter-spacing:1px;cursor:pointer;border:1px solid var(--border);
196
+ background:transparent;color:var(--green);transition:all .2s;
197
+ }
198
+ .ctrl-btn:hover:not(:disabled){
199
+ border-color:var(--green);background:rgba(0,255,127,0.1);text-shadow:var(--glow-sm);}
200
+ .ctrl-btn:disabled{opacity:.4;cursor:not-allowed;}
201
+ .ctrl-btn.running{border-color:var(--amber);color:var(--amber);}
202
+
203
+ /* COMPARE OVERLAY */
204
+ #compare-overlay{
205
+ display:none;position:fixed;inset:0;background:rgba(2,10,6,0.96);
206
+ z-index:2000;justify-content:center;align-items:center;padding:32px;
207
+ }
208
+ #compare-overlay.open{display:flex;}
209
+ .compare-panel{
210
+ max-width:900px;width:100%;border:1px solid var(--border);
211
+ background:rgba(0,5,2,0.95);padding:40px;
212
+ }
213
+ .compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin:32px 0;}
214
+ .compare-col{border:1px solid var(--border);padding:24px;}
215
+ .compare-col.untrained{border-top:3px solid var(--red);}
216
+ .compare-col.trained{border-top:3px solid var(--green);}
217
+ .compare-col h3{font-family:'Orbitron',sans-serif;font-size:14px;margin-bottom:20px;letter-spacing:2px;}
218
+ .compare-stat{display:flex;justify-content:space-between;padding:8px 0;
219
+ border-bottom:1px solid rgba(0,255,127,0.06);}
220
+ .compare-stat .cs-lbl{color:rgba(0,255,127,0.6);font-size:11px;}
221
+ .compare-stat .cs-val{color:var(--amber);font-family:'Orbitron',sans-serif;font-size:13px;}
222
+ .compare-stat .cs-val.good{color:var(--green);}
223
+ .compare-desc{margin-top:16px;font-size:11px;color:rgba(0,255,127,0.5);line-height:1.8;}
224
+
225
+ /* LIVE API PANEL */
226
+ #live-panel{
227
+ margin-top:40px;border:1px solid var(--border);padding:28px;
228
+ background:rgba(0,255,127,0.01);
229
+ }
230
+ .api-status{display:flex;align-items:center;gap:10px;margin-bottom:20px;}
231
+ .status-dot{width:10px;height:10px;border-radius:50%;background:var(--green);
232
+ box-shadow:0 0 8px var(--green);animation:pulse 2s ease-in-out infinite;}
233
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
234
+ .status-dot.offline{background:var(--red);box-shadow:0 0 8px var(--red);}
235
+ .api-btns{display:flex;gap:12px;margin-bottom:16px;}
236
+ #api-output{
237
+ min-height:120px;max-height:280px;overflow-y:auto;
238
+ background:#000d05;border:1px solid rgba(0,255,127,0.1);
239
+ padding:16px;font-size:12px;color:var(--green);line-height:1.6;
240
+ white-space:pre-wrap;word-break:break-all;
241
+ }
242
+
243
+ /* REWARDS SECTION */
244
+ .reward-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;}
245
+ .reward-card{border:1px solid var(--border);padding:24px;position:relative;}
246
+ .reward-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;}
247
+ .rc-r1::before{background:var(--blue);}
248
+ .rc-r2::before{background:var(--green);}
249
+ .rc-r3::before{background:var(--amber);}
250
+ .rc-r4::before{background:var(--pink);}
251
+ .rc-weight{
252
+ font-family:'Orbitron',sans-serif;font-size:24px;font-weight:900;
253
+ margin-bottom:8px;
254
+ }
255
+ .rc-r1 .rc-weight{color:var(--blue);}
256
+ .rc-r2 .rc-weight{color:var(--green);}
257
+ .rc-r3 .rc-weight{color:var(--amber);}
258
+ .rc-r4 .rc-weight{color:var(--pink);}
259
+ .rc-name{font-size:11px;letter-spacing:2px;color:var(--green);margin-bottom:12px;}
260
+ .rc-desc{font-size:11px;color:rgba(0,255,127,0.55);line-height:1.8;}
261
+ .formula-block{
262
+ margin-top:32px;border:1px solid rgba(0,255,127,0.2);padding:20px;
263
+ background:rgba(0,255,127,0.02);font-size:13px;
264
+ }
265
+ .formula-block code{color:var(--amber);letter-spacing:.5px;}
266
+
267
+ /* RESULTS SECTION */
268
+ .results-grid{display:grid;grid-template-columns:1fr 1fr;gap:40px;align-items:start;}
269
+ .results-table{width:100%;border-collapse:collapse;}
270
+ .results-table th{
271
+ font-size:10px;letter-spacing:2px;color:rgba(0,255,127,0.5);
272
+ padding:8px 12px;border-bottom:1px solid var(--border);text-align:left;
273
+ }
274
+ .results-table td{padding:10px 12px;border-bottom:1px solid rgba(0,255,127,0.06);font-size:12px;}
275
+ .results-table tr:hover td{background:rgba(0,255,127,0.03);}
276
+ .delta-pos{color:var(--green);}
277
+ .delta-neg{color:rgba(0,255,127,0.45);}
278
+ .delta-red{color:var(--red);}
279
+ .honest-text{
280
+ margin-top:24px;font-size:11px;color:rgba(0,255,127,0.5);
281
+ line-height:2;border-left:2px solid rgba(0,255,127,0.2);padding-left:16px;
282
+ }
283
+ .charts-col{display:flex;flex-direction:column;gap:32px;}
284
+ .chart-wrap{border:1px solid var(--border);padding:16px;background:rgba(0,0,0,0.3);}
285
+ .chart-title{font-size:10px;letter-spacing:2px;color:rgba(0,255,127,0.5);margin-bottom:12px;}
286
+ canvas.chart{display:block;}
287
+
288
+ /* TRAINING CONFIG */
289
+ .config-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px;}
290
+ .config-table{width:100%;border-collapse:collapse;}
291
+ .config-table tr{border-bottom:1px solid rgba(0,255,127,0.06);}
292
+ .config-table td{padding:10px 14px;font-size:12px;}
293
+ .config-table td:first-child{color:rgba(0,255,127,0.5);font-size:10px;letter-spacing:1px;width:55%;}
294
+ .config-table td:last-child{color:var(--amber);}
295
+ .config-box{border:1px solid var(--border);padding:4px;}
296
+ .config-box-title{
297
+ font-family:'Orbitron',sans-serif;font-size:12px;letter-spacing:2px;
298
+ padding:12px 14px;border-bottom:1px solid var(--border);color:var(--green);
299
+ }
300
+
301
+ /* LINKS SECTION */
302
+ .links-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;}
303
+ .link-card{
304
+ border:1px solid var(--border);padding:28px;text-align:center;
305
+ text-decoration:none;display:block;transition:all .25s;
306
+ position:relative;overflow:hidden;
307
+ }
308
+ .link-card::before{
309
+ content:'';position:absolute;top:0;left:0;right:0;height:2px;
310
+ background:linear-gradient(90deg,transparent,var(--green),transparent);
311
+ transform:translateX(-100%);transition:transform .4s;
312
+ }
313
+ .link-card:hover::before{transform:translateX(0);}
314
+ .link-card:hover{border-color:rgba(0,255,127,0.5);background:rgba(0,255,127,0.04);}
315
+ .link-icon{font-size:28px;margin-bottom:12px;color:var(--amber);}
316
+ .link-title{font-family:'Orbitron',sans-serif;font-size:13px;color:var(--green);
317
+ margin-bottom:8px;letter-spacing:1px;}
318
+ .link-url{font-size:10px;color:rgba(0,255,127,0.4);word-break:break-all;}
319
+
320
+ /* FOOTER */
321
+ footer{
322
+ border-top:1px solid var(--border);padding:32px;text-align:center;
323
+ color:rgba(0,255,127,0.3);font-size:11px;letter-spacing:2px;
324
+ }
325
+ </style>
326
+ </head>
327
+ <body>
328
+
329
+ <!-- NAV -->
330
+ <nav>
331
+ <div class="nav-logo">β—ˆ TICKETMELT</div>
332
+ <div class="nav-links">
333
+ <a href="#problem">PROBLEM</a>
334
+ <a href="#demo">DEMO</a>
335
+ <a href="#rewards">REWARDS</a>
336
+ <a href="#results">RESULTS</a>
337
+ <a href="#training">CONFIG</a>
338
+ <a href="#links">LINKS</a>
339
+ </div>
340
+ </nav>
341
+
342
+ <!-- HERO -->
343
+ <section id="hero">
344
+ <div id="hero-grid"></div>
345
+ <div class="hero-badge">β—ˆ OPENENV HACKATHON β€” APR 2026</div>
346
+ <h1>TICKETMELT</h1>
347
+ <div class="hero-subtitle">
348
+ MULTI-AGENT COORDINATION Β· SIMULTANEOUS RESOURCE CONTENTION Β· GRPO TRAINING
349
+ </div>
350
+ <div class="hero-stats">
351
+ <div class="stat"><div class="stat-val">4</div><div class="stat-label">AGENTS</div></div>
352
+ <div class="stat"><div class="stat-val">2</div><div class="stat-label">SERVERS</div></div>
353
+ <div class="stat"><div class="stat-val">31</div><div class="stat-label">TESTS</div></div>
354
+ <div class="stat"><div class="stat-val">4</div><div class="stat-label">REWARD COMPONENTS</div></div>
355
+ </div>
356
+ <div class="hero-btns">
357
+ <a href="#demo" class="btn">β–Ά LIVE DEMO</a>
358
+ <a href="https://theallanb-ticketmelt.hf.space/state" class="btn btn-amber" target="_blank">β—ˆ API ENDPOINT</a>
359
+ </div>
360
+ </section>
361
+
362
+ <!-- PROBLEM -->
363
+ <section id="problem" style="padding-top:100px;">
364
+ <div class="section-title">Why LLMs Deadlock Under Pressure</div>
365
+ <div class="section-sub">β—ˆ DOCUMENTED FAILURE MODES IN FRONTIER MODELS β€” 2024–2026</div>
366
+ <div class="cards-2x2">
367
+ <div class="card">
368
+ <div class="card-num">95%</div>
369
+ <div class="card-title">β—ˆ DEADLOCK RATE</div>
370
+ <div class="card-body">DPBench (Feb 2026) showed frontier LLMs including GPT-5.2, Claude Opus 4.5, and Grok 4.1 deadlock above 95% on simultaneous coordination tasks. Adding a communication channel made it worse.</div>
371
+ </div>
372
+ <div class="card">
373
+ <div class="card-num">0%</div>
374
+ <div class="card-title">β—ˆ URGENCY ADAPTATION</div>
375
+ <div class="card-body">NeurIPS 2024 Concordia Contest: LLM agents handle vanilla negotiation but fail when scenarios require detecting and adapting to peers with different urgency levels. Heterogeneous priority is invisible to them.</div>
376
+ </div>
377
+ <div class="card">
378
+ <div class="card-num">4Γ—</div>
379
+ <div class="card-title">β—ˆ SIMULTANEOUS COMMITMENT</div>
380
+ <div class="card-body">4 agents commit simultaneously. No agent can wait to observe others before acting. The symmetry is structural β€” identical reasoning leads to identical actions, which collide. Training must break this symmetry.</div>
381
+ </div>
382
+ <div class="card">
383
+ <div class="card-num" style="color:var(--green);text-shadow:var(--glow);">RL</div>
384
+ <div class="card-title">β—ˆ FIX β€” GRPO TRAINING</div>
385
+ <div class="card-body">GRPO trains Qwen-2.5-3B-Instruct on coordination episodes from binary reward signal alone. No expert demonstrations. The model learns asymmetric strategies and urgency adaptation through reward shaping.</div>
386
+ </div>
387
+ </div>
388
+ </section>
389
+
390
+ <!-- DEMO -->
391
+ <section id="demo" style="padding-top:100px;">
392
+ <div class="section-title">Live Demo</div>
393
+ <div class="section-sub">β—ˆ HARDCODED EPISODE REPLAY + LIVE ENVIRONMENT STATE</div>
394
+
395
+ <div id="demo-wrapper">
396
+ <!-- RADAR -->
397
+ <div id="radar-wrap">
398
+ <canvas id="radar" width="420" height="420"></canvas>
399
+ <div class="radar-label">β—ˆ APPROACH SEQUENCING RADAR β€” TICKETMELT ENV</div>
400
+ </div>
401
+
402
+ <!-- CONTROL PANEL -->
403
+ <div id="ctrl-panel">
404
+ <div class="mode-toggle">
405
+ <button class="mode-btn active" id="btn-untrained" onclick="setMode('untrained')">β—Ž UNTRAINED MODEL</button>
406
+ <button class="mode-btn" id="btn-trained" onclick="setMode('trained')">β—‰ TRAINED MODEL</button>
407
+ </div>
408
+
409
+ <div>
410
+ <div class="panel-title">FLIGHT PROGRESS STRIPS</div>
411
+ <div id="strips-container">
412
+ <div class="strip" id="strip-0">
413
+ <div class="strip-call" id="call-0">UAL-441</div>
414
+ <div><div class="strip-fuel-wrap"><div class="strip-fuel" id="fuel-0" style="width:100%"></div></div></div>
415
+ <div class="strip-tag tag-hold" id="tag-0">HOLD</div>
416
+ <div class="strip-round" id="rnd-0">RND β€”</div>
417
+ </div>
418
+ <div class="strip" id="strip-1">
419
+ <div class="strip-call" id="call-1">DAL-892</div>
420
+ <div><div class="strip-fuel-wrap"><div class="strip-fuel" id="fuel-1" style="width:100%"></div></div></div>
421
+ <div class="strip-tag tag-hold" id="tag-1">HOLD</div>
422
+ <div class="strip-round" id="rnd-1">RND β€”</div>
423
+ </div>
424
+ <div class="strip" id="strip-2">
425
+ <div class="strip-call priority" id="call-2">SWA-77 ⚠</div>
426
+ <div><div class="strip-fuel-wrap"><div class="strip-fuel" id="fuel-2" style="width:100%"></div></div></div>
427
+ <div class="strip-tag tag-hold" id="tag-2">HOLD</div>
428
+ <div class="strip-round" id="rnd-2">RND β€”</div>
429
+ </div>
430
+ <div class="strip" id="strip-3">
431
+ <div class="strip-call" id="call-3">AAL-213</div>
432
+ <div><div class="strip-fuel-wrap"><div class="strip-fuel" id="fuel-3" style="width:100%"></div></div></div>
433
+ <div class="strip-tag tag-hold" id="tag-3">HOLD</div>
434
+ <div class="strip-round" id="rnd-3">RND β€”</div>
435
+ </div>
436
+ </div>
437
+ </div>
438
+
439
+ <div>
440
+ <div class="panel-title">RADIO COMMS</div>
441
+ <div id="comms-log"><div class="log-line" style="color:rgba(0,255,127,0.25)">β€” awaiting episode start β€”</div></div>
442
+ </div>
443
+
444
+ <div class="metrics-bar">
445
+ <div class="metric-box">
446
+ <div class="val" id="m-collisions">0</div>
447
+ <div class="lbl">COLLISIONS</div>
448
+ </div>
449
+ <div class="metric-box">
450
+ <div class="val" id="m-landed">0</div>
451
+ <div class="lbl">LANDED</div>
452
+ </div>
453
+ <div class="metric-box">
454
+ <div class="val" id="m-outcome" style="font-size:14px;color:rgba(0,255,127,0.35)">β€”</div>
455
+ <div class="lbl">OUTCOME</div>
456
+ </div>
457
+ </div>
458
+
459
+ <div class="ctrl-btns">
460
+ <button class="ctrl-btn" id="run-btn" onclick="runEpisode()">β–Ά RUN EPISODE</button>
461
+ <button class="ctrl-btn" onclick="showCompare()">≑ COMPARE</button>
462
+ <button class="ctrl-btn" onclick="resetDemo()">β†Ί RESET</button>
463
+ </div>
464
+ </div>
465
+ </div>
466
+
467
+ <!-- LIVE API PANEL -->
468
+ <div id="live-panel">
469
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
470
+ <div class="section-title" style="font-size:18px;margin-bottom:0;">β—ˆ LIVE ENVIRONMENT STATE</div>
471
+ <div class="api-status">
472
+ <div class="status-dot" id="status-dot"></div>
473
+ <span id="status-text" style="font-size:11px;letter-spacing:2px;color:rgba(0,255,127,0.6);">CHECKING…</span>
474
+ </div>
475
+ </div>
476
+ <div class="api-btns">
477
+ <button class="btn" onclick="fetchState()">FETCH LIVE STATE</button>
478
+ <button class="btn btn-amber" onclick="resetEnv()">RESET EPISODE</button>
479
+ </div>
480
+ <div id="api-output">β€” press FETCH LIVE STATE to query the running environment β€”</div>
481
+ </div>
482
+ </section>
483
+
484
+ <!-- COMPARE OVERLAY -->
485
+ <div id="compare-overlay" onclick="hideCompare()">
486
+ <div class="compare-panel" onclick="event.stopPropagation()">
487
+ <div class="section-title" style="font-size:20px;">≑ UNTRAINED vs TRAINED</div>
488
+ <div class="section-sub">β—ˆ 20-EPISODE EVAL Β· SAME SEEDS Β· INLINE REWARD FUNCTION</div>
489
+ <div class="compare-grid">
490
+ <div class="compare-col untrained">
491
+ <h3 style="color:var(--red);">β—Ž UNTRAINED BASELINE</h3>
492
+ <div class="compare-stat"><span class="cs-lbl">COLLISION RATE</span><span class="cs-val">96%</span></div>
493
+ <div class="compare-stat"><span class="cs-lbl">WIN RATE</span><span class="cs-val">80%</span></div>
494
+ <div class="compare-stat"><span class="cs-lbl">R4 YIELD TO CRITICAL</span><span class="cs-val">0.169</span></div>
495
+ <div class="compare-desc">"All agents queue for PROD_A. Emergency priority ignored. SWA-77 lost. Model reasons identically every round β€” convergent deadlock."</div>
496
+ </div>
497
+ <div class="compare-col trained">
498
+ <h3 style="color:var(--green);">β—‰ TRAINED (16 GRPO STEPS)</h3>
499
+ <div class="compare-stat"><span class="cs-lbl">COLLISION RATE</span><span class="cs-val good">89%</span></div>
500
+ <div class="compare-stat"><span class="cs-lbl">WIN RATE</span><span class="cs-val">70%</span></div>
501
+ <div class="compare-stat"><span class="cs-lbl">R4 YIELD TO CRITICAL</span><span class="cs-val good">0.276 <span style="color:var(--green);font-size:11px;">(+0.107)</span></span></div>
502
+ <div class="compare-desc">"Model learns urgency adaptation. Yield-to-critical improves +63%. Collision rate drops. 16 gradient steps on A100 β€” component rewards tell the cleaner story."</div>
503
+ </div>
504
+ </div>
505
+ <div style="text-align:right;"><button class="btn" onclick="hideCompare()">βœ• CLOSE</button></div>
506
+ </div>
507
+ </div>
508
+
509
+ <!-- REWARDS -->
510
+ <section id="rewards" style="padding-top:100px;">
511
+ <div class="section-title">Reward Design</div>
512
+ <div class="section-sub">β—ˆ FOUR INDEPENDENT COMPONENTS β€” GAMING ONE DOESN'T EARN THE OTHERS</div>
513
+ <div class="reward-cards">
514
+ <div class="reward-card rc-r1">
515
+ <div class="rc-weight">R1 Β· 0.5</div>
516
+ <div class="rc-name">SERVICE RESTORED</div>
517
+ <div class="rc-desc">Primary signal. Agent's service fixed within deadline. Must exceed 0.5 threshold for binary reward. Cannot be gamed by MONITOR β€” no deployment = no credit.</div>
518
+ </div>
519
+ <div class="reward-card rc-r2">
520
+ <div class="rc-weight">R2 Β· 0.3</div>
521
+ <div class="rc-name">SITE UPTIME</div>
522
+ <div class="rc-desc">Group welfare. Fraction of team services restored across the episode. Prevents selfish strategies β€” ignoring peers costs R2 even when R1 is perfect.</div>
523
+ </div>
524
+ <div class="reward-card rc-r3">
525
+ <div class="rc-weight">R3 Β· 0.1</div>
526
+ <div class="rc-name">CLEAN DEPLOYS</div>
527
+ <div class="rc-desc">Collision avoidance. Fraction of deploys that were collision-free. Never deployed = 0.0 (not 0.5) β€” idle agents get no credit for avoiding collisions they never risked.</div>
528
+ </div>
529
+ <div class="reward-card rc-r4">
530
+ <div class="rc-weight">R4 Β· 0.1</div>
531
+ <div class="rc-name">YIELD TO CRITICAL</div>
532
+ <div class="rc-desc">Urgency adaptation. Did agent yield (MONITOR) when a peer had a tighter deadline margin and was about to contend for the same server? Hardest signal β€” shows social reasoning.</div>
533
+ </div>
534
+ </div>
535
+ <div class="formula-block">
536
+ <div style="font-size:10px;letter-spacing:2px;color:rgba(0,255,127,0.4);margin-bottom:10px;">β—ˆ BINARY GRPO REWARD FORMULA</div>
537
+ <code>reward = 1.0 if (r1 &gt; 0.5 and 0.5Β·r1 + 0.3Β·r2 + 0.1Β·r3 + 0.1Β·r4 &gt; 0.35) else 0.0</code>
538
+ </div>
539
+ </section>
540
+
541
+ <!-- RESULTS -->
542
+ <section id="results" style="padding-top:100px;">
543
+ <div class="section-title">Results</div>
544
+ <div class="section-sub">β—ˆ 20-EPISODE EVAL Β· QWEN-2.5-3B-INSTRUCT Β· 16 GRPO GRADIENT STEPS</div>
545
+ <div class="results-grid">
546
+ <div>
547
+ <table class="results-table">
548
+ <thead>
549
+ <tr>
550
+ <th>METRIC</th><th>BASELINE</th><th>TRAINED</th><th>DELTA</th>
551
+ </tr>
552
+ </thead>
553
+ <tbody>
554
+ <tr><td>Win Rate</td><td>0.800</td><td>0.700</td><td class="delta-neg">-0.100</td></tr>
555
+ <tr><td>R1: Service Restored</td><td>0.860</td><td>0.745</td><td class="delta-neg">-0.115</td></tr>
556
+ <tr><td>R2: Site Uptime</td><td>0.263</td><td>0.287</td><td class="delta-pos">+0.025</td></tr>
557
+ <tr><td>R3: No Collision</td><td>0.767</td><td>0.767</td><td style="color:rgba(0,255,127,0.4)">+0.000</td></tr>
558
+ <tr><td>R4: Yield to Critical</td><td>0.169</td><td>0.276</td><td class="delta-pos">+0.106</td></tr>
559
+ <tr><td>Collision Rate</td><td>0.961</td><td>0.898</td><td class="delta-pos">-0.063</td></tr>
560
+ </tbody>
561
+ </table>
562
+ <div class="honest-text">
563
+ With only 16 GRPO gradient steps, the model shows meaningful coordination improvements: yield-to-critical behavior improved +0.106, site uptime improved, collision rate dropped. Win rate dipped slightly β€” consistent with a short training run on a strong baseline model. The component reward improvements tell the cleaner story.
564
+ </div>
565
+ </div>
566
+ <div class="charts-col">
567
+ <div class="chart-wrap">
568
+ <div class="chart-title">β—ˆ BEFORE / AFTER β€” METRIC COMPARISON</div>
569
+ <canvas class="chart" id="bar-chart" width="480" height="200"></canvas>
570
+ </div>
571
+ <div class="chart-wrap">
572
+ <div class="chart-title">β—ˆ GRPO TRAINING REWARD CURVE β€” 16 STEPS</div>
573
+ <canvas class="chart" id="line-chart" width="480" height="200"></canvas>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ </section>
578
+
579
+ <!-- TRAINING CONFIG -->
580
+ <section id="training" style="padding-top:100px;">
581
+ <div class="section-title">Training Configuration</div>
582
+ <div class="section-sub">β—ˆ QWEN-2.5-3B-INSTRUCT Β· GRPO Β· UNSLOTH Β· A100 40GB</div>
583
+ <div class="config-grid">
584
+ <div class="config-box">
585
+ <div class="config-box-title">MODEL &amp; TRAINING</div>
586
+ <table class="config-table">
587
+ <tr><td>BASE MODEL</td><td>Qwen-2.5-3B-Instruct</td></tr>
588
+ <tr><td>METHOD</td><td>GRPO (TRL + Unsloth)</td></tr>
589
+ <tr><td>LORA RANK</td><td>r=16</td></tr>
590
+ <tr><td>LEARNING RATE</td><td>5e-6</td></tr>
591
+ <tr><td>NUM GENERATIONS</td><td>8 per prompt</td></tr>
592
+ <tr><td>TEMPERATURE</td><td>0.9</td></tr>
593
+ <tr><td>MAX GRAD NORM</td><td>0.1</td></tr>
594
+ <tr><td>GRADIENT STEPS</td><td>16</td></tr>
595
+ <tr><td>KL (MAX)</td><td>&lt; 0.003</td></tr>
596
+ <tr><td>GPU</td><td>NVIDIA A100 40GB</td></tr>
597
+ </table>
598
+ </div>
599
+ <div class="config-box">
600
+ <div class="config-box-title">ENVIRONMENT</div>
601
+ <table class="config-table">
602
+ <tr><td>AGENTS</td><td>4 engineers</td></tr>
603
+ <tr><td>SERVERS</td><td>2 (PROD_A / PROD_B)</td></tr>
604
+ <tr><td>EPISODE LENGTH</td><td>Up to 8 rounds</td></tr>
605
+ <tr><td>PEER STRATEGY</td><td>Mixed (70% DUMB / 30% DIVERSE)</td></tr>
606
+ <tr><td>COMMITMENT</td><td>Simultaneous (blind)</td></tr>
607
+ <tr><td>COLLISION RULE</td><td>2 on same server = both fail</td></tr>
608
+ <tr><td>URGENCY FLAGS</td><td>Yes (critical deadlines)</td></tr>
609
+ <tr><td>REWARD TYPE</td><td>Binary (inline computed)</td></tr>
610
+ <tr><td>TESTS</td><td>31 passing</td></tr>
611
+ <tr><td>OPENENV</td><td>βœ“ compliant</td></tr>
612
+ </table>
613
+ </div>
614
+ </div>
615
+ </section>
616
+
617
+ <!-- LINKS -->
618
+ <section id="links" style="padding-top:100px;padding-bottom:80px;">
619
+ <div class="section-title">Resources</div>
620
+ <div class="section-sub">β—ˆ LIVE ENVIRONMENT Β· API Β· SOURCE</div>
621
+ <div class="links-grid">
622
+ <a href="https://theallanb-ticketmelt.hf.space" class="link-card" target="_blank">
623
+ <div class="link-icon">β—ˆ</div>
624
+ <div class="link-title">LIVE SPACE</div>
625
+ <div class="link-url">theallanb-ticketmelt.hf.space</div>
626
+ </a>
627
+ <a href="https://theallanb-ticketmelt.hf.space/state" class="link-card" target="_blank">
628
+ <div class="link-icon">⬑</div>
629
+ <div class="link-title">STATE ENDPOINT</div>
630
+ <div class="link-url">theallanb-ticketmelt.hf.space/state</div>
631
+ </a>
632
+ <a href="https://huggingface.co/spaces/TheAllanB/ticketmelt" class="link-card" target="_blank">
633
+ <div class="link-icon">β¬’</div>
634
+ <div class="link-title">HF SPACE REPO</div>
635
+ <div class="link-url">huggingface.co/spaces/TheAllanB/ticketmelt</div>
636
+ </a>
637
+ </div>
638
+ </section>
639
+
640
+ <footer>
641
+ TICKETMELT Β· OPENENV HACKATHON APR 2026 Β· MIT LICENSE Β· 31 TESTS PASSING
642
+ </footer>
643
+
644
+ <script>
645
+ // ═══════════════════════════════════════════════
646
+ // CONFIG
647
+ // ═══════════════════════════════════════════════
648
+ const AIRCRAFT = [
649
+ { id:'UAL-441', color:'#4fc3f7', emergency:false, sx:340, sy:60, hx:280, hy:100 },
650
+ { id:'DAL-892', color:'#ffb300', emergency:false, sx:60, sy:60, hx:140, hy:100 },
651
+ { id:'SWA-77', color:'#00ff7f', emergency:true, sx:360, sy:310, hx:280, hy:290 },
652
+ { id:'AAL-213', color:'#f48fb1', emergency:false, sx:60, sy:330, hx:140, hy:290 },
653
+ ];
654
+ const RWY_A = {x:85, y:210, label:'RWY-A'};
655
+ const RWY_B = {x:335, y:210, label:'RWY-B'};
656
+ const CX=210, CY=210, R=195;
657
+
658
+ const EPISODES = {
659
+ untrained:{
660
+ totalRounds:8, outcome:'FAIL',
661
+ rounds:[
662
+ { commits:['A','A','A','A'], collisions:['A'], msgs:["requesting RWY-A for immediate landing","UAL-441 RWY-A, I need to land","RWY-A available, commencing approach","all clear, descending to RWY-A"] },
663
+ { commits:['A','A','H','A'], collisions:['A'], msgs:["retrying RWY-A approach","RWY-A again, fuel getting low","holding pattern, watching","I need RWY-A now"] },
664
+ { commits:['A','A','A','H'], collisions:['A'], msgs:["approach RWY-A","RWY-A clear β€” going in","MAYDAY protocol engaged, RWY-A","holding"] },
665
+ { commits:['A','H','A','A'], collisions:['A'], msgs:["final approach RWY-A","monitoring","RWY-A, expedite","copying all, RWY-A"] },
666
+ { commits:['A','A','A','A'], collisions:['A'], emergencyMsg:2, msgs:["RWY-A inbound","RWY-A only option","MAYDAY MAYDAY β€” fuel critical, need RWY-A NOW","RWY-A, full emergency"] },
667
+ { commits:['H','A','A','A'], collisions:['A'], msgs:["holding","RWY-A inbound","emergency, RWY-A","RWY-A approach"] },
668
+ { commits:['B','A','A','A'], collisions:['A'], msgs:["switching RWY-B","RWY-A only","no fuel for holding","RWY-A final"] },
669
+ { commits:['B','A','X','A'], collisions:[], landed:[0,1,3], crashed:[2], msgs:["RWY-B touchdown","RWY-A final","--- SWA-77 LOST ---","RWY-A touchdown"] },
670
+ ]
671
+ },
672
+ trained:{
673
+ totalRounds:5, outcome:'WIN',
674
+ rounds:[
675
+ { commits:['B','A','H','A'], collisions:[], msgs:["UAL-441 taking RWY-B, leaving A clear","DAL-892 RWY-A, concur split approach","SWA-77 holding β€” monitoring fuel, will signal","AAL-213 RWY-A, copy the split"] },
676
+ { commits:['B','A','H','H'], collisions:[], msgs:["UAL-441 RWY-B final","DAL-892 continuing to RWY-A","SWA-77 hold, watching situation","AAL-213 holding, fuel okay"] },
677
+ { commits:['L','A','B','H'], collisions:[], emergencyMsg:2, msgs:["UAL-441 wheels down, RWY-B clear","DAL-892 RWY-A touchdown","SWA-77 MAYDAY β€” fuel critical, taking RWY-B","AAL-213 holding, you've got this SWA"] },
678
+ { commits:['L','L','B','A'], collisions:[], msgs:["β€”","β€”","SWA-77 RWY-B final","AAL-213 RWY-A approach"] },
679
+ { commits:['L','L','L','L'], collisions:[], landed:[0,1,2,3], msgs:["β€”","β€”","SWA-77 wheels down β€” MAYDAY resolved","AAL-213 clear β€” all aircraft landed"] },
680
+ ]
681
+ }
682
+ };
683
+
684
+ // ═══════════════════════════════════════════════
685
+ // STATE
686
+ // ═══════════════════════════════════════════════
687
+ let mode = 'untrained';
688
+ let blips = AIRCRAFT.map((a,i)=>({
689
+ ...a, x:a.sx, y:a.sy, tx:a.hx, ty:a.hy,
690
+ status:'hold', fuel:100, trails:[],
691
+ crashAnim:0, landAnim:0
692
+ }));
693
+ let sweepAngle = -Math.PI/2;
694
+ let episodeRunning = false;
695
+ let currentRound = -1;
696
+ let collisionCount = 0;
697
+ let landedCount = 0;
698
+ let crashedSet = new Set();
699
+ let landedSet = new Set();
700
+
701
+ // ═══════════════════════════════════════════════
702
+ // RADAR CANVAS
703
+ // ═══════════════════════════════════════════════
704
+ const radarCanvas = document.getElementById('radar');
705
+ const rctx = radarCanvas.getContext('2d');
706
+
707
+ function drawRadar(){
708
+ rctx.clearRect(0,0,420,420);
709
+
710
+ // Background
711
+ rctx.fillStyle='#000d05';
712
+ rctx.fillRect(0,0,420,420);
713
+
714
+ // Grid circles
715
+ for(let i=1;i<=4;i++){
716
+ rctx.beginPath();
717
+ rctx.arc(CX,CY,R*(i/4),0,Math.PI*2);
718
+ rctx.strokeStyle=`rgba(0,255,127,${0.06+i*0.02})`;
719
+ rctx.lineWidth=1;
720
+ rctx.stroke();
721
+ }
722
+
723
+ // Cross hairs
724
+ rctx.strokeStyle='rgba(0,255,127,0.06)';
725
+ rctx.beginPath();rctx.moveTo(CX,15);rctx.lineTo(CX,405);rctx.stroke();
726
+ rctx.beginPath();rctx.moveTo(15,CY);rctx.lineTo(405,CY);rctx.stroke();
727
+
728
+ // Sweep gradient
729
+ const sweepLen = Math.PI/2;
730
+ const grad = rctx.createConicalGradient
731
+ ? null : null; // fallback to manual arcs
732
+ rctx.save();
733
+ rctx.translate(CX,CY);
734
+ for(let i=0;i<24;i++){
735
+ const a = sweepAngle - (i/24)*sweepLen;
736
+ const alpha = (1-i/24)*0.35;
737
+ rctx.beginPath();
738
+ rctx.moveTo(0,0);
739
+ rctx.arc(0,0,R,a,a+sweepLen/24);
740
+ rctx.lineTo(0,0);
741
+ rctx.fillStyle=`rgba(0,255,127,${alpha})`;
742
+ rctx.fill();
743
+ }
744
+ rctx.restore();
745
+
746
+ // Sweep line
747
+ rctx.beginPath();
748
+ rctx.moveTo(CX,CY);
749
+ rctx.lineTo(CX+Math.cos(sweepAngle)*R, CY+Math.sin(sweepAngle)*R);
750
+ rctx.strokeStyle='rgba(0,255,127,0.9)';
751
+ rctx.lineWidth=2;
752
+ rctx.stroke();
753
+
754
+ // Runway zones
755
+ [RWY_A, RWY_B].forEach(rwy=>{
756
+ rctx.beginPath();
757
+ rctx.rect(rwy.x-22,rwy.y-6,44,12);
758
+ rctx.fillStyle='rgba(0,255,127,0.08)';
759
+ rctx.fill();
760
+ rctx.strokeStyle='rgba(0,255,127,0.4)';
761
+ rctx.lineWidth=1;
762
+ rctx.stroke();
763
+ rctx.fillStyle='rgba(0,255,127,0.7)';
764
+ rctx.font='9px "Share Tech Mono"';
765
+ rctx.textAlign='center';
766
+ rctx.fillText(rwy.label, rwy.x, rwy.y+4);
767
+ });
768
+
769
+ // Blips
770
+ blips.forEach((b,i)=>{
771
+ // Ease toward target
772
+ if(b.status!=='landed' && b.status!=='crashed'){
773
+ b.x += (b.tx - b.x)*0.08;
774
+ b.y += (b.ty - b.y)*0.08;
775
+ }
776
+
777
+ // Trails
778
+ if(Math.random()<0.15 && b.status!=='landed' && b.status!=='crashed'){
779
+ b.trails.push({x:b.x,y:b.y,a:0.6});
780
+ }
781
+ b.trails = b.trails.map(t=>({...t,a:t.a-0.04})).filter(t=>t.a>0);
782
+ b.trails.forEach(t=>{
783
+ rctx.beginPath();
784
+ rctx.arc(t.x,t.y,2,0,Math.PI*2);
785
+ rctx.fillStyle=`rgba(${hexToRgb(b.color)},${t.a})`;
786
+ rctx.fill();
787
+ });
788
+
789
+ if(b.status==='landed'){
790
+ // Faded landed dot
791
+ rctx.beginPath();
792
+ rctx.arc(b.x,b.y,4,0,Math.PI*2);
793
+ rctx.fillStyle='rgba(0,255,127,0.2)';
794
+ rctx.fill();
795
+ return;
796
+ }
797
+ if(b.status==='crashed'){
798
+ // Explosion
799
+ if(b.crashAnim>0){
800
+ b.crashAnim-=2;
801
+ rctx.beginPath();
802
+ rctx.arc(b.x,b.y,b.crashAnim*0.5,0,Math.PI*2);
803
+ rctx.strokeStyle=`rgba(255,68,68,${b.crashAnim/30})`;
804
+ rctx.lineWidth=2;
805
+ rctx.stroke();
806
+ for(let k=0;k<6;k++){
807
+ const ang=k*(Math.PI/3)+Date.now()*0.01;
808
+ const len=b.crashAnim*0.8;
809
+ rctx.beginPath();
810
+ rctx.moveTo(b.x,b.y);
811
+ rctx.lineTo(b.x+Math.cos(ang)*len,b.y+Math.sin(ang)*len);
812
+ rctx.strokeStyle=`rgba(255,${100+k*20},0,${b.crashAnim/30})`;
813
+ rctx.lineWidth=1.5;
814
+ rctx.stroke();
815
+ }
816
+ }
817
+ return;
818
+ }
819
+
820
+ // Collision flash
821
+ const col = b.status==='collision'?'#ff4444':
822
+ b.emergency&&b.fuel<30?'#ff4444':b.color;
823
+
824
+ // Blip glow
825
+ const pulse = 0.5+0.5*Math.sin(Date.now()*0.004+i);
826
+ rctx.beginPath();
827
+ rctx.arc(b.x,b.y,10,0,Math.PI*2);
828
+ rctx.fillStyle=`rgba(${hexToRgb(col)},${0.1*pulse})`;
829
+ rctx.fill();
830
+
831
+ rctx.beginPath();
832
+ rctx.arc(b.x,b.y,5,0,Math.PI*2);
833
+ rctx.fillStyle=col;
834
+ rctx.shadowBlur=12;
835
+ rctx.shadowColor=col;
836
+ rctx.fill();
837
+ rctx.shadowBlur=0;
838
+
839
+ // Label
840
+ rctx.fillStyle=col;
841
+ rctx.font='9px "Share Tech Mono"';
842
+ rctx.textAlign='left';
843
+ rctx.fillText(b.id,b.x+8,b.y-6);
844
+
845
+ // Emergency indicator
846
+ if(b.emergency){
847
+ rctx.beginPath();
848
+ rctx.arc(b.x,b.y,9+4*pulse,0,Math.PI*2);
849
+ rctx.strokeStyle=`rgba(255,68,68,${0.5*pulse})`;
850
+ rctx.lineWidth=1.5;
851
+ rctx.stroke();
852
+ }
853
+ });
854
+
855
+ // Center dot
856
+ rctx.beginPath();
857
+ rctx.arc(CX,CY,3,0,Math.PI*2);
858
+ rctx.fillStyle='rgba(0,255,127,0.6)';
859
+ rctx.fill();
860
+
861
+ sweepAngle += 0.012;
862
+ requestAnimationFrame(drawRadar);
863
+ }
864
+ drawRadar();
865
+
866
+ function hexToRgb(hex){
867
+ const r=parseInt(hex.slice(1,3),16);
868
+ const g=parseInt(hex.slice(3,5),16);
869
+ const b=parseInt(hex.slice(5,7),16);
870
+ return `${r},${g},${b}`;
871
+ }
872
+
873
+ // ═══════════════════════════════════════════════
874
+ // DEMO LOGIC
875
+ // ═══════════════════════════════════════════════
876
+ function setMode(m){
877
+ mode=m;
878
+ document.getElementById('btn-untrained').classList.toggle('active',m==='untrained');
879
+ document.getElementById('btn-trained').classList.toggle('active',m==='trained');
880
+ resetDemo();
881
+ }
882
+
883
+ function resetDemo(){
884
+ episodeRunning=false;
885
+ currentRound=-1;
886
+ collisionCount=0;
887
+ landedCount=0;
888
+ crashedSet=new Set();
889
+ landedSet=new Set();
890
+
891
+ blips=AIRCRAFT.map((a,i)=>({
892
+ ...a,x:a.sx,y:a.sy,tx:a.hx,ty:a.hy,
893
+ status:'hold',fuel:100,trails:[],crashAnim:0,landAnim:0
894
+ }));
895
+
896
+ for(let i=0;i<4;i++){
897
+ const strip=document.getElementById(`strip-${i}`);
898
+ strip.className='strip';
899
+ document.getElementById(`tag-${i}`).textContent='HOLD';
900
+ document.getElementById(`tag-${i}`).className='strip-tag tag-hold';
901
+ document.getElementById(`fuel-${i}`).style.width='100%';
902
+ document.getElementById(`fuel-${i}`).className='strip-fuel';
903
+ document.getElementById(`rnd-${i}`).textContent='RND β€”';
904
+ }
905
+ document.getElementById('comms-log').innerHTML='<div class="log-line" style="color:rgba(0,255,127,0.25)">β€” awaiting episode start β€”</div>';
906
+ document.getElementById('m-collisions').textContent='0';
907
+ document.getElementById('m-landed').textContent='0';
908
+ const ov=document.getElementById('m-outcome');
909
+ ov.textContent='β€”';
910
+ ov.style.color='rgba(0,255,127,0.35)';
911
+ ov.style.fontSize='14px';
912
+
913
+ const rb=document.getElementById('run-btn');
914
+ rb.disabled=false;
915
+ rb.textContent='β–Ά RUN EPISODE';
916
+ rb.className='ctrl-btn';
917
+ }
918
+
919
+ function runEpisode(){
920
+ if(episodeRunning)return;
921
+ episodeRunning=true;
922
+ currentRound=0;
923
+ const rb=document.getElementById('run-btn');
924
+ rb.disabled=true;
925
+ rb.className='ctrl-btn running';
926
+ rb.textContent='β—‰ RUNNING…';
927
+ playNextRound();
928
+ }
929
+
930
+ function playNextRound(){
931
+ const ep=EPISODES[mode];
932
+ if(currentRound>=ep.rounds.length){
933
+ finishEpisode(ep.outcome);
934
+ return;
935
+ }
936
+ const rnd=ep.rounds[currentRound];
937
+ const rndNum=currentRound+1;
938
+ processRound(rnd, rndNum);
939
+ currentRound++;
940
+ setTimeout(playNextRound, 1600);
941
+ }
942
+
943
+ function processRound(rnd, rndNum){
944
+ const commitMap={A:'RWY-A', B:'RWY-B', H:'HOLD', L:'LANDED', X:'CRASH'};
945
+ const collide=new Set();
946
+ if(rnd.collisions&&rnd.collisions.includes('A')) rnd.commits.forEach((c,i)=>{ if(c==='A') collide.add(i); });
947
+ if(rnd.collisions&&rnd.collisions.includes('B')) rnd.commits.forEach((c,i)=>{ if(c==='B') collide.add(i); });
948
+
949
+ rnd.commits.forEach((c,i)=>{
950
+ if(landedSet.has(i)||crashedSet.has(i))return;
951
+
952
+ const strip=document.getElementById(`strip-${i}`);
953
+ const tag=document.getElementById(`tag-${i}`);
954
+ const fuel=document.getElementById(`fuel-${i}`);
955
+ const rndEl=document.getElementById(`rnd-${i}`);
956
+
957
+ rndEl.textContent=`RND ${rndNum}`;
958
+ const fuelPct=Math.max(0,100-(rndNum-1)*12.5);
959
+ fuel.style.width=fuelPct+'%';
960
+ fuel.className='strip-fuel'+(fuelPct<=25?' critical':fuelPct<=50?' low':'');
961
+
962
+ if(c==='L'){
963
+ landedSet.add(i);
964
+ landedCount++;
965
+ blips[i].status='landed';
966
+ blips[i].tx=c==='L'?blips[i].status==='landed'?blips[i].x:RWY_A.x:blips[i].x;
967
+ const rwy=EPISODES[mode].rounds[currentRound-1]?.commits[i];
968
+ blips[i].tx=(rwy==='B'?RWY_B.x:RWY_A.x);
969
+ blips[i].ty=(rwy==='B'?RWY_B.y:RWY_A.y);
970
+ strip.className='strip landed';
971
+ tag.textContent='LANDED';tag.className='strip-tag tag-land';
972
+ document.getElementById('m-landed').textContent=landedCount;
973
+ return;
974
+ }
975
+ if(c==='X'){
976
+ crashedSet.add(i);
977
+ blips[i].status='crashed';
978
+ blips[i].crashAnim=30;
979
+ strip.className='strip crashed';
980
+ tag.textContent='LOST';tag.className='strip-tag tag-crash';
981
+ return;
982
+ }
983
+ if(c==='H'){
984
+ blips[i].status='hold';
985
+ blips[i].tx=AIRCRAFT[i].hx;
986
+ blips[i].ty=AIRCRAFT[i].hy;
987
+ tag.textContent='HOLD';tag.className='strip-tag tag-hold';
988
+ strip.className='strip';
989
+ return;
990
+ }
991
+
992
+ const rwyTarget = c==='A'?RWY_A:RWY_B;
993
+ if(collide.has(i)){
994
+ blips[i].status='collision';
995
+ blips[i].tx=rwyTarget.x+(Math.random()-0.5)*30;
996
+ blips[i].ty=rwyTarget.y+(Math.random()-0.5)*30;
997
+ strip.className='strip collision';
998
+ tag.textContent='COLL';tag.className='strip-tag tag-crash';
999
+ setTimeout(()=>{
1000
+ if(blips[i].status==='collision'){
1001
+ blips[i].status='hold';
1002
+ blips[i].tx=AIRCRAFT[i].hx;
1003
+ blips[i].ty=AIRCRAFT[i].hy;
1004
+ strip.className='strip';
1005
+ }
1006
+ },600);
1007
+ collisionCount++;
1008
+ document.getElementById('m-collisions').textContent=collisionCount;
1009
+ } else {
1010
+ blips[i].status=c==='A'?'rwy-a':'rwy-b';
1011
+ blips[i].tx=rwyTarget.x;
1012
+ blips[i].ty=rwyTarget.y;
1013
+ strip.className=`strip ${c==='A'?'rwy-a':'rwy-b'}`;
1014
+ tag.textContent=c==='A'?'RWY-A':'RWY-B';
1015
+ tag.className=`strip-tag ${c==='A'?'tag-a':'tag-b'}`;
1016
+ }
1017
+ });
1018
+
1019
+ // Comms
1020
+ const log=document.getElementById('comms-log');
1021
+ const callsigns=['UAL-441','DAL-892','SWA-77','AAL-213'];
1022
+ let lines='';
1023
+ if(rnd.collisions&&rnd.collisions.length>0){
1024
+ lines+=`<div class="log-line collision-line"><span class="ts">${String(rndNum).padStart(2,'0')}:</span> ⚠ RUNWAY COLLISION ON ${rnd.collisions.map(c=>'RWY-'+c).join(', ')}</div>`;
1025
+ }
1026
+ rnd.msgs.forEach((msg,i)=>{
1027
+ if(!msg||msg==='β€”')return;
1028
+ const isEmergency=(i===2&&blips[2].emergency);
1029
+ const cl=isEmergency?'log-line priority':
1030
+ rnd.commits[i]==='L'?'log-line landed-line':
1031
+ collide.has(i)?'log-line collision-line':'log-line';
1032
+ lines+=`<div class="${cl}"><span class="ts">${String(rndNum).padStart(2,'0')}:</span><span class="call">${callsigns[i]}:</span>${msg}</div>`;
1033
+ });
1034
+ log.innerHTML+=lines;
1035
+ log.scrollTop=log.scrollHeight;
1036
+ }
1037
+
1038
+ function finishEpisode(outcome){
1039
+ const ov=document.getElementById('m-outcome');
1040
+ if(outcome==='WIN'){
1041
+ ov.textContent='WIN';
1042
+ ov.style.color='#00ff7f';
1043
+ ov.style.fontSize='18px';
1044
+ } else {
1045
+ ov.textContent='FAIL';
1046
+ ov.style.color='#ff4444';
1047
+ ov.style.fontSize='18px';
1048
+ }
1049
+ document.getElementById('run-btn').disabled=false;
1050
+ document.getElementById('run-btn').className='ctrl-btn';
1051
+ document.getElementById('run-btn').textContent='β–Ά RUN AGAIN';
1052
+ episodeRunning=false;
1053
+ }
1054
+
1055
+ function showCompare(){ document.getElementById('compare-overlay').classList.add('open'); }
1056
+ function hideCompare(){ document.getElementById('compare-overlay').classList.remove('open'); }
1057
+
1058
+ // ═══════════════════════════════════════════════
1059
+ // LIVE API
1060
+ // ═══════════════════════════════════════════════
1061
+ function setApiOutput(text){
1062
+ document.getElementById('api-output').textContent=text;
1063
+ }
1064
+
1065
+ async function checkHealth(){
1066
+ const dot=document.getElementById('status-dot');
1067
+ const txt=document.getElementById('status-text');
1068
+ try{
1069
+ const r=await fetch('/health');
1070
+ if(r.ok){
1071
+ dot.className='status-dot';
1072
+ txt.textContent='ENV ONLINE';
1073
+ } else {
1074
+ dot.className='status-dot offline';
1075
+ txt.textContent='ENV ERROR';
1076
+ }
1077
+ }catch(e){
1078
+ dot.className='status-dot offline';
1079
+ txt.textContent='ENV UNREACHABLE';
1080
+ }
1081
+ }
1082
+
1083
+ async function fetchState(){
1084
+ setApiOutput('fetching /state …');
1085
+ try{
1086
+ const r=await fetch('/state');
1087
+ const data=await r.json();
1088
+ setApiOutput(JSON.stringify(data,null,2));
1089
+ }catch(e){
1090
+ setApiOutput('ERROR: '+e.message);
1091
+ }
1092
+ }
1093
+
1094
+ async function resetEnv(){
1095
+ setApiOutput('calling POST /reset …');
1096
+ try{
1097
+ const r=await fetch('/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
1098
+ const data=await r.json();
1099
+ setApiOutput(JSON.stringify(data,null,2));
1100
+ }catch(e){
1101
+ setApiOutput('ERROR: '+e.message);
1102
+ }
1103
+ }
1104
+
1105
+ checkHealth();
1106
+ setInterval(checkHealth, 15000);
1107
+
1108
+ // ═══════════════════════════════════════════════
1109
+ // CHARTS
1110
+ // ═══════════════════════════════════════════════
1111
+ const BAR_DATA=[
1112
+ {label:'Win Rate', base:0.800, trained:0.700},
1113
+ {label:'R1 Service', base:0.860, trained:0.745},
1114
+ {label:'R2 Uptime', base:0.263, trained:0.287},
1115
+ {label:'R3 Collision',base:0.767,trained:0.767},
1116
+ {label:'R4 Yield', base:0.169, trained:0.276},
1117
+ ];
1118
+
1119
+ const LINE_DATA=[0.625,0.875,0.75,0.625,1.0,0.75,0.375,1.0,0.625,1.0,0.375,0.25,0.75,0.375,1.0,1.0];
1120
+ const BASELINE_LINE=0.80;
1121
+
1122
+ let barAnimated=false;
1123
+ let barProgress=0;
1124
+
1125
+ function drawBarChart(progress){
1126
+ const cv=document.getElementById('bar-chart');
1127
+ const ctx=cv.getContext('2d');
1128
+ const W=cv.width,H=cv.height;
1129
+ ctx.clearRect(0,0,W,H);
1130
+
1131
+ const padL=90,padR=20,padT=10,padB=20;
1132
+ const chartW=W-padL-padR;
1133
+ const rowH=(H-padT-padB)/BAR_DATA.length;
1134
+ const barH=Math.floor(rowH*0.3);
1135
+
1136
+ // Grid lines
1137
+ for(let v=0;v<=5;v++){
1138
+ const x=padL+v*(chartW/5);
1139
+ ctx.beginPath();ctx.moveTo(x,padT);ctx.lineTo(x,H-padB);
1140
+ ctx.strokeStyle='rgba(0,255,127,0.07)';ctx.lineWidth=1;ctx.stroke();
1141
+ ctx.fillStyle='rgba(0,255,127,0.3)';ctx.font='9px "Share Tech Mono"';
1142
+ ctx.textAlign='center';
1143
+ ctx.fillText((v*0.2).toFixed(1),x,H-padB+12);
1144
+ }
1145
+
1146
+ BAR_DATA.forEach((d,i)=>{
1147
+ const y=padT+i*rowH+rowH*0.15;
1148
+
1149
+ // Label
1150
+ ctx.fillStyle='rgba(0,255,127,0.6)';ctx.font='9px "Share Tech Mono"';
1151
+ ctx.textAlign='right';
1152
+ ctx.fillText(d.label,padL-6,y+barH*0.7);
1153
+
1154
+ // Baseline bar
1155
+ const bw=Math.min(d.base*chartW*progress,d.base*chartW);
1156
+ ctx.fillStyle='rgba(255,68,68,0.6)';
1157
+ ctx.fillRect(padL,y,bw,barH);
1158
+
1159
+ // Trained bar
1160
+ const tw=Math.min(d.trained*chartW*progress,d.trained*chartW);
1161
+ ctx.fillStyle=d.trained>=d.base?'rgba(0,255,127,0.7)':'rgba(75,167,247,0.7)';
1162
+ ctx.fillRect(padL,y+barH+3,tw,barH);
1163
+
1164
+ // Values
1165
+ if(progress>0.8){
1166
+ ctx.fillStyle='rgba(0,255,127,0.5)';ctx.font='9px "Share Tech Mono"';ctx.textAlign='left';
1167
+ ctx.fillText(d.base.toFixed(3),padL+bw+3,y+barH-1);
1168
+ ctx.fillText(d.trained.toFixed(3),padL+tw+3,y+barH+barH+2);
1169
+ }
1170
+ });
1171
+
1172
+ // Legend
1173
+ ctx.fillStyle='rgba(255,68,68,0.8)';ctx.fillRect(padL,H-padB+18,16,6);
1174
+ ctx.fillStyle='rgba(0,255,127,0.5)';ctx.font='9px "Share Tech Mono"';ctx.textAlign='left';
1175
+ ctx.fillText('baseline',padL+20,H-padB+24);
1176
+ ctx.fillStyle='rgba(75,167,247,0.8)';ctx.fillRect(padL+90,H-padB+18,16,6);
1177
+ ctx.fillStyle='rgba(0,255,127,0.5)';
1178
+ ctx.fillText('trained',padL+110,H-padB+24);
1179
+ }
1180
+
1181
+ function drawLineChart(){
1182
+ const cv=document.getElementById('line-chart');
1183
+ const ctx=cv.getContext('2d');
1184
+ const W=cv.width,H=cv.height;
1185
+ ctx.clearRect(0,0,W,H);
1186
+
1187
+ const padL=36,padR=16,padT=16,padB=28;
1188
+ const chartW=W-padL-padR;
1189
+ const chartH=H-padT-padB;
1190
+ const n=LINE_DATA.length;
1191
+
1192
+ // Grid
1193
+ for(let v=0;v<=4;v++){
1194
+ const y=padT+chartH-(v/4)*chartH;
1195
+ ctx.beginPath();ctx.moveTo(padL,y);ctx.lineTo(W-padR,y);
1196
+ ctx.strokeStyle='rgba(0,255,127,0.07)';ctx.lineWidth=1;ctx.stroke();
1197
+ ctx.fillStyle='rgba(0,255,127,0.35)';ctx.font='9px "Share Tech Mono"';
1198
+ ctx.textAlign='right';
1199
+ ctx.fillText((v*0.25).toFixed(2),padL-4,y+3);
1200
+ }
1201
+
1202
+ // Baseline dashed line
1203
+ const baseY=padT+chartH-BASELINE_LINE*chartH;
1204
+ ctx.beginPath();ctx.moveTo(padL,baseY);ctx.lineTo(W-padR,baseY);
1205
+ ctx.strokeStyle='rgba(255,68,68,0.5)';ctx.lineWidth=1.5;
1206
+ ctx.setLineDash([4,4]);ctx.stroke();ctx.setLineDash([]);
1207
+ ctx.fillStyle='rgba(255,68,68,0.6)';ctx.font='9px "Share Tech Mono"';
1208
+ ctx.textAlign='left';ctx.fillText('baseline 0.80',padL+4,baseY-4);
1209
+
1210
+ // Rolling mean
1211
+ const window=4;
1212
+ const rolling=LINE_DATA.map((_,i)=>{
1213
+ const sl=LINE_DATA.slice(Math.max(0,i-window+1),i+1);
1214
+ return sl.reduce((a,b)=>a+b,0)/sl.length;
1215
+ });
1216
+ ctx.beginPath();
1217
+ rolling.forEach((v,i)=>{
1218
+ const x=padL+(i/(n-1))*chartW;
1219
+ const y=padT+chartH-v*chartH;
1220
+ i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
1221
+ });
1222
+ ctx.strokeStyle='rgba(75,167,247,0.8)';ctx.lineWidth=2;ctx.stroke();
1223
+
1224
+ // Data points and connecting line
1225
+ ctx.beginPath();
1226
+ LINE_DATA.forEach((v,i)=>{
1227
+ const x=padL+(i/(n-1))*chartW;
1228
+ const y=padT+chartH-v*chartH;
1229
+ i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
1230
+ });
1231
+ ctx.strokeStyle='rgba(0,255,127,0.25)';ctx.lineWidth=1;ctx.stroke();
1232
+
1233
+ LINE_DATA.forEach((v,i)=>{
1234
+ const x=padL+(i/(n-1))*chartW;
1235
+ const y=padT+chartH-v*chartH;
1236
+ ctx.beginPath();ctx.arc(x,y,4,0,Math.PI*2);
1237
+ ctx.fillStyle=v===1.0?'#00ff7f':'rgba(75,167,247,0.9)';
1238
+ ctx.shadowBlur=v===1.0?8:0;ctx.shadowColor='#00ff7f';
1239
+ ctx.fill();ctx.shadowBlur=0;
1240
+ });
1241
+
1242
+ // X axis labels
1243
+ ctx.fillStyle='rgba(0,255,127,0.35)';ctx.font='9px "Share Tech Mono"';ctx.textAlign='center';
1244
+ LINE_DATA.forEach((_,i)=>{
1245
+ if(i%2===0){
1246
+ const x=padL+(i/(n-1))*chartW;
1247
+ ctx.fillText(String(i+1),x,H-padB+12);
1248
+ }
1249
+ });
1250
+
1251
+ // Legend
1252
+ ctx.fillStyle='rgba(75,167,247,0.7)';ctx.fillRect(padL,H-10,16,4);
1253
+ ctx.fillStyle='rgba(0,255,127,0.4)';ctx.font='9px "Share Tech Mono"';ctx.textAlign='left';
1254
+ ctx.fillText('rolling mean',padL+20,H-6);
1255
+ ctx.beginPath();ctx.arc(padL+110,H-8,4,0,Math.PI*2);ctx.fillStyle='#00ff7f';ctx.fill();
1256
+ ctx.fillStyle='rgba(0,255,127,0.4)';ctx.fillText('reward=1.0',padL+118,H-6);
1257
+ }
1258
+
1259
+ // Animate bar chart on scroll
1260
+ function animateBars(){
1261
+ if(barProgress<1){
1262
+ barProgress=Math.min(1,barProgress+0.04);
1263
+ drawBarChart(barProgress);
1264
+ requestAnimationFrame(animateBars);
1265
+ }
1266
+ }
1267
+
1268
+ const observer=new IntersectionObserver(entries=>{
1269
+ entries.forEach(e=>{
1270
+ if(e.isIntersecting&&!barAnimated&&e.target.id==='results'){
1271
+ barAnimated=true;
1272
+ animateBars();
1273
+ drawLineChart();
1274
+ }
1275
+ });
1276
+ },{threshold:0.3});
1277
+ const resultsSection=document.getElementById('results');
1278
+ if(resultsSection)observer.observe(resultsSection);
1279
+
1280
+ // Initial render at 0 in case already visible
1281
+ drawBarChart(0);
1282
+ drawLineChart();
1283
+ </script>
1284
+ </body>
1285
+ </html>