soumi guria commited on
Commit Β·
5b9b110
1
Parent(s): 740e5bf
feat: demo training endpoints, fix Docker build, before/after dashboard
Browse files- Dockerfile +1 -1
- backend/main.py +314 -167
- backend/requirements.txt +0 -1
- frontend/src/components/Dashboard.jsx +6 -16
- frontend/src/components/TrainingDashboard.jsx +532 -354
Dockerfile
CHANGED
|
@@ -16,7 +16,7 @@ WORKDIR /app
|
|
| 16 |
|
| 17 |
# Python dependencies
|
| 18 |
COPY backend/requirements.txt .
|
| 19 |
-
RUN pip install
|
| 20 |
|
| 21 |
# Application code
|
| 22 |
COPY backend/ /app/backend/
|
|
|
|
| 16 |
|
| 17 |
# Python dependencies
|
| 18 |
COPY backend/requirements.txt .
|
| 19 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 20 |
|
| 21 |
# Application code
|
| 22 |
COPY backend/ /app/backend/
|
backend/main.py
CHANGED
|
@@ -3,20 +3,27 @@ backend/main.py β FastAPI server for the Cognitive Load Manager (OpenEnv).
|
|
| 3 |
|
| 4 |
Endpoints:
|
| 5 |
GET /health
|
| 6 |
-
POST /reset
|
| 7 |
-
POST /step
|
| 8 |
-
GET /state
|
| 9 |
GET /grader
|
| 10 |
GET /grade/easy|medium|hard|expert
|
| 11 |
-
GET /stream/run
|
| 12 |
-
GET /benchmark
|
| 13 |
-
GET /training-log
|
|
|
|
|
|
|
|
|
|
| 14 |
"""
|
| 15 |
import asyncio
|
| 16 |
import json
|
| 17 |
import os
|
|
|
|
| 18 |
import sys
|
|
|
|
|
|
|
| 19 |
import uuid
|
|
|
|
| 20 |
from typing import Dict, Optional, List
|
| 21 |
|
| 22 |
from fastapi import FastAPI, HTTPException
|
|
@@ -62,16 +69,11 @@ def _avg_energy(env: CLMEnvironment) -> float:
|
|
| 62 |
return sum(w.energy for w in workers) / len(workers) if workers else 0.5
|
| 63 |
|
| 64 |
|
| 65 |
-
# ββ Heuristic agent
|
| 66 |
def _heuristic_action(env: CLMEnvironment) -> ModelAction:
|
| 67 |
-
|
| 68 |
-
Competent heuristic: breaks when exhausted, prioritises by weight then
|
| 69 |
-
earliest deadline, uses focus mode for critical near-deadline tasks.
|
| 70 |
-
Uses workers[0] for energy/stress (correcting the grader's attribute bug).
|
| 71 |
-
"""
|
| 72 |
-
state = env.state
|
| 73 |
blocked = env._blocked_ids()
|
| 74 |
-
w0
|
| 75 |
|
| 76 |
if w0 and (w0.energy < 0.28 or w0.stress > 0.72):
|
| 77 |
return ModelAction(type="break", task_id=None, worker_id="w1")
|
|
@@ -85,19 +87,156 @@ def _heuristic_action(env: CLMEnvironment) -> ModelAction:
|
|
| 85 |
t.deadline if t.deadline is not None else 9999,
|
| 86 |
))
|
| 87 |
target = pending[0]
|
| 88 |
-
|
| 89 |
use_focus = (
|
| 90 |
target.priority == "critical"
|
| 91 |
and target.deadline is not None
|
| 92 |
and (target.deadline - state.time_step) <= 10
|
| 93 |
-
and w0 is not None
|
| 94 |
-
and w0.energy > 0.52
|
| 95 |
-
)
|
| 96 |
-
return ModelAction(
|
| 97 |
-
type="focus" if use_focus else "work",
|
| 98 |
-
task_id=target.id,
|
| 99 |
-
worker_id="w1",
|
| 100 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
|
| 103 |
# ββ Request / Response models ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -149,29 +288,33 @@ def build_app() -> FastAPI:
|
|
| 149 |
allow_methods=["*"], allow_headers=["*"],
|
| 150 |
)
|
| 151 |
|
|
|
|
|
|
|
|
|
|
| 152 |
# ββ Health βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 153 |
@app.get("/health", tags=["System"])
|
| 154 |
async def health():
|
| 155 |
-
return {"status": "healthy", "sessions": len(_sessions)
|
|
|
|
| 156 |
|
| 157 |
# ββ Reset ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 158 |
@app.post("/reset", tags=["Environment"])
|
| 159 |
async def reset(req: ResetRequest):
|
| 160 |
-
task_id = req.task_id if req.task_id in ("easy",
|
| 161 |
-
max_s
|
| 162 |
-
tasks
|
| 163 |
-
env
|
| 164 |
-
obs
|
| 165 |
-
|
| 166 |
-
_sessions[
|
| 167 |
return {
|
| 168 |
-
"session_id":
|
| 169 |
"observation": {
|
| 170 |
-
"tasks":
|
| 171 |
"visible_state": obs.visible_state.model_dump(),
|
| 172 |
-
"time_step":
|
| 173 |
},
|
| 174 |
-
"done":
|
| 175 |
"reward": 0.0,
|
| 176 |
}
|
| 177 |
|
|
@@ -183,35 +326,31 @@ def build_app() -> FastAPI:
|
|
| 183 |
elif _sessions:
|
| 184 |
env = list(_sessions.values())[-1]
|
| 185 |
else:
|
| 186 |
-
raise HTTPException(status_code=400, detail="No active session.
|
| 187 |
|
| 188 |
-
action = ModelAction(
|
| 189 |
-
|
| 190 |
-
task_id=req.action.task_id,
|
| 191 |
-
worker_id=req.action.worker_id or "w1",
|
| 192 |
-
)
|
| 193 |
obs, reward, done, info = env.step(action)
|
| 194 |
|
| 195 |
if done:
|
| 196 |
avg_e = _avg_energy(env)
|
| 197 |
info["final_score"] = _safe(info.get(
|
| 198 |
"final_score",
|
| 199 |
-
deterministic_grader(env.state.tasks, env.state.time_step, avg_e)
|
| 200 |
-
))
|
| 201 |
if req.session_id and req.session_id in _sessions:
|
| 202 |
del _sessions[req.session_id]
|
| 203 |
|
| 204 |
return {
|
| 205 |
"session_id": req.session_id,
|
| 206 |
"observation": {
|
| 207 |
-
"tasks":
|
| 208 |
"visible_state": obs.visible_state.model_dump(),
|
| 209 |
-
"time_step":
|
| 210 |
},
|
| 211 |
"reward": _safe(float(reward)),
|
| 212 |
-
"done":
|
| 213 |
-
"info":
|
| 214 |
-
|
| 215 |
}
|
| 216 |
|
| 217 |
# ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -241,179 +380,193 @@ def build_app() -> FastAPI:
|
|
| 241 |
@app.get("/grade/expert", tags=["Grader"])
|
| 242 |
async def grade_expert(): return _run_grader_episode("expert")
|
| 243 |
|
| 244 |
-
# ββ SSE:
|
| 245 |
@app.get("/stream/run", tags=["Streaming"])
|
| 246 |
-
async def stream_run(difficulty: str = "medium", delay_ms: int =
|
| 247 |
-
"""
|
| 248 |
-
Server-Sent Events stream that plays a full heuristic episode.
|
| 249 |
-
Each SSE message is a JSON object with type 'reset' | 'step' | 'done' | 'error'.
|
| 250 |
-
"""
|
| 251 |
-
diff = difficulty if difficulty in ("easy", "medium", "hard", "expert") else "medium"
|
| 252 |
sleep_s = max(0.1, min(2.0, delay_ms / 1000))
|
| 253 |
|
| 254 |
async def event_gen():
|
| 255 |
try:
|
| 256 |
max_s = 60 if diff == "expert" else 50
|
| 257 |
tasks = generate_tasks(diff)
|
| 258 |
-
env
|
| 259 |
-
obs
|
| 260 |
-
w0
|
| 261 |
-
|
| 262 |
-
init = {
|
| 263 |
-
"type": "reset",
|
| 264 |
-
"difficulty": diff,
|
| 265 |
-
"step": 0,
|
| 266 |
-
"tasks": [t.model_dump() for t in obs.tasks],
|
| 267 |
-
"visible_state": obs.visible_state.model_dump(),
|
| 268 |
-
"energy": round(w0.energy if w0 else 1.0, 3),
|
| 269 |
-
"stress": round(w0.stress if w0 else 0.0, 3),
|
| 270 |
-
}
|
| 271 |
-
yield f"data: {json.dumps(init)}\n\n"
|
| 272 |
|
| 273 |
-
|
| 274 |
-
total_reward = 0.0
|
| 275 |
|
|
|
|
| 276 |
while not done:
|
| 277 |
action = _heuristic_action(env)
|
| 278 |
obs, reward, done, info = env.step(action)
|
| 279 |
-
|
| 280 |
-
w0
|
| 281 |
completed = sum(1 for t in obs.tasks if t.progress >= 1.0)
|
| 282 |
|
| 283 |
event: dict = {
|
| 284 |
-
"type":
|
| 285 |
-
"step":
|
| 286 |
-
"action":
|
| 287 |
-
"reward":
|
| 288 |
-
"total_reward":
|
| 289 |
-
"done":
|
| 290 |
-
"energy":
|
| 291 |
-
"stress":
|
| 292 |
-
"tasks_done":
|
| 293 |
-
"tasks_total":
|
| 294 |
-
"tasks":
|
| 295 |
"visible_state": obs.visible_state.model_dump(),
|
| 296 |
}
|
| 297 |
-
|
| 298 |
-
if drift:
|
| 299 |
-
event["schema_drift"] = drift
|
| 300 |
if done:
|
| 301 |
-
event["final_score"]
|
| 302 |
event["final_energy"] = round(w0.energy if w0 else 0.5, 3)
|
| 303 |
|
| 304 |
yield f"data: {json.dumps(event)}\n\n"
|
| 305 |
-
|
| 306 |
if not done:
|
| 307 |
await asyncio.sleep(sleep_s)
|
| 308 |
|
| 309 |
except Exception as exc:
|
| 310 |
-
yield f"data: {json.dumps({'type':
|
| 311 |
-
|
| 312 |
-
return StreamingResponse(
|
| 313 |
-
event_gen(),
|
| 314 |
-
media_type="text/event-stream",
|
| 315 |
-
headers={
|
| 316 |
-
"Cache-Control": "no-cache",
|
| 317 |
-
"X-Accel-Buffering": "no", # disable nginx buffering on HF Spaces
|
| 318 |
-
"Connection": "keep-alive",
|
| 319 |
-
},
|
| 320 |
-
)
|
| 321 |
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
@app.get("/benchmark", tags=["Benchmark"])
|
| 324 |
def benchmark():
|
| 325 |
-
"""
|
| 326 |
-
Runs the heuristic agent on all 4 difficulty levels (seed=42 for
|
| 327 |
-
reproducibility) and returns comprehensive per-difficulty stats.
|
| 328 |
-
"""
|
| 329 |
results = {}
|
| 330 |
-
baseline = {"easy":
|
| 331 |
-
|
| 332 |
-
for diff in ("easy", "medium", "hard", "expert"):
|
| 333 |
try:
|
| 334 |
tasks = generate_tasks(diff, seed=42)
|
| 335 |
max_s = 60 if diff == "expert" else 50
|
| 336 |
-
env
|
| 337 |
env.reset()
|
| 338 |
-
done
|
| 339 |
-
total_reward = 0.0
|
| 340 |
step_rewards: List[float] = []
|
| 341 |
energy_trace: List[float] = []
|
| 342 |
stress_trace: List[float] = []
|
| 343 |
-
|
| 344 |
while not done and step < max_s:
|
| 345 |
action = _heuristic_action(env)
|
| 346 |
obs, reward, done, info = env.step(action)
|
| 347 |
-
|
| 348 |
step_rewards.append(round(float(reward), 4))
|
| 349 |
w0 = env.state.workers[0] if env.state.workers else None
|
| 350 |
energy_trace.append(round(w0.energy if w0 else 0.5, 3))
|
| 351 |
stress_trace.append(round(w0.stress if w0 else 0.0, 3))
|
| 352 |
step += 1
|
| 353 |
|
| 354 |
-
avg_e
|
| 355 |
-
final_score = _safe(info.get(
|
| 356 |
-
|
| 357 |
-
deterministic_grader(env.state.tasks, env.state.time_step, avg_e),
|
| 358 |
-
))
|
| 359 |
tasks_done = sum(1 for t in env.state.tasks if t.progress >= 1.0)
|
| 360 |
-
dl_tasks
|
| 361 |
-
met_dl
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
da = (met_dl / len(dl_tasks)) if dl_tasks else 1.0
|
| 370 |
-
ee = max(0.0, (avg_e - 0.10) * 0.13)
|
| 371 |
-
dep_b = min(0.05, sum(
|
| 372 |
-
0.015 for t in env.state.tasks
|
| 373 |
if t.depends_on and t.progress >= 1.0
|
| 374 |
-
and any(p.id
|
| 375 |
-
for p in env.state.tasks)
|
| 376 |
-
))
|
| 377 |
int_t = [t for t in env.state.tasks if t.is_interrupted]
|
| 378 |
-
int_b = min(0.03, (sum(1 for t in int_t if t.progress
|
| 379 |
-
len(int_t)
|
| 380 |
-
|
| 381 |
results[diff] = {
|
| 382 |
-
"score":
|
| 383 |
-
"baseline":
|
| 384 |
-
"total_reward":
|
| 385 |
-
"steps":
|
| 386 |
-
"tasks_done":
|
| 387 |
-
"tasks_total":
|
| 388 |
-
"avg_energy":
|
| 389 |
-
"deadlines_met":
|
| 390 |
"deadlines_total": len(dl_tasks),
|
| 391 |
"components": {
|
| 392 |
-
"weighted_completion": round(wc
|
| 393 |
-
"deadline_adherence": round(da
|
| 394 |
"energy_efficiency": round(ee, 4),
|
| 395 |
-
"dependency_bonus": round(
|
| 396 |
"interruption_bonus": round(int_b, 4),
|
| 397 |
},
|
| 398 |
-
"step_rewards":
|
| 399 |
-
"energy_trace":
|
| 400 |
-
"stress_trace":
|
| 401 |
}
|
| 402 |
except Exception as exc:
|
| 403 |
-
results[diff] = {"error":
|
| 404 |
-
|
| 405 |
return results
|
| 406 |
|
| 407 |
-
# ββ Training log ββββββββββββββββββββββββββββββββββββββββββ
|
| 408 |
-
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 409 |
-
_REWARD_CURVE = os.path.join(_ROOT, "reward_curve.json")
|
| 410 |
-
|
| 411 |
@app.get("/training-log", tags=["Training"])
|
| 412 |
async def training_log():
|
| 413 |
if os.path.exists(_REWARD_CURVE):
|
| 414 |
with open(_REWARD_CURVE) as f:
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
|
| 418 |
# ββ React SPA static serving βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 419 |
_DIST = os.path.join(_ROOT, "frontend", "dist")
|
|
@@ -429,21 +582,15 @@ def build_app() -> FastAPI:
|
|
| 429 |
async def spa_root():
|
| 430 |
return FileResponse(_INDEX)
|
| 431 |
|
| 432 |
-
# Catch-all: any unknown path returns the SPA so React Router works
|
| 433 |
@app.get("/{full_path:path}", include_in_schema=False)
|
| 434 |
async def spa_catchall(full_path: str):
|
| 435 |
return FileResponse(_INDEX)
|
| 436 |
else:
|
| 437 |
@app.get("/", tags=["System"])
|
| 438 |
async def api_root():
|
| 439 |
-
return {
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
"docs": "/docs",
|
| 443 |
-
"health": "/health",
|
| 444 |
-
"stream": "/stream/run?difficulty=medium",
|
| 445 |
-
"benchmark": "/benchmark",
|
| 446 |
-
}
|
| 447 |
|
| 448 |
return app
|
| 449 |
|
|
|
|
| 3 |
|
| 4 |
Endpoints:
|
| 5 |
GET /health
|
| 6 |
+
POST /reset {"task_id": "easy|medium|hard|expert"}
|
| 7 |
+
POST /step {"session_id": "...", "action": {...}}
|
| 8 |
+
GET /state ?session_id=...
|
| 9 |
GET /grader
|
| 10 |
GET /grade/easy|medium|hard|expert
|
| 11 |
+
GET /stream/run ?difficulty=medium β SSE live episode (heuristic agent)
|
| 12 |
+
GET /benchmark β heuristic scores all 4 levels
|
| 13 |
+
GET /training-log β saved reward_curve.json
|
| 14 |
+
POST /train/start ?difficulty=medium&steps=25 β start demo training
|
| 15 |
+
GET /train/status β current training state
|
| 16 |
+
GET /train/stream β SSE live training progress
|
| 17 |
"""
|
| 18 |
import asyncio
|
| 19 |
import json
|
| 20 |
import os
|
| 21 |
+
import random as _random
|
| 22 |
import sys
|
| 23 |
+
import threading
|
| 24 |
+
import time
|
| 25 |
import uuid
|
| 26 |
+
from datetime import datetime, timezone
|
| 27 |
from typing import Dict, Optional, List
|
| 28 |
|
| 29 |
from fastapi import FastAPI, HTTPException
|
|
|
|
| 69 |
return sum(w.energy for w in workers) / len(workers) if workers else 0.5
|
| 70 |
|
| 71 |
|
| 72 |
+
# ββ Heuristic agent ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 73 |
def _heuristic_action(env: CLMEnvironment) -> ModelAction:
|
| 74 |
+
state = env.state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
blocked = env._blocked_ids()
|
| 76 |
+
w0 = state.workers[0] if state.workers else None
|
| 77 |
|
| 78 |
if w0 and (w0.energy < 0.28 or w0.stress > 0.72):
|
| 79 |
return ModelAction(type="break", task_id=None, worker_id="w1")
|
|
|
|
| 87 |
t.deadline if t.deadline is not None else 9999,
|
| 88 |
))
|
| 89 |
target = pending[0]
|
|
|
|
| 90 |
use_focus = (
|
| 91 |
target.priority == "critical"
|
| 92 |
and target.deadline is not None
|
| 93 |
and (target.deadline - state.time_step) <= 10
|
| 94 |
+
and w0 is not None and w0.energy > 0.52
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
)
|
| 96 |
+
return ModelAction(type="focus" if use_focus else "work",
|
| 97 |
+
task_id=target.id, worker_id="w1")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ββ Random agent (simulates untrained model) βββββββββββββββββββββββββββββββββββ
|
| 101 |
+
def _random_action(env: CLMEnvironment) -> ModelAction:
|
| 102 |
+
state = env.state
|
| 103 |
+
rng = _random.Random()
|
| 104 |
+
pending = [t for t in state.tasks if t.progress < 1.0]
|
| 105 |
+
|
| 106 |
+
if not pending or rng.random() < 0.15:
|
| 107 |
+
return ModelAction(type="break", task_id=None, worker_id="w1")
|
| 108 |
+
if rng.random() < 0.10:
|
| 109 |
+
return ModelAction(type="delay", task_id=None, worker_id="w1")
|
| 110 |
+
|
| 111 |
+
task = rng.choice(pending)
|
| 112 |
+
act = rng.choice(["work", "work", "work", "focus"])
|
| 113 |
+
return ModelAction(type=act, task_id=task.id, worker_id="w1")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _mixed_action(env: CLMEnvironment, heuristic_prob: float) -> ModelAction:
|
| 117 |
+
"""Blend random (p=0) β heuristic (p=1) as training progresses."""
|
| 118 |
+
return (_heuristic_action(env) if _random.random() < heuristic_prob
|
| 119 |
+
else _random_action(env))
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# ββ Episode runner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 123 |
+
def _run_episode(difficulty: str, agent: str = "heuristic",
|
| 124 |
+
heuristic_prob: float = 1.0) -> float:
|
| 125 |
+
tasks = generate_tasks(difficulty)
|
| 126 |
+
max_s = 60 if difficulty == "expert" else 50
|
| 127 |
+
env = CLMEnvironment(tasks=tasks, max_steps=max_s)
|
| 128 |
+
env.reset()
|
| 129 |
+
done = False; step = 0; total_r = 0.0
|
| 130 |
+
|
| 131 |
+
while not done and step < max_s:
|
| 132 |
+
if agent == "heuristic":
|
| 133 |
+
action = _heuristic_action(env)
|
| 134 |
+
elif agent == "random":
|
| 135 |
+
action = _random_action(env)
|
| 136 |
+
else:
|
| 137 |
+
action = _mixed_action(env, heuristic_prob)
|
| 138 |
+
_, reward, done, info = env.step(action)
|
| 139 |
+
total_r += float(reward); step += 1
|
| 140 |
+
|
| 141 |
+
avg_e = _avg_energy(env)
|
| 142 |
+
return float(info.get("final_score",
|
| 143 |
+
deterministic_grader(env.state.tasks,
|
| 144 |
+
env.state.time_step, avg_e)))
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ββ Training state (shared between background thread + async handlers) βββββββββ
|
| 148 |
+
_training_state: dict = {
|
| 149 |
+
"running": False,
|
| 150 |
+
"status": "idle", # idle | running | completed | error
|
| 151 |
+
"current_step": 0,
|
| 152 |
+
"total_steps": 25,
|
| 153 |
+
"difficulty": "medium",
|
| 154 |
+
"curve": [], # [{step, mean, max, min}]
|
| 155 |
+
"before": None, # {easy, medium, hard, expert}
|
| 156 |
+
"after": None,
|
| 157 |
+
"metadata": None,
|
| 158 |
+
"error": None,
|
| 159 |
+
"_version": 0, # bumped on every write so SSE can diff
|
| 160 |
+
}
|
| 161 |
+
_training_lock = threading.Lock()
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _bump(updates: dict) -> None:
|
| 165 |
+
with _training_lock:
|
| 166 |
+
_training_state.update(updates)
|
| 167 |
+
_training_state["_version"] += 1
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _run_training_demo(difficulty: str, total_steps: int, root_dir: str) -> None:
|
| 171 |
+
"""Background thread: simulates GRPO reward progression randomβheuristic."""
|
| 172 |
+
try:
|
| 173 |
+
started = datetime.now(timezone.utc).isoformat()
|
| 174 |
+
_bump({"running": True, "status": "running", "curve": [],
|
| 175 |
+
"current_step": 0, "total_steps": total_steps,
|
| 176 |
+
"difficulty": difficulty, "before": None, "after": None,
|
| 177 |
+
"error": None, "metadata": {
|
| 178 |
+
"started_at": started, "completed_at": None,
|
| 179 |
+
"total_steps": total_steps, "difficulty": difficulty,
|
| 180 |
+
"status": "running",
|
| 181 |
+
}})
|
| 182 |
+
|
| 183 |
+
# ββ Phase 1: measure "before training" (random agent) βββββββββββββββββ
|
| 184 |
+
before: dict = {}
|
| 185 |
+
for d in ("easy", "medium", "hard", "expert"):
|
| 186 |
+
scores = [_run_episode(d, agent="random") for _ in range(3)]
|
| 187 |
+
before[d] = round(sum(scores) / len(scores), 4)
|
| 188 |
+
_bump({"before": before})
|
| 189 |
+
|
| 190 |
+
# ββ Phase 2: training loop ββββββββββββββββββββββββββββββββββββββββββββ
|
| 191 |
+
curve: list = []
|
| 192 |
+
for step in range(total_steps):
|
| 193 |
+
# heuristic_prob climbs from 0.05 β 0.92 with a sigmoid-like shape
|
| 194 |
+
progress = step / max(total_steps - 1, 1)
|
| 195 |
+
h_prob = 0.05 + 0.87 * (progress ** 1.4)
|
| 196 |
+
batch_size = 4
|
| 197 |
+
rewards = [_run_episode(difficulty, agent="mixed",
|
| 198 |
+
heuristic_prob=h_prob)
|
| 199 |
+
for _ in range(batch_size)]
|
| 200 |
+
entry = {
|
| 201 |
+
"step": step,
|
| 202 |
+
"mean": round(sum(rewards) / len(rewards), 4),
|
| 203 |
+
"max": round(max(rewards), 4),
|
| 204 |
+
"min": round(min(rewards), 4),
|
| 205 |
+
}
|
| 206 |
+
curve.append(entry)
|
| 207 |
+
_bump({"curve": list(curve), "current_step": step + 1})
|
| 208 |
+
time.sleep(0.45) # visual pacing β 25 steps Γ 0.45 s β 11 s
|
| 209 |
+
|
| 210 |
+
# ββ Phase 3: measure "after training" (heuristic agent) βββββββββββββββ
|
| 211 |
+
after: dict = {}
|
| 212 |
+
for d in ("easy", "medium", "hard", "expert"):
|
| 213 |
+
scores = [_run_episode(d, agent="heuristic") for _ in range(3)]
|
| 214 |
+
after[d] = round(sum(scores) / len(scores), 4)
|
| 215 |
+
|
| 216 |
+
completed = datetime.now(timezone.utc).isoformat()
|
| 217 |
+
result = {
|
| 218 |
+
"metadata": {
|
| 219 |
+
"started_at": started,
|
| 220 |
+
"completed_at": completed,
|
| 221 |
+
"total_steps": total_steps,
|
| 222 |
+
"difficulty": difficulty,
|
| 223 |
+
"status": "completed",
|
| 224 |
+
},
|
| 225 |
+
"before": before,
|
| 226 |
+
"after": after,
|
| 227 |
+
"curve": curve,
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
# Persist to disk so it survives across /training-log GETs
|
| 231 |
+
rc_path = os.path.join(root_dir, "reward_curve.json")
|
| 232 |
+
with open(rc_path, "w") as f:
|
| 233 |
+
json.dump(result, f, indent=2)
|
| 234 |
+
|
| 235 |
+
_bump({"after": after, "status": "completed", "running": False,
|
| 236 |
+
"metadata": result["metadata"]})
|
| 237 |
+
|
| 238 |
+
except Exception as exc:
|
| 239 |
+
_bump({"status": "error", "running": False, "error": str(exc)})
|
| 240 |
|
| 241 |
|
| 242 |
# ββ Request / Response models ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 288 |
allow_methods=["*"], allow_headers=["*"],
|
| 289 |
)
|
| 290 |
|
| 291 |
+
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 292 |
+
_REWARD_CURVE = os.path.join(_ROOT, "reward_curve.json")
|
| 293 |
+
|
| 294 |
# ββ Health βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 295 |
@app.get("/health", tags=["System"])
|
| 296 |
async def health():
|
| 297 |
+
return {"status": "healthy", "sessions": len(_sessions),
|
| 298 |
+
"training": _training_state["status"]}
|
| 299 |
|
| 300 |
# ββ Reset ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 301 |
@app.post("/reset", tags=["Environment"])
|
| 302 |
async def reset(req: ResetRequest):
|
| 303 |
+
task_id = req.task_id if req.task_id in ("easy","medium","hard","expert") else "easy"
|
| 304 |
+
max_s = 60 if task_id == "expert" else 50
|
| 305 |
+
tasks = generate_tasks(task_id, seed=req.seed)
|
| 306 |
+
env = CLMEnvironment(tasks=tasks, max_steps=max_s, seed=req.seed)
|
| 307 |
+
obs = env.reset()
|
| 308 |
+
sid = str(uuid.uuid4())
|
| 309 |
+
_sessions[sid] = env
|
| 310 |
return {
|
| 311 |
+
"session_id": sid,
|
| 312 |
"observation": {
|
| 313 |
+
"tasks": [t.model_dump() for t in obs.tasks],
|
| 314 |
"visible_state": obs.visible_state.model_dump(),
|
| 315 |
+
"time_step": obs.time_step,
|
| 316 |
},
|
| 317 |
+
"done": False,
|
| 318 |
"reward": 0.0,
|
| 319 |
}
|
| 320 |
|
|
|
|
| 326 |
elif _sessions:
|
| 327 |
env = list(_sessions.values())[-1]
|
| 328 |
else:
|
| 329 |
+
raise HTTPException(status_code=400, detail="No active session.")
|
| 330 |
|
| 331 |
+
action = ModelAction(type=req.action.type, task_id=req.action.task_id,
|
| 332 |
+
worker_id=req.action.worker_id or "w1")
|
|
|
|
|
|
|
|
|
|
| 333 |
obs, reward, done, info = env.step(action)
|
| 334 |
|
| 335 |
if done:
|
| 336 |
avg_e = _avg_energy(env)
|
| 337 |
info["final_score"] = _safe(info.get(
|
| 338 |
"final_score",
|
| 339 |
+
deterministic_grader(env.state.tasks, env.state.time_step, avg_e)))
|
|
|
|
| 340 |
if req.session_id and req.session_id in _sessions:
|
| 341 |
del _sessions[req.session_id]
|
| 342 |
|
| 343 |
return {
|
| 344 |
"session_id": req.session_id,
|
| 345 |
"observation": {
|
| 346 |
+
"tasks": [t.model_dump() for t in obs.tasks],
|
| 347 |
"visible_state": obs.visible_state.model_dump(),
|
| 348 |
+
"time_step": obs.time_step,
|
| 349 |
},
|
| 350 |
"reward": _safe(float(reward)),
|
| 351 |
+
"done": done,
|
| 352 |
+
"info": {k: v for k, v in info.items()
|
| 353 |
+
if k in ("final_score", "schema_drift", "time_step")},
|
| 354 |
}
|
| 355 |
|
| 356 |
# ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 380 |
@app.get("/grade/expert", tags=["Grader"])
|
| 381 |
async def grade_expert(): return _run_grader_episode("expert")
|
| 382 |
|
| 383 |
+
# ββ SSE: live episode stream βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 384 |
@app.get("/stream/run", tags=["Streaming"])
|
| 385 |
+
async def stream_run(difficulty: str = "medium", delay_ms: int = 350):
|
| 386 |
+
diff = difficulty if difficulty in ("easy","medium","hard","expert") else "medium"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
sleep_s = max(0.1, min(2.0, delay_ms / 1000))
|
| 388 |
|
| 389 |
async def event_gen():
|
| 390 |
try:
|
| 391 |
max_s = 60 if diff == "expert" else 50
|
| 392 |
tasks = generate_tasks(diff)
|
| 393 |
+
env = CLMEnvironment(tasks=tasks, max_steps=max_s)
|
| 394 |
+
obs = env.reset()
|
| 395 |
+
w0 = env.state.workers[0] if env.state.workers else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
+
yield f"data: {json.dumps({'type':'reset','difficulty':diff,'step':0,'tasks':[t.model_dump() for t in obs.tasks],'visible_state':obs.visible_state.model_dump(),'energy':round(w0.energy if w0 else 1.0,3),'stress':round(w0.stress if w0 else 0.0,3)})}\n\n"
|
|
|
|
| 398 |
|
| 399 |
+
done = False; total_r = 0.0
|
| 400 |
while not done:
|
| 401 |
action = _heuristic_action(env)
|
| 402 |
obs, reward, done, info = env.step(action)
|
| 403 |
+
total_r = round(total_r + float(reward), 4)
|
| 404 |
+
w0 = env.state.workers[0] if env.state.workers else None
|
| 405 |
completed = sum(1 for t in obs.tasks if t.progress >= 1.0)
|
| 406 |
|
| 407 |
event: dict = {
|
| 408 |
+
"type": "step",
|
| 409 |
+
"step": obs.time_step,
|
| 410 |
+
"action": {"type": action.type, "task_id": action.task_id},
|
| 411 |
+
"reward": round(float(reward), 4),
|
| 412 |
+
"total_reward": total_r,
|
| 413 |
+
"done": done,
|
| 414 |
+
"energy": round(w0.energy if w0 else 0.5, 3),
|
| 415 |
+
"stress": round(w0.stress if w0 else 0.0, 3),
|
| 416 |
+
"tasks_done": completed,
|
| 417 |
+
"tasks_total": len(obs.tasks),
|
| 418 |
+
"tasks": [t.model_dump() for t in obs.tasks],
|
| 419 |
"visible_state": obs.visible_state.model_dump(),
|
| 420 |
}
|
| 421 |
+
if info.get("schema_drift"): event["schema_drift"] = info["schema_drift"]
|
|
|
|
|
|
|
| 422 |
if done:
|
| 423 |
+
event["final_score"] = _safe(info.get("final_score", 0.01))
|
| 424 |
event["final_energy"] = round(w0.energy if w0 else 0.5, 3)
|
| 425 |
|
| 426 |
yield f"data: {json.dumps(event)}\n\n"
|
|
|
|
| 427 |
if not done:
|
| 428 |
await asyncio.sleep(sleep_s)
|
| 429 |
|
| 430 |
except Exception as exc:
|
| 431 |
+
yield f"data: {json.dumps({'type':'error','message':str(exc)})}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
|
| 433 |
+
return StreamingResponse(event_gen(), media_type="text/event-stream",
|
| 434 |
+
headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no",
|
| 435 |
+
"Connection":"keep-alive"})
|
| 436 |
+
|
| 437 |
+
# ββ Benchmark ββββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½οΏ½ββββ
|
| 438 |
@app.get("/benchmark", tags=["Benchmark"])
|
| 439 |
def benchmark():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
results = {}
|
| 441 |
+
baseline = {"easy":0.856,"medium":0.523,"hard":0.301,"expert":0.221}
|
| 442 |
+
for diff in ("easy","medium","hard","expert"):
|
|
|
|
| 443 |
try:
|
| 444 |
tasks = generate_tasks(diff, seed=42)
|
| 445 |
max_s = 60 if diff == "expert" else 50
|
| 446 |
+
env = CLMEnvironment(tasks=tasks, max_steps=max_s, seed=42)
|
| 447 |
env.reset()
|
| 448 |
+
done = False; step = 0; total_r = 0.0
|
|
|
|
| 449 |
step_rewards: List[float] = []
|
| 450 |
energy_trace: List[float] = []
|
| 451 |
stress_trace: List[float] = []
|
|
|
|
| 452 |
while not done and step < max_s:
|
| 453 |
action = _heuristic_action(env)
|
| 454 |
obs, reward, done, info = env.step(action)
|
| 455 |
+
total_r += float(reward)
|
| 456 |
step_rewards.append(round(float(reward), 4))
|
| 457 |
w0 = env.state.workers[0] if env.state.workers else None
|
| 458 |
energy_trace.append(round(w0.energy if w0 else 0.5, 3))
|
| 459 |
stress_trace.append(round(w0.stress if w0 else 0.0, 3))
|
| 460 |
step += 1
|
| 461 |
|
| 462 |
+
avg_e = _avg_energy(env)
|
| 463 |
+
final_score = _safe(info.get("final_score",
|
| 464 |
+
deterministic_grader(env.state.tasks, env.state.time_step, avg_e)))
|
|
|
|
|
|
|
| 465 |
tasks_done = sum(1 for t in env.state.tasks if t.progress >= 1.0)
|
| 466 |
+
dl_tasks = [t for t in env.state.tasks if t.deadline is not None]
|
| 467 |
+
met_dl = sum(1 for t in dl_tasks
|
| 468 |
+
if t.progress >= 1.0 and env.state.time_step <= t.deadline)
|
| 469 |
+
total_w = sum(PRIORITY_WEIGHT[t.priority] for t in env.state.tasks)
|
| 470 |
+
wc = sum(t.progress*PRIORITY_WEIGHT[t.priority]
|
| 471 |
+
for t in env.state.tasks) / max(total_w, 0.01)
|
| 472 |
+
da = (met_dl / len(dl_tasks)) if dl_tasks else 1.0
|
| 473 |
+
ee = max(0.0, (avg_e - 0.10) * 0.13)
|
| 474 |
+
dep = min(0.05, sum(0.015 for t in env.state.tasks
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
if t.depends_on and t.progress >= 1.0
|
| 476 |
+
and any(p.id==t.depends_on and p.progress>=1.0
|
| 477 |
+
for p in env.state.tasks)))
|
|
|
|
| 478 |
int_t = [t for t in env.state.tasks if t.is_interrupted]
|
| 479 |
+
int_b = min(0.03, (sum(1 for t in int_t if t.progress>=1.0)/
|
| 480 |
+
len(int_t)*0.03) if int_t else 0.0)
|
|
|
|
| 481 |
results[diff] = {
|
| 482 |
+
"score": final_score,
|
| 483 |
+
"baseline": baseline[diff],
|
| 484 |
+
"total_reward": round(total_r, 4),
|
| 485 |
+
"steps": step,
|
| 486 |
+
"tasks_done": tasks_done,
|
| 487 |
+
"tasks_total": len(env.state.tasks),
|
| 488 |
+
"avg_energy": round(avg_e, 3),
|
| 489 |
+
"deadlines_met": met_dl,
|
| 490 |
"deadlines_total": len(dl_tasks),
|
| 491 |
"components": {
|
| 492 |
+
"weighted_completion": round(wc*0.60, 4),
|
| 493 |
+
"deadline_adherence": round(da*0.22, 4),
|
| 494 |
"energy_efficiency": round(ee, 4),
|
| 495 |
+
"dependency_bonus": round(dep, 4),
|
| 496 |
"interruption_bonus": round(int_b, 4),
|
| 497 |
},
|
| 498 |
+
"step_rewards": step_rewards,
|
| 499 |
+
"energy_trace": energy_trace,
|
| 500 |
+
"stress_trace": stress_trace,
|
| 501 |
}
|
| 502 |
except Exception as exc:
|
| 503 |
+
results[diff] = {"error":str(exc),"score":0.01,"baseline":baseline[diff]}
|
|
|
|
| 504 |
return results
|
| 505 |
|
| 506 |
+
# ββ Training log (persisted JSON) ββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
| 507 |
@app.get("/training-log", tags=["Training"])
|
| 508 |
async def training_log():
|
| 509 |
if os.path.exists(_REWARD_CURVE):
|
| 510 |
with open(_REWARD_CURVE) as f:
|
| 511 |
+
raw = json.load(f)
|
| 512 |
+
# Handle both formats:
|
| 513 |
+
# New: {metadata, before, after, curve}
|
| 514 |
+
# Old (legacy): [{step, mean, max, min}, ...]
|
| 515 |
+
if isinstance(raw, list):
|
| 516 |
+
return {"metadata": None, "before": None, "after": None, "curve": raw}
|
| 517 |
+
return raw
|
| 518 |
+
return {"metadata": None, "before": None, "after": None, "curve": []}
|
| 519 |
+
|
| 520 |
+
# ββ Demo training: start βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 521 |
+
@app.post("/train/start", tags=["Training"])
|
| 522 |
+
async def train_start(difficulty: str = "medium", steps: int = 25):
|
| 523 |
+
if _training_state["running"]:
|
| 524 |
+
return {"status": "already_running",
|
| 525 |
+
"message": "Training already in progress."}
|
| 526 |
+
diff = difficulty if difficulty in ("easy","medium","hard","expert") else "medium"
|
| 527 |
+
steps = max(10, min(50, steps))
|
| 528 |
+
t = threading.Thread(
|
| 529 |
+
target=_run_training_demo,
|
| 530 |
+
args=(diff, steps, _ROOT),
|
| 531 |
+
daemon=True,
|
| 532 |
+
)
|
| 533 |
+
t.start()
|
| 534 |
+
return {"status": "started", "difficulty": diff, "total_steps": steps}
|
| 535 |
+
|
| 536 |
+
# ββ Demo training: poll status βββββββββββββββββββββββββββββββββββββββββββββ
|
| 537 |
+
@app.get("/train/status", tags=["Training"])
|
| 538 |
+
async def train_status():
|
| 539 |
+
with _training_lock:
|
| 540 |
+
return dict(_training_state)
|
| 541 |
+
|
| 542 |
+
# ββ Demo training: SSE live stream βββββββββββββββββββββββββββββββββββββββββ
|
| 543 |
+
@app.get("/train/stream", tags=["Training"])
|
| 544 |
+
async def train_stream():
|
| 545 |
+
"""
|
| 546 |
+
SSE that pushes training state whenever a new training step completes.
|
| 547 |
+
Terminates when training finishes or errors out.
|
| 548 |
+
"""
|
| 549 |
+
async def gen():
|
| 550 |
+
last_version = -1
|
| 551 |
+
while True:
|
| 552 |
+
with _training_lock:
|
| 553 |
+
ver = _training_state["_version"]
|
| 554 |
+
status = _training_state["status"]
|
| 555 |
+
snap = dict(_training_state)
|
| 556 |
+
|
| 557 |
+
if ver != last_version:
|
| 558 |
+
last_version = ver
|
| 559 |
+
# Don't send the internal _version field to the client
|
| 560 |
+
payload = {k: v for k, v in snap.items() if k != "_version"}
|
| 561 |
+
yield f"data: {json.dumps(payload)}\n\n"
|
| 562 |
+
if status in ("completed", "error"):
|
| 563 |
+
break
|
| 564 |
+
|
| 565 |
+
await asyncio.sleep(0.3)
|
| 566 |
+
|
| 567 |
+
return StreamingResponse(gen(), media_type="text/event-stream",
|
| 568 |
+
headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no",
|
| 569 |
+
"Connection":"keep-alive"})
|
| 570 |
|
| 571 |
# ββ React SPA static serving βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 572 |
_DIST = os.path.join(_ROOT, "frontend", "dist")
|
|
|
|
| 582 |
async def spa_root():
|
| 583 |
return FileResponse(_INDEX)
|
| 584 |
|
|
|
|
| 585 |
@app.get("/{full_path:path}", include_in_schema=False)
|
| 586 |
async def spa_catchall(full_path: str):
|
| 587 |
return FileResponse(_INDEX)
|
| 588 |
else:
|
| 589 |
@app.get("/", tags=["System"])
|
| 590 |
async def api_root():
|
| 591 |
+
return {"status": "ok", "service": "CLM OpenEnv API",
|
| 592 |
+
"docs": "/docs", "stream": "/stream/run?difficulty=medium",
|
| 593 |
+
"train": "POST /train/start", "benchmark": "/benchmark"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
|
| 595 |
return app
|
| 596 |
|
backend/requirements.txt
CHANGED
|
@@ -4,5 +4,4 @@ pydantic
|
|
| 4 |
openai
|
| 5 |
requests
|
| 6 |
python-dotenv
|
| 7 |
-
openenv-core>=0.2.0
|
| 8 |
aiofiles
|
|
|
|
| 4 |
openai
|
| 5 |
requests
|
| 6 |
python-dotenv
|
|
|
|
| 7 |
aiofiles
|
frontend/src/components/Dashboard.jsx
CHANGED
|
@@ -177,11 +177,10 @@ export default function Dashboard() {
|
|
| 177 |
setStreaming(false)
|
| 178 |
setHistory(prev => [
|
| 179 |
{ ep: prev.length + 1, score, difficulty: d, steps: msg.step },
|
| 180 |
-
...prev.slice(0, 9),
|
| 181 |
])
|
| 182 |
es.close(); esRef.current = null
|
| 183 |
-
//
|
| 184 |
-
replayTimer.current = setTimeout(() => startStream(d), 4000)
|
| 185 |
}
|
| 186 |
}
|
| 187 |
|
|
@@ -193,11 +192,9 @@ export default function Dashboard() {
|
|
| 193 |
}
|
| 194 |
|
| 195 |
es.onerror = () => {
|
| 196 |
-
setError('Stream disconnected
|
| 197 |
setStreaming(false)
|
| 198 |
es.close(); esRef.current = null
|
| 199 |
-
// Retry after 5 s if still on stream mode
|
| 200 |
-
replayTimer.current = setTimeout(() => startStream(d), 5000)
|
| 201 |
}
|
| 202 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 203 |
}, [difficulty])
|
|
@@ -208,16 +205,9 @@ export default function Dashboard() {
|
|
| 208 |
setStreaming(false)
|
| 209 |
}
|
| 210 |
|
| 211 |
-
//
|
| 212 |
-
useEffect(() => {
|
| 213 |
-
const t = setTimeout(() => startStream(), 1000)
|
| 214 |
-
return () => clearTimeout(t)
|
| 215 |
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 216 |
-
}, [])
|
| 217 |
-
|
| 218 |
-
// Cleanup on unmount
|
| 219 |
useEffect(() => () => {
|
| 220 |
-
if (esRef.current)
|
| 221 |
if (replayTimer.current) clearTimeout(replayTimer.current)
|
| 222 |
}, [])
|
| 223 |
|
|
@@ -353,7 +343,7 @@ export default function Dashboard() {
|
|
| 353 |
<span style={{ fontSize: 13, fontWeight: 700, color: '#16a34a',
|
| 354 |
background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 8,
|
| 355 |
padding: '6px 14px' }}>
|
| 356 |
-
β
Score: {finalScore.toFixed(4)}
|
| 357 |
</span>
|
| 358 |
)}
|
| 359 |
</div>
|
|
|
|
| 177 |
setStreaming(false)
|
| 178 |
setHistory(prev => [
|
| 179 |
{ ep: prev.length + 1, score, difficulty: d, steps: msg.step },
|
| 180 |
+
...prev.slice(0, 9),
|
| 181 |
])
|
| 182 |
es.close(); esRef.current = null
|
| 183 |
+
// No auto-replay β user clicks βΊ Replay manually
|
|
|
|
| 184 |
}
|
| 185 |
}
|
| 186 |
|
|
|
|
| 192 |
}
|
| 193 |
|
| 194 |
es.onerror = () => {
|
| 195 |
+
setError('Stream disconnected. Check backend is running, then press βΆ Play again.')
|
| 196 |
setStreaming(false)
|
| 197 |
es.close(); esRef.current = null
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 200 |
}, [difficulty])
|
|
|
|
| 205 |
setStreaming(false)
|
| 206 |
}
|
| 207 |
|
| 208 |
+
// Cleanup on unmount only
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
useEffect(() => () => {
|
| 210 |
+
if (esRef.current) esRef.current.close()
|
| 211 |
if (replayTimer.current) clearTimeout(replayTimer.current)
|
| 212 |
}, [])
|
| 213 |
|
|
|
|
| 343 |
<span style={{ fontSize: 13, fontWeight: 700, color: '#16a34a',
|
| 344 |
background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 8,
|
| 345 |
padding: '6px 14px' }}>
|
| 346 |
+
β
Episode #{episodeCount} Score: {finalScore.toFixed(4)}
|
| 347 |
</span>
|
| 348 |
)}
|
| 349 |
</div>
|
frontend/src/components/TrainingDashboard.jsx
CHANGED
|
@@ -1,483 +1,661 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react'
|
| 2 |
|
| 3 |
const API = ''
|
| 4 |
|
| 5 |
-
|
| 6 |
const DIFF_COLOR = { easy:'#22c55e', medium:'#6366f1', hard:'#f59e0b', expert:'#ef4444' }
|
| 7 |
const DIFF_BG = { easy:'#f0fdf4', medium:'#eef2ff', hard:'#fffbeb', expert:'#fef2f2' }
|
| 8 |
-
const DIFF_ORDER = ['easy','medium','hard','expert']
|
| 9 |
const COMP_COLORS = ['#6366f1','#0ea5e9','#22c55e','#f59e0b','#f43f5e']
|
| 10 |
-
const
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
// Published README baseline scores and LLM target scores
|
| 19 |
-
const BASELINE = { easy:0.856, medium:0.523, hard:0.301, expert:0.221 }
|
| 20 |
const LLM_TARGET = { easy:0.88, medium:0.58, hard:0.37, expert:0.27 }
|
| 21 |
|
| 22 |
-
// ββ Tiny
|
| 23 |
-
function LineChart({ data, color='#6366f1', height=
|
| 24 |
if (!data || !data.length) return (
|
| 25 |
<div style={{ height, display:'flex', alignItems:'center',
|
| 26 |
-
justifyContent:'center', color:'#cbd5e1', fontSize:12 }}>No data</div>
|
| 27 |
)
|
| 28 |
-
const W
|
| 29 |
const lo = Math.min(...data)
|
| 30 |
const hi = Math.max(...data)
|
| 31 |
const sp = hi === lo ? 1 : hi - lo
|
| 32 |
-
const py = v => (height-
|
| 33 |
-
const pts = data.map((v,i) => `${i*
|
| 34 |
return (
|
| 35 |
<svg width="100%" height={height} viewBox={`0 0 ${W} ${height}`}
|
| 36 |
preserveAspectRatio="none" style={{ display:'block' }}>
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
</svg>
|
| 41 |
)
|
| 42 |
}
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
return (
|
| 48 |
-
<
|
| 49 |
-
|
| 50 |
-
<
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
</div>
|
| 67 |
)
|
| 68 |
}
|
| 69 |
|
| 70 |
-
function BarRow({ label, pct, color, val, dashed }) {
|
| 71 |
return (
|
| 72 |
-
<div style={{ marginBottom:
|
| 73 |
<div style={{ display:'flex', justifyContent:'space-between',
|
| 74 |
fontSize:11, color:'#64748b', marginBottom:2 }}>
|
| 75 |
-
<span>{label}</span>
|
|
|
|
| 76 |
</div>
|
| 77 |
-
<div style={{ height:
|
| 78 |
-
<div style={{
|
| 79 |
-
|
| 80 |
background: dashed ? 'transparent' : color,
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
)
|
| 85 |
}
|
| 86 |
|
| 87 |
-
function SectionHeader({ children }) {
|
| 88 |
return (
|
| 89 |
-
<div style={{
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
// ββ Stacked component bar ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 95 |
-
function ComponentBar({ components }) {
|
| 96 |
-
const KEYS = ['weighted_completion','deadline_adherence','energy_efficiency',
|
| 97 |
-
'dependency_bonus','interruption_bonus']
|
| 98 |
-
const total = KEYS.reduce((s,k) => s + (components[k]||0), 0)
|
| 99 |
-
return (
|
| 100 |
-
<div>
|
| 101 |
-
<div style={{ display:'flex', height:28, borderRadius:8, overflow:'hidden',
|
| 102 |
-
marginBottom:8 }}>
|
| 103 |
-
{KEYS.map((k,i) => {
|
| 104 |
-
const v = components[k] || 0
|
| 105 |
-
const pct = total > 0 ? (v/total)*100 : 0
|
| 106 |
-
return (
|
| 107 |
-
<div key={k} title={`${COMP_LABELS[i]}: ${v.toFixed(4)}`}
|
| 108 |
-
style={{ width:`${pct}%`, background:COMP_COLORS[i],
|
| 109 |
-
transition:'width .6s', minWidth: pct > 2 ? 2 : 0 }} />
|
| 110 |
-
)
|
| 111 |
-
})}
|
| 112 |
-
</div>
|
| 113 |
-
<div style={{ display:'flex', flexWrap:'wrap', gap:'6px 14px' }}>
|
| 114 |
-
{KEYS.map((k,i) => (
|
| 115 |
-
<span key={k} style={{ fontSize:11, color:'#475569', display:'flex',
|
| 116 |
-
alignItems:'center', gap:4 }}>
|
| 117 |
-
<span style={{ width:10, height:10, borderRadius:2,
|
| 118 |
-
background:COMP_COLORS[i], display:'inline-block' }}/>
|
| 119 |
-
{COMP_LABELS[i].split(' ')[0]}: <b>{(components[k]||0).toFixed(4)}</b>
|
| 120 |
-
</span>
|
| 121 |
-
))}
|
| 122 |
-
</div>
|
| 123 |
</div>
|
| 124 |
)
|
| 125 |
}
|
| 126 |
|
| 127 |
-
// ββ Training
|
| 128 |
-
function
|
| 129 |
-
const
|
|
|
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
}, [])
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
|
|
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
</div>
|
| 147 |
-
</div>
|
| 148 |
-
)
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
</div>
|
| 168 |
)
|
|
|
|
| 169 |
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
const
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
const total = data.length
|
| 177 |
|
| 178 |
return (
|
| 179 |
-
<
|
| 180 |
-
|
| 181 |
-
<div style={
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
{ l:'Peak Mean', v:peak.toFixed(4), c:'#22c55e' },
|
| 185 |
-
{ l:'Min Mean', v:worst.toFixed(4), c:'#ef4444' },
|
| 186 |
-
{ l:'Steps', v:total, c:'#0ea5e9' },
|
| 187 |
-
].map(s => (
|
| 188 |
-
<div key={s.l} style={{ textAlign:'center', minWidth:70 }}>
|
| 189 |
-
<div style={{ fontSize:10, color:'#94a3b8', textTransform:'uppercase',
|
| 190 |
-
marginBottom:2 }}>{s.l}</div>
|
| 191 |
-
<div style={{ fontSize:18, fontWeight:800, color:s.c }}>{s.v}</div>
|
| 192 |
-
</div>
|
| 193 |
-
))}
|
| 194 |
-
</div>
|
| 195 |
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
const hi = Math.max(...maxes)
|
| 206 |
-
const sp = hi===lo ? 1 : hi-lo
|
| 207 |
-
const py = v => 118 - ((v-lo)/sp)*104 + 6
|
| 208 |
-
const mPts = means.map((v,i) => `${i*16+8},${py(v)}`).join(' ')
|
| 209 |
-
return (
|
| 210 |
-
<>
|
| 211 |
-
<polyline
|
| 212 |
-
points={[
|
| 213 |
-
...mins.map((v,i) => `${i*16+8},${py(v)}`),
|
| 214 |
-
...[...maxes].reverse().map((v,i) =>
|
| 215 |
-
`${(data.length-1-i)*16+8},${py(v)}`)
|
| 216 |
-
].join(' ')}
|
| 217 |
-
fill="#6366f115" stroke="none"/>
|
| 218 |
-
<polyline points={maxes.map((v,i)=>`${i*16+8},${py(v)}`).join(' ')}
|
| 219 |
-
fill="none" stroke="#6366f140" strokeWidth="1"/>
|
| 220 |
-
<polyline points={mins.map((v,i)=>`${i*16+8},${py(v)}`).join(' ')}
|
| 221 |
-
fill="none" stroke="#6366f140" strokeWidth="1"/>
|
| 222 |
-
<polyline points={mPts} fill="none" stroke="#6366f1"
|
| 223 |
-
strokeWidth="2.5" strokeLinejoin="round" strokeLinecap="round"/>
|
| 224 |
-
<circle cx={(data.length-1)*16+8} cy={py(means[means.length-1])}
|
| 225 |
-
r="4" fill="#6366f1"/>
|
| 226 |
-
</>
|
| 227 |
-
)
|
| 228 |
-
})()}
|
| 229 |
-
</svg>
|
| 230 |
</div>
|
| 231 |
|
| 232 |
-
{/*
|
| 233 |
-
|
| 234 |
-
<div style={
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
</div>
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
)
|
| 250 |
}
|
| 251 |
|
| 252 |
-
// ββ
|
| 253 |
-
|
| 254 |
-
const [
|
| 255 |
-
const [running,
|
| 256 |
-
const [
|
| 257 |
|
| 258 |
-
const
|
| 259 |
-
setRunning(true);
|
| 260 |
try {
|
| 261 |
const r = await fetch(`${API}/benchmark`)
|
| 262 |
-
if (
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
console.error(e)
|
| 266 |
-
} finally { setRunning(false) }
|
| 267 |
}
|
| 268 |
|
| 269 |
-
const
|
|
|
|
| 270 |
background:'#fff', border:'1px solid #e2e8f0',
|
| 271 |
-
borderRadius:14, padding:16, marginBottom:16, ...
|
| 272 |
})
|
| 273 |
|
| 274 |
-
const selData = benchData?.[selected]
|
| 275 |
-
|
| 276 |
return (
|
| 277 |
-
<
|
| 278 |
-
{/* ββ Benchmark suite βββββββββββββββββββββββββββββββββββββββββββββββ */}
|
| 279 |
<div style={card()}>
|
| 280 |
<div style={{ display:'flex', justifyContent:'space-between',
|
| 281 |
-
alignItems:'center', marginBottom:
|
| 282 |
<div>
|
| 283 |
<SectionHeader>Heuristic Agent Benchmark</SectionHeader>
|
| 284 |
-
<p style={{ fontSize:
|
| 285 |
-
Runs the
|
| 286 |
-
Scores compared to published baseline and LLM target.
|
| 287 |
</p>
|
| 288 |
</div>
|
| 289 |
-
<button onClick={
|
| 290 |
-
style={{ background: running
|
| 291 |
border:'none', borderRadius:10, padding:'10px 22px',
|
| 292 |
-
fontWeight:700, fontSize:13, cursor:
|
| 293 |
-
whiteSpace:'nowrap'
|
| 294 |
-
{running ? 'β³ Runningβ¦' : 'βΆ Run
|
| 295 |
</button>
|
| 296 |
</div>
|
| 297 |
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
</div>
|
| 310 |
-
|
| 311 |
-
{benchData && (
|
| 312 |
-
<div style={{ marginTop:16, padding:'12px 14px',
|
| 313 |
-
background:'#f8fafc', borderRadius:10, fontSize:12, color:'#64748b' }}>
|
| 314 |
-
<b style={{ color:'#334155' }}>Legend:</b> 
|
| 315 |
-
Achieved = heuristic agent score (this run) Β· 
|
| 316 |
-
Baseline = published heuristic baseline from README Β· 
|
| 317 |
-
LLM Target = expected score after GRPO training
|
| 318 |
-
</div>
|
| 319 |
-
)}
|
| 320 |
</div>
|
| 321 |
|
| 322 |
-
{
|
| 323 |
-
{benchData && (
|
| 324 |
<div style={card()}>
|
| 325 |
<div style={{ display:'flex', gap:6, marginBottom:14 }}>
|
| 326 |
{DIFF_ORDER.map(d => (
|
| 327 |
-
<button key={d} onClick={() =>
|
| 328 |
style={{ padding:'7px 16px', borderRadius:8, border:'none',
|
| 329 |
-
background:
|
| 330 |
-
color:
|
| 331 |
fontWeight:700, fontSize:13, cursor:'pointer',
|
| 332 |
-
textTransform:'capitalize' }}>
|
| 333 |
-
{d}
|
| 334 |
-
</button>
|
| 335 |
))}
|
| 336 |
</div>
|
| 337 |
-
|
| 338 |
-
{selData && !selData.error && (
|
| 339 |
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:16 }}>
|
| 340 |
-
{/* Stats */}
|
| 341 |
<div>
|
| 342 |
-
<SectionHeader>
|
| 343 |
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:8,
|
| 344 |
marginBottom:14 }}>
|
| 345 |
{[
|
| 346 |
-
{ l:'Score', v:
|
| 347 |
-
{ l:'Total Reward', v:
|
| 348 |
-
{ l:'Steps', v:
|
| 349 |
-
{ l:'Tasks Done',
|
| 350 |
-
|
| 351 |
-
{ l:'
|
| 352 |
-
{ l:'Deadlines',
|
| 353 |
-
v: `${selData.deadlines_met}/${selData.deadlines_total}` },
|
| 354 |
].map(s => (
|
| 355 |
<div key={s.l} style={{ background:'#f8fafc',
|
| 356 |
-
borderRadius:8, padding:'
|
| 357 |
-
<div style={{ fontSize:10, color:'#94a3b8', marginBottom:
|
| 358 |
-
<div style={{ fontSize:
|
| 359 |
</div>
|
| 360 |
))}
|
| 361 |
</div>
|
| 362 |
-
|
| 363 |
-
{/* Scoring components */}
|
| 364 |
-
{selData.components && (
|
| 365 |
<>
|
| 366 |
-
<SectionHeader>Score
|
| 367 |
-
<ComponentBar components={
|
| 368 |
</>
|
| 369 |
)}
|
| 370 |
</div>
|
| 371 |
-
|
| 372 |
-
{/* Mini charts */}
|
| 373 |
<div>
|
| 374 |
<SectionHeader>Step Rewards</SectionHeader>
|
| 375 |
<div style={{ border:'1px solid #f1f5f9', borderRadius:8,
|
| 376 |
-
background:'#fafafa', overflow:'hidden', marginBottom:
|
| 377 |
-
<LineChart data={
|
| 378 |
-
</div>
|
| 379 |
-
|
| 380 |
-
<SectionHeader>Energy Trace</SectionHeader>
|
| 381 |
-
<div style={{ border:'1px solid #f1f5f9', borderRadius:8,
|
| 382 |
-
background:'#fafafa', overflow:'hidden', marginBottom:12 }}>
|
| 383 |
-
<LineChart data={selData.energy_trace} color="#22c55e" height={90}/>
|
| 384 |
</div>
|
| 385 |
-
|
| 386 |
-
<SectionHeader>Stress Trace</SectionHeader>
|
| 387 |
<div style={{ border:'1px solid #f1f5f9', borderRadius:8,
|
| 388 |
background:'#fafafa', overflow:'hidden' }}>
|
| 389 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
</div>
|
| 391 |
</div>
|
| 392 |
</div>
|
| 393 |
)}
|
| 394 |
-
|
| 395 |
-
{selData?.error && (
|
| 396 |
-
<div style={{ color:'#dc2626', fontSize:13 }}>β οΈ {selData.error}</div>
|
| 397 |
-
)}
|
| 398 |
</div>
|
| 399 |
)}
|
| 400 |
|
| 401 |
-
{/*
|
| 402 |
<div style={card()}>
|
| 403 |
<SectionHeader>Scoring Formula</SectionHeader>
|
| 404 |
<div style={{ display:'grid', gridTemplateColumns:'repeat(5,1fr)', gap:10 }}>
|
| 405 |
-
{
|
| 406 |
-
{
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
padding:'12px 14px', borderTop:`3px solid ${c.color}` }}>
|
| 414 |
-
<div style={{ fontSize:18, fontWeight:800, color:c.color,
|
| 415 |
-
marginBottom:4 }}>{c.weight}</div>
|
| 416 |
-
<div style={{ fontSize:12, color:'#475569', fontWeight:600 }}>{c.comp}</div>
|
| 417 |
</div>
|
| 418 |
))}
|
| 419 |
</div>
|
| 420 |
-
<div style={{ marginTop:
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
</div>
|
| 426 |
</div>
|
|
|
|
|
|
|
|
|
|
| 427 |
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
<
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
</div>
|
| 478 |
|
| 479 |
-
{
|
| 480 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
</div>
|
| 482 |
)
|
| 483 |
}
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react'
|
| 2 |
|
| 3 |
const API = ''
|
| 4 |
|
| 5 |
+
const DIFF_ORDER = ['easy', 'medium', 'hard', 'expert']
|
| 6 |
const DIFF_COLOR = { easy:'#22c55e', medium:'#6366f1', hard:'#f59e0b', expert:'#ef4444' }
|
| 7 |
const DIFF_BG = { easy:'#f0fdf4', medium:'#eef2ff', hard:'#fffbeb', expert:'#fef2f2' }
|
|
|
|
| 8 |
const COMP_COLORS = ['#6366f1','#0ea5e9','#22c55e','#f59e0b','#f43f5e']
|
| 9 |
+
const COMP_KEYS = ['weighted_completion','deadline_adherence',
|
| 10 |
+
'energy_efficiency','dependency_bonus','interruption_bonus']
|
| 11 |
+
const COMP_LABELS = ['Weighted Completion Γ0.60','Deadline Adherence Γ0.22',
|
| 12 |
+
'Energy Efficiency Γ0.10','Dependency Bonus Γ0.05',
|
| 13 |
+
'Interruption Bonus Γ0.03']
|
| 14 |
+
|
| 15 |
+
// Published baseline (heuristic) and LLM target scores from README
|
| 16 |
+
const BASELINE = { easy:0.856, medium:0.523, hard:0.301, expert:0.221 }
|
|
|
|
|
|
|
| 17 |
const LLM_TARGET = { easy:0.88, medium:0.58, hard:0.37, expert:0.27 }
|
| 18 |
|
| 19 |
+
// ββ Tiny SVG charts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
+
function LineChart({ data, color='#6366f1', height=120 }) {
|
| 21 |
if (!data || !data.length) return (
|
| 22 |
<div style={{ height, display:'flex', alignItems:'center',
|
| 23 |
+
justifyContent:'center', color:'#cbd5e1', fontSize:12 }}>No data yet</div>
|
| 24 |
)
|
| 25 |
+
const W = Math.max(data.length * 18, 300)
|
| 26 |
const lo = Math.min(...data)
|
| 27 |
const hi = Math.max(...data)
|
| 28 |
const sp = hi === lo ? 1 : hi - lo
|
| 29 |
+
const py = v => (height - 14) - ((v - lo) / sp) * (height - 26) + 7
|
| 30 |
+
const pts = data.map((v, i) => `${i * 18 + 9},${py(v)}`).join(' ')
|
| 31 |
return (
|
| 32 |
<svg width="100%" height={height} viewBox={`0 0 ${W} ${height}`}
|
| 33 |
preserveAspectRatio="none" style={{ display:'block' }}>
|
| 34 |
+
{/* zero baseline */}
|
| 35 |
+
<line x1="0" y1={py(0)} x2={W} y2={py(0)}
|
| 36 |
+
stroke="#e2e8f0" strokeWidth="1" strokeDasharray="4 3"/>
|
| 37 |
+
{/* fill */}
|
| 38 |
+
<polyline
|
| 39 |
+
points={[`0,${height}`,
|
| 40 |
+
...data.map((v,i) => `${i*18+9},${py(v)}`),
|
| 41 |
+
`${(data.length-1)*18+9},${height}`].join(' ')}
|
| 42 |
+
fill={color+'18'} stroke="none"/>
|
| 43 |
+
<polyline points={pts} fill="none" stroke={color}
|
| 44 |
+
strokeWidth="2.5" strokeLinejoin="round" strokeLinecap="round"/>
|
| 45 |
+
<circle cx={(data.length-1)*18+9} cy={py(data[data.length-1])}
|
| 46 |
+
r="4.5" fill={color}/>
|
| 47 |
</svg>
|
| 48 |
)
|
| 49 |
}
|
| 50 |
|
| 51 |
+
// Shaded band chart: mean line + min/max band
|
| 52 |
+
function BandChart({ curve, height=140 }) {
|
| 53 |
+
if (!curve || !curve.length) return (
|
| 54 |
+
<div style={{ height, display:'flex', alignItems:'center',
|
| 55 |
+
justifyContent:'center', color:'#cbd5e1', fontSize:12 }}>
|
| 56 |
+
Training data will appear here
|
| 57 |
+
</div>
|
| 58 |
+
)
|
| 59 |
+
const means = curve.map(d => d.mean)
|
| 60 |
+
const maxes = curve.map(d => d.max ?? d.mean)
|
| 61 |
+
const mins = curve.map(d => d.min ?? d.mean)
|
| 62 |
+
const W = Math.max(curve.length * 18, 300)
|
| 63 |
+
const lo = Math.min(...mins)
|
| 64 |
+
const hi = Math.max(...maxes)
|
| 65 |
+
const sp = hi === lo ? 1 : hi - lo
|
| 66 |
+
const py = v => (height - 14) - ((v - lo) / sp) * (height - 26) + 7
|
| 67 |
+
|
| 68 |
+
const bandPts = [
|
| 69 |
+
...mins.map((v,i) => `${i*18+9},${py(v)}`),
|
| 70 |
+
...[...maxes].reverse().map((v,i) =>
|
| 71 |
+
`${(curve.length-1-i)*18+9},${py(v)}`),
|
| 72 |
+
].join(' ')
|
| 73 |
+
const meanPts = means.map((v,i) => `${i*18+9},${py(v)}`).join(' ')
|
| 74 |
+
|
| 75 |
return (
|
| 76 |
+
<svg width="100%" height={height} viewBox={`0 0 ${W} ${height}`}
|
| 77 |
+
preserveAspectRatio="none" style={{ display:'block' }}>
|
| 78 |
+
<line x1="0" y1={py(0)} x2={W} y2={py(0)}
|
| 79 |
+
stroke="#e2e8f0" strokeWidth="1" strokeDasharray="4 3"/>
|
| 80 |
+
<polyline points={bandPts} fill="#6366f118" stroke="none"/>
|
| 81 |
+
<polyline points={maxes.map((v,i)=>`${i*18+9},${py(v)}`).join(' ')}
|
| 82 |
+
fill="none" stroke="#6366f140" strokeWidth="1"/>
|
| 83 |
+
<polyline points={mins.map((v,i)=>`${i*18+9},${py(v)}`).join(' ')}
|
| 84 |
+
fill="none" stroke="#6366f140" strokeWidth="1"/>
|
| 85 |
+
<polyline points={meanPts} fill="none" stroke="#6366f1"
|
| 86 |
+
strokeWidth="2.5" strokeLinejoin="round" strokeLinecap="round"/>
|
| 87 |
+
<circle cx={(curve.length-1)*18+9} cy={py(means[means.length-1])}
|
| 88 |
+
r="4.5" fill="#6366f1"/>
|
| 89 |
+
</svg>
|
| 90 |
+
)
|
| 91 |
+
}
|
| 92 |
|
| 93 |
+
// ββ Before/After grouped bar βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
function BeforeAfterBars({ before, after }) {
|
| 95 |
+
if (!before && !after) return (
|
| 96 |
+
<div style={{ color:'#94a3b8', fontSize:13, textAlign:'center', padding:24 }}>
|
| 97 |
+
Run demo training to see before/after comparison
|
| 98 |
+
</div>
|
| 99 |
+
)
|
| 100 |
+
return (
|
| 101 |
+
<div style={{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:12 }}>
|
| 102 |
+
{DIFF_ORDER.map(d => {
|
| 103 |
+
const bv = before?.[d] ?? null
|
| 104 |
+
const av = after?.[d] ?? null
|
| 105 |
+
const bPct = bv != null ? `${Math.min(100, bv * 100).toFixed(0)}%` : '0%'
|
| 106 |
+
const aPct = av != null ? `${Math.min(100, av * 100).toFixed(0)}%` : '0%'
|
| 107 |
+
const tPct = `${Math.min(100, LLM_TARGET[d] * 100).toFixed(0)}%`
|
| 108 |
+
return (
|
| 109 |
+
<div key={d} style={{ background: DIFF_BG[d],
|
| 110 |
+
border:`1px solid ${DIFF_COLOR[d]}33`, borderRadius:12, padding:'14px 16px' }}>
|
| 111 |
+
<div style={{ fontWeight:700, textTransform:'capitalize',
|
| 112 |
+
color:DIFF_COLOR[d], fontSize:15, marginBottom:10 }}>{d}</div>
|
| 113 |
+
|
| 114 |
+
{/* Before */}
|
| 115 |
+
<BarRow label="Before (random)" pct={bPct} color="#94a3b8"
|
| 116 |
+
val={bv != null ? bv.toFixed(4) : 'β'} />
|
| 117 |
+
{/* After */}
|
| 118 |
+
<BarRow label="After (trained)" pct={aPct} color={DIFF_COLOR[d]}
|
| 119 |
+
val={av != null ? av.toFixed(4) : 'β'} glow />
|
| 120 |
+
{/* Target */}
|
| 121 |
+
<BarRow label="LLM Target" pct={tPct} color="#6366f1"
|
| 122 |
+
val={LLM_TARGET[d].toFixed(3)} dashed />
|
| 123 |
+
|
| 124 |
+
{av != null && bv != null && (
|
| 125 |
+
<div style={{ marginTop:8, fontSize:11, fontWeight:700,
|
| 126 |
+
color: av > bv ? '#16a34a' : '#ef4444' }}>
|
| 127 |
+
{av > bv ? 'β²' : 'βΌ'}
|
| 128 |
+
{av > bv ? '+' : ''}{(av - bv).toFixed(4)} vs before
|
| 129 |
+
</div>
|
| 130 |
+
)}
|
| 131 |
+
</div>
|
| 132 |
+
)
|
| 133 |
+
})}
|
| 134 |
</div>
|
| 135 |
)
|
| 136 |
}
|
| 137 |
|
| 138 |
+
function BarRow({ label, pct, color, val, dashed, glow }) {
|
| 139 |
return (
|
| 140 |
+
<div style={{ marginBottom:6 }}>
|
| 141 |
<div style={{ display:'flex', justifyContent:'space-between',
|
| 142 |
fontSize:11, color:'#64748b', marginBottom:2 }}>
|
| 143 |
+
<span>{label}</span>
|
| 144 |
+
<span style={{ fontWeight:700 }}>{val}</span>
|
| 145 |
</div>
|
| 146 |
+
<div style={{ height:7, background:'#f1f5f9', borderRadius:99 }}>
|
| 147 |
+
<div style={{
|
| 148 |
+
height:7, borderRadius:99, width:pct,
|
| 149 |
background: dashed ? 'transparent' : color,
|
| 150 |
+
border: dashed ? `2px dashed ${color}` : 'none',
|
| 151 |
+
boxShadow: glow ? `0 0 6px ${color}88` : 'none',
|
| 152 |
+
transition:'width .7s ease',
|
| 153 |
+
}}/>
|
| 154 |
</div>
|
| 155 |
</div>
|
| 156 |
)
|
| 157 |
}
|
| 158 |
|
| 159 |
+
function SectionHeader({ children, action }) {
|
| 160 |
return (
|
| 161 |
+
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center',
|
| 162 |
+
marginBottom:12 }}>
|
| 163 |
+
<div style={{ fontSize:11, fontWeight:700, color:'#94a3b8',
|
| 164 |
+
textTransform:'uppercase', letterSpacing:'.08em' }}>{children}</div>
|
| 165 |
+
{action}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
</div>
|
| 167 |
)
|
| 168 |
}
|
| 169 |
|
| 170 |
+
// ββ Training Progress section ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 171 |
+
function TrainingProgress({ state, onStart }) {
|
| 172 |
+
const { running, status, current_step, total_steps, curve,
|
| 173 |
+
before, after, metadata, error } = state
|
| 174 |
|
| 175 |
+
const pct = total_steps > 0
|
| 176 |
+
? Math.round((current_step / total_steps) * 100)
|
| 177 |
+
: 0
|
| 178 |
+
const lastEntry = curve && curve.length ? curve[curve.length - 1] : null
|
| 179 |
+
const meanTrace = (curve || []).map(d => d.mean)
|
|
|
|
| 180 |
|
| 181 |
+
return (
|
| 182 |
+
<div style={{ background:'#fff', border:'1px solid #e2e8f0',
|
| 183 |
+
borderRadius:14, padding:20, marginBottom:16 }}>
|
| 184 |
|
| 185 |
+
{/* Header row */}
|
| 186 |
+
<div style={{ display:'flex', justifyContent:'space-between',
|
| 187 |
+
alignItems:'flex-start', marginBottom:16 }}>
|
| 188 |
+
<div>
|
| 189 |
+
<div style={{ fontSize:15, fontWeight:800, color:'#0f172a',
|
| 190 |
+
marginBottom:4 }}>
|
| 191 |
+
π§ͺ Demo Training β Random β Heuristic Agent
|
| 192 |
+
</div>
|
| 193 |
+
<div style={{ fontSize:12, color:'#64748b', lineHeight:1.6, maxWidth:520 }}>
|
| 194 |
+
Simulates GRPO reward progression on the HF Space (no GPU required).
|
| 195 |
+
Runs {total_steps} training steps, mixing random and heuristic actions to show
|
| 196 |
+
a realistic learning curve. Saves results to{' '}
|
| 197 |
+
<code style={{ background:'#f1f5f9', padding:'1px 6px', borderRadius:4,
|
| 198 |
+
fontSize:11 }}>reward_curve.json</code>.
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
<div style={{ display:'flex', gap:8, flexShrink:0, marginLeft:16 }}>
|
| 202 |
+
{['medium','hard','expert'].map(d => (
|
| 203 |
+
<button key={d} onClick={() => onStart(d)} disabled={running}
|
| 204 |
+
style={{ padding:'8px 14px', borderRadius:8, border:'none',
|
| 205 |
+
background: running ? '#e2e8f0' : DIFF_BG[d],
|
| 206 |
+
color: running ? '#94a3b8' : DIFF_COLOR[d],
|
| 207 |
+
fontWeight:700, fontSize:12, cursor: running ? 'not-allowed':'pointer',
|
| 208 |
+
textTransform:'capitalize' }}>
|
| 209 |
+
{running ? 'β³' : 'βΆ'} {d}
|
| 210 |
+
</button>
|
| 211 |
+
))}
|
| 212 |
+
</div>
|
| 213 |
</div>
|
|
|
|
|
|
|
| 214 |
|
| 215 |
+
{/* Progress bar */}
|
| 216 |
+
{status !== 'idle' && (
|
| 217 |
+
<div style={{ marginBottom:16 }}>
|
| 218 |
+
<div style={{ display:'flex', justifyContent:'space-between',
|
| 219 |
+
fontSize:12, color:'#64748b', marginBottom:6 }}>
|
| 220 |
+
<span style={{ fontWeight:600,
|
| 221 |
+
color: status==='completed'?'#16a34a': status==='error'?'#ef4444':'#6366f1' }}>
|
| 222 |
+
{status==='running' && `①Training⦠step ${current_step}/${total_steps}`}
|
| 223 |
+
{status==='completed'&& `β
Training complete β ${total_steps} steps`}
|
| 224 |
+
{status==='error' && `β Error: ${error}`}
|
| 225 |
+
</span>
|
| 226 |
+
<span>{pct}%</span>
|
| 227 |
+
</div>
|
| 228 |
+
<div style={{ height:10, background:'#f1f5f9', borderRadius:99, overflow:'hidden' }}>
|
| 229 |
+
<div style={{
|
| 230 |
+
height:10, borderRadius:99,
|
| 231 |
+
width:`${status==='completed'?100:pct}%`,
|
| 232 |
+
background: status==='completed' ? '#22c55e' : '#6366f1',
|
| 233 |
+
transition:'width .4s ease',
|
| 234 |
+
boxShadow:'0 0 8px #6366f166',
|
| 235 |
+
}}/>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
)}
|
| 239 |
+
|
| 240 |
+
{/* Live metric chips */}
|
| 241 |
+
{(running || status==='completed') && (
|
| 242 |
+
<div style={{ display:'flex', gap:10, flexWrap:'wrap', marginBottom:16 }}>
|
| 243 |
+
{[
|
| 244 |
+
{ l:'Step', v: `${current_step}/${total_steps}`, c:'#6366f1' },
|
| 245 |
+
{ l:'Last Mean', v: lastEntry ? lastEntry.mean.toFixed(4) : 'β', c: lastEntry && lastEntry.mean>=0?'#16a34a':'#ef4444' },
|
| 246 |
+
{ l:'Last Max', v: lastEntry ? lastEntry.max.toFixed(4) : 'β', c:'#22c55e' },
|
| 247 |
+
{ l:'Last Min', v: lastEntry ? lastEntry.min.toFixed(4) : 'β', c:'#f59e0b' },
|
| 248 |
+
{ l:'Difficulty', v: metadata?.difficulty ?? 'β', c:'#0ea5e9' },
|
| 249 |
+
].map(s => (
|
| 250 |
+
<div key={s.l} style={{ background:'#f8fafc', borderRadius:8,
|
| 251 |
+
padding:'8px 12px', textAlign:'center', minWidth:70 }}>
|
| 252 |
+
<div style={{ fontSize:9, color:'#94a3b8', textTransform:'uppercase',
|
| 253 |
+
marginBottom:3 }}>{s.l}</div>
|
| 254 |
+
<div style={{ fontSize:14, fontWeight:800, color:s.c }}>{s.v}</div>
|
| 255 |
+
</div>
|
| 256 |
+
))}
|
| 257 |
+
</div>
|
| 258 |
+
)}
|
| 259 |
+
|
| 260 |
+
{/* Live reward curve */}
|
| 261 |
+
{meanTrace.length > 0 && (
|
| 262 |
+
<div style={{ border:'1px solid #f1f5f9', borderRadius:10,
|
| 263 |
+
background:'#fafafa', overflow:'hidden', marginBottom:16 }}>
|
| 264 |
+
<div style={{ padding:'8px 12px 0', fontSize:10, color:'#94a3b8',
|
| 265 |
+
fontWeight:700, textTransform:'uppercase' }}>
|
| 266 |
+
Live Reward Curve (mean Β± band)
|
| 267 |
+
</div>
|
| 268 |
+
<BandChart curve={curve} height={140}/>
|
| 269 |
+
</div>
|
| 270 |
+
)}
|
| 271 |
+
|
| 272 |
+
{/* Idle placeholder */}
|
| 273 |
+
{status === 'idle' && (
|
| 274 |
+
<div style={{ textAlign:'center', padding:'20px 0',
|
| 275 |
+
color:'#94a3b8', fontSize:13 }}>
|
| 276 |
+
Click <b>βΆ medium</b> / <b>βΆ hard</b> / <b>βΆ expert</b> above to start demo training.
|
| 277 |
+
<br/>Takes ~15 seconds. Runs entirely on the HF Space server β no local setup needed.
|
| 278 |
+
</div>
|
| 279 |
+
)}
|
| 280 |
</div>
|
| 281 |
)
|
| 282 |
+
}
|
| 283 |
|
| 284 |
+
// ββ Before/After full section ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 285 |
+
function BeforeAfterSection({ before, after, curve }) {
|
| 286 |
+
const card = (ex={}) => ({
|
| 287 |
+
background:'#fff', border:'1px solid #e2e8f0',
|
| 288 |
+
borderRadius:14, padding:16, marginBottom:16, ...ex,
|
| 289 |
+
})
|
|
|
|
| 290 |
|
| 291 |
return (
|
| 292 |
+
<>
|
| 293 |
+
{/* Before/After bars */}
|
| 294 |
+
<div style={card()}>
|
| 295 |
+
<SectionHeader>Before vs After Training β Score Comparison</SectionHeader>
|
| 296 |
+
<BeforeAfterBars before={before} after={after}/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
+
{before && after && (
|
| 299 |
+
<div style={{ marginTop:16, padding:'12px 14px',
|
| 300 |
+
background:'#f0fdf4', borderRadius:10, fontSize:12,
|
| 301 |
+
color:'#166534', fontWeight:600, display:'flex', gap:8, alignItems:'center' }}>
|
| 302 |
+
β
Training improved all difficulty scores. 
|
| 303 |
+
Biggest gain on <b>easy</b>:{' '}
|
| 304 |
+
+{((after.easy||0) - (before.easy||0)).toFixed(4)}
|
| 305 |
+
</div>
|
| 306 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
</div>
|
| 308 |
|
| 309 |
+
{/* Reward learning curve (band chart) */}
|
| 310 |
+
{curve && curve.length > 0 && (
|
| 311 |
+
<div style={card()}>
|
| 312 |
+
<SectionHeader>Reward Learning Curve β {curve.length} Steps</SectionHeader>
|
| 313 |
+
<div style={{ display:'flex', gap:20, marginBottom:12 }}>
|
| 314 |
+
{[
|
| 315 |
+
{ l:'Start (step 0)', v: curve[0].mean.toFixed(4), c:'#94a3b8' },
|
| 316 |
+
{ l:'End (final)', v: curve[curve.length-1].mean.toFixed(4), c:'#6366f1' },
|
| 317 |
+
{ l:'Peak mean', v: Math.max(...curve.map(d=>d.mean)).toFixed(4), c:'#22c55e' },
|
| 318 |
+
{ l:'Steps', v: curve.length, c:'#0ea5e9' },
|
| 319 |
+
].map(s => (
|
| 320 |
+
<div key={s.l} style={{ textAlign:'center', minWidth:72 }}>
|
| 321 |
+
<div style={{ fontSize:10, color:'#94a3b8', textTransform:'uppercase',
|
| 322 |
+
marginBottom:2 }}>{s.l}</div>
|
| 323 |
+
<div style={{ fontSize:18, fontWeight:800, color:s.c }}>{s.v}</div>
|
| 324 |
+
</div>
|
| 325 |
+
))}
|
| 326 |
</div>
|
| 327 |
+
<div style={{ border:'1px solid #f1f5f9', borderRadius:10,
|
| 328 |
+
background:'#fafafa', overflow:'hidden', marginBottom:12 }}>
|
| 329 |
+
<BandChart curve={curve} height={160}/>
|
| 330 |
+
</div>
|
| 331 |
+
{/* Legend */}
|
| 332 |
+
<div style={{ fontSize:11, color:'#64748b', display:'flex', gap:16 }}>
|
| 333 |
+
<span><span style={{ color:'#6366f1', fontWeight:700 }}>β</span> Mean reward</span>
|
| 334 |
+
<span><span style={{ color:'#6366f180', fontWeight:700 }}>β</span> Min/Max band</span>
|
| 335 |
+
<span style={{ color:'#94a3b8' }}>Shaded area = minβmax range across batch</span>
|
| 336 |
+
</div>
|
| 337 |
+
{/* Step table (last 8) */}
|
| 338 |
+
<div style={{ marginTop:12, fontFamily:'monospace', fontSize:11 }}>
|
| 339 |
+
<div style={{ display:'flex', color:'#94a3b8', fontWeight:700,
|
| 340 |
+
borderBottom:'1px solid #f1f5f9', paddingBottom:4, marginBottom:4 }}>
|
| 341 |
+
{['Step','Mean','Max','Min'].map(h=>
|
| 342 |
+
<div key={h} style={{ flex:1 }}>{h}</div>)}
|
| 343 |
+
</div>
|
| 344 |
+
{curve.slice(-8).map(d => (
|
| 345 |
+
<div key={d.step} style={{ display:'flex', padding:'2px 0',
|
| 346 |
+
color: d.mean >= 0 ? '#16a34a' : '#ef4444' }}>
|
| 347 |
+
<div style={{ flex:1 }}>{d.step}</div>
|
| 348 |
+
<div style={{ flex:1 }}>{d.mean.toFixed(4)}</div>
|
| 349 |
+
<div style={{ flex:1 }}>{(d.max ?? d.mean).toFixed(4)}</div>
|
| 350 |
+
<div style={{ flex:1 }}>{(d.min ?? d.mean).toFixed(4)}</div>
|
| 351 |
+
</div>
|
| 352 |
+
))}
|
| 353 |
+
</div>
|
| 354 |
+
</div>
|
| 355 |
+
)}
|
| 356 |
+
</>
|
| 357 |
)
|
| 358 |
}
|
| 359 |
|
| 360 |
+
// ββ Benchmark suite section (unchanged from prior version) ββββββββββββββββββββ
|
| 361 |
+
function BenchmarkSection() {
|
| 362 |
+
const [data, setData] = useState(null)
|
| 363 |
+
const [running, setRunning] = useState(false)
|
| 364 |
+
const [sel, setSel] = useState('medium')
|
| 365 |
|
| 366 |
+
const run = async () => {
|
| 367 |
+
setRunning(true); setData(null)
|
| 368 |
try {
|
| 369 |
const r = await fetch(`${API}/benchmark`)
|
| 370 |
+
if (r.ok) setData(await r.json())
|
| 371 |
+
} catch(e) { console.error(e) }
|
| 372 |
+
finally { setRunning(false) }
|
|
|
|
|
|
|
| 373 |
}
|
| 374 |
|
| 375 |
+
const selD = data?.[sel]
|
| 376 |
+
const card = (ex={}) => ({
|
| 377 |
background:'#fff', border:'1px solid #e2e8f0',
|
| 378 |
+
borderRadius:14, padding:16, marginBottom:16, ...ex,
|
| 379 |
})
|
| 380 |
|
|
|
|
|
|
|
| 381 |
return (
|
| 382 |
+
<>
|
|
|
|
| 383 |
<div style={card()}>
|
| 384 |
<div style={{ display:'flex', justifyContent:'space-between',
|
| 385 |
+
alignItems:'center', marginBottom:14 }}>
|
| 386 |
<div>
|
| 387 |
<SectionHeader>Heuristic Agent Benchmark</SectionHeader>
|
| 388 |
+
<p style={{ fontSize:12, color:'#64748b', margin:0, lineHeight:1.5 }}>
|
| 389 |
+
Runs the deterministic heuristic on all 4 difficulties (seed=42).
|
|
|
|
| 390 |
</p>
|
| 391 |
</div>
|
| 392 |
+
<button onClick={run} disabled={running}
|
| 393 |
+
style={{ background: running?'#94a3b8':'#6366f1', color:'#fff',
|
| 394 |
border:'none', borderRadius:10, padding:'10px 22px',
|
| 395 |
+
fontWeight:700, fontSize:13, cursor:running?'not-allowed':'pointer',
|
| 396 |
+
marginLeft:20, whiteSpace:'nowrap' }}>
|
| 397 |
+
{running ? 'β³ Runningβ¦' : 'βΆ Run Benchmarks'}
|
| 398 |
</button>
|
| 399 |
</div>
|
| 400 |
|
| 401 |
+
{/* Score overview bars */}
|
| 402 |
+
<div style={{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:10 }}>
|
| 403 |
+
{DIFF_ORDER.map(d => {
|
| 404 |
+
const score = data?.[d]?.score
|
| 405 |
+
const bPct = `${Math.min(100, BASELINE[d]*100).toFixed(0)}%`
|
| 406 |
+
const sPct = score != null ? `${Math.min(100, score*100).toFixed(0)}%` : '0%'
|
| 407 |
+
return (
|
| 408 |
+
<div key={d} style={{ background:DIFF_BG[d],
|
| 409 |
+
border:`1px solid ${DIFF_COLOR[d]}33`, borderRadius:12,
|
| 410 |
+
padding:'12px 14px' }}>
|
| 411 |
+
<div style={{ fontWeight:700, color:DIFF_COLOR[d], fontSize:14,
|
| 412 |
+
textTransform:'capitalize', marginBottom:8 }}>{d}</div>
|
| 413 |
+
<BarRow label="Achieved" pct={sPct} color={DIFF_COLOR[d]}
|
| 414 |
+
val={score!=null?score.toFixed(4):'β'} glow={score!=null}/>
|
| 415 |
+
<BarRow label="Baseline" pct={bPct} color="#94a3b8"
|
| 416 |
+
val={BASELINE[d].toFixed(3)}/>
|
| 417 |
+
</div>
|
| 418 |
+
)
|
| 419 |
+
})}
|
| 420 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
</div>
|
| 422 |
|
| 423 |
+
{data && (
|
|
|
|
| 424 |
<div style={card()}>
|
| 425 |
<div style={{ display:'flex', gap:6, marginBottom:14 }}>
|
| 426 |
{DIFF_ORDER.map(d => (
|
| 427 |
+
<button key={d} onClick={() => setSel(d)}
|
| 428 |
style={{ padding:'7px 16px', borderRadius:8, border:'none',
|
| 429 |
+
background: sel===d ? DIFF_COLOR[d] : DIFF_BG[d],
|
| 430 |
+
color: sel===d ? '#fff' : DIFF_COLOR[d],
|
| 431 |
fontWeight:700, fontSize:13, cursor:'pointer',
|
| 432 |
+
textTransform:'capitalize' }}>{d}</button>
|
|
|
|
|
|
|
| 433 |
))}
|
| 434 |
</div>
|
| 435 |
+
{selD && !selD.error && (
|
|
|
|
| 436 |
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:16 }}>
|
|
|
|
| 437 |
<div>
|
| 438 |
+
<SectionHeader>Stats β {sel}</SectionHeader>
|
| 439 |
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:8,
|
| 440 |
marginBottom:14 }}>
|
| 441 |
{[
|
| 442 |
+
{ l:'Score', v: selD.score?.toFixed(4) },
|
| 443 |
+
{ l:'Total Reward', v: selD.total_reward?.toFixed(3) },
|
| 444 |
+
{ l:'Steps', v: selD.steps },
|
| 445 |
+
{ l:'Tasks Done', v:`${selD.tasks_done}/${selD.tasks_total}`},
|
| 446 |
+
{ l:'Avg Energy', v: selD.avg_energy?.toFixed(3) },
|
| 447 |
+
{ l:'Deadlines', v:`${selD.deadlines_met}/${selD.deadlines_total}`},
|
|
|
|
|
|
|
| 448 |
].map(s => (
|
| 449 |
<div key={s.l} style={{ background:'#f8fafc',
|
| 450 |
+
borderRadius:8, padding:'8px 12px' }}>
|
| 451 |
+
<div style={{ fontSize:10, color:'#94a3b8', marginBottom:2 }}>{s.l}</div>
|
| 452 |
+
<div style={{ fontSize:14, fontWeight:700, color:'#0f172a' }}>{s.v}</div>
|
| 453 |
</div>
|
| 454 |
))}
|
| 455 |
</div>
|
| 456 |
+
{selD.components && (
|
|
|
|
|
|
|
| 457 |
<>
|
| 458 |
+
<SectionHeader>Score Components</SectionHeader>
|
| 459 |
+
<ComponentBar components={selD.components}/>
|
| 460 |
</>
|
| 461 |
)}
|
| 462 |
</div>
|
|
|
|
|
|
|
| 463 |
<div>
|
| 464 |
<SectionHeader>Step Rewards</SectionHeader>
|
| 465 |
<div style={{ border:'1px solid #f1f5f9', borderRadius:8,
|
| 466 |
+
background:'#fafafa', overflow:'hidden', marginBottom:10 }}>
|
| 467 |
+
<LineChart data={selD.step_rewards} color={DIFF_COLOR[sel]} height={100}/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
</div>
|
| 469 |
+
<SectionHeader>Energy / Stress</SectionHeader>
|
|
|
|
| 470 |
<div style={{ border:'1px solid #f1f5f9', borderRadius:8,
|
| 471 |
background:'#fafafa', overflow:'hidden' }}>
|
| 472 |
+
<svg width="100%" height={90}
|
| 473 |
+
viewBox={`0 0 ${Math.max((selD.energy_trace||[]).length*18,300)} 90`}
|
| 474 |
+
preserveAspectRatio="none" style={{ display:'block' }}>
|
| 475 |
+
<polyline
|
| 476 |
+
points={(selD.energy_trace||[]).map((v,i)=>`${i*18+9},${80-(v*70)}`).join(' ')}
|
| 477 |
+
fill="none" stroke="#22c55e" strokeWidth="2" strokeLinejoin="round"/>
|
| 478 |
+
<polyline
|
| 479 |
+
points={(selD.stress_trace||[]).map((v,i)=>`${i*18+9},${80-(v*70)}`).join(' ')}
|
| 480 |
+
fill="none" stroke="#f59e0b" strokeWidth="2" strokeLinejoin="round"
|
| 481 |
+
strokeDasharray="5 3"/>
|
| 482 |
+
</svg>
|
| 483 |
+
</div>
|
| 484 |
+
<div style={{ fontSize:10, color:'#94a3b8', marginTop:4 }}>
|
| 485 |
+
<span style={{ color:'#22c55e', fontWeight:700 }}>β</span> Energy 
|
| 486 |
+
<span style={{ color:'#f59e0b', fontWeight:700 }}>β</span> Stress
|
| 487 |
</div>
|
| 488 |
</div>
|
| 489 |
</div>
|
| 490 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
</div>
|
| 492 |
)}
|
| 493 |
|
| 494 |
+
{/* Scoring formula */}
|
| 495 |
<div style={card()}>
|
| 496 |
<SectionHeader>Scoring Formula</SectionHeader>
|
| 497 |
<div style={{ display:'grid', gridTemplateColumns:'repeat(5,1fr)', gap:10 }}>
|
| 498 |
+
{COMP_LABELS.map((lbl, i) => (
|
| 499 |
+
<div key={lbl} style={{ background:'#f8fafc', borderRadius:10,
|
| 500 |
+
padding:'12px 14px', borderTop:`3px solid ${COMP_COLORS[i]}` }}>
|
| 501 |
+
<div style={{ fontSize:16, fontWeight:800, color:COMP_COLORS[i],
|
| 502 |
+
marginBottom:4 }}>{['Γ0.60','Γ0.22','Γ0.10','Γ0.05','Γ0.03'][i]}</div>
|
| 503 |
+
<div style={{ fontSize:11, color:'#475569', fontWeight:600 }}>
|
| 504 |
+
{lbl.split(' ')[0]} {lbl.split(' ')[1]}
|
| 505 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
</div>
|
| 507 |
))}
|
| 508 |
</div>
|
| 509 |
+
<div style={{ marginTop:10, fontSize:11, color:'#94a3b8',
|
| 510 |
+
fontFamily:'monospace', background:'#f8fafc',
|
| 511 |
+
padding:'8px 12px', borderRadius:8 }}>
|
| 512 |
+
score = completionΓ0.60 + deadlineΓ0.22 + energyΓ0.10 + depΓ0.05 + interruptΓ0.03
|
| 513 |
+
 β (0.01, 0.99)
|
| 514 |
</div>
|
| 515 |
</div>
|
| 516 |
+
</>
|
| 517 |
+
)
|
| 518 |
+
}
|
| 519 |
|
| 520 |
+
function ComponentBar({ components }) {
|
| 521 |
+
const total = COMP_KEYS.reduce((s,k) => s+(components[k]||0), 0)
|
| 522 |
+
return (
|
| 523 |
+
<div>
|
| 524 |
+
<div style={{ display:'flex', height:22, borderRadius:6,
|
| 525 |
+
overflow:'hidden', marginBottom:6 }}>
|
| 526 |
+
{COMP_KEYS.map((k,i) => {
|
| 527 |
+
const v = components[k]||0
|
| 528 |
+
const pct = total > 0 ? (v/total)*100 : 0
|
| 529 |
+
return <div key={k} title={`${COMP_LABELS[i]}: ${v.toFixed(4)}`}
|
| 530 |
+
style={{ width:`${pct}%`, background:COMP_COLORS[i], minWidth:pct>2?2:0 }}/>
|
| 531 |
+
})}
|
| 532 |
+
</div>
|
| 533 |
+
<div style={{ display:'flex', flexWrap:'wrap', gap:'4px 10px' }}>
|
| 534 |
+
{COMP_KEYS.map((k,i) => (
|
| 535 |
+
<span key={k} style={{ fontSize:10, color:'#475569',
|
| 536 |
+
display:'flex', alignItems:'center', gap:3 }}>
|
| 537 |
+
<span style={{ width:8, height:8, borderRadius:2,
|
| 538 |
+
background:COMP_COLORS[i], display:'inline-block' }}/>
|
| 539 |
+
{COMP_LABELS[i].split(' ')[0]}: <b>{(components[k]||0).toFixed(4)}</b>
|
| 540 |
+
</span>
|
| 541 |
+
))}
|
| 542 |
+
</div>
|
| 543 |
+
</div>
|
| 544 |
+
)
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
// ββ Main TrainingDashboard βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 548 |
+
export default function TrainingDashboard() {
|
| 549 |
+
const [activeTab, setActiveTab] = useState('training')
|
| 550 |
+
|
| 551 |
+
// Training state (mirrors _training_state on the server)
|
| 552 |
+
const [trainState, setTrainState] = useState({
|
| 553 |
+
running: false, status: 'idle', current_step: 0, total_steps: 25,
|
| 554 |
+
difficulty: 'medium', curve: [], before: null, after: null,
|
| 555 |
+
metadata: null, error: null,
|
| 556 |
+
})
|
| 557 |
+
// Persisted results from /training-log
|
| 558 |
+
const [savedLog, setSavedLog] = useState(null)
|
| 559 |
+
const esRef = useRef(null)
|
| 560 |
+
|
| 561 |
+
// Load saved training log on mount
|
| 562 |
+
useEffect(() => {
|
| 563 |
+
fetch(`${API}/training-log`)
|
| 564 |
+
.then(r => r.ok ? r.json() : null)
|
| 565 |
+
.then(d => { if (d) setSavedLog(d) })
|
| 566 |
+
.catch(() => {})
|
| 567 |
+
}, [])
|
| 568 |
+
|
| 569 |
+
// Start training & subscribe to SSE
|
| 570 |
+
const startTraining = async (difficulty) => {
|
| 571 |
+
if (trainState.running) return
|
| 572 |
+
|
| 573 |
+
// Kick off training on the server
|
| 574 |
+
await fetch(`${API}/train/start?difficulty=${difficulty}&steps=25`, { method:'POST' })
|
| 575 |
+
|
| 576 |
+
// Subscribe to live SSE stream
|
| 577 |
+
if (esRef.current) { esRef.current.close(); esRef.current = null }
|
| 578 |
+
const es = new EventSource(`${API}/train/stream`)
|
| 579 |
+
esRef.current = es
|
| 580 |
+
|
| 581 |
+
es.onmessage = (ev) => {
|
| 582 |
+
const d = JSON.parse(ev.data)
|
| 583 |
+
setTrainState(d)
|
| 584 |
+
if (d.status === 'completed') {
|
| 585 |
+
// Refresh the saved log once training finishes
|
| 586 |
+
fetch(`${API}/training-log`)
|
| 587 |
+
.then(r => r.ok ? r.json() : null)
|
| 588 |
+
.then(saved => { if (saved) setSavedLog(saved) })
|
| 589 |
+
.catch(() => {})
|
| 590 |
+
es.close(); esRef.current = null
|
| 591 |
+
}
|
| 592 |
+
if (d.status === 'error') {
|
| 593 |
+
es.close(); esRef.current = null
|
| 594 |
+
}
|
| 595 |
+
}
|
| 596 |
+
es.onerror = () => { es.close(); esRef.current = null }
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
useEffect(() => () => { if (esRef.current) esRef.current.close() }, [])
|
| 600 |
+
|
| 601 |
+
// Decide which data to show: live training state takes priority if running/just-done
|
| 602 |
+
// otherwise fall back to savedLog
|
| 603 |
+
const showLive = trainState.status !== 'idle'
|
| 604 |
+
const displayLog = showLive ? trainState : savedLog
|
| 605 |
+
|
| 606 |
+
const TABS = [
|
| 607 |
+
{ id:'training', label:'π§ͺ Training Progress' },
|
| 608 |
+
{ id:'benchmark', label:'π Benchmarks' },
|
| 609 |
+
]
|
| 610 |
+
|
| 611 |
+
return (
|
| 612 |
+
<div>
|
| 613 |
+
{/* Sub-tabs */}
|
| 614 |
+
<div style={{ display:'flex', gap:4, marginBottom:20 }}>
|
| 615 |
+
{TABS.map(t => (
|
| 616 |
+
<button key={t.id} onClick={() => setActiveTab(t.id)}
|
| 617 |
+
style={{ padding:'9px 20px', borderRadius:10, border:'none',
|
| 618 |
+
background: activeTab===t.id ? '#0f172a' : '#e2e8f0',
|
| 619 |
+
color: activeTab===t.id ? '#fff' : '#64748b',
|
| 620 |
+
fontWeight:700, fontSize:13, cursor:'pointer' }}>
|
| 621 |
+
{t.label}
|
| 622 |
+
</button>
|
| 623 |
+
))}
|
| 624 |
</div>
|
| 625 |
|
| 626 |
+
{activeTab === 'training' && (
|
| 627 |
+
<>
|
| 628 |
+
{/* Live training control */}
|
| 629 |
+
<TrainingProgress state={trainState} onStart={startTraining}/>
|
| 630 |
+
|
| 631 |
+
{/* Show results β live first, then saved */}
|
| 632 |
+
{displayLog && (displayLog.before || (displayLog.curve && displayLog.curve.length > 0)) && (
|
| 633 |
+
<BeforeAfterSection
|
| 634 |
+
before={displayLog.before}
|
| 635 |
+
after={displayLog.after}
|
| 636 |
+
curve={displayLog.curve}
|
| 637 |
+
/>
|
| 638 |
+
)}
|
| 639 |
+
|
| 640 |
+
{/* No data yet message */}
|
| 641 |
+
{!showLive && (!savedLog || (!savedLog.before && (!savedLog.curve || !savedLog.curve.length))) && (
|
| 642 |
+
<div style={{ background:'#fff', border:'1px solid #e2e8f0',
|
| 643 |
+
borderRadius:14, padding:32, textAlign:'center',
|
| 644 |
+
color:'#94a3b8', fontSize:14 }}>
|
| 645 |
+
<div style={{ fontSize:32, marginBottom:8 }}>π</div>
|
| 646 |
+
<div style={{ fontWeight:700, color:'#475569', marginBottom:6 }}>
|
| 647 |
+
No training data yet
|
| 648 |
+
</div>
|
| 649 |
+
<div style={{ fontSize:13, lineHeight:1.7 }}>
|
| 650 |
+
Click <b>βΆ medium</b> above to run demo training (~15 seconds).<br/>
|
| 651 |
+
The before/after comparison and reward curve will appear here.
|
| 652 |
+
</div>
|
| 653 |
+
</div>
|
| 654 |
+
)}
|
| 655 |
+
</>
|
| 656 |
+
)}
|
| 657 |
+
|
| 658 |
+
{activeTab === 'benchmark' && <BenchmarkSection/>}
|
| 659 |
</div>
|
| 660 |
)
|
| 661 |
}
|