Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>SupplyMind V2</title> | |
| <style> | |
| :root { --bg:#edf3fb; --ink:#172033; --muted:#64748b; --panel:#fff; --line:#d8e0ec; --nav:#10263d; --accent:#2563eb; --cyan:#0891b2; --good:#047857; --bad:#b42318; } | |
| * { box-sizing: border-box; } | |
| body { margin:0; font-family:Inter, Segoe UI, Arial, sans-serif; background:var(--bg); color:var(--ink); } | |
| header { background:var(--nav); color:white; padding:14px 18px; display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap:wrap; } | |
| h1 { font-size:21px; margin:0; } | |
| h2 { font-size:15px; margin:0 0 10px; } | |
| main { display:grid; grid-template-columns:minmax(0,1.25fr) minmax(380px,.75fr); gap:14px; padding:14px; } | |
| section,.card { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:12px; } | |
| .stack { display:grid; gap:12px; } | |
| .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:10px; } | |
| .controls { display:flex; gap:8px; flex-wrap:wrap; align-items:center; } | |
| button,select,input { height:34px; border-radius:6px; font:inherit; } | |
| select,input { border:1px solid #cbd5e1; padding:0 8px; } | |
| button { border:0; padding:0 12px; color:white; background:var(--accent); cursor:pointer; } | |
| button.secondary { background:var(--cyan); } | |
| button.ghost { background:white; color:var(--ink); border:1px solid var(--line); } | |
| textarea { width:100%; min-height:260px; border:1px solid var(--line); border-radius:8px; padding:10px; font:12px Consolas, monospace; } | |
| pre { white-space:pre-wrap; background:#0f172a; color:#e2e8f0; border-radius:8px; padding:10px; max-height:320px; overflow:auto; font-size:12px; } | |
| .muted { color:var(--muted); } | |
| .metric { border:1px solid var(--line); border-radius:8px; padding:10px; } | |
| .metrics { display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:8px; } | |
| .metric strong { display:block; font-size:18px; } | |
| .badge { border:1px solid #bfdbfe; color:#1d4ed8; background:#eff6ff; border-radius:999px; padding:2px 7px; font-size:12px; } | |
| .bars { display:grid; gap:6px; margin-top:8px; } | |
| .barrow { display:grid; grid-template-columns:104px 1fr 30px; gap:8px; align-items:center; font-size:12px; } | |
| .bar { height:8px; border-radius:999px; background:#e2e8f0; overflow:hidden; } | |
| .bar span { display:block; height:100%; background:linear-gradient(90deg,var(--accent),var(--cyan)); } | |
| .timeline { display:grid; gap:8px; max-height:420px; overflow:auto; } | |
| .timeline-item { border:1px solid var(--line); border-radius:8px; padding:10px; background:white; } | |
| .timeline-item h3 { margin:0 0 6px; display:flex; justify-content:space-between; font-size:13px; } | |
| .chips { display:flex; flex-wrap:wrap; gap:5px; margin-top:6px; } | |
| .chip { border:1px solid var(--line); border-radius:999px; padding:2px 7px; color:var(--muted); font-size:12px; } | |
| .pos { color:var(--good); } .neg { color:var(--bad); } | |
| @media (max-width:900px){ main{ grid-template-columns:1fr; } } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>SupplyMind V2 Multi-Agent</h1> | |
| <div class="controls"> | |
| <select id="task"> | |
| <option value="train_easy">train_easy</option> | |
| <option value="train_medium">train_medium</option> | |
| <option value="train_hard">train_hard</option> | |
| <option value="easy">easy</option> | |
| <option value="medium" selected>medium</option> | |
| <option value="hard">hard</option> | |
| </select> | |
| <input id="seed" type="number" placeholder="optional seed" /> | |
| <button id="reset">Reset</button> | |
| <button id="heuristic" class="secondary">Load Joint Heuristic</button> | |
| <button id="step">Step</button> | |
| <button id="run" class="secondary">Run Episode</button> | |
| <a href="/" style="color:white">V1 UI</a> | |
| </div> | |
| </header> | |
| <main> | |
| <div class="stack"> | |
| <section><h2>Rewards</h2><div id="metrics" class="metrics"></div></section> | |
| <section><h2>Center</h2><div id="center" class="grid"></div></section> | |
| <section><h2>Warehouses</h2><div id="warehouses" class="grid"></div></section> | |
| <section><h2>Episode Timeline</h2><div id="timeline" class="timeline"></div></section> | |
| </div> | |
| <aside class="stack"> | |
| <section><h2>Joint Action JSON</h2><textarea id="action">{"warehouse_actions":{},"central_action":{}}</textarea></section> | |
| <section><h2>Reward Components</h2><pre id="components">{}</pre></section> | |
| <section><h2>Events</h2><pre id="events">[]</pre></section> | |
| </aside> | |
| </main> | |
| <script> | |
| let state = null, lastInfo = {}, episode = [], running = false; | |
| const $ = id => document.getElementById(id); | |
| async function reset() { | |
| const seed = $("seed").value.trim(); | |
| const url = `/v2/reset?task_id=${$("task").value}${seed ? `&seed=${seed}` : ""}`; | |
| const res = await fetch(url, { method:"POST" }); | |
| state = await res.json(); | |
| lastInfo = {}; | |
| episode = [{ round:state.round_index, reward:0, cumulative:0, action:{}, events:["episode reset"], components:{} }]; | |
| await loadHeuristic(); | |
| render(); | |
| } | |
| async function loadHeuristic() { | |
| const res = await fetch("/v2/heuristic-joint-action"); | |
| $("action").value = JSON.stringify(await res.json(), null, 2); | |
| } | |
| async function step() { | |
| let payload; | |
| try { payload = JSON.parse($("action").value); } catch { alert("Invalid JSON"); return true; } | |
| const before = state.round_index; | |
| const res = await fetch("/v2/step", { method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(payload) }); | |
| const data = await res.json(); | |
| state = data.observation; | |
| lastInfo = data.info || {}; | |
| episode.push({ round:before, reward:data.reward.step_reward, cumulative:data.reward.cumulative_reward, action:payload, events:state.feedback.events || [], components:lastInfo.reward_components || data.reward.components || {}, done:data.done, summary:lastInfo.episode_summary }); | |
| render(); | |
| if (!data.done) await loadHeuristic(); | |
| return data.done; | |
| } | |
| async function runEpisode() { | |
| if (running) return; running = true; | |
| let done = false, guard = 0; | |
| while (!done && guard < 50) { done = await step(); guard += 1; await new Promise(r => setTimeout(r, 120)); } | |
| running = false; | |
| } | |
| function render() { | |
| if (!state) return; | |
| const rewards = latestAgentRewards(); | |
| $("metrics").innerHTML = ` | |
| <div class="metric"><span class="muted">Round</span><strong>${state.round_index}</strong></div> | |
| <div class="metric"><span class="muted">Global</span><strong>${Number(state.feedback.cumulative_reward||0).toFixed(2)}</strong></div> | |
| <div class="metric"><span class="muted">Center</span><strong>${Number(rewards.center||0).toFixed(2)}</strong></div> | |
| <div class="metric"><span class="muted">Avg Warehouse</span><strong>${avgWarehouse(rewards).toFixed(2)}</strong></div>`; | |
| $("center").innerHTML = `<div class="card"><h2>Depot <span class="badge">${state.center.depot_trucks_available} truck(s)</span></h2>${bars(state.center.depot_inventory)}<p class="muted">Inbound: ${(state.center.inbound_procurements||[]).map(x=>`${x.units} ${x.sku}@${x.arrival_round}`).join(", ") || "none"}</p></div>`; | |
| $("warehouses").innerHTML = Object.values(state.warehouses).map(w => `<div class="card"><h2>${w.label} <span class="badge">${w.region}</span></h2>${bars(w.inventory)}<p class="muted">drivers ${w.drivers_available}; orders ${w.local_orders.length}; proposals ${w.pending_transfer_proposals.length}</p><p class="muted">reward ${Number(rewards[w.warehouse_id]||0).toFixed(2)}</p></div>`).join(""); | |
| $("components").textContent = JSON.stringify(episode[episode.length - 1]?.components || {}, null, 2); | |
| $("events").textContent = JSON.stringify(state.feedback.events || [], null, 2); | |
| $("timeline").innerHTML = episode.slice().reverse().map(item => `<div class="timeline-item"><h3>Round ${item.round}<span class="${item.reward>=0?'pos':'neg'}">${Number(item.reward).toFixed(2)}</span></h3><div class="muted">cumulative ${Number(item.cumulative).toFixed(2)}${item.done ? " - done" : ""}</div><div class="chips">${chips(item.action)}</div><ul>${(item.events||[]).slice(0,5).map(e=>`<li class="muted">${e}</li>`).join("")}</ul>${item.summary ? `<pre>${JSON.stringify(item.summary,null,2)}</pre>` : ""}</div>`).join(""); | |
| } | |
| function bars(inv){ return `<div class="bars">${Object.entries(inv||{}).map(([k,v])=>`<div class="barrow"><span>${k}</span><div class="bar"><span style="width:${Math.min(100,v*10)}%"></span></div><span>${v}</span></div>`).join("")}</div>`; } | |
| function avgWarehouse(rewards){ const vals = Object.entries(rewards||{}).filter(([k])=>k!=="center").map(([,v])=>Number(v)); return vals.length ? vals.reduce((a,b)=>a+b,0)/vals.length : 0; } | |
| function latestAgentRewards(){ return lastInfo.agent_rewards || {}; } | |
| function chips(action){ const c=action?.central_action||{}, w=action?.warehouse_actions||{}; return [`wh ${Object.keys(w).length}`,`buy ${(c.central_procurements||[]).length}`,`liq ${(c.central_liquidations||[]).length}`,`ship ${(c.central_replenishments||[]).length}`,`prop ${(c.inventory_transfer_proposals||[]).length}`,`match ${(c.offer_matches||[]).length}`].map(x=>`<span class="chip">${x}</span>`).join(""); } | |
| $("reset").onclick = reset; $("heuristic").onclick = loadHeuristic; $("step").onclick = step; $("run").onclick = runEpisode; reset(); | |
| </script> | |
| </body> | |
| </html> | |