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 = `

${taskId}

${data.difficulty} | ${data.room_size[0]}x${data.room_size[1]} Room

${data.description}

`; 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 = `🛋️ ${id} [${pi.x},${pi.y}] R:${pi.rotation}°`; invListEl.appendChild(li); } // Unplaced obs.unplaced_items.forEach(u => { const li = document.createElement('li'); li.className = 'unplaced'; li.innerHTML = `📦 ${u.id} Unplaced (${u.width}x${u.height})`; 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 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;