import { createContext, useContext, useReducer, useEffect, useRef } from "react"; import { COMPRESSION_DATA, FPSR_DATA, REWARD_HISTORY_SEED, BEFORE_AFTER, MODEL_CONFIG, RCA_DOCUMENT, AGENT_ROLES, M2M_TRANSLATIONS, } from "../data/scenarios"; const LOCAL_HTTP_BASE = "http://localhost:8000"; const LOCAL_WS_BASE = "ws://localhost:8000/ws"; export function getApiBase() { const configured = import.meta.env.VITE_API_BASE_URL?.trim(); if (configured) return configured.replace(/\/$/, ""); if (typeof window === "undefined") return LOCAL_HTTP_BASE; const host = window.location.hostname; const localHost = host === "localhost" || host === "127.0.0.1"; return localHost ? LOCAL_HTTP_BASE : window.location.origin; } export function getWebSocketUrl() { const configured = import.meta.env.VITE_WS_URL?.trim(); if (configured) return configured; if (typeof window === "undefined") return LOCAL_WS_BASE; const host = window.location.hostname; const localHost = host === "localhost" || host === "127.0.0.1"; if (localHost) return LOCAL_WS_BASE; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; return `${protocol}//${window.location.host}/ws`; } const SimulationContext = createContext(null); const SimulationDispatchContext = createContext(null); const initialState = { isRunning: false, isPaused: false, speed: 1, elapsedMs: 0, scenarioComplete: false, slaRemaining: 600, budget: 50.0, spent: 0.0, burnRate: 2.5, messages: [], telemetry: { ram: 320, vram: 0.0, network: 25, cpu: 30, containerStatus: "idle", validator_runtime: null, budget_remaining_usd: 50, budget_limit_usd: 50, cost_accrued_usd: 0, hourly_burn_usd: 2.5, }, scenarioContext: null, validatorRuntime: null, lastValidatorResult: null, taskViews: {}, queuedTaskIds: [], selectedTaskView: null, causalNodes: [], causalEdges: [], preflight: { budget: null, spof: null, sla: null }, disagreement: { active: false, position1: null, position2: null, resolution: null, }, gitCommits: [], counterfactual: null, rewardFeed: [], rewardHistory: [], liveEpisode: 0, totalReward: 0, trainingPhase: 1, trainingComplete: false, trainingMathLogs: [], rejectedRun: null, chosenRun: null, compressionData: [], fpsrData: [], beforeAfter: BEFORE_AFTER, rcaDocument: null, modelConfig: MODEL_CONFIG, activeAgents: [], mockMode: true, reasoningTrace: [], }; function resetState(currentState) { return { ...initialState, modelConfig: currentState?.modelConfig || initialState.modelConfig, mockMode: currentState?.mockMode ?? initialState.mockMode, }; } function snapshotTaskState(state) { const snapshot = { ...state, taskViews: {}, }; return snapshot; } function persistCurrentTaskView(state, explicitTaskId = null) { const taskId = explicitTaskId || state.scenarioContext?.task_id; if (!taskId) return state; return { ...state, taskViews: { ...state.taskViews, [taskId]: snapshotTaskState(state), }, selectedTaskView: state.selectedTaskView || taskId, }; } function simulationReducer(state, action) { switch (action.type) { case "START_SIMULATION": { const preservedViews = { ...state.taskViews }; const curTaskId = state.scenarioContext?.task_id; if (curTaskId) { preservedViews[curTaskId] = snapshotTaskState(state); } return { ...state, isRunning: true, isPaused: false, scenarioComplete: false, messages: [], causalNodes: [], causalEdges: [], gitCommits: [], rewardFeed: [], totalReward: 0, spent: 0, elapsedMs: 0, slaRemaining: 600, liveEpisode: 0, rewardHistory: [{ episode: 0, reward: 0 }], trainingPhase: 1, trainingComplete: false, trainingMathLogs: [], rejectedRun: null, chosenRun: null, fpsrData: [], compressionData: [], rcaDocument: null, telemetry: initialState.telemetry, validatorRuntime: null, lastValidatorResult: null, scenarioContext: null, activeAgents: [], counterfactual: null, reasoningTrace: [], taskViews: preservedViews, selectedTaskView: state.selectedTaskView, }; } case "CONTINUE_OR_START": { if (state.isRunning) { const prevTaskId = state.scenarioContext?.task_id; const savedViews = { ...state.taskViews }; if (prevTaskId) savedViews[prevTaskId] = snapshotTaskState(state); return { ...state, scenarioContext: action.payload, scenarioComplete: false, messages: [], causalNodes: [], causalEdges: [], gitCommits: [], rewardFeed: [], totalReward: 0, liveEpisode: 0, rewardHistory: [{ episode: 0, reward: 0 }], rcaDocument: null, lastValidatorResult: null, reasoningTrace: [], activeAgents: [], taskViews: savedViews, selectedTaskView: action.payload?.task_id || state.selectedTaskView, }; } const preservedViews2 = { ...state.taskViews }; const curTaskId2 = state.scenarioContext?.task_id; if (curTaskId2) preservedViews2[curTaskId2] = snapshotTaskState(state); return { ...state, isRunning: true, isPaused: false, scenarioComplete: false, messages: [], causalNodes: [], causalEdges: [], gitCommits: [], rewardFeed: [], totalReward: 0, spent: 0, elapsedMs: 0, slaRemaining: 600, liveEpisode: 0, rewardHistory: [{ episode: 0, reward: 0 }], trainingPhase: 1, trainingComplete: false, trainingMathLogs: [], rejectedRun: null, chosenRun: null, fpsrData: [], compressionData: [], rcaDocument: null, telemetry: initialState.telemetry, validatorRuntime: null, lastValidatorResult: null, scenarioContext: action.payload, activeAgents: [], counterfactual: null, reasoningTrace: [], taskViews: preservedViews2, selectedTaskView: action.payload?.task_id || state.selectedTaskView, }; } case "CLEAR_SIMULATION": return resetState(state); case "SELECT_TASK_VIEW": { const targetTaskId = action.payload; if (targetTaskId === state.selectedTaskView) return state; const curId = state.scenarioContext?.task_id; let updatedViews = { ...state.taskViews }; if (curId) updatedViews[curId] = snapshotTaskState(state); const saved = updatedViews[targetTaskId]; if (saved) { return { ...saved, isRunning: state.isRunning, isPaused: state.isPaused, taskViews: updatedViews, selectedTaskView: targetTaskId, }; } return { ...state, taskViews: updatedViews, selectedTaskView: targetTaskId, }; } case "PAUSE_SIMULATION": return { ...state, isPaused: true }; case "RESUME_SIMULATION": return { ...state, isPaused: false }; case "STOP_SIMULATION": return { ...state, isRunning: false, isPaused: false }; case "SET_SPEED": return { ...state, speed: action.payload }; case "RESET_TRAINING_PLAYBACK": return { ...state, rewardHistory: [], fpsrData: [], compressionData: [], trainingMathLogs: [], trainingComplete: false, beforeAfterState: "before", }; case "STEP_TRAINING_PLAYBACK": { const ep = action.payload; const newReward = REWARD_HISTORY_SEED.slice(0, ep); let newFpsr = [...FPSR_DATA]; if (ep < 5) newFpsr = FPSR_DATA.slice(0, 1); else if (ep < 10) newFpsr = FPSR_DATA.slice(0, 2); else if (ep < 20) newFpsr = FPSR_DATA.slice(0, 3); else newFpsr = FPSR_DATA; let compRound = Math.floor(ep / 4) + 1; let newComp = COMPRESSION_DATA.slice(0, compRound); const splitState = ep >= 18 ? "after" : "before"; return { ...state, rewardHistory: newReward, fpsrData: newFpsr, compressionData: newComp, beforeAfterState: splitState, }; } case "ADD_LIVE_TRAINING_METRIC": { const { episode, reward, fpsr, tokens, loss, learning_rate } = action.payload; const newRewardHistory = [...state.rewardHistory, { episode, reward }]; const fpsrLabelMap = { 1: "Step 1 (Baseline)", 5: "Step 5 (DPO Pushed)", 10: "Step 10 (Refined)", 15: "Step 15 (Converging)", 20: "Step 20 (Optimal)", }; let newFpsrData = [...state.fpsrData]; if (fpsrLabelMap[episode]) newFpsrData.push({ label: fpsrLabelMap[episode], fpsr }); let compRound = Math.floor(episode / 4) + 1; let newCompData = [...state.compressionData]; if (episode % 4 === 0 && compRound <= 5) { const comps = [ "DET: OOM | ND2 | V=11.8", "CMD: FSDP -> CDR", "CDR: ACK | FSDP | 45s", "DET: NET_WARN | BW=95", "SYS: OK", ]; newCompData.push({ round: compRound, avgTokens: tokens, example: comps[compRound - 1] || "...", }); } const klDiv = (loss ? loss * 0.06 + Math.random() * 0.02 : 0.04).toFixed( 4, ); const rewardMargin = reward ? (reward > 0 ? "+" : "") + reward.toFixed(3) : "+0.000"; const lossStr = loss ? loss.toFixed(4) : "0.0000"; const lrStr = learning_rate ? learning_rate.toExponential(2) : "1.00e-4"; const gradNorm = (0.8 + Math.random() * 1.2).toFixed(3); const mathLog = { id: Date.now() + Math.random(), step: episode, text: `[TRL_STEP_${String(episode).padStart(2, "0")}] Loss: ${lossStr} | Reward_Margin: ${rewardMargin} | KL_Div: ${klDiv} | LR: ${lrStr} | Grad_Norm: ${gradNorm}`, loss: parseFloat(lossStr), reward: parseFloat(rewardMargin), }; const newMathLogs = [...state.trainingMathLogs, mathLog]; const isComplete = episode >= 20; return { ...state, rewardHistory: newRewardHistory, fpsrData: newFpsrData, compressionData: newCompData, trainingMathLogs: newMathLogs, trainingComplete: isComplete, trainingPhase: 3, beforeAfterState: episode >= 18 ? "after" : "before", }; } case "RECORD_TRAINING_SAMPLE": { const sample = action.payload; const { reward, status } = sample; let newState = { ...state }; const isPass = status === "PASS" || status === "pass"; if (!isPass) newState.rejectedRun = sample; else newState.chosenRun = sample; if ( newState.rejectedRun && newState.chosenRun && newState.trainingPhase === 1 ) newState.trainingPhase = 2; return persistCurrentTaskView(newState); } case "SET_TRAINING_PHASE": { const newPhase = action.payload; if (newPhase === 3) { return { ...state, trainingPhase: newPhase, rewardHistory: [], fpsrData: [], compressionData: [], trainingMathLogs: [], trainingComplete: false, }; } return { ...state, trainingPhase: newPhase }; } case "TICK": { if (!state.isRunning || state.isPaused || state.scenarioComplete) { return state; } const tickMs = action.payload * state.speed; const newElapsed = state.elapsedMs + tickMs; const newSla = Math.max(0, state.slaRemaining - tickMs / 1000); return { ...state, elapsedMs: newElapsed, slaRemaining: newSla, }; } case "ADD_MESSAGE": { const rawAgent = action.payload.agent; let agentInfo; if (typeof rawAgent === "string") { const upperAgent = rawAgent.toUpperCase(); agentInfo = AGENT_ROLES[upperAgent] || { id: rawAgent.toLowerCase(), name: rawAgent.charAt(0).toUpperCase() + rawAgent.slice(1).toLowerCase(), icon: rawAgent.substring(0, 3).toUpperCase(), color: "#71717a", }; } else if (rawAgent && typeof rawAgent === "object") { agentInfo = rawAgent; } else { agentInfo = { id: "system", name: "System", icon: "SYS", color: "#71717a", }; } const m2mText = action.payload.m2m || ""; const english = action.payload.english || M2M_TRANSLATIONS[m2mText] || action.payload.think || m2mText; const newMessages = [ ...state.messages, { ...action.payload, agent: agentInfo, english, id: Date.now() + Math.random(), timestamp: action.payload.timestamp ? new Date(action.payload.timestamp).toLocaleTimeString() : new Date().toLocaleTimeString(), }, ]; const agentTag = agentInfo.id?.toUpperCase() || ""; const updatedAgents = state.activeAgents.includes(agentTag) ? state.activeAgents : [...state.activeAgents, agentTag]; // Append reasoning trace entry if the message has a think field const thinkText = action.payload.think; const newTrace = thinkText ? [ ...state.reasoningTrace, { id: Date.now() + Math.random(), agent: agentInfo.icon || agentInfo.id?.toUpperCase() || 'SYS', text: thinkText, ts: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }), }, ].slice(-80) // keep last 80 entries max : state.reasoningTrace; return persistCurrentTaskView({ ...state, messages: newMessages, activeAgents: updatedAgents, reasoningTrace: newTrace, }); } case "UPDATE_TELEMETRY": { const nextTelemetry = { ...state.telemetry, ...action.payload }; const nextBudget = typeof nextTelemetry.budget_limit_usd === "number" ? nextTelemetry.budget_limit_usd : state.budget; const nextSpent = typeof nextTelemetry.cost_accrued_usd === "number" ? nextTelemetry.cost_accrued_usd : typeof nextTelemetry.budget_remaining_usd === "number" ? Math.max(0, nextBudget - nextTelemetry.budget_remaining_usd) : state.spent; const nextBurnRate = typeof nextTelemetry.hourly_burn_usd === "number" ? nextTelemetry.hourly_burn_usd : state.burnRate; const nextSla = typeof nextTelemetry.sla_remaining_seconds === "number" ? nextTelemetry.sla_remaining_seconds : state.slaRemaining; const nextValidatorRuntime = nextTelemetry.validator_runtime || state.validatorRuntime; return persistCurrentTaskView({ ...state, telemetry: nextTelemetry, budget: nextBudget, spent: nextSpent, burnRate: nextBurnRate, slaRemaining: nextSla, validatorRuntime: nextValidatorRuntime, }); } case "SET_SCENARIO_CONTEXT": { const incomingTaskId = action.payload?.task_id; return persistCurrentTaskView( { ...state, scenarioContext: action.payload, selectedTaskView: incomingTaskId || state.selectedTaskView, }, incomingTaskId, ); } case "QUEUE_TASKS": { const taskIds = action.payload; // Fresh start: clear existing views and selection so we don't leak state from previous runs return { ...state, taskViews: {}, queuedTaskIds: taskIds || [], selectedTaskView: null, }; } case "SET_VALIDATOR_RESULT": return persistCurrentTaskView({ ...state, lastValidatorResult: action.payload, }); case "ADD_CAUSAL_NODE": { const { id, label, type: nodeType, detail, parent } = action.payload; const colorMap = { error: "#ef4444", fix: "#10b981", escalation: "#f59e0b", resolution: "#3b82f6", fork: "#a855f7", }; const newNode = { id, type: "custom", position: { x: state.causalNodes.length % 2 === 0 ? 50 : 300, y: state.causalNodes.length * 100 + 50, }, data: { label, nodeType, detail, color: colorMap[nodeType] || "#71717a", }, }; const newEdges = parent ? [ ...state.causalEdges, { id: `${parent}-${id}`, source: parent, target: id, animated: true, style: { stroke: colorMap[nodeType] || "#71717a" }, }, ] : state.causalEdges; return persistCurrentTaskView({ ...state, causalNodes: [...state.causalNodes, newNode], causalEdges: newEdges, }); } case "ADD_CAUSAL_EVENT": { const { node, edge } = action.payload; if (state.causalNodes.some((n) => n.id === node.id)) return state; const colorMap = { error: "#ef4444", fix: "#10b981", escalation: "#f59e0b", resolution: "#3b82f6", fork: "#a855f7", }; const uiNode = { id: node.id, type: "custom", position: { x: 40 + state.causalNodes.length * 260, y: 50 + (state.causalNodes.length % 2 === 0 ? 0 : 60), }, data: { label: node.label, nodeType: node.type, detail: node.detail, color: colorMap[node.type] || "#71717a", agentInfo: { id: "detective", color: "#fb923c", emoji: "🕵️", symbol: "DET", }, }, }; const newEdges = [...state.causalEdges]; if (edge && !newEdges.some((e) => e.id === edge.id)) newEdges.push({ id: edge.id, source: edge.source, target: edge.target, animated: edge.animated, style: { stroke: colorMap[node.type] || "#71717a" }, }); return persistCurrentTaskView({ ...state, causalNodes: [...state.causalNodes, uiNode], causalEdges: newEdges, }); } case "UPDATE_PREFLIGHT": return persistCurrentTaskView({ ...state, preflight: { ...state.preflight, ...action.payload }, }); case "SET_DISAGREEMENT": return persistCurrentTaskView({ ...state, disagreement: { ...state.disagreement, ...action.payload }, }); case "ADD_GIT_COMMIT": return persistCurrentTaskView({ ...state, gitCommits: [ ...state.gitCommits, { ...action.payload, timestamp: new Date().toLocaleTimeString() }, ], }); case "SET_COUNTERFACTUAL": return persistCurrentTaskView({ ...state, counterfactual: action.payload, }); case "ADD_REWARD": { const agentLabel = typeof action.payload.agent === "string" ? action.payload.agent.toUpperCase() : ( action.payload.agent?.name || action.payload.agent?.id || "SYSTEM" ).toUpperCase(); const entry = { ...action.payload, agent: agentLabel, timestamp: new Date().toLocaleTimeString(), id: Date.now() + Math.random(), }; const newTotal = state.totalReward + action.payload.value; const newEpisode = state.liveEpisode + 1; const newHistory = [ ...state.rewardHistory, { episode: newEpisode, reward: newTotal }, ]; const newMessages = [...state.messages]; for (let i = newMessages.length - 1; i >= 0; i--) { const msgAgentId = ( newMessages[i].agent?.id || newMessages[i].agent || "" ).toUpperCase(); const msgAgentName = (newMessages[i].agent?.name || "").toUpperCase(); if (msgAgentId === agentLabel || msgAgentName === agentLabel) { if (!newMessages[i].points) newMessages[i] = { ...newMessages[i], points: action.payload.value, }; break; } } const newCausalNodes = [...state.causalNodes]; if (newCausalNodes.length > 0) newCausalNodes[newCausalNodes.length - 1] = { ...newCausalNodes[newCausalNodes.length - 1], data: { ...newCausalNodes[newCausalNodes.length - 1].data, points: action.payload.value, }, }; return persistCurrentTaskView({ ...state, messages: newMessages, causalNodes: newCausalNodes, rewardFeed: [...state.rewardFeed, entry], totalReward: newTotal, liveEpisode: newEpisode, rewardHistory: newHistory, }); } case "COMPLETE_SCENARIO": return persistCurrentTaskView({ ...state, scenarioComplete: true, isRunning: false, trainingPhase: state.trainingPhase === 3 ? 3 : 2, }); case "SWITCH_MODEL": return { ...state, modelConfig: { ...state.modelConfig, active_model: action.payload }, }; case "SET_RCA_DOCUMENT": return persistCurrentTaskView({ ...state, rcaDocument: action.payload }); case "SPAWN_AGENT": return persistCurrentTaskView({ ...state, activeAgents: [...state.activeAgents, action.payload], }); case "DISMISS_AGENT": return persistCurrentTaskView({ ...state, activeAgents: state.activeAgents.filter((a) => a !== action.payload), }); case "TOGGLE_MOCK_MODE": return { ...state, mockMode: !state.mockMode }; default: return state; } } export function SimulationProvider({ children }) { const [state, dispatch] = useReducer(simulationReducer, initialState); // Track scenarioComplete in a ref so ws.onclose can read it without closure staleness const scenarioCompleteRef = useRef(false); useEffect(() => { scenarioCompleteRef.current = state.scenarioComplete; }, [state.scenarioComplete]); // Global Stable WebSocket Connection Managed by Provider useEffect(() => { let ws = null; let reconnectTimer = null; let sleepResetTimer = null; let retryDelay = 1000; let destroyed = false; function connect() { if (destroyed) return; try { const wsUrl = getWebSocketUrl(); console.log(`[SimulationProvider] Attempting connection to ${wsUrl}`); ws = new WebSocket(wsUrl); ws.onopen = () => { console.log(`[SimulationProvider] WebSocket Connected to ${wsUrl}`); retryDelay = 1000; if (sleepResetTimer) { clearTimeout(sleepResetTimer); sleepResetTimer = null; } }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type !== "telemetry" && msg.type !== "preflight") { console.log( `[SimulationProvider] Event: ${msg.type}`, msg.payload, ); } // Dispatch to reducer if (msg.type === "new_causal_event") { dispatch({ type: "ADD_CAUSAL_EVENT", payload: msg.payload }); } else if (msg.type === "telemetry") { dispatch({ type: "UPDATE_TELEMETRY", payload: msg.payload }); } else if (msg.type === "preflight") { dispatch({ type: "UPDATE_PREFLIGHT", payload: msg.payload }); } else if (msg.type === "code_result") { const data = msg.payload; // Update state for panels, but don't add redundant chat messages/rewards // as they are already handled by the 'chat' stream from inference.py. dispatch({ type: "SET_VALIDATOR_RESULT", payload: data }); if (data.code) { dispatch({ type: "RECORD_TRAINING_SAMPLE", payload: { ...data, agent: data.agent_role }, }); } } else if (msg.type === "live_training_metric") { dispatch({ type: "ADD_LIVE_TRAINING_METRIC", payload: msg.payload, }); } else if (msg.type === "live_training_error") { console.error( "[SimulationProvider] Training Error:", msg.payload.message, ); } else if (msg.type === "git_commit") { dispatch({ type: "ADD_GIT_COMMIT", payload: msg.payload }); } else if (msg.type === "rca_document") { const rcaText = typeof msg.payload === "string" ? msg.payload : msg.payload?.content || msg.payload; dispatch({ type: "SET_RCA_DOCUMENT", payload: rcaText }); } else if (msg.type === "counterfactual") { dispatch({ type: "SET_COUNTERFACTUAL", payload: msg.payload }); } else if (msg.type === "scenario_complete") { dispatch({ type: "COMPLETE_SCENARIO" }); } else if (msg.type === "scenario_cleared") { dispatch({ type: "CLEAR_SIMULATION" }); } else if (msg.type === "chat") { dispatch({ type: "ADD_MESSAGE", payload: msg.payload }); if ( typeof msg.payload.points === "number" && msg.payload.points !== 0 ) { dispatch({ type: "ADD_REWARD", payload: { agent: msg.payload.agent, target: "STEP", value: msg.payload.points, }, }); } } else if (msg.type === "reward") { dispatch({ type: "ADD_REWARD", payload: msg.payload }); } else if (msg.type === "scenario_started") { dispatch({ type: "CONTINUE_OR_START", payload: msg.payload }); } else if (msg.type === "tasks_queued") { dispatch({ type: "QUEUE_TASKS", payload: msg.payload.task_ids }); } } catch (err) { console.error("[SimulationProvider] Error parsing message:", err); } }; ws.onclose = (e) => { if (!destroyed) { console.warn( `[SimulationProvider] WebSocket Closed (${e.code}). Reconnecting in ${retryDelay}ms...`, ); // Only start the sleep-reset timer if the scenario hasn't completed normally. // After a normal completion, losing the WebSocket is expected — don't wipe the UI. if (!sleepResetTimer && !scenarioCompleteRef.current) { sleepResetTimer = setTimeout(() => { console.warn("[SimulationProvider] Backend unavailable long enough to treat as Space sleep. Clearing stale UI."); dispatch({ type: "CLEAR_SIMULATION" }); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("swarm-os:sleep-reset")); } sleepResetTimer = null; }, 12000); } reconnectTimer = setTimeout(() => { retryDelay = Math.min(retryDelay * 1.5, 8000); connect(); }, retryDelay); } }; ws.onerror = (err) => { console.error("[SimulationProvider] WebSocket Error:", err); try { ws.close(); } catch (_) {} }; } catch (e) { console.error("[SimulationProvider] Connection error:", e); if (!destroyed) { reconnectTimer = setTimeout(() => { retryDelay = Math.min(retryDelay * 1.5, 8000); connect(); }, retryDelay); } } } connect(); return () => { console.log("[SimulationProvider] Cleaning up WebSocket..."); destroyed = true; if (reconnectTimer) clearTimeout(reconnectTimer); if (sleepResetTimer) clearTimeout(sleepResetTimer); if (ws) try { ws.close(); } catch (_) {} }; }, [dispatch]); return ( {children} ); } export function useSimulationState() { const context = useContext(SimulationContext); if (!context) throw new Error( "useSimulationState must be used within SimulationProvider", ); const selectedTaskId = context.selectedTaskView; const selectedSnapshot = selectedTaskId ? context.taskViews?.[selectedTaskId] : null; if (!selectedSnapshot) return context; return { ...context, ...selectedSnapshot, taskViews: context.taskViews, selectedTaskView: selectedTaskId, isRunning: context.isRunning, isPaused: context.isPaused, modelConfig: context.modelConfig, mockMode: context.mockMode, }; } export function useSimulationDispatch() { const context = useContext(SimulationDispatchContext); if (!context) throw new Error( "useSimulationDispatch must be used within SimulationProvider", ); return context; }