roboreplan / viz_standalone.html
jshah13's picture
Upload viz_standalone.html with huggingface_hub
8105c34 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tabletop Planning Env</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #0f0f13;
color: #e0e0e0;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
gap: 20px;
}
h1 { color: #7eb8f7; font-size: 1.2rem; letter-spacing: 0.05em; }
.layout { display: flex; gap: 24px; align-items: flex-start; width: 100%; max-width: 1280px; }
.canvas-wrap {
background: #1a1a24; border: 1px solid #333; border-radius: 8px;
padding: 16px; flex-shrink: 0;
}
canvas { display: block; border-radius: 4px; }
.panel {
background: #1a1a24; border: 1px solid #333; border-radius: 8px;
padding: 16px; flex: 1; min-width: 260px;
display: flex; flex-direction: column; gap: 14px;
}
.panel h2 { font-size: 0.7rem; color: #777; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 2px; }
.instruction { color: #f5ce72; font-size: 0.92rem; line-height: 1.5; }
.obj-list { display: flex; flex-direction: column; gap: 5px; }
.obj-row { display: flex; gap: 8px; align-items: center; font-size: 0.8rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.tag { background: #2a2a36; border-radius: 4px; padding: 1px 6px; font-size: 0.68rem; color: #aaa; }
.tag.blocked { background: #3a1a1a; color: #e06060; }
.tag.hidden { background: #2a2a1a; color: #b8a060; }
.tag.held { background: #1a3a1a; color: #60e090; }
.tag.fragile { background: #2a1a3a; color: #c080f0; }
.tag.heavy { background: #1a2a3a; color: #6090d0; }
.reasoning-box { background: #131320; border-left: 5px solid #5a8fd5; border-radius: 4px; padding: 10px 12px; font-size: 0.82rem; color: #a8ccee; line-height: 1.6; font-style: italic; margin-bottom: 8px; white-space: pre-wrap; word-break: break-word; }
.action-box { background: #0e2a0e; border-left: 5px solid #3a9a3a; border-radius: 4px; padding: 8px 12px; font-size: 0.92rem; color: #80e880; font-family: monospace; font-weight: bold; letter-spacing: 0.08em; }
.log { display: flex; flex-direction: column; gap: 3px; max-height: 180px; overflow-y: auto; }
.log-row { font-size: 0.73rem; display: flex; gap: 8px; }
.log-row .sn { color: #555; width: 26px; flex-shrink: 0; }
.log-row .act { color: #7eb8f7; flex: 1; }
.log-row .res.ok { color: #60e090; }
.log-row .res.fail { color: #e06060; }
.log-row .rew { color: #f0c36d; }
.stats { display: flex; gap: 16px; flex-wrap: wrap; }
.stat .label { font-size: 0.65rem; color: #888; text-transform: uppercase; letter-spacing: 0.06em; }
.stat .value { font-size: 1.4rem; color: #d4eaff; font-weight: bold; }
.controls { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 4px; }
.diff-controls { display: flex; gap: 6px; margin-top: 8px; }
.diff-btn.active { background: #2a5a8a; border-color: #4a8ad0; color: #cfe6ff; }
button {
background: #252535; border: 1px solid #444; color: #ccc;
padding: 5px 12px; border-radius: 6px; cursor: pointer;
font-size: 0.75rem; font-family: inherit; transition: background 0.12s;
}
button:hover { background: #33334a; }
button.primary { background: #2a3a5a; border-color: #4a6a9a; color: #7eb8f7; }
button.danger { background: #3a1a1a; border-color: #6a2a2a; color: #e06060; }
.list-items .item { font-size: 0.75rem; line-height: 1.6; }
.item.sg { color: #60e090; } .item.sg::before { content: "✓ "; }
.item.fai { color: #e06060; } .item.fai::before { content: "✗ "; }
.item.con { color: #f0c36d; } .item.con::before { content: "⚠ "; }
.none { font-size: 0.72rem; color: #444; }
.holding-banner {
background: #1a3a1a; border: 1px solid #2a6a2a; border-radius: 6px;
padding: 5px 12px; font-size: 0.8rem; color: #60e090;
}
.done-banner {
background: #1a2a3a; border: 1px solid #2a5a8a; border-radius: 6px;
padding: 7px 14px; font-size: 0.88rem; color: #60c0ff; text-align: center;
}
.mid-task-banner {
background: #2a1505; border: 2px solid #f07828; border-radius: 6px;
padding: 10px 16px; font-size: 0.92rem; color: #ffb060; text-align: center;
font-weight: bold; letter-spacing: 0.03em;
animation: pulse-border 1.2s ease-in-out infinite;
}
@keyframes pulse-border {
0%,100% { border-color: #f07828; box-shadow: none; }
50% { border-color: #ffd080; box-shadow: 0 0 18px #f0782888; }
}
.rbar-bg { background: #222; border-radius: 4px; height: 10px; overflow: hidden; margin-top: 6px; }
.rbar { height: 100%; border-radius: 4px; transition: width 0.3s, background 0.3s; }
select { background:#252535; color:#ccc; border:1px solid #444; border-radius:4px; padding:4px; font-family:inherit; }
.manual-grid { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 4px; }
</style>
</head>
<body>
<h1>RoboReplan — Tabletop Planning &amp; Replanning Environment</h1>
<div class="layout">
<div style="display:flex;flex-direction:column;gap:10px;">
<div class="canvas-wrap">
<div style="font-size:0.7rem;color:#888;margin-bottom:6px;letter-spacing:0.05em;">TOP-DOWN VIEW &nbsp;&nbsp; <span style="color:#f0c36d"></span> gripper &nbsp;&nbsp; <span style="color:#e06060">×</span> blocked</div>
<canvas id="canvas" width="400" height="340"></canvas>
</div>
<div class="canvas-wrap">
<div style="font-size:0.7rem;color:#888;margin-bottom:6px;letter-spacing:0.05em;">SIDE VIEW &nbsp;&nbsp; stacking &amp; blocking</div>
<canvas id="canvas-side" width="400" height="140"></canvas>
</div>
</div>
<div class="panel">
<div>
<h2>Instruction</h2>
<div class="instruction" id="instruction"></div>
</div>
<div id="mid-task-sec" style="display:none">
<div class="mid-task-banner" id="mid-task-txt">⚡ Mid-task instruction change!</div>
</div>
<div>
<h2>Scene Objects</h2>
<div style="font-size:0.65rem;color:#666;margin-bottom:4px;"><span class="tag blocked">blocked</span> = something in the way &nbsp; <span class="tag hidden">hidden</span> = not yet scanned</div>
<div class="obj-list" id="obj-list"></div>
</div>
<div id="holding-sec" style="display:none">
<div class="holding-banner" id="holding-txt"></div>
</div>
<div>
<h2>Constraints</h2>
<div class="list-items" id="constraints"></div>
</div>
<div>
<h2>Completed Subgoals</h2>
<div class="list-items" id="subgoals"></div>
</div>
<div>
<h2>Known Failures</h2>
<div class="list-items" id="failures"></div>
</div>
<div>
<h2>Stats</h2>
<div class="stats">
<div class="stat"><div class="label">Steps left</div><div class="value" id="steps"></div></div>
<div class="stat"><div class="label">Reward</div><div class="value" id="reward">0.00</div></div>
<div class="stat"><div class="label">Episode</div><div class="value" id="episode">1</div></div>
<div class="stat"><div class="label">Success</div><div class="value" id="successes">0</div></div>
</div>
<div class="rbar-bg"><div class="rbar" id="rbar" style="width:50%;background:#4a8a4a"></div></div>
</div>
<div>
<h2>Model Diagnostics</h2>
<div class="list-items" id="diag"></div>
</div>
<div>
<h2>Model Reasoning</h2>
<div class="list-items" id="raw-model"><div class="none">none</div></div>
</div>
<div id="done-sec" style="display:none">
<div class="done-banner" id="done-txt"></div>
</div>
<div>
<h2>Action Log</h2>
<div class="log" id="log"></div>
</div>
<div>
<h2>Controls</h2>
<div class="controls">
<button class="primary" onclick="resetEp()" title="Randomizes a new scenario each time">Reset 🎲</button>
<button onclick="togglePlay()" id="play-btn">▶ Run Agent</button>
<button onclick="modelStep()" id="model-btn">🤖 Model Step</button>
<button onclick="oracleStep()" id="oracle-btn" style="background:#1a2a3a;border-color:#3a6a9a;color:#7ec8f7">🎯 Oracle Step</button>
<button onclick="toggleOraclePlay()" id="oracle-play-btn" style="background:#1a2a3a;border-color:#3a6a9a;color:#7ec8f7">🎯 Run Oracle</button>
<button class="danger" onclick="stopAll()">■ Stop</button>
<select id="speed">
<option value="700">Slow</option>
<option value="350" selected>Normal</option>
<option value="120">Fast</option>
<option value="20">Max</option>
</select>
</div>
<div class="diff-controls">
<button id="diff-easy" class="diff-btn" onclick="setDifficulty('easy')">Easy</button>
<button id="diff-medium" class="diff-btn" onclick="setDifficulty('medium')">Medium</button>
<button id="diff-hard" class="diff-btn" onclick="setDifficulty('hard')">Hard</button>
</div>
<div style="display:flex;gap:6px;margin-top:8px;align-items:center;">
<span style="font-size:0.65rem;color:#666;text-transform:uppercase;letter-spacing:.08em;">Scene</span>
<select id="pack-select" onchange="setScenarioPack(this.value)" style="flex:1">
<option value="default">🟥 Colored blocks (default)</option>
<option value="pharmacy">💊 Pharmacy (PS 3.1)</option>
<option value="warehouse">📦 Warehouse (PS 3.1)</option>
<option value="lab">🔬 Lab (PS 3.1)</option>
</select>
</div>
<h2 style="margin-top:10px">Manual Actions</h2>
<div style="font-size:0.65rem;color:#666;margin-bottom:6px;">Tip: MOVE_TO_&lt;color&gt; first, then PICK. Disabled buttons = invalid for current state.</div>
<div class="manual-grid" id="manual-grid"></div>
</div>
</div>
</div>
<script>
// ─────────────────────────────────────────────────────────────────────
// Environment (mirrors Python logic exactly)
// ─────────────────────────────────────────────────────────────────────
const INSTRUCTIONS = [
"Place the red block in bin A.",
"Place the red block in bin A. Fragile items first.",
"Place the red block in bin A, then the green block in bin B.",
"Place the yellow block in bin B.",
"Place the purple block in bin A, then the red block in bin B.",
];
const MAX_STEPS = 20;
const URL_PARAMS = new URLSearchParams(window.location.search);
let ACTIVE_DIFFICULTY = (URL_PARAMS.get('difficulty') || 'medium').toLowerCase();
let ACTIVE_PACK = (URL_PARAMS.get('pack') || 'default').toLowerCase();
function makeObject(name, x, y, reachable, blocking) {
return { name, x, y, reachable, blocking: blocking||null, inBin: null, isHeld: false };
}
class Env {
constructor() { this.reset(); }
reset() {
this.navMode = (ACTIVE_DIFFICULTY === 'hard');
const blocked = Math.random() < 0.5;
this.objects = {
red_block: makeObject('red_block', 0.10, 0.00, !blocked, null),
blue_block: makeObject('blue_block', 0.00, 0.00, true, blocked ? 'red_block' : null),
green_block: makeObject('green_block',-0.10, 0.00, true, null),
yellow_block: makeObject('yellow_block', 0.20, 0.06, true, null),
purple_block: makeObject('purple_block', -0.20, 0.06, true, null),
};
this.gripperX = 0.00;
this.gripperY = 0.25;
this.gripperFacing = 'N';
this.holding = null;
this.steps = 0;
this.done = false;
this.actionHistory = [];
this.completedSubgoals = [];
this.knownFailures = [];
this.activeConstraints = [];
this.instruction = INSTRUCTIONS[Math.floor(Math.random() * INSTRUCTIONS.length)];
if (this.instruction.includes('Fragile')) this.activeConstraints.push('fragile_first');
this.requiredPlacements = {};
if (this.instruction.includes('red block in bin A')) this.requiredPlacements['red_block'] = 'A';
if (this.instruction.includes('red block in bin B')) this.requiredPlacements['red_block'] = 'B';
if (this.instruction.includes('green block in bin B')) this.requiredPlacements['green_block'] = 'B';
if (this.instruction.includes('yellow block in bin B')) this.requiredPlacements['yellow_block'] = 'B';
if (this.instruction.includes('purple block in bin A')) this.requiredPlacements['purple_block'] = 'A';
return this._obs(null, null);
}
step(action) {
if (this.done) throw new Error('Episode done. Call reset().');
const result = this._execute(action);
this.actionHistory.push(action);
this.steps++;
const reward = this._reward(action, result);
this._updateState(action, result);
const done = this.done || this.steps >= MAX_STEPS;
if (this.steps >= MAX_STEPS) this.done = true;
return { observation: this._obs(action, result), reward, done, info: { result } };
}
_execute(action) {
const o = this.objects;
if (action === 'SCAN_SCENE') return 'SUCCESS';
if (action === 'MOVE_NORTH') { this.gripperY = Math.min(0.35, this.gripperY + 0.1); return 'SUCCESS'; }
if (action === 'MOVE_SOUTH') { this.gripperY = Math.max(-0.25, this.gripperY - 0.1); return 'SUCCESS'; }
if (action === 'MOVE_EAST') { this.gripperX = Math.min(0.35, this.gripperX + 0.1); return 'SUCCESS'; }
if (action === 'MOVE_WEST') { this.gripperX = Math.max(-0.35, this.gripperX - 0.1); return 'SUCCESS'; }
if (action === 'ROTATE_LEFT') {
const dirs = ['N','W','S','E'];
this.gripperFacing = dirs[(dirs.indexOf(this.gripperFacing) + 1) % 4];
return 'SUCCESS';
}
if (action === 'ROTATE_RIGHT') {
const dirs = ['N','E','S','W'];
this.gripperFacing = dirs[(dirs.indexOf(this.gripperFacing) + 1) % 4];
return 'SUCCESS';
}
if (['MOVE_TO_RED','MOVE_TO_BLUE','MOVE_TO_GREEN','MOVE_TO_YELLOW','MOVE_TO_PURPLE'].includes(action)) {
if (this.navMode) return 'FAILED_INVALID';
const map = {
MOVE_TO_RED:'red_block',
MOVE_TO_BLUE:'blue_block',
MOVE_TO_GREEN:'green_block',
MOVE_TO_YELLOW:'yellow_block',
MOVE_TO_PURPLE:'purple_block'
};
const name = map[action];
if (!o[name]) return 'FAILED_INVALID';
if (!o[name].reachable) return 'FAILED_BLOCKED';
this.gripperX = o[name].x;
this.gripperY = o[name].y + 0.05;
return 'SUCCESS';
}
if (action === 'PICK') {
if (this.holding) return 'FAILED_INVALID';
for (const obj of Object.values(o)) {
if (obj.reachable && !obj.isHeld && !obj.inBin) {
const dist = Math.hypot(this.gripperX - obj.x, this.gripperY - obj.y);
if (dist < 0.15) {
obj.isHeld = true;
this.holding = obj.name;
return 'SUCCESS';
}
}
}
return 'FAILED_EMPTY';
}
if (action === 'PLACE_BIN_A' || action === 'PLACE_BIN_B') {
if (!this.holding) return 'FAILED_EMPTY';
const bin = action === 'PLACE_BIN_A' ? 'A' : 'B';
const obj = o[this.holding];
obj.inBin = bin; obj.isHeld = false; obj.reachable = false;
this.holding = null;
return 'SUCCESS';
}
if (action === 'CLEAR_BLOCKER') {
for (const obj of Object.values(o)) {
if (obj.blocking && obj.reachable) {
const target = obj.blocking;
obj.blocking = null;
obj.x += 0.28;
if (o[target]) o[target].reachable = true;
return 'SUCCESS';
}
}
return 'FAILED_INVALID';
}
return 'FAILED_INVALID';
}
_reward(action, result) {
let r = -0.05;
if (result !== 'SUCCESS') {
const key = action + ':' + result;
r += this.knownFailures.includes(key) ? -2.0 : -1.0;
return r;
}
if (action === 'CLEAR_BLOCKER') r += 2.0;
if (action === 'PICK') r += 2.0;
if (action === 'PLACE_BIN_A' || action === 'PLACE_BIN_B') {
// only reward correct placement — wrong bin gets a penalty instead
const placedObj = Object.values(this.objects).find(o => o.inBin !== null && o.isHeld === false && result === 'SUCCESS');
// check via requiredPlacements: was this the right bin?
const bin = action === 'PLACE_BIN_A' ? 'A' : 'B';
// find what we just placed (holding was cleared in _execute, check inBin)
let correct = false;
for (const [name, reqBin] of Object.entries(this.requiredPlacements)) {
if (this.objects[name]?.inBin === bin) { correct = (reqBin === bin); break; }
}
r += correct ? 2.0 : -3.0; // correct bin +2, wrong bin -3
}
if (this.knownFailures.length && result === 'SUCCESS' && action !== 'SCAN_SCENE') {
if (!this.completedSubgoals.includes('recovery')) r += 1.0;
}
if (this._allGoalsDone()) { r += 10.0; this.done = true; }
return r;
}
_updateState(action, result) {
if (result !== 'SUCCESS') {
const key = action + ':' + result;
if (!this.knownFailures.includes(key)) this.knownFailures.push(key);
} else {
if (action === 'CLEAR_BLOCKER' && !this.completedSubgoals.includes('cleared_blocker'))
this.completedSubgoals.push('cleared_blocker');
if (action === 'PICK' && !this.completedSubgoals.includes('recovery') && this.knownFailures.length)
this.completedSubgoals.push('recovery');
}
for (const [name, bin] of Object.entries(this.requiredPlacements)) {
const key = `placed_${name}_in_bin_${bin}`;
if (!this.completedSubgoals.includes(key) && this.objects[name]?.inBin === bin)
this.completedSubgoals.push(key);
}
}
_allGoalsDone() {
for (const [name, bin] of Object.entries(this.requiredPlacements))
if (this.objects[name]?.inBin !== bin) return false;
return true;
}
_validActions() {
const actions = ['SCAN_SCENE'];
const o = this.objects;
if (this.navMode) actions.push('MOVE_NORTH','MOVE_SOUTH','MOVE_EAST','MOVE_WEST','ROTATE_LEFT','ROTATE_RIGHT');
if (!this.holding) {
if (!this.navMode) {
const moveMap = [
['MOVE_TO_RED', 'red_block'],
['MOVE_TO_BLUE', 'blue_block'],
['MOVE_TO_GREEN', 'green_block'],
['MOVE_TO_YELLOW', 'yellow_block'],
['MOVE_TO_PURPLE', 'purple_block'],
];
for (const [action, name] of moveMap) {
const obj = o[name];
if (obj && obj.reachable && !obj.inBin) actions.push(action);
}
}
let canPick = false;
for (const obj of Object.values(o)) {
if (!obj.reachable || obj.isHeld || obj.inBin) continue;
const dist = Math.hypot(this.gripperX - obj.x, this.gripperY - obj.y);
if (dist < 0.15) { canPick = true; break; }
}
if (canPick) actions.push('PICK');
} else {
actions.push('PLACE_BIN_A', 'PLACE_BIN_B');
}
for (const obj of Object.values(o)) {
if (obj.blocking && obj.reachable) {
actions.push('CLEAR_BLOCKER');
break;
}
}
return actions;
}
_obs(lastAction, lastResult) {
const totalGoals = Object.keys(this.requiredPlacements).length;
const completedGoals = Object.entries(this.requiredPlacements)
.filter(([name, bin]) => this.objects[name]?.inBin === bin).length;
const goalProgress = totalGoals ? (completedGoals / totalGoals) : 1.0;
return {
instruction: this.instruction,
stepsRemaining: MAX_STEPS - this.steps,
visibleObjects: Object.values(this.objects).map(o => ({
name: o.name, reachable: o.reachable, blocking: o.blocking, inBin: o.inBin
})),
holding: this.holding,
completedSubgoals: [...this.completedSubgoals],
knownFailures: [...this.knownFailures],
activeConstraints: [...this.activeConstraints],
actionHistory: [...this.actionHistory],
validActions: this._validActions(),
navMode: this.navMode,
gripperFacing: this.gripperFacing,
goalProgress,
goalsRemaining: Math.max(0, totalGoals - completedGoals),
lastAction, lastResult,
// expose raw sim state for canvas
_objects: this.objects,
_gripperX: this.gripperX,
_gripperY: this.gripperY,
};
}
}
// ─────────────────────────────────────────────────────────────────────
// Scripted agent
// ─────────────────────────────────────────────────────────────────────
function scriptedAgent(obs) {
const valid = new Set(obs.validActions || []);
const pickFirst = ['PICK','CLEAR_BLOCKER','PLACE_BIN_A','PLACE_BIN_B',
'MOVE_TO_RED','MOVE_TO_BLUE','MOVE_TO_GREEN','MOVE_TO_YELLOW','MOVE_TO_PURPLE',
'MOVE_NORTH','MOVE_SOUTH','MOVE_EAST','MOVE_WEST','ROTATE_LEFT','ROTATE_RIGHT','SCAN_SCENE'];
// In backend-driven mode, always stay within valid actions.
if (obs.validActions && obs.validActions.length) {
for (const a of pickFirst) {
if (valid.has(a)) return a;
}
return obs.validActions[0];
}
const failures = new Set(obs.knownFailures);
const reachable = {};
obs.visibleObjects.forEach(o => reachable[o.name] = o.reachable);
const completed = new Set(obs.completedSubgoals);
if (obs.lastAction?.startsWith('MOVE_TO') && obs.lastResult === 'SUCCESS') return 'PICK';
if (obs.holding === 'red_block') return 'PLACE_BIN_A';
if (obs.holding === 'green_block') return 'PLACE_BIN_B';
if (failures.has('MOVE_TO_RED:FAILED_BLOCKED') || failures.has('PICK:FAILED_EMPTY')) return 'CLEAR_BLOCKER';
if (!completed.has('placed_red_block_in_bin_A')) {
if (reachable['red_block']) return 'MOVE_TO_RED';
return 'CLEAR_BLOCKER';
}
if (!completed.has('placed_green_block_in_bin_B') && reachable['green_block']) return 'MOVE_TO_GREEN';
return 'SCAN_SCENE';
}
// ─────────────────────────────────────────────────────────────────────
// Canvas rendering
// ─────────────────────────────────────────────────────────────────────
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const COLORS = {
red_block: '#e05555',
blue_block: '#5599e0',
green_block: '#55c055',
yellow_block:'#d2c343',
purple_block:'#9a68d8',
gripper: '#f0c36d',
// pharmacy pack
morphine_vial: '#e05580',
saline_bag: '#55b8e0',
insulin_pen: '#55e090',
blood_sample: '#c03030',
contrast_agent: '#b068d8',
// warehouse pack
fragile_package:'#e0a055',
heavy_pallet: '#7070a0',
urgent_parcel: '#e05555',
standard_box: '#8080a0',
hazmat_drum: '#d2c343',
// lab pack
reagent_alpha: '#e05555',
catalyst_beta: '#5599e0',
sample_gamma: '#55c055',
solvent_delta: '#d2c343',
enzyme_epsilon: '#9a68d8',
};
const BIN_POS = { A: {x:0.18,y:-0.22}, B: {x:-0.18,y:-0.22} };
const BIN_COL = { A: '#9955ee', B: '#ee9955' };
function w2c(wx, wy) {
return [W/2 + wx * 580, H/2 - wy * 580];
}
function drawScene(obs, blockedByMap) {
blockedByMap = blockedByMap || {};
if (!obs) return;
ctx.clearRect(0,0,W,H);
// Table bg
ctx.fillStyle = '#13131e';
ctx.fillRect(0,0,W,H);
// Grid
ctx.strokeStyle = '#2a2a3a'; ctx.lineWidth = 1;
for (let i = -6; i <= 6; i++) {
let [x1,y1] = w2c(i*0.07,-0.42), [x2,y2] = w2c(i*0.07,0.42);
ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
let [x3,y3] = w2c(-0.42,i*0.07), [x4,y4] = w2c(0.42,i*0.07);
ctx.beginPath(); ctx.moveTo(x3,y3); ctx.lineTo(x4,y4); ctx.stroke();
}
// Bins
for (const [label, pos] of Object.entries(BIN_POS)) {
const [cx,cy] = w2c(pos.x, pos.y);
const col = BIN_COL[label];
ctx.strokeStyle = col; ctx.lineWidth = 2.5;
ctx.strokeRect(cx-32,cy-32,64,64);
ctx.fillStyle = col+'18'; ctx.fillRect(cx-32,cy-32,64,64);
ctx.fillStyle = col;
ctx.font = 'bold 13px monospace'; ctx.textAlign = 'center';
ctx.fillText('Bin '+label, cx, cy+5);
// show what's in it
const inBin = obs.visibleObjects.filter(o => o.inBin === label);
inBin.forEach((o,i) => {
const c2 = COLORS[o.name] || '#aaa';
ctx.fillStyle = c2+'bb';
ctx.fillRect(cx - 14 + i*12, cy-26, 12, 12);
});
}
// Helper: draw a 3-D bevel cube centered at (cx,cy)
function drawCube(cx, cy, col, held, blocked, label) {
const S = 18; // half-size of front face
const BX = 9; // bevel offset x (right face width)
const BY = 7; // bevel offset y (top face height)
// Lighten / darken helpers
function tint(hex, amt) {
const n = parseInt(hex.slice(1), 16);
const r = Math.min(255, Math.max(0, (n>>16) + amt));
const g = Math.min(255, Math.max(0, ((n>>8)&0xff) + amt));
const b = Math.min(255, Math.max(0, (n&0xff) + amt));
return '#' + [r,g,b].map(v=>v.toString(16).padStart(2,'0')).join('');
}
const alpha = blocked ? '55' : held ? 'ee' : 'cc';
const baseCol = col + alpha;
const topCol = tint(col, 55) + alpha;
const sideCol = tint(col, -50) + alpha;
// Drop shadow
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.beginPath();
ctx.ellipse(cx + BX/2, cy + S + BY/2, S + 4, 5, 0, 0, Math.PI*2);
ctx.fill();
// Right face (darkest)
ctx.beginPath();
ctx.moveTo(cx+S, cy-S+BY);
ctx.lineTo(cx+S+BX, cy-S);
ctx.lineTo(cx+S+BX, cy+S);
ctx.lineTo(cx+S, cy+S+BY);
ctx.closePath();
ctx.fillStyle = sideCol;
ctx.fill();
if (!blocked) { ctx.strokeStyle = tint(col,-70)+alpha; ctx.lineWidth=0.8; ctx.stroke(); }
// Top face (lightest)
ctx.beginPath();
ctx.moveTo(cx-S, cy-S+BY);
ctx.lineTo(cx-S+BX, cy-S);
ctx.lineTo(cx+S+BX, cy-S);
ctx.lineTo(cx+S, cy-S+BY);
ctx.closePath();
ctx.fillStyle = topCol;
ctx.fill();
if (!blocked) { ctx.strokeStyle = tint(col,20)+alpha; ctx.lineWidth=0.8; ctx.stroke(); }
// Front face
ctx.beginPath();
ctx.moveTo(cx-S, cy-S+BY);
ctx.lineTo(cx+S, cy-S+BY);
ctx.lineTo(cx+S, cy+S+BY);
ctx.lineTo(cx-S, cy+S+BY);
ctx.closePath();
ctx.fillStyle = baseCol;
ctx.fill();
// Front face border
ctx.strokeStyle = held ? '#ffffff' : blocked ? '#555' : tint(col, 40);
ctx.lineWidth = held ? 2.5 : 1.5;
ctx.stroke();
// Blocked overlay: × for physically blocked, ? for hidden/unscanned
if (blocked) {
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.fillRect(cx-S, cy-S+BY, S*2, S*2);
if (arguments[5]) {
// hidden — amber question mark
ctx.fillStyle = '#c8a040'; ctx.font = 'bold 16px monospace'; ctx.textAlign = 'center';
ctx.fillText('?', cx, cy + BY + 6);
} else {
// physically blocked — red X
ctx.strokeStyle = '#e06060cc'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(cx-10,cy-10+BY); ctx.lineTo(cx+10,cy+10+BY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx+10,cy-10+BY); ctx.lineTo(cx-10,cy+10+BY); ctx.stroke();
}
}
// Held glow
if (held) {
ctx.shadowColor = col; ctx.shadowBlur = 18;
ctx.strokeStyle = '#ffffff88'; ctx.lineWidth = 1;
ctx.strokeRect(cx-S-2, cy-S+BY-2, S*2+4, S*2+4);
ctx.shadowBlur = 0;
}
// Label on front face
ctx.fillStyle = held ? '#fff' : blocked ? '#999' : '#fff';
ctx.font = (held ? 'bold ' : '') + '9px monospace';
ctx.textAlign = 'center';
ctx.fillText(label, cx, cy + BY + 5);
if (held) {
ctx.fillStyle = '#90ffb0'; ctx.font = '7px monospace';
ctx.fillText('HELD', cx, cy + BY + 15);
}
}
// Sort: draw blockers AFTER their targets so blockers appear on top visually
const allObjs = Object.entries(obs._objects).filter(([,o]) => !o.inBin);
const blockerNames = new Set(allObjs.filter(([,o]) => o.blocking).map(([n]) => n));
const drawOrder = [...allObjs.filter(([n]) => !blockerNames.has(n)),
...allObjs.filter(([n]) => blockerNames.has(n))];
for (const [name, obj] of drawOrder) {
const [cx,cy] = w2c(obj.x, obj.y);
const col = COLORS[name] || '#aaa';
const held = obj.isHeld;
const blocked = !obj.reachable;
const shortLabel = name.replace('_block','').replace('_vial','').replace('_bag','').replace('_pen','').replace('_sample','').replace('_agent','').replace('_package','').replace('_pallet','').replace('_parcel','').replace('_box','').replace('_drum','').replace('_alpha','α').replace('_beta','β').replace('_gamma','γ').replace('_delta','δ').replace('_epsilon','ε').toUpperCase().slice(0, 6);
// hidden = unreachable but NOT because something is physically on top
const isHidden = blocked && !blockedByMap[name];
// Append trait icon to label if revealed
const trait = (blockedByMap && obs.discoveredTraits) ? obs.discoveredTraits[name] : null;
const traitSuffix = trait === 'fragile' ? ' 💜' : trait === 'heavy' ? ' 🔵' : '';
// For blockers: draw slightly above the blocked object (offset up to show "on top")
const yOff = obj.blocking ? -6 : 0;
drawCube(cx, cy + yOff, col, held, blocked, shortLabel + traitSuffix, isHidden);
}
// Gripper
const [gx,gy] = w2c(obs._gripperX, obs._gripperY);
ctx.shadowColor = COLORS.gripper; ctx.shadowBlur = 10;
ctx.strokeStyle = COLORS.gripper; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(gx,gy,13,0,Math.PI*2); ctx.stroke();
ctx.fillStyle = COLORS.gripper+'22'; ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = COLORS.gripper; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(gx-8,gy); ctx.lineTo(gx+8,gy); ctx.stroke();
ctx.beginPath(); ctx.moveTo(gx,gy-8); ctx.lineTo(gx,gy+8); ctx.stroke();
}
// ── Side view ─────────────────────────────────────────────────────────
const canvasSide = document.getElementById('canvas-side');
const ctxS = canvasSide.getContext('2d');
const SW = canvasSide.width, SH = canvasSide.height;
function drawSideView(obs) {
if (!obs) return;
ctxS.clearRect(0,0,SW,SH);
ctxS.fillStyle = '#13131e'; ctxS.fillRect(0,0,SW,SH);
// Table surface
const tableY = SH - 28;
ctxS.fillStyle = '#2a2a1a'; ctxS.fillRect(0, tableY, SW, 6);
ctxS.strokeStyle = '#554'; ctxS.lineWidth = 1.5;
ctxS.beginPath(); ctxS.moveTo(0, tableY); ctxS.lineTo(SW, tableY); ctxS.stroke();
// Group objects by x position to show stacking
const onTable = Object.entries(obs._objects).filter(([n,o]) => !o.inBin && !o.isHeld);
// Objects that are drawn as the base of a blocker stack — skip them in the main loop
const drawnAsBase = new Set(onTable.filter(([,o]) => o.blocking).map(([,o]) => o.blocking));
const BSIZE = 34;
const visibleItems = onTable.filter(([n]) => !drawnAsBase.has(n));
const spacing = Math.min(70, (SW - 40) / Math.max(visibleItems.length, 1));
const startX = SW/2 - (visibleItems.length-1) * spacing/2;
visibleItems.forEach(([name, obj], i) => {
const col = COLORS[name] || '#aaa';
const blocked = !obj.reachable;
const cx = startX + i * spacing;
// Check if this block is blocking something (draw blocker on top)
const isBlocker = obj.blocking;
const blockH = BSIZE;
let stackY = tableY - blockH;
if (isBlocker) {
// Draw the blocked block first (bottom)
const targetCol = COLORS[obj.blocking] || '#aaa';
ctxS.fillStyle = targetCol + '44';
ctxS.fillRect(cx - BSIZE/2, stackY, BSIZE, blockH);
ctxS.strokeStyle = '#444'; ctxS.lineWidth = 1;
ctxS.strokeRect(cx - BSIZE/2, stackY, BSIZE, blockH);
ctxS.fillStyle = '#888'; ctxS.font = '8px monospace'; ctxS.textAlign = 'center';
ctxS.fillText(obj.blocking.replace('_block','').toUpperCase(), cx, stackY + blockH/2 + 3);
// X mark
ctxS.strokeStyle = '#e06060'; ctxS.lineWidth = 1.5;
ctxS.beginPath(); ctxS.moveTo(cx-10, stackY+8); ctxS.lineTo(cx+10, stackY+blockH-8); ctxS.stroke();
ctxS.beginPath(); ctxS.moveTo(cx+10, stackY+8); ctxS.lineTo(cx-10, stackY+blockH-8); ctxS.stroke();
stackY -= blockH; // blocker goes on top
}
// Draw this block
ctxS.fillStyle = blocked ? col+'44' : col+'aa';
ctxS.fillRect(cx - BSIZE/2, stackY, BSIZE, blockH);
ctxS.strokeStyle = isBlocker ? '#f09040' : blocked ? '#444' : col;
ctxS.lineWidth = isBlocker ? 2.5 : 1.5;
ctxS.strokeRect(cx - BSIZE/2, stackY, BSIZE, blockH);
// Label
ctxS.fillStyle = '#fff'; ctxS.font = 'bold 9px monospace'; ctxS.textAlign = 'center';
ctxS.fillText(name.replace('_block','').toUpperCase(), cx, stackY + blockH/2 + 3);
if (isBlocker) {
ctxS.fillStyle = '#f09040'; ctxS.font = '7px monospace';
ctxS.fillText('ON TOP', cx, stackY - 4);
}
});
// Held block (floating above)
const held = Object.entries(obs._objects).find(([n,o]) => o.isHeld);
if (held) {
const [name] = held;
const col = COLORS[name] || '#aaa';
ctxS.fillStyle = col+'cc';
ctxS.fillRect(SW/2 - BSIZE/2, 8, BSIZE, BSIZE);
ctxS.strokeStyle = '#fff'; ctxS.lineWidth = 2;
ctxS.strokeRect(SW/2 - BSIZE/2, 8, BSIZE, BSIZE);
ctxS.fillStyle = '#fff'; ctxS.font = 'bold 9px monospace'; ctxS.textAlign = 'center';
ctxS.fillText(name.replace('_block','').toUpperCase(), SW/2, 8 + BSIZE/2 + 3);
ctxS.fillStyle = '#90ffb0'; ctxS.font = '7px monospace';
ctxS.fillText('HELD BY GRIPPER', SW/2, 8 + BSIZE + 10);
}
// Bin labels
ctxS.fillStyle = BIN_COL.A + 'aa'; ctxS.font = 'bold 10px monospace'; ctxS.textAlign = 'left';
ctxS.fillText('BIN A', 6, tableY + 18);
ctxS.fillStyle = BIN_COL.B + 'aa'; ctxS.textAlign = 'right';
ctxS.fillText('BIN B', SW - 6, tableY + 18);
}
// ─────────────────────────────────────────────────────────────────────
// UI
// ─────────────────────────────────────────────────────────────────────
const ACTIONS = [
'SCAN_SCENE',
'MOVE_TO_RED','MOVE_TO_BLUE','MOVE_TO_GREEN','MOVE_TO_YELLOW','MOVE_TO_PURPLE',
'MOVE_NORTH','MOVE_SOUTH','MOVE_EAST','MOVE_WEST','ROTATE_LEFT','ROTATE_RIGHT',
'PICK','PLACE_BIN_A','PLACE_BIN_B','CLEAR_BLOCKER'
];
let env = new Env(); // fallback only if backend unavailable
let currentObs = null;
let currentDone = false;
let totalReward = 0;
let stepCount = 0;
let episodeCount = 1;
let successCount = 0;
let playing = false;
let oraclePlaying = false;
let playTimer = null;
let modelBusy = false;
let lastModelRaw = '';
let lastModelReasoning = '';
let lastModelAction = '';
let lastOracleReasoning = '';
let isOracleMode = false;
let lastStepInfo = {};
let recentActions = [];
let recentRewards = [];
let backendHealthy = true;
const OBJECT_LAYOUT = {
red_block: { x: 0.10, y: 0.00 },
blue_block: { x: 0.00, y: 0.00 },
green_block: { x: -0.10, y: 0.00 },
yellow_block: { x: 0.20, y: 0.06 },
purple_block: { x: -0.20, y: 0.06 },
};
function parseCell(cellStr) {
if (!cellStr || typeof cellStr !== 'string' || !cellStr.includes(',')) return [0, 0.25];
const [xs, ys] = cellStr.split(',');
const x = Number(xs), y = Number(ys);
if (!Number.isFinite(x) || !Number.isFinite(y)) return [0, 0.25];
return [x * 0.1, y * 0.1];
}
function normalizeObs(raw) {
const visible = (raw.visible_objects || []).map((o) => ({
name: o.name,
reachable: !!o.reachable,
blocking: o.blocking || null,
inBin: o.in_bin || null,
isHeld: !!o.is_held,
}));
const obs = {
instruction: raw.instruction || '—',
stepsRemaining: raw.steps_remaining ?? 0,
visibleObjects: visible,
holding: raw.holding || null,
completedSubgoals: raw.completed_subgoals || [],
knownFailures: raw.known_failures || [],
activeConstraints: raw.active_constraints || [],
actionHistory: raw.action_history || [],
lastAction: raw.last_action || null,
lastResult: raw.last_result || null,
validActions: raw.valid_actions || [],
actionPreconditions: raw.action_preconditions || {},
goalProgress: raw.goal_progress ?? 0,
goalsRemaining: raw.goals_remaining ?? 0,
oracleHint: raw.oracle_hint || null,
navMode: !!raw.nav_mode,
midTaskChanged: !!raw.mid_task_changed,
gripperCell: raw.gripper_cell || null,
gripperFacing: raw.gripper_facing || null,
nextTargetCell: raw.next_target_cell || null,
distanceToNextGoal: raw.distance_to_next_goal ?? null,
deadlineStatus: raw.deadline_status || {},
discoveredTraits: raw.discovered_traits || {},
_objects: {},
_gripperX: 0,
_gripperY: 0.25,
};
const [gx, gy] = parseCell(obs.gripperCell);
obs._gripperX = gx;
obs._gripperY = gy;
for (const o of obs.visibleObjects) {
const base = OBJECT_LAYOUT[o.name] || { x: 0, y: 0 };
obs._objects[o.name] = {
name: o.name,
x: base.x,
y: base.y,
reachable: !!o.reachable,
blocking: o.blocking || null,
inBin: o.inBin || null,
isHeld: !!o.isHeld || (obs.holding === o.name),
};
}
return obs;
}
async function apiReset() {
const url = `/demo/reset?difficulty=${encodeURIComponent(ACTIVE_DIFFICULTY)}&scenario_pack=${encodeURIComponent(ACTIVE_PACK)}`;
const resp = await fetch(url, { method: 'POST' });
if (!resp.ok) throw new Error(`reset failed ${resp.status}`);
const data = await resp.json();
return { observation: normalizeObs(data.observation), reward: data.reward ?? 0, done: !!data.done, info: data.info || {} };
}
async function apiStep(action) {
const resp = await fetch(`/demo/step?action=${encodeURIComponent(action)}`, { method: 'POST' });
if (!resp.ok) throw new Error(`step failed ${resp.status}`);
const data = await resp.json();
return { observation: normalizeObs(data.observation), reward: data.reward ?? 0, done: !!data.done, info: data.info || {} };
}
async function apiOracle() {
const resp = await fetch('/demo/oracle', { method: 'GET' });
if (!resp.ok) throw new Error(`oracle failed ${resp.status}`);
const data = await resp.json();
return {
observation: normalizeObs(data.observation),
reward: data.reward ?? 0,
done: !!data.done,
info: data.info || {},
action_taken: data.action_taken || null,
reasoning: data.reasoning || '',
};
}
function renderManualButtons(validActions) {
const grid = document.getElementById('manual-grid');
grid.innerHTML = '';
const valid = new Set(validActions || []);
for (const a of ACTIONS) {
const b = document.createElement('button');
b.textContent = a.replace(/_/g,' ');
b.disabled = !valid.has(a);
b.style.opacity = b.disabled ? '0.45' : '1';
b.onclick = () => manualStep(a);
grid.appendChild(b);
}
}
function updateUI(obs, reward, done) {
currentObs = obs;
currentDone = !!done;
document.getElementById('instruction').textContent = obs.instruction;
// Mid-task instruction change banner
const midSec = document.getElementById('mid-task-sec');
const midTxt = document.getElementById('mid-task-txt');
if (obs.midTaskChanged) {
midSec.style.display = '';
// Re-trigger animation by cloning the element
const clone = midTxt.cloneNode(true);
midSec.replaceChild(clone, midTxt);
// Extract the [UPDATE: ...] part from instruction if present
const updateMatch = obs.instruction.match(/\[UPDATE:(.*?)\]/);
clone.textContent = updateMatch
? `⚡ Instruction changed! ${updateMatch[0]}`
: '⚡ Mid-task instruction change!';
} else {
midSec.style.display = 'none';
}
document.getElementById('steps').textContent = obs.stepsRemaining;
document.getElementById('reward').textContent = totalReward.toFixed(2);
document.getElementById('episode').textContent = episodeCount;
document.getElementById('successes').textContent = successCount;
// reward bar
const norm = Math.min(1, Math.max(0, (totalReward + 5) / 22));
const bar = document.getElementById('rbar');
bar.style.width = (norm*100)+'%';
bar.style.background = totalReward >= 0 ? '#4a8a4a' : '#8a3a3a';
// objects — distinguish physically blocked (has a blocker object) from hidden (perception noise)
const blockedByMap = {};
for (const o of obs.visibleObjects) {
if (o.blocking) blockedByMap[o.blocking] = o.name;
}
document.getElementById('obj-list').innerHTML = obs.visibleObjects.map(o => {
const col = COLORS[o.name] || '#aaa';
const tags = [];
if (!o.reachable && !o.inBin) {
if (blockedByMap[o.name]) tags.push('<span class="tag blocked">blocked</span>');
else tags.push('<span class="tag hidden">🔍 scan to reveal</span>');
}
const trait = obs.discoveredTraits[o.name];
if (trait === 'fragile') tags.push('<span class="tag fragile">💜 fragile</span>');
else if (trait === 'heavy') tags.push('<span class="tag heavy">🔵 heavy</span>');
if (o.name === obs.holding) tags.push('<span class="tag held">held</span>');
if (o.inBin) tags.push(`<span class="tag">in bin ${o.inBin}</span>`);
return `<div class="obj-row"><div class="dot" style="background:${col}"></div><span>${o.name}</span>${tags.join('')}</div>`;
}).join('');
// holding
const hs = document.getElementById('holding-sec');
if (obs.holding) { hs.style.display=''; document.getElementById('holding-txt').textContent='Holding: '+obs.holding; }
else hs.style.display='none';
// constraints / subgoals / failures
const mkItems = (arr, cls) => arr.length
? arr.map(s=>`<div class="item ${cls}">${s}</div>`).join('')
: '<div class="none">none</div>';
document.getElementById('constraints').innerHTML = mkItems(obs.activeConstraints,'con');
document.getElementById('subgoals').innerHTML = mkItems(obs.completedSubgoals,'sg');
document.getElementById('failures').innerHTML = mkItems(obs.knownFailures,'fai');
// log row
if (obs.lastAction) {
recentActions.push(obs.lastAction);
recentRewards.push(reward);
if (recentActions.length > 30) recentActions.shift();
if (recentRewards.length > 30) recentRewards.shift();
const ok = obs.lastResult?.startsWith('SUCCESS');
const row = document.createElement('div');
row.className = 'log-row';
row.innerHTML = `<span class="sn">#${stepCount}</span><span class="act">${obs.lastAction}</span><span class="res ${ok?'ok':'fail'}">${obs.lastResult}</span><span class="rew">${reward>=0?'+':''}${reward?.toFixed(2)}</span>`;
const log = document.getElementById('log');
log.prepend(row);
if (log.children.length > 40) log.removeChild(log.lastChild);
}
// diagnostics
const counts = {};
for (const a of recentActions) counts[a] = (counts[a] || 0) + 1;
const sorted = Object.entries(counts).sort((a,b) => b[1]-a[1]);
const top = sorted.length ? `${sorted[0][0]} (${sorted[0][1]}/${recentActions.length})` : 'none';
const avgRecentReward = recentRewards.length
? (recentRewards.reduce((x,y)=>x+y,0)/recentRewards.length).toFixed(3)
: '0.000';
const loopRisk = (sorted.length && (sorted[0][1] / Math.max(1, recentActions.length)) > 0.7)
? 'HIGH'
: 'LOW';
const loopColor = loopRisk === 'HIGH' ? 'color:#e06060' : 'color:#60e090';
document.getElementById('diag').innerHTML = `
<div class="item con">Top recent action: ${top}</div>
<div class="item con">Avg recent reward: ${avgRecentReward}</div>
<div class="item" style="${loopColor}">Loop risk: ${loopRisk}</div>
`;
const rawEl = document.getElementById('raw-model');
const esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const activeReasoning = isOracleMode ? lastOracleReasoning : lastModelReasoning;
const activeAction = isOracleMode ? (obs.lastAction || '') : lastModelAction;
if (activeReasoning || activeAction) {
rawEl.innerHTML =
(activeReasoning ? `<div class="reasoning-box">💭 ${esc(activeReasoning)}</div>` : '') +
(activeAction ? `<div class="action-box">▶ ${esc(activeAction)}</div>` : '');
} else if (lastModelRaw) {
rawEl.innerHTML = `<div class="item con">${esc(lastModelRaw)}</div>`;
} else {
rawEl.innerHTML = '<div class="none">none</div>';
}
renderManualButtons(obs.validActions);
// done
const ds = document.getElementById('done-sec');
if (done) {
ds.style.display = '';
const win = (obs.goalProgress || 0) >= 1.0;
if (win) successCount++;
document.getElementById('successes').textContent = successCount;
document.getElementById('done-txt').textContent =
win ? `✓ Task Complete! Reward: ${totalReward.toFixed(2)}`
: `✗ Episode ended. Reward: ${totalReward.toFixed(2)}`;
stopAll();
} else {
ds.style.display = 'none';
}
// blockedByMap already built above for obj-list; reuse for canvas
drawScene(obs, blockedByMap);
drawSideView(obs);
}
function resetEp() {
resetEpAsync();
}
async function resetEpAsync() {
stopAll();
totalReward = 0; stepCount = 0; episodeCount++;
recentActions = [];
recentRewards = [];
lastModelRaw = ''; lastModelReasoning = ''; lastModelAction = '';
lastOracleReasoning = ''; isOracleMode = false;
document.getElementById('raw-model').innerHTML = '<div class="none">none</div>';
document.getElementById('log').innerHTML = '';
try {
const r = await apiReset();
backendHealthy = true;
document.getElementById('done-sec').style.display = 'none';
updateUI(r.observation, 0, false);
} catch (e) {
backendHealthy = false;
const obs = env.reset();
document.getElementById('done-sec').style.display = 'none';
updateUI(obs, 0, false);
}
}
function manualStep(action) {
manualStepAsync(action);
}
async function manualStepAsync(action) {
if (currentDone) { await resetEpAsync(); return; }
let r;
if (backendHealthy) {
try {
r = await apiStep(action);
} catch (e) {
backendHealthy = false;
}
}
if (!backendHealthy) {
r = env.step(action);
}
totalReward += r.reward;
stepCount++;
lastStepInfo = r.info || {};
updateUI(r.observation, r.reward, r.done);
}
function buildPolicyPrompt(obs) {
const objects = obs.visibleObjects
.map(o => {
const tags = [];
tags.push(o.reachable ? 'reachable' : 'BLOCKED');
if (o.inBin) tags.push(`in_bin=${o.inBin}`);
if (o.name === obs.holding) tags.push('HELD');
if (o.blocking) tags.push(`blocking=${o.blocking}`);
return `${o.name}(${tags.join(',')})`;
})
.join(', ');
const valid = (obs.validActions && obs.validActions.length) ? obs.validActions.join(', ') : 'any';
const preconditions = obs.actionPreconditions ? JSON.stringify(obs.actionPreconditions) : '{}';
const history = (obs.actionHistory && obs.actionHistory.length)
? obs.actionHistory.slice(-5).join(' -> ')
: 'none';
const constraints = (obs.activeConstraints && obs.activeConstraints.length)
? obs.activeConstraints.join('; ')
: 'none';
const failures = (obs.knownFailures && obs.knownFailures.length) ? obs.knownFailures.join('; ') : 'none';
const subgoals = (obs.completedSubgoals && obs.completedSubgoals.length) ? obs.completedSubgoals.join('; ') : 'none yet';
return (
"You are a robot planning agent on a tabletop. Choose ONE action.\n"
+ "Actions: SCAN_SCENE | MOVE_TO_RED | MOVE_TO_BLUE | MOVE_TO_GREEN | MOVE_TO_YELLOW | MOVE_TO_PURPLE | MOVE_NORTH | MOVE_SOUTH | MOVE_EAST | MOVE_WEST | ROTATE_LEFT | ROTATE_RIGHT | PICK | PLACE_BIN_A | PLACE_BIN_B | CLEAR_BLOCKER\n\n"
+ `Instruction: ${obs.instruction}\n`
+ `Scene: ${objects}\n`
+ `Holding: ${obs.holding || 'nothing'}\n`
+ `Goal progress: ${Math.round((obs.goalProgress || 0) * 100)}% Goals remaining: ${obs.goalsRemaining ?? '?'}\n`
+ `Completed: ${subgoals}\n`
+ `Failures: ${failures}\n`
+ `Constraints: ${constraints}\n`
+ `History: ${history}\n`
+ `Last: ${obs.lastAction || 'none'} -> ${obs.lastResult || 'n/a'}\n`
+ `Valid now: ${valid}\n`
+ `Preconditions: ${preconditions}\n`
+ `Distance to next goal: ${obs.distanceToNextGoal ?? 'n/a'}\n`
+ `Deadlines: ${JSON.stringify(obs.deadlineStatus || {})}\n`
+ `Steps left: ${obs.stepsRemaining}\n\n`
+ "Pick one action from Valid now. Output only the action name."
);
}
function chooseNonScanFallback(validActions) {
const valid = new Set(validActions || []);
const priority = [
'PLACE_BIN_A','PLACE_BIN_B',
'PICK',
'CLEAR_BLOCKER',
'MOVE_TO_RED','MOVE_TO_BLUE','MOVE_TO_GREEN','MOVE_TO_YELLOW','MOVE_TO_PURPLE',
'MOVE_NORTH','MOVE_SOUTH','MOVE_EAST','MOVE_WEST',
'ROTATE_LEFT','ROTATE_RIGHT',
'SCAN_SCENE',
];
for (const a of priority) {
if (valid.has(a)) return a;
}
return 'SCAN_SCENE';
}
async function modelStep() {
if (modelBusy || !currentObs) return;
modelBusy = true;
const btn = document.getElementById('model-btn');
const prev = btn.textContent;
btn.textContent = '…Model';
try {
const resp = await fetch('/demo/policy_action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: buildPolicyPrompt(currentObs),
valid_actions: currentObs.validActions || [],
}),
});
const data = await resp.json();
const raw = data.raw_output || '';
const rawAction = data.raw_action || 'n/a';
const chosen = data.action || 'n/a';
const valid = (data.valid_actions_used || []).join(', ');
// Use structured reasoning field if available, otherwise parse <think> tags
lastModelReasoning = data.reasoning || (() => {
const m = raw.match(/<think>([\s\S]*?)<\/think>/i);
return m ? m[1].trim() : '';
})();
lastModelAction = chosen;
lastModelRaw = raw;
if (data.error) lastModelRaw += ` [error=${data.error}]`;
let action = data.action || 'SCAN_SCENE';
await manualStepAsync(action);
} catch (e) {
lastModelRaw = `ERROR: ${String(e)}`;
await manualStepAsync('SCAN_SCENE');
} finally {
modelBusy = false;
btn.textContent = prev;
}
}
async function oracleStep() {
if (currentDone) { await resetEpAsync(); return; }
isOracleMode = true;
try {
const r = await apiOracle();
lastOracleReasoning = r.reasoning || '';
totalReward += r.reward;
stepCount++;
lastStepInfo = r.info || {};
updateUI(r.observation, r.reward, r.done);
} catch (e) {
isOracleMode = false;
console.error('Oracle step failed:', e);
}
}
async function playLoop() {
if (!playing) return;
if (currentDone) { await new Promise(res=>setTimeout(res,600)); await resetEpAsync(); return; }
await modelStep();
if (currentDone) {
if (playing) playTimer = setTimeout(playLoop, 1000);
return;
}
if (playing) {
const delay = parseInt(document.getElementById('speed').value);
playTimer = setTimeout(playLoop, delay);
}
}
function togglePlay() {
const btn = document.getElementById('play-btn');
if (playing) { playing=false; clearTimeout(playTimer); btn.textContent='▶ Run Agent'; }
else { oraclePlaying=false; document.getElementById('oracle-play-btn').textContent='🎯 Run Oracle'; playing=true; btn.textContent='⏸ Pause'; playLoop(); }
}
async function oraclePlayLoop() {
if (!oraclePlaying) return;
if (currentDone) { await new Promise(res=>setTimeout(res,600)); await resetEpAsync(); }
else await oracleStep();
if (oraclePlaying) {
const delay = parseInt(document.getElementById('speed').value);
playTimer = setTimeout(oraclePlayLoop, delay);
}
}
function toggleOraclePlay() {
const btn = document.getElementById('oracle-play-btn');
if (oraclePlaying) { oraclePlaying=false; clearTimeout(playTimer); btn.textContent='🎯 Run Oracle'; }
else { playing=false; document.getElementById('play-btn').textContent='▶ Run Agent'; oraclePlaying=true; btn.textContent='⏸ Pause Oracle'; oraclePlayLoop(); }
}
function stopAll() {
playing=false; oraclePlaying=false; clearTimeout(playTimer);
document.getElementById('play-btn').textContent='▶ Run Agent';
document.getElementById('oracle-play-btn').textContent='🎯 Run Oracle';
}
function renderDifficultyButtons() {
['easy', 'medium', 'hard'].forEach((d) => {
const el = document.getElementById(`diff-${d}`);
if (!el) return;
if (d === ACTIVE_DIFFICULTY) el.classList.add('active');
else el.classList.remove('active');
});
}
async function setDifficulty(level) {
ACTIVE_DIFFICULTY = level;
renderDifficultyButtons();
await resetEpAsync();
}
async function setScenarioPack(pack) {
ACTIVE_PACK = pack;
await resetEpAsync();
}
// init
renderDifficultyButtons();
const packSel = document.getElementById('pack-select');
if (packSel) packSel.value = ACTIVE_PACK;
resetEpAsync();
</script>
</body>
</html>