import { useEffect, useMemo, useRef, useState } from "react"; import Globe from "react-globe.gl"; import { motion, AnimatePresence } from "framer-motion"; import { Activity, CheckCircle2, Circle, Factory, Gauge, Package, RefreshCcw, Sun, Truck, Wrench, Zap, } from "lucide-react"; const TASKS = ["easy", "medium", "hard"]; const STEP_INTERVAL_MS = 2200; const explicitApiBase = import.meta.env.VITE_API_BASE?.replace(/\/$/, ""); const isLocalPreview = typeof window !== "undefined" && ["localhost", "127.0.0.1"].includes(window.location.hostname) && window.location.port === "4173"; const API_BASE = explicitApiBase || (isLocalPreview ? "http://127.0.0.1:8000" : ""); const EARTH_TEXTURE = "https://unpkg.com/three-globe/example/img/earth-blue-marble.jpg"; const EARTH_BUMP = "https://unpkg.com/three-globe/example/img/earth-topology.png"; const STARFIELD = "https://unpkg.com/three-globe/example/img/night-sky.png"; function apiUrl(path) { return `${API_BASE}${path}`; } // ─── data hook ─────────────────────────────────────────────────────────────── function useDemoData() { const [snapshot, setSnapshot] = useState(null); const [loading, setLoading] = useState(true); const [playing, setPlaying] = useState(true); const [taskName, setTaskName] = useState("medium"); const [error, setError] = useState(""); const steppingRef = useRef(false); async function loadSnapshot() { try { const res = await fetch(apiUrl("/api/ui/demo")); if (!res.ok) throw new Error(`Snapshot ${res.status}`); const data = await res.json(); setSnapshot(data); setTaskName(data.task_name ?? "medium"); setError(""); } catch (err) { setError(err instanceof Error ? err.message : "Unable to load snapshot"); } finally { setLoading(false); } } async function resetDemo(nextTask) { setLoading(true); try { const res = await fetch( apiUrl(`/api/ui/demo/reset?task_name=${encodeURIComponent(nextTask)}`), { method: "POST" } ); if (!res.ok) throw new Error(`Reset ${res.status}`); const data = await res.json(); setSnapshot(data); setTaskName(data.task_name ?? nextTask); setPlaying(true); setError(""); } catch (err) { setError(err instanceof Error ? err.message : "Unable to reset"); } finally { setLoading(false); } } async function stepDemo() { if (steppingRef.current || loading) return; if (snapshot?.done || (snapshot?.step_count ?? 0) >= (snapshot?.max_steps ?? Infinity)) { setPlaying(false); return; } steppingRef.current = true; try { const res = await fetch(apiUrl("/api/ui/demo/step"), { method: "POST" }); if (!res.ok) throw new Error(`Step ${res.status}`); const data = await res.json(); setSnapshot(data); if (data.done || (data.step_count ?? 0) >= (data.max_steps ?? Infinity)) setPlaying(false); setError(""); } catch (err) { setPlaying(false); setError(err instanceof Error ? err.message : "Unable to step"); } finally { steppingRef.current = false; } } useEffect(() => { loadSnapshot(); }, []); useEffect(() => { if (!playing || loading || snapshot?.done || (snapshot?.step_count ?? 0) >= (snapshot?.max_steps ?? Infinity)) return; const timer = window.setInterval(stepDemo, STEP_INTERVAL_MS); return () => window.clearInterval(timer); }, [playing, loading, snapshot]); return { snapshot, loading, playing, taskName, error, setPlaying, resetDemo, stepDemo }; } // ─── colour helpers ────────────────────────────────────────────────────────── function platformColor(action) { switch (action) { case "produce": return "#ffb86b"; case "assemble": return "#9caaff"; case "deliver": return "#7cf7c9"; case "recharge": return "#59b8ff"; default: return "#95a2bb"; } } function energyColor(v) { if (v < 20) return "#ff7171"; if (v < 40) return "#ffb86b"; return "#7cf7c9"; } // ─── reward sparkline ──────────────────────────────────────────────────────── function RewardSparkline({ history }) { if (!history || history.length < 2) return null; const W = 160, H = 36; const min = Math.min(...history); const max = Math.max(...history); const range = max - min || 1; const pts = history.map((v, i) => { const x = (i / (history.length - 1)) * W; const y = H - ((v - min) / range) * (H - 4) - 2; return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(" "); const zeroY = H - ((0 - min) / range) * (H - 4) - 2; return ( ); } // ─── Globe view ─────────────────────────────────────────────────────────────── function GlobeView({ snapshot, globeSize }) { const globeRef = useRef(null); const pathStoreRef = useRef([]); const snapAnimRef = useRef(null); const [pathData, setPathData] = useState([]); // Lock camera controls + snap back to equator after user interaction useEffect(() => { if (!globeRef.current) return; const controls = globeRef.current.controls(); controls.autoRotate = true; controls.autoRotateSpeed = 0.6; controls.enablePan = false; controls.enableZoom = false; controls.minDistance = 210; controls.maxDistance = 310; controls.minPolarAngle = Math.PI * 0.25; controls.maxPolarAngle = Math.PI * 0.75; const snapToEquator = () => { if (snapAnimRef.current) cancelAnimationFrame(snapAnimRef.current); const camera = globeRef.current.camera(); const tick = () => { const tx = controls.target.x, ty = controls.target.y, tz = controls.target.z; const dx = camera.position.x - tx; const dy = camera.position.y - ty; const dz = camera.position.z - tz; const r = Math.sqrt(dx * dx + dy * dy + dz * dz); const phi = Math.atan2(Math.sqrt(dx * dx + dz * dz), dy); // current polar angle const diff = Math.PI / 2 - phi; // distance from equator if (Math.abs(diff) < 0.002) { controls.update(); return; } const newPhi = phi + diff * 0.08; // ease toward equator const theta = Math.atan2(dz, dx); const sinPhi = Math.sin(newPhi); const cosPhi = Math.cos(newPhi); camera.position.set( tx + r * sinPhi * Math.cos(theta), ty + r * cosPhi, tz + r * sinPhi * Math.sin(theta), ); controls.update(); snapAnimRef.current = requestAnimationFrame(tick); }; snapAnimRef.current = requestAnimationFrame(tick); }; // Cancel snap if user grabs the globe again const cancelSnap = () => { if (snapAnimRef.current) cancelAnimationFrame(snapAnimRef.current); }; controls.addEventListener('end', snapToEquator); controls.addEventListener('start', cancelSnap); return () => { controls.removeEventListener('end', snapToEquator); controls.removeEventListener('start', cancelSnap); if (snapAnimRef.current) cancelAnimationFrame(snapAnimRef.current); }; }, []); // Build/update path data from snapshot useEffect(() => { const platforms = snapshot?.platforms ?? []; const stepCount = snapshot?.step_count ?? 0; if (!platforms.length) { pathStoreRef.current = []; setPathData([]); return; } if (stepCount === 0) { // Fresh reset — build all paths from scratch const next = []; for (const p of platforms) { const alt = 0.11 + Math.min(p.altitude_km / 8000, 0.1); const color = platformColor(p.last_action); // Full orbit ring const orbitPts = (p.route ?? []).map(n => ({ lat: n.latitude, lng: n.longitude, alt })); if (orbitPts.length > 1) next.push({ id: `orbit-${p.id}`, color, points: orbitPts }); // Moving trail next.push({ id: `trail-${p.id}`, color, points: [{ lat: p.latitude, lng: p.longitude, alt }] }); // Collapsed delivery arc (pre-create so it never pops in) next.push({ id: `deliver-${p.id}`, color: "#7cf7c9", points: [{ lat: p.latitude, lng: p.longitude, alt }, { lat: p.latitude, lng: p.longitude, alt: 0.01 }], active: false }); } pathStoreRef.current = next; } else { const pathMap = new Map(pathStoreRef.current.map(p => [p.id, p])); for (const p of platforms) { const alt = 0.11 + Math.min(p.altitude_km / 8000, 0.1); const color = platformColor(p.last_action); const pt = { lat: p.latitude, lng: p.longitude, alt }; // Update orbit ring colour const orbit = pathMap.get(`orbit-${p.id}`); if (orbit) orbit.color = color; // Extend trail const trail = pathMap.get(`trail-${p.id}`); if (trail) { trail.color = color; const last = trail.points[trail.points.length - 1]; const moved = !last || Math.abs(last.lat - pt.lat) > 0.0001 || Math.abs(last.lng - pt.lng) > 0.0001; if (moved) { trail.points.push(pt); if (trail.points.length > 120) trail.points.shift(); } } // Delivery arc — show when action is "deliver" const arc = pathMap.get(`deliver-${p.id}`); if (arc) { if (p.last_action === "deliver") { // Point to a notional ground station below arc.points = [{ lat: p.latitude, lng: p.longitude, alt }, { lat: 0, lng: p.longitude, alt: 0.01 }]; arc.color = "#7cf7c9"; arc.active = true; } else if (arc.active) { const start = arc.points[0] ?? pt; arc.points = [start, { ...start, alt: start.alt - 0.001 }]; arc.active = false; } } } pathStoreRef.current = [...pathMap.values()]; } setPathData([...pathStoreRef.current]); }, [snapshot]); // HTML marker nodes for platforms const markerNodes = useMemo(() => { return (snapshot?.platforms ?? []).map(p => ({ ...p, lat: p.latitude, lng: p.longitude, altitude: 0.1 + Math.min(p.altitude_km / 8000, 0.1), color: platformColor(p.last_action), size: 0.44, })); }, [snapshot]); return (
{ const id = String(path.id); if (id.startsWith("orbit-")) return 0.6; if (id.startsWith("trail-")) return 1.5; return 1.2; }} pathDashLength={path => { const id = String(path.id); return (id.startsWith("orbit-") || id.startsWith("trail-")) ? 0 : 0.05; }} pathDashGap={path => { const id = String(path.id); return (id.startsWith("orbit-") || id.startsWith("trail-")) ? 0 : 0.1; }} pathDashAnimateTime={path => { const id = String(path.id); return (id.startsWith("deliver-") && path.active) ? 1600 : 0; }} htmlElementsData={markerNodes} htmlLat="lat" htmlLng="lng" htmlAltitude="altitude" htmlElement={p => { const el = document.createElement("div"); el.className = "globe-marker platform"; el.style.setProperty("--marker-color", p.color); el.style.setProperty("--marker-size", `${p.size}rem`); el.innerHTML = `
Platform ${p.id}
Action ${p.last_action ?? "idle"}
Energy${p.energy?.toFixed(0) ?? "?"}%
Materials${p.material_stock?.toFixed(0) ?? "?"}
Components${p.component_stock?.toFixed(0) ?? "?"}
Products${p.product_stock ?? 0}
Alt${p.altitude_km?.toFixed(0) ?? "?"}km
`; return el; }} />

Orbital Manufacturing Console

Live platform tracking

Platforms {snapshot?.platforms?.length ?? 0}
Delivering {snapshot?.platforms?.filter(p => p.last_action === "deliver").length ?? 0}
); } // ─── agent stats ───────────────────────────────────────────────────────────── function AgentStats({ snapshot }) { const metrics = snapshot?.metrics ?? {}; const history = snapshot?.reward_history ?? []; const score = snapshot?.mission_score ?? 0; const step = snapshot?.step_count ?? 0; const maxStep = snapshot?.max_steps ?? 1; const pct = Math.min(100, Math.round((step / maxStep) * 100)); const lastReward = history.length ? history[history.length - 1] : 0; const avgReward = history.length ? (history.reduce((a, b) => a + b, 0) / history.length).toFixed(2) : "0.00"; const posSteps = history.filter(r => r > 0).length; const efficiency = history.length ? Math.round((posSteps / history.length) * 100) : 0; return (

RL Agent

Performance

0.7 ? "#7cf7c9" : score > 0.4 ? "#ffb86b" : "#ff8f8f"} strokeWidth="5" strokeLinecap="round" strokeDasharray={`${score * 78.5} 78.5`} /> {Math.round(score * 100)}% Mission Score
Avg reward/step = 0 ? "pos" : "neg"}> {Number(avgReward) >= 0 ? "+" : ""}{avgReward}
Positive steps{efficiency}%
Last step = 0 ? "pos" : "neg"}> {lastReward >= 0 ? "+" : ""}{lastReward.toFixed(2)}
Step rewards
Episode {step}/{maxStep} {pct}% complete
Produced: {metrics.production_runs ?? 0} Assembled: {metrics.assemblies_completed ?? 0} Delivered: {metrics.deliveries_completed ?? 0} On-time: {metrics.on_time_deliveries ?? 0} Invalid: {metrics.invalid_actions ?? 0}
); } // ─── platform fleet ────────────────────────────────────────────────────────── function PlatformFleet({ snapshot }) { const platforms = snapshot?.platforms ?? []; return (

Fleet

Platforms

{platforms.map(p => (
Platform {p.id} {p.last_action ?? "idle"}
{p.energy.toFixed(0)}%
{p.material_stock.toFixed(0)}
{p.component_stock.toFixed(0)}
{p.product_stock}
))}
); } // ─── delivery orders ───────────────────────────────────────────────────────── function DeliveryOrders({ snapshot }) { const windows = snapshot?.delivery_windows ?? []; const orders = snapshot?.pending_orders ?? []; const step = snapshot?.step_count ?? 0; return (

Logistics

Delivery Orders

{windows.slice(0, 8).map(w => { const urgency = w.deadline - step; const isUrgent = urgency <= 10; return (
Order #{w.order_id} — {w.product_type} Deadline: step {w.deadline} ({urgency > 0 ? `${urgency} left` : "overdue"})
{isUrgent ? "URGENT" : `${urgency}s`}
); })} {windows.length === 0 && (

All deliveries complete

)}
{(snapshot?.metrics?.deliveries_completed ?? 0)} delivered {orders.length} pending orders
); } // ─── solar conditions ───────────────────────────────────────────────────────── function SolarConditions({ snapshot }) { const solar = snapshot?.solar_conditions ?? {}; return (

Power

Solar Conditions

{Object.entries(solar).map(([zone, irr]) => (
{zone.replace(/_/g, " ")} {Math.round(Number(irr) * 100)}%
))}
); } // ─── main app ───────────────────────────────────────────────────────────────── export default function App() { const { snapshot, loading, playing, taskName, error, setPlaying, resetDemo, stepDemo } = useDemoData(); const [globeSize, setGlobeSize] = useState(540); const panelRef = useRef(null); useEffect(() => { function resize() { if (!panelRef.current) return; const { width, height } = panelRef.current.getBoundingClientRect(); setGlobeSize(Math.floor(Math.min(width, height, 620))); } resize(); window.addEventListener("resize", resize); return () => window.removeEventListener("resize", resize); }, []); const score = snapshot?.mission_score ?? 0; const scoreStr = `${Math.round(score * 100)}%`; const scoreClass = score > 0.7 ? "good" : score > 0.4 ? "warn" : "bad"; return (
{/* ── header ── */}

OpenEnv · RL Benchmark

Space Manufacturing Control

{TASKS.map(t => ( ))}
Mission {scoreStr}
{error &&
{error}
} {/* ── main grid ── */}
{/* Globe panel */}
{loading ?
Loading orbital telemetry…
: }
{/* Middle column */}
{/* Right column */}
); }