Spaces:
Running
Running
Download App.jsx from ishraddhapawar/openenv: direct link, hf CLI and curl.
- Browser
- Download file 29.1 kB
-
https://huggingface.co/spaces/ishraddhapawar/openenv/resolve/main/App.jsx
- Command line
-
hf download hf://spaces/ishraddhapawar/openenv/App.jsx
-
curl -L -o App.jsx https://huggingface.co/spaces/ishraddhapawar/openenv/resolve/main/App.jsx
29.1 kB
| import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; | |
| import { Truck, Ship, Package, DollarSign, TrendingUp, AlertTriangle, Play, Factory, Activity, BarChart2, CheckCircle, Navigation, Download, Settings, RotateCcw, Zap, Globe } from 'lucide-react'; | |
| import { LineChart, Line, AreaChart, Area, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'; | |
| import { MapContainer, TileLayer, Marker, Polyline, Popup } from 'react-leaflet'; | |
| import L from 'leaflet'; | |
| import 'leaflet/dist/leaflet.css'; | |
| import toast, { Toaster } from 'react-hot-toast'; | |
| import './App.css'; | |
| import EditorialNavigation from './components/Navigation'; | |
| import Hero from './components/Hero'; | |
| import StatsGrid from './components/StatsGrid'; | |
| import TextReveal from './components/TextReveal'; | |
| import Workflow from './components/Workflow'; | |
| import UseCaseTabs from './components/UseCaseTabs'; | |
| import ContactForm from './components/ContactForm'; | |
| import Footer from './components/Footer'; | |
| import InteractiveWavesBackground from './components/InteractiveWaves'; | |
| const API = '/api'; | |
| // ββ Map Data ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const MAP_NODES = { | |
| factories: [ | |
| { id: 'china', name: 'China Factory', lat: 22.5, lng: 114, cost: 50 }, | |
| { id: 'india', name: 'India Factory', lat: 19, lng: 72.8, cost: 45 }, | |
| { id: 'mexico', name: 'Mexico Factory', lat: 25.6, lng: -100.3, cost: 70 }, | |
| { id: 'vietnam', name: 'Vietnam Factory', lat: 10.8, lng: 106.6, cost: 42 }, | |
| { id: 'brazil', name: 'Brazil Factory', lat: -23.5, lng: -46.6, cost: 65 }, | |
| { id: 'germany', name: 'Germany Factory', lat: 48.8, lng: 9.2, cost: 90 }, | |
| ], | |
| ports: [ | |
| { id: 'shanghai', name: 'Shanghai Port', lat: 31.23, lng: 121.47, delay: 1 }, | |
| { id: 'rotterdam', name: 'Rotterdam Port', lat: 51.92, lng: 4.48, delay: 2 }, | |
| { id: 'la', name: 'LA Port', lat: 33.74, lng: -118.27, delay: 1 }, | |
| { id: 'singapore', name: 'Singapore Port', lat: 1.35, lng: 103.82, delay: 1 }, | |
| { id: 'busan', name: 'Busan Port', lat: 35.1, lng: 129.03, delay: 1 }, | |
| { id: 'dubai', name: 'Dubai Port', lat: 25.0, lng: 55.1, delay: 2 }, | |
| { id: 'santos', name: 'Santos Port', lat: -23.96, lng: -46.3, delay: 2 }, | |
| ], | |
| warehouses: [ | |
| { id: 'usa', name: 'USA Warehouse', lat: 39.83, lng: -98.58 }, | |
| { id: 'eu', name: 'EU Warehouse', lat: 50.11, lng: 8.68 }, | |
| { id: 'asia', name: 'Asia Warehouse', lat: 35.68, lng: 139.69 }, | |
| { id: 'mideast', name: 'Middle East Warehouse', lat: 25.2, lng: 55.3 }, | |
| { id: 'latam', name: 'LatAm Warehouse', lat: -23.5, lng: -46.6 }, | |
| { id: 'oceania', name: 'Oceania Warehouse', lat: -33.87, lng: 151.21 }, | |
| ], | |
| }; | |
| const ROUTES = [ | |
| // Factory β Port (gold lines) | |
| { from: [22.5, 114], to: [31.23, 121.47], color: '#b08d3a' }, | |
| { from: [22.5, 114], to: [1.35, 103.82], color: '#b08d3a' }, | |
| { from: [19, 72.8], to: [1.35, 103.82], color: '#b08d3a' }, | |
| { from: [19, 72.8], to: [25.0, 55.1], color: '#b08d3a' }, | |
| { from: [25.6, -100.3], to: [33.74, -118.27], color: '#b08d3a' }, | |
| { from: [10.8, 106.6], to: [1.35, 103.82], color: '#b08d3a' }, | |
| { from: [10.8, 106.6], to: [35.1, 129.03], color: '#b08d3a' }, | |
| { from: [-23.5, -46.6], to: [-23.96, -46.3], color: '#b08d3a' }, | |
| { from: [48.8, 9.2], to: [51.92, 4.48], color: '#b08d3a' }, | |
| // Port β Warehouse (green lines) | |
| { from: [31.23, 121.47], to: [39.83, -98.58], color: '#3d7068' }, | |
| { from: [31.23, 121.47], to: [35.68, 139.69], color: '#3d7068' }, | |
| { from: [33.74, -118.27], to: [39.83, -98.58], color: '#3d7068' }, | |
| { from: [51.92, 4.48], to: [50.11, 8.68], color: '#3d7068' }, | |
| { from: [1.35, 103.82], to: [35.68, 139.69], color: '#3d7068' }, | |
| { from: [1.35, 103.82], to: [-33.87, 151.21], color: '#3d7068' }, | |
| { from: [35.1, 129.03], to: [35.68, 139.69], color: '#3d7068' }, | |
| { from: [25.0, 55.1], to: [25.2, 55.3], color: '#3d7068' }, | |
| { from: [25.0, 55.1], to: [50.11, 8.68], color: '#3d7068' }, | |
| { from: [-23.96, -46.3], to: [-23.5, -46.6], color: '#3d7068' }, | |
| { from: [-23.96, -46.3], to: [39.83, -98.58], color: '#3d7068' }, | |
| ]; | |
| // ββ Custom Marker Icons βββββββββββββββββββββββββββββββββββββββββββ | |
| function createIcon(type, label) { | |
| return L.divIcon({ | |
| className: '', | |
| html: `<div class="map-marker-label ${type}">${label}</div>`, | |
| iconSize: [0, 0], | |
| iconAnchor: [0, 0], | |
| }); | |
| } | |
| // ββ Interactive Map Component βββββββββββββββββββββββββββββββββββββ | |
| function InteractiveMap({ state }) { | |
| const inventory = state?.inventory || { usa: 0, eu: 0, asia: 0 }; | |
| const portDelays = state?.portDelays || []; | |
| return ( | |
| <div className="map-container"> | |
| <MapContainer | |
| center={[25, 20]} | |
| zoom={2} | |
| minZoom={2} | |
| maxZoom={6} | |
| style={{ width: '100%', height: '100%' }} | |
| zoomControl={true} | |
| attributionControl={true} | |
| > | |
| <TileLayer | |
| url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png" | |
| attribution='© <a href="https://carto.com/">CARTO</a>' | |
| /> | |
| {/* Factory Markers */} | |
| {(state?.factoryCosts || []).map(f => { | |
| const node = MAP_NODES.factories.find(n => n.id === f.id); | |
| if (!node) return null; | |
| return ( | |
| <Marker key={f.id} position={[node.lat, node.lng]} icon={createIcon('factory', `π $${f.currentCost}`)}> | |
| <Popup><b>{f.name}</b><br/>Unit Cost: ${f.currentCost}</Popup> | |
| </Marker> | |
| ); | |
| })} | |
| {/* Port Markers */} | |
| {portDelays.map(p => { | |
| const node = MAP_NODES.ports.find(n => n.id === p.id); | |
| if (!node) return null; | |
| return ( | |
| <Marker key={p.id} position={[node.lat, node.lng]} icon={createIcon('port', `β ${p.isClosed ? 'β' : p.currentDelay + 'm'}`)}> | |
| <Popup><b>{p.name}</b><br/>Delay: {p.currentDelay}mo<br/>Status: {p.isClosed ? 'π΄ Closed' : 'π’ Open'}</Popup> | |
| </Marker> | |
| ); | |
| })} | |
| {/* Warehouse Markers */} | |
| {MAP_NODES.warehouses.map(w => ( | |
| <Marker key={w.id} position={[w.lat, w.lng]} icon={createIcon('warehouse', `π¦ ${inventory[w.id] ?? 0}`)}> | |
| <Popup><b>{w.name}</b><br/>Inventory: {inventory[w.id] ?? 0} units</Popup> | |
| </Marker> | |
| ))} | |
| {/* Route Lines */} | |
| {ROUTES.map((r, i) => ( | |
| <Polyline key={i} positions={[r.from, r.to]} pathOptions={{ color: r.color, weight: 2, opacity: 0.6, dashArray: '8 6' }} /> | |
| ))} | |
| </MapContainer> | |
| </div> | |
| ); | |
| } | |
| // ββ Chart Tooltip βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const ChartTooltip = ({ active, payload, label }) => { | |
| if (!active || !payload?.length) return null; | |
| return ( | |
| <div style={{ | |
| background: '#f7f6f2', border: '1px solid #e5e4de', | |
| borderRadius: 2, padding: '10px 14px', fontSize: '0.8rem', fontFamily: "'Space Mono', monospace" | |
| }}> | |
| <div style={{ color: '#B4B4B4', marginBottom: 4, fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.2em' }}>{label}</div> | |
| {payload.map((p, i) => ( | |
| <div key={i} style={{ color: p.color, fontWeight: 700, fontSize: '0.85rem' }}>{p.name}: {typeof p.value === 'number' ? p.value.toLocaleString() : p.value}</div> | |
| ))} | |
| </div> | |
| ); | |
| }; | |
| // ββ Main App ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function App() { | |
| const [state, setState] = useState(null); | |
| const [loading, setLoading] = useState(false); | |
| const [factory, setFactory] = useState('china'); | |
| const [port, setPort] = useState('shanghai'); | |
| const [destination, setDestination] = useState('usa'); | |
| const [quantity, setQuantity] = useState(100); | |
| const [oracleAdvice, setOracleAdvice] = useState(''); | |
| const [autoPlaying, setAutoPlaying] = useState(false); | |
| // ββ Fetch state βββββββββββββββββββββββββββββββββββββββββββββββ | |
| const fetchState = useCallback(async () => { | |
| try { | |
| const res = await fetch(`${API}/state`); | |
| if (!res.ok) throw new Error('Failed to fetch'); | |
| const data = await res.json(); | |
| setState(data); | |
| } catch (err) { | |
| console.error('Fetch error:', err); | |
| } | |
| }, []); | |
| useEffect(() => { fetchState(); }, [fetchState]); | |
| // ββ Submit order (matches backend OrderRequest: factory, quantity, port, dest) ββ | |
| const submitOrder = useCallback(async () => { | |
| try { | |
| const res = await fetch(`${API}/order`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ factory, port, dest: destination, quantity }), | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json(); | |
| toast.error(err.detail || 'Order failed'); | |
| return; | |
| } | |
| toast.success(`Order: ${quantity} units via ${port}`); | |
| await fetchState(); | |
| } catch (err) { | |
| toast.error('Order failed'); | |
| } | |
| }, [factory, port, destination, quantity, fetchState]); | |
| // ββ Advance turn ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const advanceTurn = useCallback(async () => { | |
| setLoading(true); | |
| try { | |
| const res = await fetch(`${API}/advance`, { method: 'POST' }); | |
| const data = await res.json(); | |
| setState(data); | |
| toast.success(`Month ${data.turn} complete`); | |
| } catch (err) { | |
| toast.error('Advance failed'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, []); | |
| // ββ Reset βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const resetGame = useCallback(async () => { | |
| try { | |
| await fetch(`${API}/reset`, { method: 'POST' }); | |
| await fetchState(); | |
| toast.success('Simulation reset'); | |
| } catch (err) { | |
| toast.error('Reset failed'); | |
| } | |
| }, [fetchState]); | |
| // ββ Oracle ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const fetchOracle = useCallback(async () => { | |
| try { | |
| const res = await fetch(`${API}/oracle`); | |
| const data = await res.json(); | |
| setOracleAdvice(data.advice || 'No advice available'); | |
| } catch { | |
| setOracleAdvice('Oracle unavailable'); | |
| } | |
| }, []); | |
| useEffect(() => { if (state) fetchOracle(); }, [state, fetchOracle]); | |
| // ββ Auto-play βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const autoPlay = useCallback(async () => { | |
| setAutoPlaying(true); | |
| try { | |
| const res = await fetch(`${API}/autoplay`, { method: 'POST' }); | |
| const data = await res.json(); | |
| setState(data); | |
| toast.success('AI Auto-Play complete'); | |
| } catch { | |
| toast.error('Auto-play failed'); | |
| } finally { | |
| setAutoPlaying(false); | |
| } | |
| }, []); | |
| // ββ Export CSV βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const exportCSV = useCallback(() => { | |
| if (!state?.history?.length) return; | |
| const headers = ['Month', 'Revenue', 'Balance', 'Profit', 'Penalties']; | |
| const rows = state.history.map(h => [h.month, h.revenue, h.balance, h.profit, h.penalties]); | |
| const csv = [headers, ...rows].map(r => r.join(',')).join('\n'); | |
| const blob = new Blob([csv], { type: 'text/csv' }); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.setAttribute('hidden', ''); | |
| a.setAttribute('href', url); | |
| a.setAttribute('download', 'supply_chain_report.csv'); | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| toast.success('CSV Exported'); | |
| }, [state]); | |
| // ββ Decision preview ββββββββββββββββββββββββββββββββββββββββββ | |
| const preview = useMemo(() => { | |
| if (!state) return { cost: 0, eta: 2, affordable: true, costPerUnit: 0 }; | |
| const f = (state.factoryCosts || []).find(x => x.id === factory); | |
| const p = (state.portDelays || []).find(x => x.id === port); | |
| const unitCost = f ? f.currentCost : 50; | |
| const containers = Math.ceil(quantity / 100); | |
| const cost = unitCost * quantity + containers * 3000; | |
| const eta = p ? p.currentDelay + 1 : 2; | |
| const affordable = cost <= (state.balance || 0); | |
| return { cost, eta, affordable, costPerUnit: unitCost }; | |
| }, [factory, port, quantity, state]); | |
| // ββ History for charts ββββββββββββββββββββββββββββββββββββββββ | |
| const chartData = useMemo(() => { | |
| if (!state?.history) return []; | |
| return state.history.map(h => ({ | |
| name: `M${h.month}`, | |
| revenue: h.revenue || 0, | |
| balance: h.balance || 0, | |
| profit: h.profit || 0, | |
| })); | |
| }, [state]); | |
| // ββ Derived values ββββββββββββββββββββββββββββββββββββββββββββ | |
| const totalInventory = state ? Object.values(state.inventory || {}).reduce((a, b) => a + b, 0) : 0; | |
| const totalDemand = state ? Object.values(state.demands || {}).reduce((a, b) => a + b, 0) : 0; | |
| // ββ Render ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| return ( | |
| <> | |
| <Toaster position="top-right" toastOptions={{ | |
| style: { background: '#f7f6f2', color: '#1c1c1c', border: '1px solid #e5e4de', borderRadius: 2, fontSize: '0.85rem', fontFamily: "'Space Mono', monospace" } | |
| }} /> | |
| {/* Editorial background grid */} | |
| <div className="bg-grid" /> | |
| {/* Interactive Perlin noise waves β covers entire UI */} | |
| <InteractiveWavesBackground | |
| lineColor="rgba(61, 112, 104, 0.35)" | |
| backgroundColor="transparent" | |
| waveSpeedX={0.012} | |
| waveSpeedY={0.008} | |
| waveAmpX={40} | |
| waveAmpY={20} | |
| friction={0.92} | |
| tension={0.008} | |
| maxCursorMove={120} | |
| xGap={14} | |
| yGap={36} | |
| /> | |
| {/* Landing Page Sections */} | |
| <EditorialNavigation /> | |
| <div className="landing-page"> | |
| <Hero /> | |
| <StatsGrid /> | |
| <TextReveal /> | |
| <Workflow /> | |
| <UseCaseTabs /> | |
| <ContactForm /> | |
| </div> | |
| {/* Simulator Section */} | |
| <div id="simulator" className="simulator-section"> | |
| {!state ? ( | |
| <div className="loading-screen" style={{ minHeight: '60vh' }}> | |
| <img src="/logo.png" alt="TradeStorm" className="loading-logo" /> | |
| <div className="loading-spinner" /> | |
| <div className="loading-text">Connecting to simulation engine...</div> | |
| </div> | |
| ) : ( | |
| <> | |
| {/* βββ TOP BAR βββ */} | |
| <header className="top-bar"> | |
| <div className="top-bar-right" style={{ marginLeft: 'auto' }}> | |
| <span className="scenario-pill"> | |
| <Zap size={12} /> SCENARIO: {(state.scenario || 'normal').toUpperCase()} | |
| </span> | |
| <span className="month-badge">MONTH {state.turn || 1}</span> | |
| </div> | |
| </header> | |
| {/* βββ MAIN CONTENT βββ */} | |
| <div className="main-content"> | |
| {/* βββ SIDEBAR βββ */} | |
| <aside className="sidebar"> | |
| {/* Command Center */} | |
| <div className="panel"> | |
| <div className="panel-header"> | |
| <div className="panel-title"><Navigation size={16} /> Command Center</div> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label">Source Factory</label> | |
| <select className="form-select" value={factory} onChange={e => setFactory(e.target.value)}> | |
| {(state.factoryCosts || []).map(f => ( | |
| <option key={f.id} value={f.id}>{f.name} (${f.currentCost}/unit)</option> | |
| ))} | |
| </select> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label">Shipping Port</label> | |
| <select className="form-select" value={port} onChange={e => setPort(e.target.value)}> | |
| {(state.portDelays || []).map(p => ( | |
| <option key={p.id} value={p.id}>{p.name} ({p.currentDelay}mo delay){p.isClosed ? ' β CLOSED' : ''}</option> | |
| ))} | |
| </select> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label">Destination</label> | |
| <select className="form-select" value={destination} onChange={e => setDestination(e.target.value)}> | |
| {MAP_NODES.warehouses.map(w => ( | |
| <option key={w.id} value={w.id}>{w.name}</option> | |
| ))} | |
| </select> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label">Quantity</label> | |
| <input className="form-input" type="number" min="10" max="500" step="10" value={quantity} onChange={e => setQuantity(Number(e.target.value))} /> | |
| </div> | |
| {/* Decision Preview */} | |
| <div style={{ margin: '0.5rem 0' }}> | |
| <div className="form-label" style={{ marginBottom: 6 }}>Decision Preview</div> | |
| <div className="preview-grid"> | |
| <div className="preview-item"> | |
| <div className="label">Total Cost</div> | |
| <div className={`value ${preview.affordable ? 'affordable' : 'expensive'}`}>${preview.cost.toLocaleString()}</div> | |
| </div> | |
| <div className="preview-item"> | |
| <div className="label">ETA</div> | |
| <div className="value">{preview.eta} months</div> | |
| </div> | |
| <div className="preview-item"> | |
| <div className="label">Cost/Unit</div> | |
| <div className="value">${preview.costPerUnit}</div> | |
| </div> | |
| <div className="preview-item"> | |
| <div className="label">Affordable?</div> | |
| <div className={`value ${preview.affordable ? 'affordable' : 'expensive'}`}>{preview.affordable ? 'β Yes' : 'β No'}</div> | |
| </div> | |
| </div> | |
| </div> | |
| <button className="btn btn-primary btn-block" onClick={submitOrder}> | |
| <CheckCircle size={16} /> Approve Order | |
| </button> | |
| </div> | |
| {/* Actions */} | |
| <div className="panel"> | |
| <div className="panel-header"> | |
| <div className="panel-title"><Play size={16} /> Simulation</div> | |
| </div> | |
| <div className="flex flex-col gap-sm"> | |
| <button className="btn btn-success btn-block" onClick={advanceTurn} disabled={loading}> | |
| {loading ? <span className="loading-spinner" style={{ width: 16, height: 16, borderWidth: 2 }} /> : <Play size={16} />} | |
| Simulate Turn {(state.turn || 0) + 1} | |
| </button> | |
| <button className="btn btn-warning btn-block" onClick={autoPlay} disabled={autoPlaying}> | |
| <Zap size={16} /> {autoPlaying ? 'AI Thinking...' : 'AI Auto-Play'} | |
| </button> | |
| <div className="flex gap-sm"> | |
| <button className="btn btn-outline btn-sm" style={{ flex: 1 }} onClick={resetGame}> | |
| <RotateCcw size={14} /> Reset | |
| </button> | |
| <button className="btn btn-outline btn-sm" style={{ flex: 1 }} onClick={exportCSV}> | |
| <Download size={14} /> Export | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Alerts */} | |
| {(state.alerts || []).length > 0 && ( | |
| <div className="panel"> | |
| <div className="panel-header"> | |
| <div className="panel-title"><AlertTriangle size={16} /> Smart Alerts</div> | |
| </div> | |
| <div className="event-list"> | |
| {state.alerts.map((a, i) => ( | |
| <div key={i} className={`event-item ${a.type || 'info'}`}>{a.msg}</div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Events */} | |
| <div className="panel"> | |
| <div className="panel-header"> | |
| <div className="panel-title"><Activity size={16} /> Events</div> | |
| </div> | |
| <div className="event-list"> | |
| {(state.events || []).length === 0 && ( | |
| <div className="event-item info">No events yet. Start simulating!</div> | |
| )} | |
| {(state.events || []).slice(-6).reverse().map((ev, i) => ( | |
| <div key={i} className={`event-item ${ev.type || 'info'}`}>{ev.msg || ev.message || ev}</div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* In Transit */} | |
| <div className="panel"> | |
| <div className="panel-header"> | |
| <div className="panel-title"><Ship size={16} /> In Transit</div> | |
| </div> | |
| <div className="shipment-list"> | |
| {(state.inTransit || []).length === 0 && ( | |
| <div style={{ fontSize: '0.82rem', color: 'var(--text-muted)', textAlign: 'center', padding: '1rem 0' }}>No active shipments</div> | |
| )} | |
| {(state.inTransit || []).map((s, i) => ( | |
| <div key={i} className="shipment-item"> | |
| <span>{s.quantity} units β {(s.dest || '').toUpperCase()}</span> | |
| <span className={`shipment-badge ${s.delayLeft <= 0 ? 'arriving' : 'transit'}`}> | |
| {s.delayLeft <= 0 ? 'β Arriving' : `${s.delayLeft}mo left`} | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </aside> | |
| {/* Layout divider */} | |
| <div className="layout-divider" /> | |
| {/* βββ CENTER CONTENT βββ */} | |
| <div style={{ display: 'flex', flexDirection: 'column', overflowY: 'auto', height: '100%', minHeight: 0 }}> | |
| {/* Oracle Banner */} | |
| {oracleAdvice && ( | |
| <div className="oracle-banner fade-in"> | |
| <div className="oracle-title"><Zap size={14} /> AI Oracle Advisory</div> | |
| <div className="oracle-text">{oracleAdvice}</div> | |
| </div> | |
| )} | |
| {/* KPI Cards */} | |
| <div className="kpi-grid"> | |
| <div className="kpi-card"> | |
| <div className="kpi-header"> | |
| <span className="kpi-label">Balance</span> | |
| <div className="kpi-icon"> | |
| <DollarSign size={14} color="#3d7068" /> | |
| </div> | |
| </div> | |
| <div className="kpi-value">${(state.balance || 0).toLocaleString()}</div> | |
| </div> | |
| <div className="kpi-card"> | |
| <div className="kpi-header"> | |
| <span className="kpi-label">Benchmark</span> | |
| <div className="kpi-icon"> | |
| <TrendingUp size={14} color="#B4B4B4" /> | |
| </div> | |
| </div> | |
| <div className="kpi-value" style={{ color: '#B4B4B4' }}>${(state.benchmark_balance || 0).toLocaleString()}</div> | |
| </div> | |
| <div className="kpi-card"> | |
| <div className="kpi-header"> | |
| <span className="kpi-label">Inventory</span> | |
| <div className="kpi-icon"> | |
| <Package size={14} color="#b08d3a" /> | |
| </div> | |
| </div> | |
| <div className="kpi-value">{totalInventory}</div> | |
| </div> | |
| <div className="kpi-card"> | |
| <div className="kpi-header"> | |
| <span className="kpi-label">Demand</span> | |
| <div className="kpi-icon"> | |
| <Activity size={14} color="#1c1c1c" /> | |
| </div> | |
| </div> | |
| <div className="kpi-value">{totalDemand}</div> | |
| </div> | |
| </div> | |
| {/* Map */} | |
| <InteractiveMap state={state} /> | |
| {/* Charts */} | |
| <div className="charts-grid"> | |
| <div className="panel" style={{ borderRight: '1px solid #e5e4de' }}> | |
| <div className="panel-header"> | |
| <div className="panel-title"><BarChart2 size={16} /> Balance & Revenue</div> | |
| </div> | |
| <div className="chart-container"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <AreaChart data={chartData}> | |
| <CartesianGrid strokeDasharray="3 3" stroke="#e5e4de" /> | |
| <XAxis dataKey="name" stroke="#B4B4B4" fontSize={10} fontFamily="Space Mono" /> | |
| <YAxis stroke="#B4B4B4" fontSize={10} fontFamily="Space Mono" /> | |
| <Tooltip content={<ChartTooltip />} /> | |
| <Area type="monotone" dataKey="balance" stroke="#3d7068" fill="rgba(61,112,104,0.08)" strokeWidth={2} name="Balance" /> | |
| <Area type="monotone" dataKey="revenue" stroke="#b08d3a" fill="rgba(176,141,58,0.08)" strokeWidth={2} name="Revenue" /> | |
| </AreaChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| <div className="panel"> | |
| <div className="panel-header"> | |
| <div className="panel-title"><TrendingUp size={16} /> Profit per Turn</div> | |
| </div> | |
| <div className="chart-container"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <BarChart data={chartData}> | |
| <CartesianGrid strokeDasharray="3 3" stroke="#e5e4de" /> | |
| <XAxis dataKey="name" stroke="#B4B4B4" fontSize={10} fontFamily="Space Mono" /> | |
| <YAxis stroke="#B4B4B4" fontSize={10} fontFamily="Space Mono" /> | |
| <Tooltip content={<ChartTooltip />} /> | |
| <Bar dataKey="profit" fill="#3d7068" radius={[1,1,0,0]} name="Profit" /> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Warehouse Inventory Bars */} | |
| <div className="panel"> | |
| <div className="panel-header"> | |
| <div className="panel-title"><Factory size={16} /> Warehouse Levels</div> | |
| </div> | |
| <div className="flex flex-col gap-sm"> | |
| {MAP_NODES.warehouses.map(w => { | |
| const inv = state.inventory?.[w.id] || 0; | |
| const demand = state.demands?.[w.id] || 0; | |
| const pct = Math.min(100, (inv / Math.max(demand, 1)) * 100); | |
| const color = pct > 80 ? '#3d7068' : pct > 40 ? '#b08d3a' : '#a04040'; | |
| return ( | |
| <div key={w.id}> | |
| <div className="flex items-center justify-between" style={{ fontSize: '0.82rem' }}> | |
| <span style={{ color: 'var(--muted)' }}>{w.name} (demand: {demand})</span> | |
| <span className="text-mono" style={{ color, fontWeight: 600 }}>{inv} units</span> | |
| </div> | |
| <div className="inventory-bar-track"> | |
| <div className="inventory-bar-fill" style={{ width: `${pct}%`, background: color }} /> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Leaderboard */} | |
| {(state.leaderboard || []).length > 0 && ( | |
| <div className="panel"> | |
| <div className="panel-header"> | |
| <div className="panel-title"><BarChart2 size={16} /> Top Routes</div> | |
| </div> | |
| <div className="shipment-list"> | |
| {state.leaderboard.map((r, i) => ( | |
| <div key={i} className="shipment-item"> | |
| <span style={{ fontWeight: 600 }}>{i + 1}. {r.route}</span> | |
| <span style={{ fontSize: '0.78rem', color: 'var(--text-muted)' }}>${r.cost} | {r.time}mo | {r.reliability}%</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| <Footer /> | |
| </> | |
| ); | |
| } | |
| export default App; | |