CoolFace
Apppublic

manzz05/kitchenflow-v2

sourceHugging Facemitupdated 5mo agoView on Hugging Face
1likes
index.html1115 linesDownload Raw Back to root
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