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');