interior-env / static /app.js
Interior Env
Exposed live Physics engine logic to Frontend Visualizer and upgraded LLM agent prompt
a373fb7
Raw History Blame Contribute Delete
8.3 kB
const API_BASE = "/api/v1";
let currentSessionId = null;
let currentTaskConfig = null;
// DOM Elements
const screenSelect = document.getElementById('selection-screen');
const screenSim = document.getElementById('simulation-screen');
const taskListEl = document.getElementById('task-list');
const invListEl = document.getElementById('inventory-list');
const logsEl = document.getElementById('logs');
const lblTask = document.getElementById('lbl-task');
const lblStep = document.getElementById('lbl-step');
const lblMax = document.getElementById('lbl-max');
const lblStatus = document.getElementById('lbl-status');
const btnSubmit = document.getElementById('btn-submit');
const modalSuccess = document.getElementById('modal-success');
const modalScore = document.getElementById('modal-score');
// Canvas
const canvas = document.getElementById('blueprint');
const ctx = canvas.getContext('2d');
async function init() {
const res = await fetch(`${API_BASE}/tasks`);
const tasks = await res.json();
for (const [taskId, data] of Object.entries(tasks)) {
const card = document.createElement('div');
card.className = 'task-card glass';
card.innerHTML = `
<h4>${taskId}</h4>
<span class="badge">${data.difficulty} | ${data.room_size[0]}x${data.room_size[1]} Room</span>
<p style="margin-top:0.8rem; color:var(--text-dim)">${data.description}</p>
`;
card.onclick = () => startSession(taskId, data);
taskListEl.appendChild(card);
}
}
async function startSession(taskId, taskData) {
const res = await fetch(`${API_BASE}/session/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: taskId })
});
if (!res.ok) return alert("Failed to start server session.");
const data = await res.json();
currentSessionId = data.session_id;
currentTaskConfig = taskData;
lblTask.textContent = taskId.toUpperCase();
screenSelect.classList.add('hidden');
screenSim.classList.remove('hidden');
renderState(data.observation);
}
async function executeStep() {
if (!currentSessionId) return;
const actionType = document.getElementById('action-type').value;
const item = document.getElementById('action-item').value;
const x = parseInt(document.getElementById('action-x').value) || 0;
const y = parseInt(document.getElementById('action-y').value) || 0;
const rot = parseInt(document.getElementById('action-rot').value) || 0;
const res = await fetch(`${API_BASE}/session/step`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: currentSessionId,
action_type: actionType,
item: item,
x: x,
y: y,
rotation: rot
})
});
if (!res.ok) return alert("API failed.");
const data = await res.json();
renderState(data.observation);
if (data.done) {
if (data.observation.placed_items && Object.keys(data.observation.unplaced_items).length === 0 && data.observation.feedback_logs.some(l => l.includes("SUCCESS"))) {
setTimeout(() => showSuccess(data.result), 600);
}
}
}
let currentObs = null;
function renderState(obs) {
currentObs = obs;
lblStep.textContent = obs.step_count;
lblMax.textContent = obs.max_steps;
const isSuccess = obs.feedback_logs.some(l => l.includes("SUCCESS"));
if (isSuccess) {
lblStatus.textContent = "VALIDATED";
lblStatus.className = "status pass";
} else {
lblStatus.textContent = "UNSTABLE";
lblStatus.className = "status fail";
}
// Render Inventory list
invListEl.innerHTML = '';
// Placed
for (const [id, pi] of Object.entries(obs.placed_items)) {
const li = document.createElement('li');
li.innerHTML = `<span>🛋️ ${id}</span> <span class="text-green">[${pi.x},${pi.y}] R:${pi.rotation}°</span>`;
invListEl.appendChild(li);
}
// Unplaced
obs.unplaced_items.forEach(u => {
const li = document.createElement('li');
li.className = 'unplaced';
li.innerHTML = `<span>📦 ${u.id}</span> <span class="hl-red">Unplaced (${u.width}x${u.height})</span>`;
invListEl.appendChild(li);
});
// Console
logsEl.innerHTML = '';
obs.feedback_logs.forEach(msg => {
const div = document.createElement('div');
div.textContent = `> ${msg}`;
if (msg.includes("SUCCESS")) div.className = "succ";
else if (msg.includes("ERROR") || msg.includes("CRITICAL") || msg.includes("Collision")) div.className = "err";
logsEl.appendChild(div);
});
logsEl.scrollTop = logsEl.scrollHeight;
drawCanvas(obs);
}
function drawCanvas(obs) {
const w = obs.room_size[0];
const h = obs.room_size[1];
// Compute cell size
const maxDim = Math.max(w, h);
const cellSize = Math.floor(600 / maxDim);
// Clear
ctx.fillStyle = '#1e2233';
ctx.fillRect(0, 0, 600, 600);
// Offset to center
const ox = (600 - (w * cellSize)) / 2;
const oy = (600 - (h * cellSize)) / 2;
// Check toggles
const showSolar = document.getElementById("toggleSolar")?.checked;
const showThermal = document.getElementById("toggleThermal")?.checked;
// Draw grid & Physics
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
for(let y=0; y<h; y++) {
for(let x=0; x<w; x++) {
// Draw Thermal Background
if (showThermal && obs.thermal_grid) {
const heat = obs.thermal_grid[y][x];
if (heat > 0) {
ctx.fillStyle = `rgba(255, 50, 50, ${Math.min(0.6, heat / 5)})`;
ctx.fillRect(ox + x*cellSize, oy + y*cellSize, cellSize, cellSize);
} else if (heat < 0) {
ctx.fillStyle = `rgba(50, 150, 255, ${Math.min(0.6, -heat / 3)})`;
ctx.fillRect(ox + x*cellSize, oy + y*cellSize, cellSize, cellSize);
}
}
// Draw Solar Background
else if (showSolar && obs.solar_grid && obs.solar_grid[y][x]) {
ctx.fillStyle = 'rgba(255, 235, 59, 0.15)';
ctx.fillRect(ox + x*cellSize, oy + y*cellSize, cellSize, cellSize);
}
// Draw Grid Lines
ctx.strokeRect(ox + x*cellSize, oy + y*cellSize, cellSize, cellSize);
}
}
// Draw Door
const dx = obs.door_position[0];
const dy = obs.door_position[1];
ctx.fillStyle = 'var(--door)';
ctx.fillRect(ox + dx*cellSize, oy + dy*cellSize, cellSize, cellSize);
ctx.fillStyle = '#000';
ctx.font = '14px sans-serif';
ctx.fillText("EXIT", ox + dx*cellSize + 5, oy + dy*cellSize + 20);
// Filter placed items to get original dimensions
const itemDefs = {};
if(currentTaskConfig) {
currentTaskConfig.items.forEach(i => itemDefs[i.id] = i);
}
// Draw Furniture
for (const [id, pi] of Object.entries(obs.placed_items)) {
if(!itemDefs[id]) continue;
const origW = itemDefs[id].width;
const origH = itemDefs[id].height;
const drawW = pi.rotation === 0 ? origW : origH;
const drawH = pi.rotation === 0 ? origH : origW;
ctx.fillStyle = 'rgba(59, 130, 246, 0.8)';
ctx.strokeStyle = '#2563eb';
ctx.lineWidth = 2;
const rectX = ox + pi.x * cellSize;
const rectY = oy + pi.y * cellSize;
// Draw with slight padding
ctx.fillRect(rectX + 2, rectY + 2, drawW * cellSize - 4, drawH * cellSize - 4);
ctx.strokeRect(rectX + 2, rectY + 2, drawW * cellSize - 4, drawH * cellSize - 4);
// Text
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px var(--mono)';
ctx.fillText(id, rectX + 10, rectY + 20);
}
}
function showSuccess(result) {
modalScore.textContent = `Score: ${result.score.toFixed(4)}`;
modalSuccess.classList.remove('hidden');
}
btnSubmit.onclick = executeStep;
window.onload = init;