/** * App — root layout and state composer of the dashboard. * * Owns auth + simulation snapshot state, renders the map canvas, the * agent windows, and all side panels, and handles error banners and * day-handoff/provider-failure packets from the backend. * * Architecture: consumed by main.jsx; renders SimCanvas, InfoBar, * ConversationFeed, EventsPanel, DebugPanel, RosterManager, LoginButton. * * Design: one component per concern, all fed from a single WebSocket * snapshot hook (useSimState). */ import { useState, useEffect, useCallback } from "react"; import useSimState from "./hooks/useSimState"; import { AuthProvider, useAuth } from "./hooks/useAuth"; import SimCanvas from "./components/SimCanvas"; import AgentWindow from "./components/AgentWindow"; import InfoBar from "./components/InfoBar"; import ConversationFeed from "./components/ConversationFeed"; import EventsPanel from "./components/EventsPanel"; import DebugPanel from "./components/DebugPanel"; import RosterManager from "./components/RosterManager"; import LoginButton from "./components/LoginButton"; import LogTerminal from "./components/LogTerminal"; import { apiUrl } from "./utils/api"; import "./App.css"; function compactTabPosition(index) { const side = index % 2; const row = Math.floor(index / 2); return { // right-hand card can then expand without its controls leaving the view. x: side ? Math.max(16, window.innerWidth - 276) : 16, y: 66 + row * 92, }; } export default function App() { return ( ); } function AppContent() { const { token, isAuthenticated } = useAuth(); const snapshot = useSimState(); const [renderSnapshot, setRenderSnapshot] = useState(null); const [agentMap, setAgentMap] = useState(null); const [focusedId, setFocusedId] = useState(null); const [expandedAgentIds, setExpandedAgentIds] = useState(() => new Set()); const [showDebug, setShowDebug] = useState(false); const [conversationFeedMinimized, setConversationFeedMinimized] = useState(false); const [controlError, setControlError] = useState(null); const [simulationRunning, setSimulationRunning] = useState(true); const [rosterOpen, setRosterOpen] = useState(false); const [showTerminal, setShowTerminal] = useState(false); useEffect(() => { if (!snapshot) return; // A handoff packet is a transient status message without `agents`. // Preserve the latest cards until the next full state arrives. if (snapshot.type === "day_reset" || snapshot.type === "reset") { setAgentMap(null); setFocusedId(null); setExpandedAgentIds(new Set()); return; } if (snapshot.agents) { setAgentMap(snapshot.agents); } // Handoff status packets intentionally omit a full world frame. Keep the // last complete frame rendered until the first new-day frame arrives. if (snapshot.tick != null && snapshot.time != null && snapshot.day != null) { setRenderSnapshot(snapshot); } if (typeof snapshot.simulation?.running === "boolean") { setSimulationRunning(snapshot.simulation.running); } }, [snapshot]); const providerFailure = snapshot?.status === "provider_failure" ? snapshot.failure : null; const simulationError = (snapshot?.status === "error" || providerFailure) ? snapshot.message : null; const agentIds = agentMap ? Object.keys(agentMap) : []; function toggleAgentInspector(agentId) { const isExpanded = expandedAgentIds.has(agentId); setExpandedAgentIds((previous) => { const next = new Set(previous); if (next.has(agentId)) next.delete(agentId); else next.add(agentId); return next; }); if (isExpanded && focusedId === agentId) setFocusedId(null); else if (!isExpanded) setFocusedId(agentId); } const focusFromMap = useCallback((agentId) => { setFocusedId(agentId); if (agentId) { setExpandedAgentIds((previous) => { if (previous.has(agentId)) return previous; const next = new Set(previous); next.add(agentId); return next; }); } }, []); async function sendControl(path, body) { try { setControlError(null); const headers = { "Content-Type": "application/json" }; if (token) headers["Authorization"] = `Bearer ${token}`; const response = await fetch(apiUrl(path), { method: "POST", headers, body: body ? JSON.stringify(body) : undefined, }); const data = await response.json(); if (!response.ok) throw new Error(data.error || data.detail || "Simulation control failed."); return data; } catch (error) { setControlError(error.message); return null; } } return (
{simulationError && (
{providerFailure?.title || "Simulation unavailable"}

{simulationError}

{providerFailure?.guidance || "Runtime data was left unchanged. Start a fresh simulation when you are ready."}
)} {controlError && (
Timeline control

{controlError}

)} setConversationFeedMinimized((value) => !value)} /> {showDebug && } setRosterOpen(false)} simulationRunning={simulationRunning} onError={setControlError} isAuthenticated={isAuthenticated} /> {isAuthenticated && showTerminal && setShowTerminal(false)} />} {agentIds.map((id, index) => { const expanded = expandedAgentIds.has(id); const expandedLayer = [...expandedAgentIds].indexOf(id); return ( toggleAgentInspector(id)} /> ); })} setShowDebug((value) => !value)} onFastForward={() => sendControl("/api/sim/fast-forward")} onSlowDown={() => sendControl("/api/sim/slow-down")} onRewind={(rewind) => sendControl("/api/sim/rewind", rewind)} simulationRunning={simulationRunning} onToggleSimulation={async () => { const result = await sendControl(simulationRunning ? "/api/sim/stop" : "/api/sim/start"); if (result && typeof result.running === "boolean") setSimulationRunning(result.running); }} onToggleRoster={() => setRosterOpen((value) => !value)} isAuthenticated={isAuthenticated} terminalOpen={showTerminal} onToggleTerminal={() => setShowTerminal((value) => !value)} />
); }