SAT / frontend /src /App.jsx
KUMARISHWETA242
Final Commit
03fa3e4
Raw
History Blame Contribute Delete
21.8 kB
import { useEffect, useRef, useState } from "react";
import Globe from "react-globe.gl";
import { motion } from "framer-motion";
import {
Activity,
Gauge,
Orbit,
Radio,
RefreshCcw,
Satellite,
Signal,
} 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}`;
}
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 response = await fetch(apiUrl("/api/ui/demo"));
if (!response.ok) {
throw new Error(`Snapshot request failed with ${response.status}`);
}
const data = await response.json();
setSnapshot(data);
setTaskName(data.task_name ?? "medium");
setError("");
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to load UI snapshot");
} finally {
setLoading(false);
}
}
async function resetDemo(nextTask) {
setLoading(true);
try {
const response = await fetch(apiUrl(`/api/ui/demo/reset?task_name=${encodeURIComponent(nextTask)}`), {
method: "POST",
});
if (!response.ok) {
throw new Error(`Reset failed with ${response.status}`);
}
const data = await response.json();
setSnapshot(data);
setTaskName(data.task_name ?? nextTask);
setError("");
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to reset demo");
} finally {
setLoading(false);
}
}
async function stepDemo() {
if (steppingRef.current || loading) {
return;
}
if (snapshot?.done || (snapshot?.step_count ?? 0) >= (snapshot?.max_steps ?? Number.POSITIVE_INFINITY)) {
setPlaying(false);
return;
}
steppingRef.current = true;
try {
const response = await fetch(apiUrl("/api/ui/demo/step"), { method: "POST" });
if (!response.ok) {
throw new Error(`Step failed with ${response.status}`);
}
const data = await response.json();
setSnapshot(data);
if (data.done || (data.step_count ?? 0) >= (data.max_steps ?? Number.POSITIVE_INFINITY)) {
setPlaying(false);
}
setError("");
} catch (err) {
setPlaying(false);
setError(err instanceof Error ? err.message : "Unable to advance demo");
} finally {
steppingRef.current = false;
}
}
useEffect(() => {
loadSnapshot();
}, []);
useEffect(() => {
if (!playing || loading || snapshot?.done || (snapshot?.step_count ?? 0) >= (snapshot?.max_steps ?? Number.POSITIVE_INFINITY)) {
return undefined;
}
const timer = window.setInterval(() => {
stepDemo();
}, STEP_INTERVAL_MS);
return () => window.clearInterval(timer);
}, [playing, loading, snapshot]);
return {
snapshot,
loading,
playing,
taskName,
error,
setPlaying,
resetDemo,
stepDemo,
};
}
function GlobeView({ snapshot }) {
const globeRef = useRef(null);
const [view, setView] = useState({ lat: 14, lng: 12, altitude: 2.35 });
const pathStoreRef = useRef([]);
const [pathData, setPathData] = useState([]);
useEffect(() => {
if (!globeRef.current) {
return;
}
const controls = globeRef.current.controls();
controls.autoRotate = false;
controls.enablePan = false;
controls.enableZoom = false;
controls.minDistance = 220;
controls.maxDistance = 320;
controls.minPolarAngle = Math.PI * 0.42;
controls.maxPolarAngle = Math.PI * 0.58;
const handleChange = () => {
const pov = globeRef.current?.pointOfView();
if (!pov) {
return;
}
const clamped = {
lat: Math.max(4, Math.min(24, pov.lat)),
lng: pov.lng,
altitude: 2.35,
};
if (
Math.abs(clamped.lat - pov.lat) > 0.01 ||
Math.abs(clamped.altitude - pov.altitude) > 0.01
) {
globeRef.current.pointOfView(clamped, 0);
}
};
controls.addEventListener("change", handleChange);
return () => {
controls.removeEventListener("change", handleChange);
};
}, []);
useEffect(() => {
if (!globeRef.current) {
return;
}
globeRef.current.pointOfView(view, 0);
}, [view]);
useEffect(() => {
const satellites = snapshot?.satellites ?? [];
const stepCount = snapshot?.step_count ?? 0;
if (!satellites.length) {
pathStoreRef.current = [];
setPathData([]);
return;
}
if (stepCount === 0) {
const nextPaths = [];
for (const satellite of satellites) {
const altitude = 0.11 + Math.min(satellite.altitude_km / 8000, 0.1);
const orbitPoints = (satellite.route ?? []).map((node) => ({
lat: node.latitude,
lng: node.longitude,
alt: altitude,
}));
if (orbitPoints.length > 1) {
nextPaths.push({
id: `orbit-${satellite.id}`,
color: satelliteColor(satellite.last_action),
points: orbitPoints,
});
}
nextPaths.push({
id: `trail-${satellite.id}`,
color: satelliteColor(satellite.last_action),
points: [
{
lat: satellite.latitude,
lng: satellite.longitude,
alt: altitude,
},
],
});
}
pathStoreRef.current = nextPaths;
} else {
const pathMap = new Map(pathStoreRef.current.map((path) => [path.id, path]));
for (const satellite of satellites) {
const orbitId = `orbit-${satellite.id}`;
const trailId = `trail-${satellite.id}`;
const color = satelliteColor(satellite.last_action);
const altitude = 0.11 + Math.min(satellite.altitude_km / 8000, 0.1);
const nextPoint = {
lat: satellite.latitude,
lng: satellite.longitude,
alt: altitude,
};
const orbitPath = pathMap.get(orbitId);
if (orbitPath) {
orbitPath.color = color;
}
const trailPath = pathMap.get(trailId);
if (!trailPath) {
pathMap.set(trailId, {
id: trailId,
color,
points: [nextPoint],
});
continue;
}
trailPath.color = color;
const lastPoint = trailPath.points[trailPath.points.length - 1];
const hasSamePoint =
lastPoint &&
Math.abs(lastPoint.lat - nextPoint.lat) < 0.0001 &&
Math.abs(lastPoint.lng - nextPoint.lng) < 0.0001;
if (!hasSamePoint) {
trailPath.points.push(nextPoint);
if (trailPath.points.length > 160) {
trailPath.points.shift();
}
}
}
pathStoreRef.current = [...pathMap.values()];
}
const pathMap = new Map(pathStoreRef.current.map((path) => [path.id, path]));
const activeTransferPaths = new Set();
for (const transfer of snapshot?.transfers ?? []) {
if (transfer.kind !== "downlink" && transfer.kind !== "capture") {
continue;
}
const pathId = `${transfer.kind}-${transfer.satellite_id}`;
activeTransferPaths.add(pathId);
const points = [
{
lat: transfer.from.latitude,
lng: transfer.from.longitude,
alt: transfer.kind === "downlink" ? 0.13 : 0.15,
},
{
lat: transfer.to.latitude,
lng: transfer.to.longitude,
alt: transfer.kind === "downlink" ? 0.015 : 0.012,
},
];
const existingPath = pathMap.get(pathId);
if (existingPath) {
existingPath.points.splice(0, existingPath.points.length, ...points);
existingPath.visible = true;
existingPath.color = transfer.kind === "downlink" ? "#7cf7c9" : "#ffb86b";
} else {
pathMap.set(pathId, {
id: pathId,
color: transfer.kind === "downlink" ? "#7cf7c9" : "#ffb86b",
points,
visible: true,
});
}
}
for (const path of pathMap.values()) {
if (
(String(path.id).startsWith("downlink-") || String(path.id).startsWith("capture-")) &&
!activeTransferPaths.has(path.id)
) {
path.visible = false;
}
}
pathStoreRef.current = [...pathMap.values()];
setPathData(pathStoreRef.current.filter((path) => path.visible !== false));
}, [snapshot]);
const satellites = (snapshot?.satellites ?? []).map((satellite) => ({
...satellite,
lat: satellite.latitude,
lng: satellite.longitude,
altitude: 0.1 + Math.min(satellite.altitude_km / 8000, 0.1),
type: "satellite",
color: satelliteColor(satellite.last_action),
size: 0.45,
label: `Sat ${satellite.id} · ${satellite.last_action}`,
}));
const groundStations = (snapshot?.ground_stations ?? []).map((station) => ({
...station,
lat: station.latitude,
lng: station.longitude,
altitude: 0.01,
type: "station",
color: "#8cffef",
size: 0.34,
label: `${station.name} · ${station.capacity.toFixed(1)} Gbps`,
}));
const captureRegions = (snapshot?.capture_regions ?? []).map((region) => ({
...region,
lat: region.latitude,
lng: region.longitude,
altitude: 0.009,
type: "region",
color: regionColor(region.cloud_cover),
size: 0.28,
label: `${region.name} · Cloud ${Math.round(Number(region.cloud_cover) * 100)}%`,
}));
const markerNodes = [...groundStations, ...captureRegions, ...satellites];
return (
<div className="globe-shell">
<div className="globe-backdrop" />
<div className="globe-frame">
<Globe
ref={globeRef}
width={760}
height={760}
backgroundColor="rgba(4, 11, 20, 0)"
backgroundImageUrl={STARFIELD}
globeImageUrl={EARTH_TEXTURE}
bumpImageUrl={EARTH_BUMP}
showAtmosphere
atmosphereColor="#7ec8ff"
atmosphereAltitude={0.18}
animateIn={false}
waitForGlobeReady={false}
pathsData={pathData}
pathPoints="points"
pathPointLat="lat"
pathPointLng="lng"
pathPointAlt="alt"
pathColor="color"
pathTransitionDuration={STEP_INTERVAL_MS * 0.82}
pathStroke={(path) => {
if (String(path.id).startsWith("orbit-")) {
return 0.7;
}
if (String(path.id).startsWith("trail-")) {
return 1.55;
}
if (String(path.id).startsWith("capture-")) {
return 1.0;
}
return 1.15;
}}
pathDashLength={(path) => {
if (String(path.id).startsWith("orbit-")) {
return 0;
}
if (String(path.id).startsWith("trail-")) {
return 0;
}
if (String(path.id).startsWith("capture-")) {
return 0.1;
}
return 0.22;
}}
pathDashGap={(path) => {
if (String(path.id).startsWith("downlink-")) {
return 0.14;
}
if (String(path.id).startsWith("capture-")) {
return 0.2;
}
return 0;
}}
pathDashAnimateTime={0}
htmlElementsData={markerNodes}
htmlLat="lat"
htmlLng="lng"
htmlAltitude="altitude"
htmlElement={(marker) => {
const element = document.createElement("div");
if (marker.type === "satellite") {
element.className = "globe-marker satellite";
} else if (marker.type === "region") {
element.className = "globe-marker region";
} else {
element.className = "globe-marker station";
}
element.style.setProperty("--marker-color", marker.color);
element.style.setProperty("--marker-size", `${marker.size}rem`);
element.setAttribute("title", marker.label);
element.innerHTML = `<span></span>`;
return element;
}}
/>
</div>
<div className="globe-overlay">
<div>
<p className="eyebrow">Orbital Flow Console</p>
<h1>Live tracking across satellites and ground stations.</h1>
</div>
<div className="overlay-metrics">
<div>
<span>Active satellites</span>
<strong>{snapshot?.satellites?.length ?? 0}</strong>
</div>
<div>
<span>Ground links</span>
<strong>{snapshot?.transfers?.filter((item) => item.kind === "downlink").length ?? 0}</strong>
</div>
</div>
</div>
</div>
);
}
function satelliteColor(action) {
if (action === "capture") {
return "#ffb86b";
}
if (action === "downlink") {
return "#7cf7c9";
}
if (action === "maintain") {
return "#9caaff";
}
return "#95a2bb";
}
function regionColor(cloudCover) {
const cloud = Number(cloudCover);
if (cloud <= 0.3) {
return "#88ffd1";
}
if (cloud <= 0.6) {
return "#ffc778";
}
return "#ff8f8f";
}
function App() {
const { snapshot, loading, playing, taskName, error, setPlaying, resetDemo, stepDemo } = useDemoData();
const weatherSummary = !snapshot?.weather_conditions
? []
: Object.entries(snapshot.weather_conditions).map(([region, cloudCover]) => ({
region,
cloudCover,
}));
return (
<div className="app-shell">
<div className="ambient ambient-a" />
<div className="ambient ambient-b" />
<header className="topbar">
<div className="brand">
<div className="brand-mark">
<Orbit size={18} />
</div>
<div>
<p className="eyebrow">OpenEnv Showcase</p>
<h2>Satellite mission tracking</h2>
</div>
</div>
<div className="task-switcher">
{TASKS.map((task) => (
<button
key={task}
type="button"
className={task === taskName ? "task-pill active" : "task-pill"}
onClick={() => resetDemo(task)}
>
{task}
</button>
))}
</div>
</header>
<main className="layout">
<section className="panel panel-main">
{loading ? <div className="loading-state">Loading orbital telemetry…</div> : <GlobeView snapshot={snapshot} />}
</section>
<section className="panel panel-side">
<div className="card controls-card">
<div className="card-head">
<div>
<p className="eyebrow">Mission Controls</p>
<h3>Playback</h3>
</div>
<Signal size={18} />
</div>
<div className="controls-row">
<button type="button" className="action-button primary" onClick={() => setPlaying(!playing)}>
{snapshot?.done ? "Playback complete" : playing ? "Pause loop" : "Resume loop"}
</button>
<button
type="button"
className="action-button"
onClick={() => stepDemo()}
disabled={loading || snapshot?.done || (snapshot?.step_count ?? 0) >= (snapshot?.max_steps ?? 0)}
>
Step once
</button>
</div>
<button type="button" className="ghost-button" onClick={() => resetDemo(taskName)}>
<RefreshCcw size={14} />
Reinitialize constellation
</button>
{error ? <p className="error-text">{error}</p> : null}
</div>
<div className="stats-grid">
<MetricCard icon={<Satellite size={18} />} label="Step" value={`${Math.min(snapshot?.step_count ?? 0, snapshot?.max_steps ?? 0)}/${snapshot?.max_steps ?? 0}`} />
<MetricCard icon={<Orbit size={18} />} label="Total reward" value={(snapshot?.total_reward ?? 0).toFixed(1)} />
<MetricCard icon={<Activity size={18} />} label="Last reward" value={(snapshot?.last_reward ?? 0).toFixed(1)} />
<MetricCard icon={<Gauge size={18} />} label="Tasks done" value={String(snapshot?.metrics?.tasks_completed ?? 0)} />
<MetricCard icon={<Radio size={18} />} label="Downlink units" value={(snapshot?.metrics?.downlink_units ?? 0).toFixed(1)} />
</div>
<div className="card">
<div className="card-head">
<div>
<p className="eyebrow">Fleet Status</p>
<h3>Satellites</h3>
</div>
</div>
<div className="list">
{(snapshot?.satellites ?? []).map((satellite) => (
<motion.div key={satellite.id} className="list-item" layout>
<div className="list-title">
<span>Sat {satellite.id}</span>
<span className={`status-badge ${satellite.last_action}`}>{satellite.last_action}</span>
</div>
<div className="meter-row">
<label>Battery</label>
<progress value={satellite.battery} max="100" />
<span>{satellite.battery.toFixed(0)}%</span>
</div>
<div className="meter-row">
<label>Storage</label>
<progress value={satellite.storage} max="100" />
<span>{satellite.storage.toFixed(0)}%</span>
</div>
<div className="meta-row">
<span>{satellite.altitude_km.toFixed(0)} km altitude</span>
<span>{satellite.longitude.toFixed(1)}° lon</span>
</div>
</motion.div>
))}
</div>
</div>
<div className="card">
<div className="card-head">
<div>
<p className="eyebrow">Network</p>
<h3>Ground stations & transfers</h3>
</div>
</div>
<div className="list condensed">
{(snapshot?.ground_stations ?? []).map((station) => (
<div key={station.id} className="compact-row">
<div>
<strong>{station.name}</strong>
<span>{station.latitude.toFixed(1)}°, {station.longitude.toFixed(1)}°</span>
</div>
<span>{station.capacity.toFixed(1)} Gbps</span>
</div>
))}
{(snapshot?.transfers ?? []).map((transfer) => (
<div key={transfer.id} className="compact-row active-transfer">
<div>
<strong>{transfer.kind === "downlink" ? "Downlink beam" : "Capture sweep"}</strong>
<span>
Sat {transfer.satellite_id} · {transfer.amount.toFixed(1)} units
{transfer.kind === "downlink" && transfer.ground_station_id !== undefined
? ` · GS ${transfer.ground_station_id}`
: ""}
{transfer.kind === "capture" && transfer.region ? ` · ${transfer.region}` : ""}
</span>
</div>
<span>{transfer.throughput.toFixed(1)} MB/s</span>
</div>
))}
</div>
</div>
<div className="card">
<div className="card-head">
<div>
<p className="eyebrow">Conditions</p>
<h3>Weather & queue</h3>
</div>
</div>
<div className="weather-grid">
{weatherSummary.map((item) => (
<div key={item.region} className="weather-card">
<span>{item.region}</span>
<strong>{Math.round(Number(item.cloudCover) * 100)}%</strong>
<small>cloud cover</small>
</div>
))}
</div>
<div className="task-list">
{(snapshot?.pending_tasks ?? []).slice(0, 5).map((task) => (
<div key={task.id} className="task-item">
<span>{task.id}</span>
<span>
{task.type.replaceAll("_", " ")}
{task.type === "image_capture" && task.region ? ` · ${task.region}` : ""}
{task.type === "data_downlink" && task.station !== undefined ? ` · GS ${task.station}` : ""}
</span>
</div>
))}
</div>
</div>
</section>
</main>
</div>
);
}
function MetricCard({ icon, label, value }) {
return (
<div className="metric-card">
<div className="metric-icon">{icon}</div>
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
export default App;