Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>SupplyMind</title> | |
| <style> | |
| :root { | |
| color-scheme: light; | |
| --ink: #172033; | |
| --muted: #64748b; | |
| --line: #d8e0ec; | |
| --panel: #ffffff; | |
| --panel-alt: #f8fbff; | |
| --bg: #edf3fb; | |
| --nav: #10263d; | |
| --accent: #2563eb; | |
| --accent-2: #0891b2; | |
| --accent-soft: #dbeafe; | |
| --warn: #b45309; | |
| --warn-soft: #fff7ed; | |
| --bad: #b42318; | |
| --good: #047857; | |
| } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; font-family: Inter, Segoe UI, Arial, sans-serif; background: var(--bg); color: var(--ink); } | |
| header { padding: 16px 22px; background: var(--nav); color: white; display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid #1e3a5f; } | |
| h1 { font-size: 22px; margin: 0; letter-spacing: 0; } | |
| main { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(380px, 0.65fr); gap: 16px; padding: 16px; } | |
| section, .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 14px; box-shadow: 0 1px 2px rgba(16, 38, 61, 0.04); } | |
| h2 { margin: 0 0 10px; font-size: 15px; } | |
| .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } | |
| select, input, button, textarea { font: inherit; } | |
| select, input { height: 34px; border: 1px solid #cbd5e1; border-radius: 6px; padding: 0 8px; background: white; color: var(--ink); } | |
| button { height: 34px; border: 0; border-radius: 6px; padding: 0 12px; background: var(--accent); color: white; cursor: pointer; } | |
| button.secondary { background: var(--accent-2); } | |
| button.ghost { background: white; color: var(--ink); border: 1px solid var(--line); } | |
| button:disabled { opacity: 0.55; cursor: wait; } | |
| .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; } | |
| .card { border: 1px solid var(--line); border-radius: 8px; padding: 12px; background: var(--panel-alt); } | |
| .card h3 { margin: 0 0 8px; font-size: 14px; display: flex; justify-content: space-between; gap: 8px; } | |
| .muted { color: var(--muted); } | |
| .badge { font-size: 12px; border: 1px solid #bfdbfe; padding: 2px 6px; border-radius: 999px; color: #1d4ed8; background: #eff6ff; } | |
| .bars { display: grid; gap: 6px; } | |
| .barrow { display: grid; grid-template-columns: 96px 1fr 28px; 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(--accent-2)); } | |
| .orders { display: grid; gap: 8px; } | |
| .order { display: grid; grid-template-columns: 1fr auto; gap: 8px; border-bottom: 1px solid var(--line); padding-bottom: 8px; font-size: 13px; } | |
| .report-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; } | |
| .chip { display: inline-flex; align-items: center; min-height: 22px; border-radius: 999px; padding: 2px 7px; border: 1px solid var(--line); font-size: 12px; color: var(--muted); background: white; } | |
| .chip.hot { border-color: #fedf89; background: var(--warn-soft); color: var(--warn); } | |
| .chip.good { border-color: #a6f4c5; background: #ecfdf3; color: var(--good); } | |
| textarea { width: 100%; min-height: 210px; border: 1px solid var(--line); border-radius: 8px; padding: 10px; font-family: Consolas, monospace; font-size: 12px; resize: vertical; } | |
| pre { white-space: pre-wrap; background: #0f172a; color: #e2e8f0; padding: 10px; border-radius: 8px; max-height: 260px; overflow: auto; font-size: 12px; } | |
| .stack { display: grid; gap: 12px; } | |
| .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; } | |
| .metric { border: 1px solid var(--line); border-radius: 8px; padding: 10px; } | |
| .metric strong { display: block; font-size: 18px; } | |
| .timeline { display: grid; gap: 8px; max-height: 360px; overflow: auto; } | |
| .timeline-item { border: 1px solid var(--line); border-radius: 8px; padding: 10px; background: #fff; } | |
| .timeline-item h3 { margin: 0 0 6px; font-size: 13px; display: flex; justify-content: space-between; gap: 8px; } | |
| .timeline-item ul { margin: 6px 0 0; padding-left: 18px; } | |
| .timeline-item li { margin: 3px 0; font-size: 12px; color: var(--muted); } | |
| .decision { background: var(--accent-soft); border-color: #93c5fd; } | |
| .component-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 5px; margin-top: 8px; } | |
| .component { display: flex; justify-content: space-between; gap: 8px; border-radius: 6px; padding: 4px 6px; background: #f8fafc; font-size: 12px; } | |
| .component.positive { color: var(--good); } | |
| .component.negative { color: var(--bad); } | |
| @media (max-width: 900px) { main { grid-template-columns: 1fr; } } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>SupplyMind</h1> | |
| <div class="controls"> | |
| <select id="task"> | |
| <option value="cooperative_restock">cooperative_restock</option> | |
| <option value="scarcity_negotiation" selected>scarcity_negotiation</option> | |
| <option value="crisis_coalition">crisis_coalition</option> | |
| </select> | |
| <input id="seed" type="number" value="12345" /> | |
| <button id="reset">Reset</button> | |
| <button id="heuristic" class="secondary">Load Heuristic</button> | |
| <button id="subagent" class="secondary">Load Subagent</button> | |
| <button id="step">Step</button> | |
| <button id="run" class="secondary">Run Heuristic</button> | |
| <button id="run-subagent" class="secondary">Run Subagent</button> | |
| <button id="load-trace" class="ghost">Load Black-box Trace</button> | |
| </div> | |
| </header> | |
| <main> | |
| <div class="stack"> | |
| <section> | |
| <h2>Central Depot</h2> | |
| <div id="depot" class="grid"></div> | |
| </section> | |
| <section> | |
| <h2>Warehouses</h2> | |
| <div id="warehouses" class="grid"></div> | |
| </section> | |
| <section> | |
| <h2>Warehouse Demand Reports</h2> | |
| <div id="reports" class="orders"></div> | |
| </section> | |
| <section> | |
| <h2>Episode Timeline</h2> | |
| <div id="timeline" class="timeline"></div> | |
| </section> | |
| <section> | |
| <h2>Market Signals</h2> | |
| <pre id="signals">[]</pre> | |
| </section> | |
| <section> | |
| <h2>Negotiation Trace</h2> | |
| <pre id="trace">[]</pre> | |
| </section> | |
| </div> | |
| <aside class="stack"> | |
| <section> | |
| <h2>Round & Reward</h2> | |
| <div id="metrics" class="metrics"></div> | |
| </section> | |
| <section> | |
| <h2>Action JSON</h2> | |
| <textarea id="action">{"central_procurements":[],"central_replenishments":[],"inventory_transfers":[],"offer_matches":[],"priority_policy":[],"defer_orders":[],"coalition_deals":[]}</textarea> | |
| </section> | |
| <section> | |
| <h2>Subagent Prompt</h2> | |
| <pre id="prompt">Prompt not loaded yet.</pre> | |
| </section> | |
| <section> | |
| <h2>Events</h2> | |
| <pre id="events">[]</pre> | |
| </section> | |
| <section> | |
| <h2>Reward Components</h2> | |
| <pre id="components">{}</pre> | |
| </section> | |
| </aside> | |
| </main> | |
| <script> | |
| let state = null; | |
| let episode = []; | |
| let running = false; | |
| const $ = (id) => document.getElementById(id); | |
| async function reset() { | |
| const task = $("task").value; | |
| const seed = $("seed").value; | |
| const res = await fetch(`/reset?task_id=${encodeURIComponent(task)}&seed=${encodeURIComponent(seed)}`, { method: "POST" }); | |
| state = await res.json(); | |
| episode = [{ | |
| round: state.round_index, | |
| reward: 0, | |
| cumulative: state.feedback.cumulative_reward, | |
| decision: emptyAction(), | |
| events: ["episode reset"], | |
| reports: state.demand_reports.length | |
| }]; | |
| $("action").value = JSON.stringify(emptyAction(), null, 2); | |
| render(); | |
| } | |
| async function loadHeuristic() { | |
| const res = await fetch("/heuristic-action"); | |
| $("action").value = JSON.stringify(await res.json(), null, 2); | |
| } | |
| async function loadSubagent() { | |
| const [actionRes, promptRes] = await Promise.all([ | |
| fetch("/subagent-action"), | |
| fetch("/subagent-prompt") | |
| ]); | |
| $("action").value = JSON.stringify(await actionRes.json(), null, 2); | |
| const prompt = await promptRes.json(); | |
| $("prompt").textContent = prompt.system_prompt; | |
| } | |
| async function loadBlackboxTrace() { | |
| const res = await fetch("/blackbox-trace"); | |
| if (!res.ok) { | |
| alert("No black-box trace found yet."); | |
| return; | |
| } | |
| const trace = await res.json(); | |
| episode = (trace.rounds || []).map(round => ({ | |
| round: round.round_index, | |
| reward: round.reward, | |
| cumulative: round.cumulative_reward, | |
| decision: round.action, | |
| events: [ | |
| ...(round.reasoning ? [`reasoning: ${round.reasoning}`] : []), | |
| ...(round.events || []) | |
| ], | |
| components: round.components, | |
| reports: round.observation_summary?.demand_reports?.length || 0, | |
| done: round.done | |
| })); | |
| $("prompt").textContent = `Black-box Codex subagent replay\nTask: ${trace.task}\nSeed: ${trace.seed}\nTotal reward: ${trace.final_episode_summary?.total_reward ?? "unknown"}\nGraded score: ${trace.final_episode_summary?.graded_score ?? "not returned"}`; | |
| render(); | |
| } | |
| function emptyAction() { | |
| return { | |
| central_procurements: [], | |
| central_replenishments: [], | |
| inventory_transfers: [], | |
| offer_matches: [], | |
| priority_policy: [], | |
| defer_orders: [], | |
| coalition_deals: [] | |
| }; | |
| } | |
| async function step() { | |
| let payload; | |
| try { payload = JSON.parse($("action").value); } | |
| catch (err) { alert("Invalid JSON action"); return; } | |
| const beforeRound = state ? state.round_index : 0; | |
| const res = await fetch("/step", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const data = await res.json(); | |
| state = data.observation; | |
| episode.push({ | |
| round: beforeRound, | |
| reward: data.reward.step_reward, | |
| cumulative: data.reward.cumulative_reward, | |
| decision: payload, | |
| events: state.feedback.recent_events, | |
| components: data.reward.components, | |
| reports: state.demand_reports.length, | |
| done: data.done | |
| }); | |
| render(data); | |
| await loadHeuristic(); | |
| return data.done; | |
| } | |
| async function runEpisode(mode = "heuristic") { | |
| if (running) return; | |
| running = true; | |
| setBusy(true); | |
| try { | |
| if (mode === "subagent") await loadSubagent(); | |
| else await loadHeuristic(); | |
| let done = false; | |
| let guard = 0; | |
| const maxSteps = Math.max(5, state?.scenario_info?.total_rounds || 40); | |
| while (!done && guard < maxSteps) { | |
| done = await step(); | |
| guard += 1; | |
| await new Promise(resolve => setTimeout(resolve, 160)); | |
| if (!done) { | |
| if (mode === "subagent") await loadSubagent(); | |
| else await loadHeuristic(); | |
| } | |
| } | |
| } finally { | |
| running = false; | |
| setBusy(false); | |
| } | |
| } | |
| function setBusy(value) { | |
| $("reset").disabled = value; | |
| $("heuristic").disabled = value; | |
| $("subagent").disabled = value; | |
| $("step").disabled = value; | |
| $("run").disabled = value; | |
| $("run-subagent").disabled = value; | |
| $("load-trace").disabled = value; | |
| } | |
| function render(lastStep) { | |
| if (!state) return; | |
| $("depot").innerHTML = ` | |
| <div class="card"> | |
| <h3>Central Depot<span class="badge">${state.central_depot.trucks_available} truck(s)</span></h3> | |
| <div class="muted">${state.central_depot.message}</div> | |
| <div class="bars" style="margin-top:10px"> | |
| ${Object.entries(state.central_depot.inventory).map(([sku, units]) => ` | |
| <div class="barrow"><span>${sku}</span><div class="bar"><span style="width:${Math.min(100, units * 10)}%"></span></div><span>${units}</span></div> | |
| `).join("")} | |
| </div> | |
| <p class="muted">Returning trucks [${state.central_depot.trucks_returning.join(", ")}], ship cap ${state.central_depot.replenishment_cap}</p> | |
| <p class="muted">Buy cap ${state.central_depot.procurement_cap}, lead ${state.central_depot.procurement_lead_time} round(s)</p> | |
| <p class="muted">${procurementSummary(state.central_depot.inbound_procurements)}</p> | |
| </div>`; | |
| $("warehouses").innerHTML = state.warehouses.map(w => ` | |
| <div class="card"> | |
| <h3>${w.label}<span class="badge">${w.region}</span></h3> | |
| <div class="muted">${w.public_message}</div> | |
| <div class="bars" style="margin-top:10px"> | |
| ${Object.entries(w.inventory).map(([sku, units]) => ` | |
| <div class="barrow"><span>${sku}</span><div class="bar"><span style="width:${Math.min(100, units * 10)}%"></span></div><span>${units}</span></div> | |
| `).join("")} | |
| </div> | |
| <p class="muted">Drivers: ${w.drivers_available} available, returning [${w.drivers_returning.join(", ")}]</p> | |
| <p class="muted">${routeSummary(w.route_costs)}</p> | |
| <p class="muted">Local utility ${state.feedback.agent_rewards[w.warehouse_id] ?? 0}</p> | |
| </div>`).join(""); | |
| $("reports").innerHTML = state.demand_reports.length ? state.demand_reports.map(r => ` | |
| <div class="order"> | |
| <div> | |
| <strong>${r.warehouse_id}</strong> ${r.sku} in ${r.region}<br> | |
| <span class="muted">${r.message}</span> | |
| <div class="report-meta"> | |
| <span class="chip ${r.urgency >= 3 ? "hot" : ""}">urgency ${r.urgency}</span> | |
| <span class="chip">requested ${r.requested_units}</span> | |
| <span class="chip">forecast ${r.forecast_units}</span> | |
| <span class="chip ${r.missed_units_last_round > 0 ? "hot" : ""}">missed ${r.missed_units_last_round}</span> | |
| <span class="chip ${r.fulfilled_units_last_round > 0 ? "good" : ""}">fulfilled ${r.fulfilled_units_last_round}</span> | |
| </div> | |
| </div> | |
| <div>${r.at_risk_units} at risk</div> | |
| </div>`).join("") : "<p class='muted'>No demand reports.</p>"; | |
| $("metrics").innerHTML = ` | |
| <div class="metric"><span class="muted">Round</span><strong>${state.round_index}</strong></div> | |
| <div class="metric"><span class="muted">Remaining</span><strong>${state.remaining_rounds}</strong></div> | |
| <div class="metric"><span class="muted">Last reward</span><strong>${state.feedback.last_step_reward}</strong></div> | |
| <div class="metric"><span class="muted">Cumulative</span><strong>${state.feedback.cumulative_reward}</strong></div>`; | |
| $("events").textContent = JSON.stringify(state.feedback.recent_events, null, 2); | |
| $("signals").textContent = JSON.stringify(state.market_signals, null, 2); | |
| $("trace").textContent = JSON.stringify(state.feedback.negotiation_trace, null, 2); | |
| $("components").textContent = JSON.stringify(state.feedback.reward_components, null, 2); | |
| $("timeline").innerHTML = episode.slice().reverse().map(item => ` | |
| <div class="timeline-item ${hasDecision(item.decision) ? "decision" : ""}"> | |
| <h3>Round ${item.round}<span>${Number(item.reward).toFixed(2)}</span></h3> | |
| <div class="muted">cumulative ${Number(item.cumulative).toFixed(2)} · next reports ${item.reports}${item.done ? " · done" : ""}</div> | |
| <div class="report-meta">${decisionChips(item.decision)}</div> | |
| <ul>${(item.events || []).slice(0, 5).map(event => `<li>${event}</li>`).join("")}</ul> | |
| <div class="component-grid">${componentBreakdown(item.components)}</div> | |
| </div> | |
| `).join(""); | |
| } | |
| function hasDecision(action) { | |
| return Object.values(action || {}).some(value => Array.isArray(value) && value.length > 0); | |
| } | |
| function decisionChips(action) { | |
| const current = action || emptyAction(); | |
| const chips = [ | |
| ["buy", current.central_procurements?.length || 0], | |
| ["depot", current.central_replenishments?.length || 0], | |
| ["transfers", current.inventory_transfers?.length || 0], | |
| ["matches", current.offer_matches?.length || 0], | |
| ["defers", current.defer_orders?.length || 0] | |
| ]; | |
| return chips.map(([label, count]) => `<span class="chip ${count ? "good" : ""}">${label} ${count}</span>`).join(""); | |
| } | |
| function componentBreakdown(components) { | |
| const entries = Object.entries(components || {}).sort((a, b) => Math.abs(b[1]) - Math.abs(a[1])).slice(0, 6); | |
| if (!entries.length) return ""; | |
| return entries.map(([key, value]) => ` | |
| <div class="component ${value >= 0 ? "positive" : "negative"}"> | |
| <span>${key}</span><strong>${Number(value).toFixed(2)}</strong> | |
| </div> | |
| `).join(""); | |
| } | |
| function routeSummary(costs) { | |
| const entries = Object.entries(costs || {}); | |
| if (!entries.length) return "Route costs unavailable"; | |
| const sorted = entries.sort((a, b) => a[1] - b[1]); | |
| const near = sorted[0]; | |
| const far = sorted[sorted.length - 1]; | |
| return `Routes: nearest ${near[0]} ${Number(near[1]).toFixed(1)}, farthest ${far[0]} ${Number(far[1]).toFixed(1)}`; | |
| } | |
| function procurementSummary(inbound) { | |
| if (!inbound || !inbound.length) return "No inbound depot purchases."; | |
| return `Inbound purchases: ${inbound.map(item => `${item.units} ${item.sku} @ round ${item.arrival_round}`).join(", ")}`; | |
| } | |
| $("reset").onclick = reset; | |
| $("heuristic").onclick = loadHeuristic; | |
| $("subagent").onclick = loadSubagent; | |
| $("step").onclick = step; | |
| $("run").onclick = () => runEpisode("heuristic"); | |
| $("run-subagent").onclick = () => runEpisode("subagent"); | |
| $("load-trace").onclick = loadBlackboxTrace; | |
| reset().then(loadSubagent); | |
| </script> | |
| </body> | |
| </html> | |