File size: 14,204 Bytes
ab38db6
 
3358e03
802c5f2
3358e03
 
 
802c5f2
 
f5ce3e8
5e65639
 
de36011
d76cba4
3358e03
 
 
f5ce3e8
 
3358e03
5e65639
 
831a6aa
f5ce3e8
831a6aa
de36011
 
 
f5ce3e8
3358e03
5e65639
831a6aa
f5ce3e8
 
 
831a6aa
d76cba4
 
 
 
 
5e65639
de36011
f5ce3e8
3358e03
5e65639
 
 
3358e03
f5ce3e8
3358e03
01c97f6
ab38db6
 
 
 
 
4ceb41c
6029f5c
f5ce3e8
b9f25ba
 
 
 
 
 
7326d3c
 
b9f25ba
c547cb1
f5ce3e8
 
c547cb1
f5ce3e8
c547cb1
 
 
 
 
 
 
ceb478a
f5ce3e8
 
 
 
 
 
b9f25ba
 
 
 
 
 
 
 
 
 
87a0116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9f25ba
3358e03
b9f25ba
 
 
3358e03
 
 
 
3a65d8a
 
01c97f6
3358e03
 
fbf9261
ab38db6
3358e03
 
3ae8c00
fbf9261
 
 
802c5f2
 
3ae8c00
802c5f2
 
831a6aa
fbf9261
b9f25ba
6029f5c
 
 
 
 
 
 
 
 
 
87e9783
3358e03
87e9783
 
 
 
 
 
 
 
 
 
3358e03
 
 
f5ce3e8
c547cb1
5e65639
3ae8c00
3358e03
01c97f6
6029f5c
fbf9261
6029f5c
3ae8c00
fbf9261
01c97f6
 
eaea692
 
01c97f6
eaea692
 
3ae8c00
 
c547cb1
 
 
01c97f6
 
 
495eca8
 
 
01c97f6
495eca8
01c97f6
 
 
87e9783
51cbaeb
01c97f6
87e9783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
01c97f6
495eca8
 
01c97f6
87e9783
01c97f6
5e65639
 
3358e03
495eca8
 
 
5e65639
 
 
 
3358e03
ad1b007
 
 
 
 
 
de36011
ad1b007
831a6aa
de36011
 
 
5e65639
831a6aa
3358e03
d80b3e5
 
 
5e65639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
831a6aa
3358e03
 
 
 
 
 
 
 
2a85ff5
3358e03
2a85ff5
 
 
 
3358e03
2a85ff5
3358e03
 
 
 
 
 
 
 
 
 
 
 
 
6029f5c
3358e03
 
495eca8
3358e03
ab38db6
01c97f6
3358e03
5e65639
3ae8c00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
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');