| import { useCallback, useRef, useEffect } from 'react'; |
| import { useSimulationState, useSimulationDispatch } from '../store/simulationStore'; |
| import { PRIMARY_SCENARIO, 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; |
| } |
|
|
| 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`; |
| } |
|
|
| function normalizePromptText(prompt) { |
| let text = (prompt || '').replace(/\r\n/g, '\n').trim(); |
| if (!text) return ''; |
|
|
| const noiseMarkers = [ |
| '\nAuto-Generated RCA', |
| '\n202', |
| '\nINFO:', |
| '\nCommander\n', |
| '\nManager\n', |
| '\nDetective\n', |
| '\nCoder\n', |
| '\nFixes Applied', |
| '\nEscalations', |
| '\nResolution', |
| ]; |
|
|
| let cutIdx = text.length; |
| noiseMarkers.forEach((marker) => { |
| const idx = text.indexOf(marker); |
| if (idx !== -1) cutIdx = Math.min(cutIdx, idx); |
| }); |
| text = text.slice(0, cutIdx).trim(); |
|
|
| for (let i = 0; i < 3; i += 1) { |
| const wrapped = text.match(/^\[ORCHESTRATION_REQUEST\]\s*Parse intent:\s*"([\s\S]*)"\s*$/i); |
| if (wrapped) { |
| text = wrapped[1].trim(); |
| continue; |
| } |
| text = text.replace(/^\[ORCHESTRATION_REQUEST\]\s*Parse intent:\s*/i, '').trim(); |
| break; |
| } |
|
|
| return text.replace(/^["']+|["']+$/g, '').trim(); |
| } |
|
|
| export function useSimulation() { |
| const state = useSimulationState(); |
| if (typeof window !== 'undefined') { |
| window.__mockModeRef = state.mockMode; |
| } |
| const dispatch = useSimulationDispatch(); |
| const timersRef = useRef([]); |
| const tickRef = useRef(null); |
| const stateRef = useRef(state); |
| const apiBase = getApiBase(); |
|
|
| useEffect(() => { |
| stateRef.current = state; |
| }, [state]); |
|
|
| useEffect(() => { |
| if (!state.isRunning || state.scenarioComplete) { |
| if (tickRef.current) { |
| clearInterval(tickRef.current); |
| tickRef.current = null; |
| } |
| } |
| }, [state.isRunning, state.scenarioComplete]); |
|
|
| const clearAllTimers = useCallback(() => { |
| timersRef.current.forEach(t => clearTimeout(t)); |
| timersRef.current = []; |
| if (tickRef.current) { |
| clearInterval(tickRef.current); |
| tickRef.current = null; |
| } |
| }, []); |
|
|
| const orchestrate = useCallback((prompt, { customOnly = false } = {}) => { |
| clearAllTimers(); |
| const cleanPrompt = normalizePromptText(prompt); |
|
|
| dispatch({ type: 'START_SIMULATION' }); |
| |
| |
| dispatch({ |
| type: 'ADD_MESSAGE', |
| payload: { |
| id: Date.now().toString(), |
| agent: 'MANAGER', |
| m2m: `[ORCHESTRATION_REQUEST] Parse intent: "${cleanPrompt}"`, |
| think: customOnly |
| ? 'Running custom prompt against a single OpenEnv task.' |
| : 'Sending prompt to the orchestrator so it can select the OpenEnv task, validator mode, and live evidence path.', |
| timestamp: new Date().toISOString() |
| } |
| }); |
|
|
| fetch(`${apiBase}/api/orchestrate`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ prompt: cleanPrompt, custom_only: customOnly }) |
| }) |
| .then(res => res.json()) |
| .then(data => { |
| if (data.status === 'error') { |
| dispatch({ |
| type: 'ADD_MESSAGE', |
| payload: { |
| id: Date.now().toString(), |
| agent: 'COMMANDER', |
| m2m: '[ORCHESTRATION_ERROR] Provider unavailable', |
| think: data.error || 'The backend could not start the live OpenEnv run.', |
| timestamp: new Date().toISOString() |
| } |
| }); |
| return; |
| } |
| |
| dispatch({ |
| type: 'ADD_MESSAGE', |
| payload: { |
| id: Date.now().toString(), |
| agent: 'COMMANDER', |
| m2m: `[JSON_PROVISION] ${JSON.stringify(data.commander_payload)}`, |
| think: `OpenEnv orchestration armed for ${data.commander_payload?.title || 'incident response'}. Validator: ${data.commander_payload?.validator_runtime?.label || 'unknown'}.`, |
| timestamp: new Date().toISOString() |
| } |
| }); |
| }) |
| .catch(err => console.warn("Backend unavailable:", err)); |
|
|
| |
| const tickInterval = 1000; |
| tickRef.current = setInterval(() => { |
| dispatch({ type: 'TICK', payload: tickInterval }); |
| }, tickInterval); |
|
|
| |
| |
| }, [apiBase, dispatch, clearAllTimers]); |
|
|
| const start = useCallback(() => { |
| orchestrate("Deploy Llama-3 with a strict 500MB constraint"); |
| }, [orchestrate]); |
|
|
| const pause = useCallback(() => { |
| dispatch({ type: 'PAUSE_SIMULATION' }); |
| if (tickRef.current) { |
| clearInterval(tickRef.current); |
| tickRef.current = null; |
| } |
| }, [dispatch]); |
|
|
| const resume = useCallback(() => { |
| dispatch({ type: 'RESUME_SIMULATION' }); |
| tickRef.current = setInterval(() => { |
| dispatch({ type: 'TICK', payload: 1000 }); |
| }, 1000); |
| }, [dispatch]); |
|
|
| const stop = useCallback(() => { |
| clearAllTimers(); |
| dispatch({ type: 'STOP_SIMULATION' }); |
| }, [dispatch, clearAllTimers]); |
|
|
| const setSpeed = useCallback((speed) => { |
| dispatch({ type: 'SET_SPEED', payload: speed }); |
| }, [dispatch]); |
|
|
| const toggleMockMode = useCallback(() => { |
| dispatch({ type: 'TOGGLE_MOCK_MODE' }); |
| }, [dispatch]); |
|
|
| |
| |
|
|
| return { start, pause, resume, stop, setSpeed, toggleMockMode, orchestrate }; |
| } |
|
|
| function processEvent(event, dispatch) { |
| switch (event.type) { |
| case 'chat': { |
| const agentInfo = AGENT_ROLES[event.payload.agent] || { id: event.payload.agent.toLowerCase(), name: event.payload.agent, emoji: '🤖', color: '#71717a' }; |
|
|
| |
| if (event.payload.m2m.startsWith('CODE_SUBMIT')) { |
| const isMock = typeof window !== 'undefined' && window.__mockModeRef !== undefined ? window.__mockModeRef : true; |
| fetch(`${getApiBase()}/api/code/submit`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| code: "# Live inference.py submission", |
| filename: event.payload.m2m.split(' | ')[1].trim(), |
| agent_role: event.payload.agent, |
| mock_mode: isMock |
| }) |
| }).catch(err => console.warn("Backend unavailable:", err)); |
| } |
|
|
| dispatch({ |
| type: 'ADD_MESSAGE', |
| payload: { |
| agent: agentInfo, |
| m2m: event.payload.m2m, |
| english: M2M_TRANSLATIONS[event.payload.m2m] || event.payload.m2m, |
| think: event.payload.think || null, |
| }, |
| }); |
| break; |
| } |
| case 'telemetry': |
| dispatch({ type: 'UPDATE_TELEMETRY', payload: event.payload }); |
| break; |
| case 'causal': |
| dispatch({ type: 'ADD_CAUSAL_NODE', payload: event.payload }); |
| break; |
| case 'preflight': |
| dispatch({ type: 'UPDATE_PREFLIGHT', payload: event.payload }); |
| break; |
| case 'disagreement': |
| dispatch({ type: 'SET_DISAGREEMENT', payload: event.payload }); |
| break; |
| case 'git': |
| |
| if (event.payload.files && event.payload.files.includes('RCA_REPORT.md')) { |
| fetch(`${getApiBase()}/api/rca`) |
| .then(res => res.json()) |
| .then(data => dispatch({ type: 'SET_RCA_DOCUMENT', payload: data.rca })) |
| .catch(() => console.warn("Using static RCA")); |
| } |
| dispatch({ type: 'ADD_GIT_COMMIT', payload: event.payload }); |
| break; |
| case 'counterfactual': |
| |
| fetch(`${getApiBase()}/api/counterfactual`) |
| .then(res => res.json()) |
| .then(data => { |
| dispatch({ type: 'SET_COUNTERFACTUAL', payload: { |
| actual: event.payload.actual, |
| dead: { |
| time: data.time_elapsed_formatted || data.time_elapsed_seconds + 's', |
| cost: `$${data.projected_cost_usd.toFixed(2)}`, |
| sla: data.sla_breached ? 'BREACHED' : 'SAFE', |
| outcome: data.outcome |
| } |
| }}); |
| }) |
| .catch(() => dispatch({ type: 'SET_COUNTERFACTUAL', payload: event.payload })); |
| break; |
| case 'reward': |
| dispatch({ type: 'ADD_REWARD', payload: event.payload }); |
| break; |
| default: |
| break; |
| } |
| } |
|
|