manzz05/kitchenflow-v2
1
1 2<!DOCTYPE html>3<html lang="en">4<head>5<meta charset="UTF-8">6<meta name="viewport" content="width=device-width, initial-scale=1.0">7<title>KitchenFlow-v2 — Ghost Kitchen Dispatcher</title>8<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap" rel="stylesheet">9<style>10 :root {11 --bg: #0a0a0a;12 --surface: #111111;13 --surface2: #1a1a1a;14 --border: #2a2a2a;15 --accent: #ff4d00;16 --accent2: #ffb800;17 --green: #00e676;18 --red: #ff1744;19 --text: #f0f0f0;20 --muted: #666;21 --mono: 'Space Mono', monospace;22 --sans: 'Syne', sans-serif;23 }24 25 * { margin: 0; padding: 0; box-sizing: border-box; }26 27 body {28 background: var(--bg);29 color: var(--text);30 font-family: var(--sans);31 min-height: 100vh;32 overflow-x: hidden;33 }34 35 /* GRID BACKGROUND */36 body::before {37 content: '';38 position: fixed;39 inset: 0;40 background-image:41 linear-gradient(rgba(255,77,0,0.03) 1px, transparent 1px),42 linear-gradient(90deg, rgba(255,77,0,0.03) 1px, transparent 1px);43 background-size: 40px 40px;44 pointer-events: none;45 z-index: 0;46 }47 48 .container {49 max-width: 1100px;50 margin: 0 auto;51 padding: 0 24px;52 position: relative;53 z-index: 1;54 }55 56 /* HEADER */57 header {58 padding: 48px 0 32px;59 border-bottom: 1px solid var(--border);60 }61 62 .header-inner {63 display: flex;64 align-items: flex-start;65 justify-content: space-between;66 gap: 24px;67 flex-wrap: wrap;68 }69 70 .logo-block {}71 72 .badge {73 display: inline-flex;74 align-items: center;75 gap: 6px;76 background: rgba(255,77,0,0.12);77 border: 1px solid rgba(255,77,0,0.3);78 color: var(--accent);79 font-family: var(--mono);80 font-size: 10px;81 padding: 4px 10px;82 letter-spacing: 0.15em;83 text-transform: uppercase;84 margin-bottom: 16px;85 }86 87 .badge-dot {88 width: 6px; height: 6px;89 background: var(--green);90 border-radius: 50%;91 animation: pulse 2s infinite;92 }93 94 @keyframes pulse {95 0%, 100% { opacity: 1; }96 50% { opacity: 0.3; }97 }98 99 h1 {100 font-family: var(--sans);101 font-size: clamp(28px, 4vw, 48px);102 font-weight: 800;103 line-height: 1.05;104 letter-spacing: -0.02em;105 }106 107 h1 span { color: var(--accent); }108 109 .tagline {110 font-family: var(--mono);111 font-size: 12px;112 color: var(--muted);113 margin-top: 12px;114 letter-spacing: 0.05em;115 }116 117 .header-stats {118 display: flex;119 gap: 24px;120 flex-wrap: wrap;121 align-items: center;122 }123 124 .stat {125 text-align: right;126 }127 128 .stat-val {129 font-family: var(--mono);130 font-size: 22px;131 font-weight: 700;132 color: var(--accent2);133 }134 135 .stat-label {136 font-size: 10px;137 color: var(--muted);138 letter-spacing: 0.1em;139 text-transform: uppercase;140 }141 142 /* MAIN GRID */143 .main-grid {144 display: grid;145 grid-template-columns: 1fr 1fr;146 gap: 16px;147 margin: 32px 0;148 }149 150 @media (max-width: 700px) {151 .main-grid { grid-template-columns: 1fr; }152 }153 154 .card {155 background: var(--surface);156 border: 1px solid var(--border);157 padding: 24px;158 position: relative;159 overflow: hidden;160 }161 162 .card::before {163 content: '';164 position: absolute;165 top: 0; left: 0; right: 0;166 height: 2px;167 background: var(--accent);168 }169 170 .card.green::before { background: var(--green); }171 .card.yellow::before { background: var(--accent2); }172 .card.full { grid-column: 1 / -1; }173 174 .card-title {175 font-family: var(--mono);176 font-size: 10px;177 color: var(--muted);178 letter-spacing: 0.15em;179 text-transform: uppercase;180 margin-bottom: 20px;181 }182 183 /* EPISODE PANEL */184 .ep-controls {185 display: flex;186 gap: 10px;187 margin-bottom: 20px;188 flex-wrap: wrap;189 }190 191 select, input[type=text] {192 background: var(--surface2);193 border: 1px solid var(--border);194 color: var(--text);195 font-family: var(--mono);196 font-size: 12px;197 padding: 8px 12px;198 flex: 1;199 min-width: 120px;200 outline: none;201 }202 203 select:focus, input:focus {204 border-color: var(--accent);205 }206 207 .btn {208 background: var(--accent);209 color: white;210 border: none;211 font-family: var(--mono);212 font-size: 11px;213 font-weight: 700;214 padding: 8px 20px;215 cursor: pointer;216 letter-spacing: 0.08em;217 text-transform: uppercase;218 transition: background 0.15s, transform 0.1s;219 white-space: nowrap;220 }221 222 .btn:hover { background: #ff6a00; }223 .btn:active { transform: scale(0.97); }224 .btn.secondary {225 background: var(--surface2);226 color: var(--text);227 border: 1px solid var(--border);228 }229 .btn.secondary:hover { border-color: var(--accent); color: var(--accent); background: var(--surface2); }230 .btn.green-btn { background: var(--green); color: #000; }231 .btn.green-btn:hover { background: #00ff88; }232 .btn:disabled { opacity: 0.4; cursor: not-allowed; }233 234 /* STATUS BAR */235 .status-bar {236 display: flex;237 gap: 20px;238 background: var(--surface2);239 border: 1px solid var(--border);240 padding: 12px 16px;241 font-family: var(--mono);242 font-size: 11px;243 margin-bottom: 16px;244 flex-wrap: wrap;245 }246 247 .status-item { display: flex; gap: 6px; align-items: center; }248 .status-key { color: var(--muted); }249 .status-val { color: var(--accent2); font-weight: 700; }250 .status-val.ok { color: var(--green); }251 .status-val.bad { color: var(--red); }252 253 /* ORDER CARDS */254 .orders-grid {255 display: flex;256 flex-direction: column;257 gap: 10px;258 }259 260 .order-card {261 background: var(--surface2);262 border: 1px solid var(--border);263 padding: 14px 16px;264 display: grid;265 grid-template-columns: auto 1fr auto;266 gap: 12px;267 align-items: center;268 transition: border-color 0.2s;269 }270 271 .order-card.active { border-color: rgba(255,77,0,0.4); }272 .order-card.delivered { border-color: rgba(0,230,118,0.3); opacity: 0.6; }273 .order-card.failed { border-color: rgba(255,23,68,0.3); opacity: 0.5; }274 275 .order-id {276 font-family: var(--mono);277 font-size: 11px;278 color: var(--accent);279 min-width: 60px;280 }281 282 .order-info { font-size: 12px; }283 .order-name { font-weight: 600; margin-bottom: 4px; font-size: 13px; }284 285 .progress-bar {286 height: 4px;287 background: var(--border);288 margin: 6px 0;289 position: relative;290 overflow: hidden;291 }292 293 .progress-fill {294 height: 100%;295 background: var(--accent);296 transition: width 0.4s ease;297 }298 299 .progress-fill.hot { background: var(--green); }300 .progress-fill.cool { background: var(--accent2); }301 302 .order-meta {303 display: flex;304 gap: 10px;305 font-family: var(--mono);306 font-size: 10px;307 color: var(--muted);308 flex-wrap: wrap;309 }310 311 .order-meta span { color: var(--text); }312 313 .order-actions {314 display: flex;315 gap: 6px;316 flex-direction: column;317 }318 319 .action-btn {320 background: var(--surface);321 border: 1px solid var(--border);322 color: var(--text);323 font-family: var(--mono);324 font-size: 10px;325 padding: 5px 10px;326 cursor: pointer;327 transition: all 0.15s;328 white-space: nowrap;329 }330 331 .action-btn:hover { border-color: var(--accent); color: var(--accent); }332 .action-btn.selected { background: var(--accent); color: white; border-color: var(--accent); }333 .action-btn.disabled { opacity: 0.3; cursor: not-allowed; }334 335 /* LOG */336 .log {337 background: var(--surface2);338 border: 1px solid var(--border);339 font-family: var(--mono);340 font-size: 11px;341 line-height: 1.7;342 padding: 16px;343 height: 220px;344 overflow-y: auto;345 color: #aaa;346 }347 348 .log::-webkit-scrollbar { width: 4px; }349 .log::-webkit-scrollbar-track { background: transparent; }350 .log::-webkit-scrollbar-thumb { background: var(--border); }351 352 .log-line { display: flex; gap: 10px; }353 .log-time { color: var(--muted); min-width: 40px; }354 .log-msg { flex: 1; }355 .log-msg.reward { color: var(--green); }356 .log-msg.penalty { color: var(--red); }357 .log-msg.chaos { color: var(--accent2); }358 .log-msg.info { color: var(--accent); }359 .log-msg.system { color: var(--muted); font-style: italic; }360 361 /* REWARD CHART */362 #rewardCanvas {363 width: 100%;364 height: 160px;365 display: block;366 }367 368 /* TASKS TABLE */369 .tasks-table {370 width: 100%;371 border-collapse: collapse;372 font-family: var(--mono);373 font-size: 11px;374 }375 376 .tasks-table th {377 text-align: left;378 color: var(--muted);379 font-weight: 400;380 letter-spacing: 0.1em;381 text-transform: uppercase;382 padding: 6px 10px;383 border-bottom: 1px solid var(--border);384 font-size: 10px;385 }386 387 .tasks-table td {388 padding: 10px 10px;389 border-bottom: 1px solid rgba(255,255,255,0.04);390 }391 392 .tasks-table tr:hover td { background: rgba(255,77,0,0.04); }393 394 .diff-badge {395 padding: 2px 8px;396 font-size: 9px;397 letter-spacing: 0.1em;398 text-transform: uppercase;399 }400 401 .diff-easy { background: rgba(0,230,118,0.15); color: var(--green); }402 .diff-medium { background: rgba(255,184,0,0.15); color: var(--accent2); }403 .diff-hard { background: rgba(255,77,0,0.15); color: var(--accent); }404 405 /* REWARD BREAKDOWN */406 .reward-list {407 display: flex;408 flex-direction: column;409 gap: 8px;410 }411 412 .reward-row {413 display: flex;414 justify-content: space-between;415 align-items: center;416 padding: 8px 12px;417 background: var(--surface2);418 border-left: 2px solid transparent;419 font-size: 12px;420 }421 422 .reward-row.pos { border-color: var(--green); }423 .reward-row.neg { border-color: var(--red); }424 425 .reward-val {426 font-family: var(--mono);427 font-weight: 700;428 font-size: 13px;429 }430 431 .reward-val.pos { color: var(--green); }432 .reward-val.neg { color: var(--red); }433 434 /* STEP BUTTON */435 .step-area {436 display: flex;437 gap: 10px;438 margin-top: 16px;439 align-items: center;440 flex-wrap: wrap;441 }442 443 .score-display {444 font-family: var(--mono);445 font-size: 24px;446 font-weight: 700;447 color: var(--accent2);448 margin-left: auto;449 }450 451 .score-display.positive { color: var(--green); }452 .score-display.negative { color: var(--red); }453 454 /* DONE OVERLAY */455 .done-banner {456 display: none;457 background: rgba(0,230,118,0.1);458 border: 1px solid var(--green);459 color: var(--green);460 font-family: var(--mono);461 font-size: 13px;462 padding: 14px 20px;463 text-align: center;464 margin-top: 16px;465 letter-spacing: 0.05em;466 }467 468 .done-banner.show { display: block; }469 .done-banner.failed-banner {470 background: rgba(255,23,68,0.1);471 border-color: var(--red);472 color: var(--red);473 }474 475 /* FOOTER */476 footer {477 border-top: 1px solid var(--border);478 padding: 24px 0;479 font-family: var(--mono);480 font-size: 10px;481 color: var(--muted);482 display: flex;483 justify-content: space-between;484 flex-wrap: wrap;485 gap: 12px;486 }487 488 footer a { color: var(--accent); text-decoration: none; }489 footer a:hover { text-decoration: underline; }490 491 /* CHAOS TAG */492 .chaos-tag {493 display: inline-block;494 background: rgba(255,184,0,0.15);495 color: var(--accent2);496 font-family: var(--mono);497 font-size: 9px;498 padding: 2px 6px;499 letter-spacing: 0.08em;500 margin-left: 6px;501 vertical-align: middle;502 }503 504 .api-section {505 margin-top: 12px;506 }507 508 .endpoint {509 display: flex;510 gap: 10px;511 align-items: baseline;512 padding: 8px 0;513 border-bottom: 1px solid rgba(255,255,255,0.04);514 font-size: 12px;515 }516 517 .method {518 font-family: var(--mono);519 font-size: 10px;520 font-weight: 700;521 padding: 2px 6px;522 min-width: 42px;523 text-align: center;524 }525 526 .method.get { background: rgba(0,230,118,0.15); color: var(--green); }527 .method.post { background: rgba(255,77,0,0.15); color: var(--accent); }528 529 .endpoint-path {530 font-family: var(--mono);531 font-size: 11px;532 color: var(--text);533 min-width: 120px;534 }535 536 .endpoint-desc { color: var(--muted); font-size: 11px; }537 538 .spinner {539 display: inline-block;540 width: 12px; height: 12px;541 border: 2px solid var(--border);542 border-top-color: var(--accent);543 border-radius: 50%;544 animation: spin 0.6s linear infinite;545 vertical-align: middle;546 margin-right: 6px;547 }548 549 @keyframes spin { to { transform: rotate(360deg); } }550</style>551</head>552<body>553 554<div class="container">555 <!-- HEADER -->556 <header>557 <div class="header-inner">558 <div class="logo-block">559 <div class="badge">560 <span class="badge-dot"></span>561 OpenEnv · v2.0.0 · Running562 </div>563 <h1>Kitchen<span>Flow</span><br>Dispatcher</h1>564 <p class="tagline">// ghost kitchen rl environment — openenv hackathon 2026</p>565 </div>566 <div class="header-stats">567 <div class="stat">568 <div class="stat-val">6</div>569 <div class="stat-label">Reward Signals</div>570 </div>571 <div class="stat">572 <div class="stat-val">5</div>573 <div class="stat-label">Tasks</div>574 </div>575 <div class="stat">576 <div class="stat-val">4</div>577 <div class="stat-label">Actions</div>578 </div>579 <div class="stat">580 <div class="stat-val" id="episodeCount">0</div>581 <div class="stat-label">Episodes Run</div>582 </div>583 </div>584 </div>585 </header>586 587 <div class="main-grid">588 589 <!-- LEFT: EPISODE CONTROL -->590 <div class="card" style="grid-column: 1 / -1;">591 <div class="card-title">// Episode Control</div>592 593 <div class="ep-controls">594 <select id="taskSelect">595 <option value="T1_single_order_dispatch">T1 — Single Order (Easy)</option>596 <option value="T2_multi_order_coordination">T2 — Multi Order (Medium)</option>597 <option value="T3_peak_hour_rush">T3 — Peak Hour Rush (Hard)</option>598 <option value="T4_chaos_dispatch">T4 — Chaos Dispatch (Medium) ⚡</option>599 <option value="T5_enterprise_lunch_rush">T5 — Enterprise Lunch Rush (Hard) ⚡⚡</option>600 </select>601 <button class="btn" id="resetBtn" onclick="resetEpisode()">▶ New Episode</button>602 <button class="btn secondary" id="autoBtn" onclick="toggleAuto()">⚡ Auto-Run</button>603 <button class="btn secondary" onclick="window.open('/docs','_blank')">API Docs →</button>604 </div>605 606 <div class="status-bar" id="statusBar">607 <div class="status-item"><span class="status-key">step</span><span class="status-val" id="stepVal">—</span></div>608 <div class="status-item"><span class="status-key">score</span><span class="status-val" id="scoreVal">—</span></div>609 <div class="status-item"><span class="status-key">traffic</span><span class="status-val" id="trafficVal">—</span></div>610 <div class="status-item"><span class="status-key">task</span><span class="status-val ok" id="taskVal">—</span></div>611 <div class="status-item"><span class="status-key">session</span><span class="status-val" id="sessionVal" style="font-size:9px;">—</span></div>612 </div>613 614 <div class="orders-grid" id="ordersGrid">615 <div style="color: var(--muted); font-family: var(--mono); font-size: 12px; padding: 20px 0; text-align: center;">616 → Click "New Episode" to start617 </div>618 </div>619 620 <div class="step-area">621 <button class="btn green-btn" id="stepBtn" onclick="takeStep()" disabled>STEP →</button>622 <button class="btn secondary" id="summonAllBtn" onclick="summonAll()" disabled>Summon All</button>623 <span style="font-family: var(--mono); font-size: 11px; color: var(--muted)" id="actionHint">select actions above, then step</span>624 <div class="score-display" id="bigScore">—</div>625 </div>626 627 <div class="done-banner" id="doneBanner"></div>628 </div>629 630 <!-- EPISODE LOG -->631 <div class="card green">632 <div class="card-title">// Episode Log</div>633 <div class="log" id="episodeLog">634 <div class="log-line"><span class="log-time">—</span><span class="log-msg system">Waiting for episode start...</span></div>635 </div>636 </div>637 638 <!-- REWARD CHART -->639 <div class="card yellow">640 <div class="card-title">// Reward This Episode</div>641 <canvas id="rewardCanvas"></canvas>642 <div style="display:flex; gap: 20px; margin-top: 12px; font-family: var(--mono); font-size: 10px; color: var(--muted);">643 <span>● <span style="color:var(--accent2)">step reward</span></span>644 <span>● <span style="color:var(--green)">cumulative</span></span>645 </div>646 </div>647 648 <!-- REWARD BREAKDOWN -->649 <div class="card">650 <div class="card-title">// Reward Signals</div>651 <div class="reward-list">652 <div class="reward-row pos"><span>Delivery base</span><span class="reward-val pos">+50</span></div>653 <div class="reward-row pos"><span>Sync bonus (±2min)</span><span class="reward-val pos">+10</span></div>654 <div class="reward-row pos"><span>Rush completion</span><span class="reward-val pos">+15</span></div>655 <div class="reward-row pos"><span>Chaos recovery</span><span class="reward-val pos">+5</span></div>656 <div class="reward-row neg"><span>Temperature (per °C below 72°)</span><span class="reward-val neg">−1</span></div>657 <div class="reward-row neg"><span>Driver wait >15min</span><span class="reward-val neg">−20</span></div>658 </div>659 </div>660 661 <!-- TASKS TABLE -->662 <div class="card">663 <div class="card-title">// Task Registry</div>664 <table class="tasks-table">665 <thead>666 <tr><th>ID</th><th>Difficulty</th><th>Orders</th><th>Chaos</th></tr>667 </thead>668 <tbody>669 <tr><td>T1</td><td><span class="diff-badge diff-easy">Easy</span></td><td>1</td><td>—</td></tr>670 <tr><td>T2</td><td><span class="diff-badge diff-medium">Medium</span></td><td>3</td><td>—</td></tr>671 <tr><td>T3</td><td><span class="diff-badge diff-hard">Hard</span></td><td>5</td><td>—</td></tr>672 <tr><td>T4</td><td><span class="diff-badge diff-medium">Medium</span></td><td>1</td><td>1 ⚡</td></tr>673 <tr><td>T5</td><td><span class="diff-badge diff-hard">Hard</span></td><td>5</td><td>2 ⚡⚡</td></tr>674 </tbody>675 </table>676 </div>677 678 <!-- API REFERENCE -->679 <div class="card full">680 <div class="card-title">// API Endpoints</div>681 <div class="api-section">682 <div class="endpoint">683 <span class="method post">POST</span>684 <span class="endpoint-path">/reset</span>685 <span class="endpoint-desc">Start a new episode. Body: {"task_id": "T1_single_order_dispatch"}</span>686 </div>687 <div class="endpoint">688 <span class="method post">POST</span>689 <span class="endpoint-path">/step</span>690 <span class="endpoint-desc">Advance 1 minute. Body: {"action": {"dispatch_decisions": {"ORD001": 1}}, "episode_id": "..."}</span>691 </div>692 <div class="endpoint">693 <span class="method get">GET</span>694 <span class="endpoint-path">/state</span>695 <span class="endpoint-desc">Current session state (step count, episode ID)</span>696 </div>697 <div class="endpoint">698 <span class="method get">GET</span>699 <span class="endpoint-path">/tasks</span>700 <span class="endpoint-desc">List all available tasks with configs</span>701 </div>702 <div class="endpoint">703 <span class="method get">GET</span>704 <span class="endpoint-path">/schema</span>705 <span class="endpoint-desc">Action and observation JSON schemas</span>706 </div>707 <div class="endpoint">708 <span class="method get">GET</span>709 <span class="endpoint-path">/health</span>710 <span class="endpoint-desc">Liveness check</span>711 </div>712 <div class="endpoint">713 <span class="method get">GET</span>714 <span class="endpoint-path">/docs</span>715 <span class="endpoint-desc">Full Swagger / OpenAPI documentation</span>716 </div>717 </div>718 </div>719 720 </div>721 722 <footer>723 <span>KitchenFlow-v2 · OpenEnv Hackathon 2026 · Mystic Coders</span>724 <span>725 <a href="/docs">API Docs</a> ·726 <a href="https://huggingface.co/spaces/manzz05/kitchenflow-v2" target="_blank">HF Space</a> ·727 <a href="https://colab.research.google.com/drive/1K5dbjojC5ZtTw-PzGf6wJbPIyUcAE2p4?usp=sharing" target="_blank">Training Notebook</a>728 </span>729 </footer>730</div>731 732<script>733 // State734 let episodeId = null;735 let currentObs = null;736 let selectedActions = {};737 let totalScore = 0;738 let stepRewards = [];739 let cumulativeRewards = [];740 let episodeCount = parseInt(localStorage.getItem('epCount') || '0');741 let autoRunning = false;742 let autoTimer = null;743 let isDone = false;744 745 document.getElementById('episodeCount').textContent = episodeCount;746 747 // Canvas chart748 const canvas = document.getElementById('rewardCanvas');749 const ctx = canvas.getContext('2d');750 751 function resizeCanvas() {752 canvas.width = canvas.offsetWidth * window.devicePixelRatio;753 canvas.height = canvas.offsetHeight * window.devicePixelRatio;754 ctx.scale(window.devicePixelRatio, window.devicePixelRatio);755 drawChart();756 }757 758 window.addEventListener('resize', resizeCanvas);759 setTimeout(resizeCanvas, 100);760 761 function drawChart() {762 const W = canvas.offsetWidth;763 const H = canvas.offsetHeight;764 ctx.clearRect(0, 0, W, H);765 766 if (stepRewards.length === 0) {767 ctx.fillStyle = '#333';768 ctx.font = '11px Space Mono, monospace';769 ctx.textAlign = 'center';770 ctx.fillText('no data yet', W/2, H/2);771 return;772 }773 774 const pad = { top: 10, right: 10, bottom: 24, left: 40 };775 const cW = W - pad.left - pad.right;776 const cH = H - pad.top - pad.bottom;777 778 const allVals = [...stepRewards, ...cumulativeRewards];779 const minV = Math.min(0, ...allVals);780 const maxV = Math.max(0, ...allVals);781 const range = maxV - minV || 1;782 783 const toX = (i) => pad.left + (i / (stepRewards.length - 1 || 1)) * cW;784 const toY = (v) => pad.top + cH - ((v - minV) / range) * cH;785 786 // Zero line787 ctx.strokeStyle = '#2a2a2a';788 ctx.lineWidth = 1;789 ctx.beginPath();790 ctx.moveTo(pad.left, toY(0));791 ctx.lineTo(pad.left + cW, toY(0));792 ctx.stroke();793 794 // Grid lines795 ctx.strokeStyle = '#1a1a1a';796 for (let i = 0; i <= 4; i++) {797 const y = pad.top + (i / 4) * cH;798 ctx.beginPath();799 ctx.moveTo(pad.left, y);800 ctx.lineTo(pad.left + cW, y);801 ctx.stroke();802 }803 804 // Step rewards (bars)805 const barW = Math.max(2, cW / stepRewards.length - 2);806 stepRewards.forEach((v, i) => {807 const x = toX(i);808 const y0 = toY(0);809 const y1 = toY(v);810 ctx.fillStyle = v >= 0 ? 'rgba(255,184,0,0.5)' : 'rgba(255,23,68,0.5)';811 ctx.fillRect(x - barW/2, Math.min(y0, y1), barW, Math.abs(y1 - y0));812 });813 814 // Cumulative line815 ctx.strokeStyle = '#00e676';816 ctx.lineWidth = 2;817 ctx.beginPath();818 cumulativeRewards.forEach((v, i) => {819 const x = toX(i);820 const y = toY(v);821 i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);822 });823 ctx.stroke();824 825 // Axis labels826 ctx.fillStyle = '#555';827 ctx.font = '9px Space Mono, monospace';828 ctx.textAlign = 'right';829 ctx.fillText(maxV.toFixed(0), pad.left - 4, pad.top + 4);830 ctx.fillText(minV.toFixed(0), pad.left - 4, pad.top + cH);831 ctx.textAlign = 'center';832 ctx.fillText('steps →', pad.left + cW/2, H - 4);833 }834 835 // Log836 function log(msg, type = '') {837 const logEl = document.getElementById('episodeLog');838 const step = currentObs ? (currentObs.time_min || currentObs.step || '?') : '—';839 const line = document.createElement('div');840 line.className = 'log-line';841 line.innerHTML = `<span class="log-time">${String(step).padStart(3,'0')}</span><span class="log-msg ${type}">${msg}</span>`;842 logEl.appendChild(line);843 logEl.scrollTop = logEl.scrollHeight;844 }845 846 function clearLog() {847 document.getElementById('episodeLog').innerHTML = '';848 }849 850 // Reset851 async function resetEpisode() {852 stopAuto();853 isDone = false;854 const taskId = document.getElementById('taskSelect').value;855 document.getElementById('resetBtn').innerHTML = '<span class="spinner"></span>Starting...';856 document.getElementById('resetBtn').disabled = true;857 858 try {859 const res = await fetch('/reset', {860 method: 'POST',861 headers: {'Content-Type': 'application/json'},862 body: JSON.stringify({task_id: taskId})863 });864 const data = await res.json();865 866 episodeId = data.episode_id;867 currentObs = data;868 selectedActions = {};869 totalScore = 0;870 stepRewards = [];871 cumulativeRewards = [];872 873 episodeCount++;874 localStorage.setItem('epCount', episodeCount);875 document.getElementById('episodeCount').textContent = episodeCount;876 877 clearLog();878 log(`Episode started — task: ${taskId}`, 'info');879 log(`Session: ${episodeId?.slice(0,8)}...`, 'system');880 881 updateStatus(data);882 renderOrders(data);883 drawChart();884 885 document.getElementById('stepBtn').disabled = false;886 document.getElementById('summonAllBtn').disabled = false;887 document.getElementById('doneBanner').className = 'done-banner';888 document.getElementById('bigScore').textContent = '0';889 document.getElementById('bigScore').className = 'score-display';890 891 } catch(e) {892 log(`Error: ${e.message}`, 'penalty');893 }894 895 document.getElementById('resetBtn').innerHTML = '▶ New Episode';896 document.getElementById('resetBtn').disabled = false;897 }898 899 // Step900 async function takeStep() {901 if (!episodeId || isDone) return;902 903 const dispatchDecisions = {};904 const orders = currentObs?.orders || [];905 906 orders.forEach(o => {907 if (!o.delivered && !o.failed) {908 dispatchDecisions[o.order_id] = selectedActions[o.order_id] ?? 0;909 }910 });911 912 // Also handle v2 format913 if (Object.keys(dispatchDecisions).length === 0 && currentObs) {914 const actionVal = selectedActions['default'] ?? 0;915 dispatchDecisions['default'] = actionVal;916 }917 918 document.getElementById('stepBtn').disabled = true;919 920 try {921 const res = await fetch('/step', {922 method: 'POST',923 headers: {'Content-Type': 'application/json'},924 body: JSON.stringify({925 action: {dispatch_decisions: dispatchDecisions},926 episode_id: episodeId927 })928 });929 const data = await res.json();930 931 currentObs = data;932 const reward = data.reward || 0;933 totalScore += reward;934 935 stepRewards.push(reward);936 cumulativeRewards.push(totalScore);937 938 // Log events939 if (data.events && data.events.length > 0) {940 data.events.forEach(ev => {941 const type = ev.includes('deliver') ? 'reward' :942 ev.includes('cancel') || ev.includes('cold') ? 'penalty' : '';943 log(ev, type);944 });945 } else {946 const rewardStr = reward >= 0 ? `+${reward.toFixed(1)}` : reward.toFixed(1);947 log(`step reward: ${rewardStr}`, reward >= 0 ? 'reward' : 'penalty');948 }949 950 if (data.chaos && data.chaos.length > 0) {951 data.chaos.forEach(c => log(`⚡ CHAOS: ${c}`, 'chaos'));952 }953 954 if (data.coordinator && data.coordinator.length > 0) {955 data.coordinator.forEach(c => log(`📡 coordinator: ${c}`, 'info'));956 }957 958 updateStatus(data);959 renderOrders(data);960 drawChart();961 962 // Update score display963 const bigScore = document.getElementById('bigScore');964 bigScore.textContent = totalScore.toFixed(1);965 bigScore.className = 'score-display ' + (totalScore > 0 ? 'positive' : totalScore < 0 ? 'negative' : '');966 967 // Reset action selections968 selectedActions = {};969 970 if (data.done) {971 isDone = true;972 stopAuto();973 const banner = document.getElementById('doneBanner');974 const delivered = (data.orders || []).filter(o => o.delivered).length;975 const total = (data.orders || []).length;976 if (delivered > 0) {977 banner.className = 'done-banner show';978 banner.textContent = `✓ EPISODE COMPLETE — ${delivered}/${total} orders delivered — Final score: ${totalScore.toFixed(1)}`;979 } else {980 banner.className = 'done-banner show failed-banner';981 banner.textContent = `✗ EPISODE FAILED — Score: ${totalScore.toFixed(1)}`;982 }983 document.getElementById('stepBtn').disabled = true;984 log(`Episode done. Final score: ${totalScore.toFixed(1)}`, 'info');985 return;986 }987 988 } catch(e) {989 log(`Step error: ${e.message}`, 'penalty');990 }991 992 document.getElementById('stepBtn').disabled = false;993 }994 995 function updateStatus(data) {996 document.getElementById('stepVal').textContent = data.time_min || data.step || '—';997 document.getElementById('scoreVal').textContent = totalScore.toFixed(1);998 const traffic = data.traffic_index || (data.orders?.[0]?.traffic_index) || '—';999 document.getElementById('trafficVal').textContent = typeof traffic === 'number' ? traffic.toFixed(2) : traffic;1000 document.getElementById('taskVal').textContent = data.task_id || document.getElementById('taskSelect').value.split('_')[0] || '—';1001 document.getElementById('sessionVal').textContent = (episodeId || '—').slice(0,12) + '...';1002 }1003 1004 function renderOrders(data) {1005 const grid = document.getElementById('ordersGrid');1006 const orders = data.orders || [];1007 1008 if (orders.length === 0) {1009 grid.innerHTML = '<div style="color: var(--muted); font-family: var(--mono); font-size: 12px; padding: 20px 0; text-align: center;">No orders in this episode</div>';1010 return;1011 }1012 1013 grid.innerHTML = orders.map(o => {1014 const status = o.delivered ? 'delivered' : o.failed ? 'failed' : 'active';1015 const prep = (o.food_prep_progress || 0) * 100;1016 const temp = o.food_temp_c || 72;1017 const tempClass = temp >= 70 ? 'hot' : temp >= 60 ? '' : 'cool';1018 const selected = selectedActions[o.order_id];1019 1020 const actions = [1021 {v: 0, label: '⏸ Wait'},1022 {v: 1, label: '🚗 Summon'},1023 {v: 2, label: '⚡ Priority'},1024 {v: 3, label: '📡 Requery GPS'},1025 ];1026 1027 const actionBtns = o.delivered || o.failed ? '' : actions.map(a =>1028 `<button class="action-btn ${selected === a.v ? 'selected' : ''}"1029 onclick="selectAction('${o.order_id}', ${a.v})">${a.label}</button>`1030 ).join('');1031 1032 const staleTag = o.data_is_stale ? '<span class="chaos-tag">STALE GPS</span>' : '';1033 const summonedTag = o.driver_summoned ? '<span style="color:var(--green);font-size:10px;font-family:var(--mono)"> ✓ summoned</span>' : '';1034 const arrivedTag = o.driver_arrived ? '<span style="color:var(--green);font-size:10px;font-family:var(--mono)"> ✓ arrived</span>' : '';1035 1036 return `1037 <div class="order-card ${status}">1038 <div class="order-id">${o.order_id}</div>1039 <div class="order-info">1040 <div class="order-name">${o.item_name || 'Order'}${staleTag}${summonedTag}${arrivedTag}</div>1041 <div class="progress-bar"><div class="progress-fill ${tempClass}" style="width:${prep}%"></div></div>1042 <div class="order-meta">1043 <span>prep: <span>${prep.toFixed(0)}%</span></span>1044 <span>temp: <span>${temp.toFixed(1)}°C</span></span>1045 <span>dist: <span>${(o.driver_dist_km||0).toFixed(1)}km</span></span>1046 <span>traffic: <span>${(o.traffic_index||1).toFixed(2)}</span></span>1047 <span>coord: <span>${o.coordinator_signal || '—'}</span></span>1048 ${o.driver_wait_minutes ? `<span>wait: <span>${o.driver_wait_minutes}min</span></span>` : ''}1049 </div>1050 </div>1051 <div class="order-actions">${actionBtns}</div>1052 </div>1053 `;1054 }).join('');1055 }1056 1057 function selectAction(orderId, val) {1058 selectedActions[orderId] = val;1059 renderOrders(currentObs);1060 }1061 1062 function summonAll() {1063 if (!currentObs) return;1064 (currentObs.orders || []).forEach(o => {1065 if (!o.delivered && !o.failed && !o.driver_summoned) {1066 selectedActions[o.order_id] = 1;1067 }1068 });1069 renderOrders(currentObs);1070 }1071 1072 function toggleAuto() {1073 if (autoRunning) {1074 stopAuto();1075 } else {1076 startAuto();1077 }1078 }1079 1080 function startAuto() {1081 if (!episodeId || isDone) return;1082 autoRunning = true;1083 document.getElementById('autoBtn').textContent = '⏹ Stop Auto';1084 document.getElementById('autoBtn').style.borderColor = 'var(--accent)';1085 document.getElementById('autoBtn').style.color = 'var(--accent)';1086 // Auto: summon when prep > 70%1087 autoTimer = setInterval(async () => {1088 if (!currentObs || isDone) { stopAuto(); return; }1089 (currentObs.orders || []).forEach(o => {1090 if (!o.delivered && !o.failed) {1091 const prep = o.food_prep_progress || 0;1092 if (prep > 0.7 && !o.driver_summoned) {1093 selectedActions[o.order_id] = 1;1094 } else if (o.data_is_stale) {1095 selectedActions[o.order_id] = 3;1096 } else {1097 selectedActions[o.order_id] = 0;1098 }1099 }1100 });1101 await takeStep();1102 }, 600);1103 }1104 1105 function stopAuto() {1106 autoRunning = false;1107 clearInterval(autoTimer);1108 document.getElementById('autoBtn').textContent = '⚡ Auto-Run';1109 document.getElementById('autoBtn').style.borderColor = '';1110 document.getElementById('autoBtn').style.color = '';1111 }1112</script>1113</body>1114</html>1115 