const API_BASE = window.location.origin; // Simulation Specs (Mirrors env.py) const SPECS = { sea: { cost: 1.0, carbon: 0.1 }, air: { cost: 5.0, carbon: 2.0 }, rail: { cost: 2.5, carbon: 0.5 } }; // Tutorial Content const TUTORIAL_STEPS = [ { title: "Mission: NetZero-Nav", text: "Your goal is to sustain a profitable electronics business while hitting zero-carbon targets. Use the Command Console to manage your supply chain.", target: "header" }, { title: "Supply Logistics", text: "Select a component and Transport Mode on the left, then click 'Order' in the center. SEA is green but takes 10 full days!", target: "#section-order" }, { title: "The Impact Preview", text: "Watch this box! Whenever you change a transport mode, we calculate the Financial Cost and Carbon Footprint in real-time.", target: "#impact-preview" }, { title: "Starting Production", text: "Once inventory arrives, click 'Start Run'. This consumes parts and fulfills active orders for revenue.", target: "#section-produce" }, { title: "Logistics Cart", text: "Track your pending shipments here! See exactly what's coming, how much, and the exact Estimated Arrival Day.", target: ".shipment-cart-section" }, { title: "Target Quotas", text: "
Current Requirements:

EcoPhone
Pending
Required: 5 units remaining
Deadline: Day 15

GreenTab
Pending
Required: 3 units remaining
Deadline: Day 25
", target: "#shipments-container" }, { title: "Time Management", text: "Use 'Advance to Next Day' to process shipments and grow your empire. Good luck!", target: ".time-controls" } ]; let currentTutStep = 0; let currentDay = 0; // State Management async function updateState() { try { const response = await fetch(`${API_BASE}/state`); if (!response.ok) throw new Error('API Unreachable'); const data = await response.json(); currentDay = data.step; const status = document.getElementById('connection-status'); status.textContent = 'ONLINE'; status.className = 'status-badge status-online'; document.getElementById('carbon-value').textContent = data.carbon.toFixed(0); document.getElementById('cash-value').textContent = data.cash.toLocaleString(); document.getElementById('chips-count').textContent = data.inventory.chips; document.getElementById('sensors-count').textContent = data.inventory.sensors; // Update Cart const cartContainer = document.getElementById('cart-container'); if (data.active_shipments && data.active_shipments.length > 0) { cartContainer.innerHTML = data.active_shipments.map(ship => `
${ship.part} (x${ship.quantity})
Mode: ${ship.mode.toUpperCase()} $${ship.cost.toFixed(0)}
Arrives in: ${ship.eta} days
`).join(''); } else { cartContainer.innerHTML = '

No active shipments in transit.

'; } const newsAlert = document.getElementById('news-alert'); if (data.news) { document.getElementById('news-text').textContent = data.news; newsAlert.classList.remove('hidden'); } else { newsAlert.classList.add('hidden'); } const container = document.getElementById('shipments-container'); if (data.orders && data.orders.length > 0) { container.innerHTML = data.orders.map(order => { const isFulfilled = order.quantity <= 0; const statusBadge = isFulfilled ? `Fulfilled` : `Pending`; return `
${order.product} ${statusBadge}
Required: ${order.quantity} units remaining
Deadline: Day ${order.due_date}
`; }).join(''); } else { container.innerHTML = '

All orders fulfilled. Environment reset recommended.

'; } } catch (error) { console.error('Update failed:', error); const status = document.getElementById('connection-status'); status.textContent = 'RECONNECTING...'; status.className = 'status-badge'; } } // UI Controls const modeSelect = document.getElementById('mode-select'); const qtyInput = document.getElementById('qty-input'); function updatePreview() { const mode = modeSelect.value; const s = SPECS[mode]; const qty = parseInt(qtyInput.value) || 1; const cost = 10 * qty * s.cost; const carbon = qty * s.carbon; document.getElementById('preview-cost').textContent = `$${cost.toFixed(0)}`; document.getElementById('preview-carbon').textContent = `+${carbon.toFixed(1)}kg CO2`; } modeSelect.addEventListener('change', updatePreview); qtyInput.addEventListener('input', updatePreview); function spawnFeedback(text, x, y) { const el = document.createElement('div'); el.className = 'floating-feedback'; el.textContent = text; el.style.left = `${x}px`; el.style.top = `${y}px`; document.body.appendChild(el); setTimeout(() => el.remove(), 1000); } function log(title, meta = "SYSTEM", subtext = '', type = 'system') { const box = document.getElementById('activity-log'); const entry = document.createElement('div'); entry.className = `history-item ${type}`; let html = ` [Day ${currentDay}] ${meta} ${title} `; if (subtext) html += `${subtext}`; entry.innerHTML = html; box.prepend(entry); } // Global Execute Function window.execute = async function(type, event, shipment_id = null) { document.querySelector('main').classList.add('transitioning'); let actionObj = { action_type: type }; if (type === 'order_parts') { const part = document.getElementById('part-select').value; const qty = parseInt(document.getElementById('qty-input').value) || 1; actionObj.part_type = part; actionObj.mode = document.getElementById('mode-select').value; actionObj.quantity = qty; } else if (type === 'produce') { actionObj.product = document.getElementById('product-select').value; const qty = parseInt(document.getElementById('produce-qty-input').value) || 1; actionObj.quantity = qty; } else if (type === 'offset') { const amt = parseFloat(document.getElementById('offset-qty-input').value) || 10; actionObj.offset_amount = amt; } else if (type === 'skip') { actionObj.action_type = 'skip'; } else if (type === 'cancel') { actionObj.action_type = 'cancel'; actionObj.shipment_id = shipment_id; } try { const response = await fetch(`${API_BASE}/step`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actionObj) }); const result = await response.json(); if (result.info && result.info.error) { log(result.info.error, "COMMAND REJECTED", "", "alert"); spawnFeedback(`❌ ${result.info.error}`, event.clientX, event.clientY); } else { if (type === 'order_parts') { const s = SPECS[actionObj.mode]; const cost = 10 * actionObj.quantity * s.cost; log(`Ordered ${actionObj.quantity}x ${actionObj.part_type}`, `LOGISTICS | ${actionObj.mode.toUpperCase()}`, `-$${cost.toFixed(0)}`, 'action'); spawnFeedback(`+${actionObj.quantity} ordered`, event.clientX, event.clientY); } else if (type === 'produce') { log(`Started run for ${actionObj.quantity}x ${actionObj.product}`, `MANUFACTURING`, "", 'action'); spawnFeedback(`Initiated x${actionObj.quantity} run`, event.clientX, event.clientY); } else if (type === 'offset') { const cost = actionObj.offset_amount * 2; log(`Bought ${actionObj.offset_amount} Carbon Offsets`, `ESG STRATEGY`, `-$${cost.toFixed(0)}`, 'action'); spawnFeedback(`Purchased ${actionObj.offset_amount} offsets`, event.clientX, event.clientY); } else if (type === 'cancel') { log(`Canceled Shipment`, `LOGISTICS`, "", 'action'); } else if (type === 'skip') { log(`Advanced to next day`, `SIMULATION`, "", 'system'); } } // Process Arrivals from backend if (result.info && result.info.arrivals) { result.info.arrivals.forEach(arrival => { log(`${arrival} received`, `INVENTORY ARRIVAL`, "+ Raw Materials", 'arrival'); }); } if (result.info && result.info.news) { const box = document.getElementById('news-alert'); document.getElementById('news-text').textContent = result.info.news; box.classList.remove('hidden'); setTimeout(() => box.classList.add('hidden'), 5000); log(result.info.news, "GLOBAL NEWS", "", 'alert'); } await updateState(); } catch (e) { log("Connection lost to simulation server", "CRITICAL ERROR", "", "alert"); } finally { setTimeout(() => { document.querySelector('main').classList.remove('transitioning'); }, 300); } } // Tutorial Logic function showStep(step) { const content = document.getElementById('tutorial-step'); const data = TUTORIAL_STEPS[step]; const noteHTML = step === 0 ? `
Important Note: This is a simulation of what the AI Agent will be doing in the RL Env.
` : ''; content.innerHTML = ` ${noteHTML}
Step ${step + 1} of ${TUTORIAL_STEPS.length}

${data.title}

${data.text}

`; document.getElementById('tut-prev').classList.toggle('hidden', step === 0); document.getElementById('tut-next').textContent = step === TUTORIAL_STEPS.length - 1 ? "Start Mission" : "Next"; const modalOverlay = document.getElementById('tutorial-modal'); modalOverlay.style.alignItems = 'center'; modalOverlay.style.paddingTop = '0'; } document.getElementById('tut-next').addEventListener('click', () => { if (currentTutStep < TUTORIAL_STEPS.length - 1) { currentTutStep++; showStep(currentTutStep); } else { document.getElementById('tutorial-modal').classList.add('hidden'); } }); document.getElementById('tut-prev').addEventListener('click', () => { if (currentTutStep > 0) { currentTutStep--; showStep(currentTutStep); } }); document.getElementById('tut-close').addEventListener('click', () => { document.getElementById('tutorial-modal').classList.add('hidden'); }); document.getElementById('info-btn').addEventListener('click', () => { currentTutStep = 0; showStep(0); document.getElementById('tutorial-modal').classList.remove('hidden'); }); // Initialization async function manualReset() { if (!confirm("Are you sure you want to reset the simulation?")) return; try { await fetch(`${API_BASE}/reset`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task: 'easy' }) }); log("Environment Reset to Day 0", "SYSTEM EVENT", "Clean Base", "system"); await updateState(); // Clear history safely document.getElementById('activity-log').innerHTML = ''; log("Dashboard Initialized", "SYSTEM", "Ready", "system"); } catch (e) { log("Reset Failed", "CRITICAL ERROR", "", "alert"); } } async function triggerSuezJam() { try { await fetch(`${API_BASE}/trigger`, { method: 'POST' }); log(`CRISIS: Suez Jam Triggered!`, 'error'); await updateState(); } catch (e) { log(`Trigger failed`, 'error'); } } document.getElementById('skip-btn').addEventListener('click', (e) => window.execute('skip', e)); document.getElementById('trigger-btn').addEventListener('click', triggerSuezJam); document.getElementById('reset-btn').addEventListener('click', manualReset); // Start updateState(); setInterval(updateState, 3000); updatePreview(); showStep(0); document.getElementById('tutorial-modal').classList.remove('hidden');