Spaces:
Sleeping
Sleeping
ashishMenon05
feat(ui): support unlimited agents with scalable terminal grid and scrollable settings
7fbbd40 | import { useState, useEffect, useCallback, useRef } from 'react'; | |
| const useWebSocket = (url) => { | |
| const [events, setEvents] = useState([]); | |
| const [gameState, setGameState] = useState({ | |
| scenario: null, | |
| active: false, | |
| status: 'AWAITING_OBJECTIVE', | |
| step: 0, | |
| reward: 0, | |
| cumulativeReward: 0, | |
| agents: {}, | |
| clues_found: [], | |
| rewardBreakdown: {}, | |
| rewardHistory: [] | |
| }); | |
| const [isConnected, setIsConnected] = useState(false); | |
| const [error, setError] = useState(null); | |
| const socketRef = useRef(null); | |
| useEffect(() => { | |
| socketRef.current = new WebSocket(url); | |
| socketRef.current.onopen = () => setIsConnected(true); | |
| socketRef.current.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| setEvents(prev => [...prev, data]); | |
| setGameState(prev => { | |
| let current = { ...prev }; | |
| if (data.type === 'episode_start') { | |
| const initialAgents = {}; | |
| if (data.agents && Array.isArray(data.agents)) { | |
| data.agents.forEach(a => { | |
| initialAgents[a.id] = { status: 'ACTIVE', messages: [] }; | |
| }); | |
| } | |
| return { | |
| ...current, | |
| scenario: data.scenario, | |
| active: true, | |
| status: 'INVESTIGATING', | |
| step: 0, | |
| reward: 0, | |
| cumulativeReward: 0, | |
| clues_found: [], | |
| agents: initialAgents | |
| }; | |
| } | |
| const newState = { ...current }; | |
| if (data.step !== undefined) { | |
| newState.step = data.step; | |
| } | |
| if (data.type === 'agent_partial') { | |
| const agentId = data.agent_id; | |
| const agents = { ...newState.agents }; | |
| const agentReference = agents[agentId] || { status: 'ACTIVE', messages: [] }; | |
| const agent = { ...agentReference }; | |
| const messages = [...(agent.messages || [])]; | |
| const lastMsg = messages[messages.length - 1]; | |
| if (lastMsg && lastMsg.type === 'message' && lastMsg.partial) { | |
| messages[messages.length - 1] = { ...lastMsg, content: data.full_message }; | |
| } else { | |
| messages.push({ | |
| type: 'message', | |
| content: data.full_message, | |
| partial: true | |
| }); | |
| } | |
| agent.messages = messages; | |
| agents[agentId] = agent; | |
| newState.agents = agents; | |
| } | |
| if (data.type === 'agent_message') { | |
| const agentId = data.agent_id; | |
| const agents = { ...newState.agents }; | |
| const agentReference = agents[agentId] || { status: 'ACTIVE', messages: [] }; | |
| const agent = { ...agentReference }; | |
| const messages = [...(agent.messages || [])]; | |
| const lastMsg = messages[messages.length - 1]; | |
| if (lastMsg && lastMsg.partial) { | |
| messages[messages.length - 1] = { ...lastMsg, content: data.message, partial: undefined }; | |
| } else { | |
| messages.push({ | |
| type: 'message', | |
| content: data.message | |
| }); | |
| } | |
| agent.messages = messages; | |
| agents[agentId] = agent; | |
| newState.agents = agents; | |
| } | |
| if (data.status === 'READY') { | |
| newState.status = 'READY_TO_INJECT'; | |
| newState.active = false; | |
| const clearedAgents = {}; | |
| Object.keys(newState.agents).forEach(k => { | |
| clearedAgents[k] = { ...newState.agents[k], messages: [] }; | |
| }); | |
| newState.agents = clearedAgents; | |
| } | |
| if (data.type === 'system_status') { | |
| if (data.paused !== undefined) { | |
| newState.status = data.paused ? 'PAUSED' : 'INVESTIGATING'; | |
| } | |
| if (data.status) { | |
| newState.status = data.status; | |
| } | |
| if (data.active !== undefined) { | |
| newState.active = data.active; | |
| } | |
| } | |
| if (data.type === 'tool_call') { | |
| const agentId = data.agent_id; | |
| const agents = { ...newState.agents }; | |
| const agent = { ...agents[agentId], messages: [...(agents[agentId].messages || [])] }; | |
| agent.messages.push({ | |
| type: 'tool_call', | |
| tool_name: data.tool_name, | |
| params: data.params | |
| }); | |
| agents[agentId] = agent; | |
| newState.agents = agents; | |
| } | |
| if (data.type === 'tool_result') { | |
| const agents = { ...newState.agents }; | |
| const agentIds = Object.keys(agents); | |
| if (agentIds.length > 0) { | |
| // Attach tool result to the most recently active agent. Or just broadcast to all/first since tool_result lacks agent_id | |
| // We will append to all agents or the first one just for display parsing. | |
| const activeId = data.agent_id || agentIds[0]; | |
| if (agents[activeId]) { | |
| const agentTarget = { ...agents[activeId], messages: [...(agents[activeId].messages || [])] }; | |
| agentTarget.messages.push({ | |
| type: 'tool_result', | |
| tool_name: data.tool_name, | |
| result: data.result, | |
| success: data.success | |
| }); | |
| agents[activeId] = agentTarget; | |
| newState.agents = agents; | |
| } | |
| } | |
| // Simple heuristic for clues if not sent explicitly | |
| const res = data.result?.toLowerCase() || ''; | |
| if (res.includes('error') || res.includes('anomaly') || res.includes('warning') || res.includes('degraded') || data.tool_name === 'propose_fix') { | |
| const currentClues = newState.clues_found || []; | |
| if (!currentClues.includes(data.result)) { | |
| newState.clues_found = [...currentClues, data.result]; | |
| } | |
| } | |
| } | |
| if (data.type === 'reward_update') { | |
| newState.reward = data.reward; | |
| newState.cumulativeReward = data.cumulative; | |
| newState.rewardBreakdown = data.breakdown || {}; | |
| newState.rewardHistory = [...(newState.rewardHistory || []), data.reward]; | |
| } | |
| if (data.type === 'episode_end') { | |
| newState.active = false; | |
| newState.status = 'COMPLETED'; | |
| newState.step = data.steps_taken || newState.step; | |
| newState.cumulativeReward = data.final_score !== undefined ? data.final_score : newState.cumulativeReward; | |
| newState.finalScore = data.final_score; | |
| newState.success = data.success; | |
| newState.fixVerified = data.fix_verified; | |
| if (data.clues_found) newState.clues_found = data.clues_found; | |
| if (data.reward_history) newState.rewardHistory = data.reward_history; | |
| if (data.final_breakdown) newState.rewardBreakdown = data.final_breakdown; | |
| const standbyAgents = {}; | |
| Object.keys(newState.agents).forEach(k => { | |
| standbyAgents[k] = { ...newState.agents[k], status: 'STANDBY' }; | |
| }); | |
| newState.agents = standbyAgents; | |
| } | |
| return newState; | |
| }); | |
| }; | |
| socketRef.current.onerror = (err) => setError(err); | |
| socketRef.current.onclose = () => setIsConnected(false); | |
| return () => socketRef.current.close(); | |
| }, [url]); | |
| const sendCommand = useCallback((command) => { | |
| if (socketRef.current && isConnected) { | |
| socketRef.current.send(JSON.stringify(command)); | |
| } | |
| }, [isConnected]); | |
| return { events, gameState, isConnected, error, sendCommand }; | |
| }; | |
| export default useWebSocket; | |