Spaces:
Sleeping
Sleeping
Aryanshh
style: Move Target Quotas to a dedicated step deeply set inside the UI tour timeline
d76cba4 | 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 <strong>Command Console</strong> to manage your supply chain.", | |
| target: "header" | |
| }, | |
| { | |
| title: "Supply Logistics", | |
| text: "Select a component and Transport Mode on the left, then click <strong>'Order'</strong> 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 <strong>Financial Cost</strong> and <strong>Carbon Footprint</strong> in real-time.", | |
| target: "#impact-preview" | |
| }, | |
| { | |
| title: "Starting Production", | |
| text: "Once inventory arrives, click <strong>'Start Run'</strong>. 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 <strong>Estimated Arrival Day</strong>.", | |
| target: ".shipment-cart-section" | |
| }, | |
| { | |
| title: "Target Quotas", | |
| text: "<div style='background: #fff; padding: 10px; border-radius: 6px; border: 1px solid #ddd; color: #333;'><strong>Current Requirements:</strong><br><br><strong>EcoPhone</strong><br>Pending<br>Required: 5 units remaining<br>Deadline: Day 15<br><br><strong>GreenTab</strong><br>Pending<br>Required: 3 units remaining<br>Deadline: Day 25</div>", | |
| target: "#shipments-container" | |
| }, | |
| { | |
| title: "Time Management", | |
| text: "Use <strong>'Advance to Next Day'</strong> 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 => ` | |
| <div class="cart-item"> | |
| <div class="cart-type"> | |
| ${ship.part} (x${ship.quantity}) | |
| <button onclick="execute('cancel', event, '${ship.id}')" class="btn btn-ghost small" style="float: right; color: #ff4136;">✕</button> | |
| </div> | |
| <div class="cart-meta">Mode: ${ship.mode.toUpperCase()} | |
| <span style="float:right; color:var(--primary-green); font-weight:600;">$${ship.cost.toFixed(0)}</span> | |
| </div> | |
| <div class="arrival-day">Arrives in: ${ship.eta} days</div> | |
| </div> | |
| `).join(''); | |
| } else { | |
| cartContainer.innerHTML = '<p class="placeholder">No active shipments in transit.</p>'; | |
| } | |
| 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 | |
| ? `<span class="order-badge fulfilled">Fulfilled</span>` | |
| : `<span class="order-badge pending">Pending</span>`; | |
| return ` | |
| <div class="shipment-item${isFulfilled ? ' fulfilled' : ''}"> | |
| <div class="shipment-header"> | |
| <span class="shipment-product">${order.product}</span> | |
| ${statusBadge} | |
| </div> | |
| <div class="shipment-detail">Required: <strong>${order.quantity}</strong> units remaining</div> | |
| <div class="shipment-detail">Deadline: Day <strong>${order.due_date}</strong></div> | |
| </div>`; | |
| }).join(''); | |
| } else { | |
| container.innerHTML = '<p class="placeholder">All orders fulfilled. Environment reset recommended.</p>'; | |
| } | |
| } 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 = ` | |
| <span class="history-meta">[Day ${currentDay}] ${meta}</span> | |
| <span class="history-text">${title}</span> | |
| `; | |
| if (subtext) html += `<span class="history-sub">${subtext}</span>`; | |
| 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 ? ` | |
| <div style="background: rgba(39, 174, 96, 0.1); padding: 0.8rem; border-radius: 6px; border-left: 4px solid var(--primary-green); margin-bottom: 1.5rem; font-size: 0.95rem; color: var(--deep-forest);"> | |
| <strong>Important Note:</strong> This is a simulation of what the AI Agent will be doing in the RL Env. | |
| </div> | |
| ` : ''; | |
| content.innerHTML = ` | |
| ${noteHTML} | |
| <div class="step-counter">Step ${step + 1} of ${TUTORIAL_STEPS.length}</div> | |
| <h2>${data.title}</h2> | |
| <p>${data.text}</p> | |
| `; | |
| 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'); | |