rushagentrush / frontend /app /page.tsx
adityaverma977
Fix TypeScript type error in page.tsx - add type assertion after filter
71640b5
"use client"
import { useEffect, useRef, useState } from "react"
import LoadingScreen from "../components/LoadingScreen"
import MapCanvas from "../components/MapCanvas"
import ChatFeed from "../components/ChatFeed"
import ModelSelector from "../components/ModelSelector"
import { startSimulation, placeVolcano } from "../lib/api"
import { createSimulationSocket } from "../lib/websocket"
import ReportPanel from "../components/ReportPanel"
type AppState = "loading" | "selecting" | "placing" | "running" | "gameover"
export default function Page() {
function buildReport(history: any[]) {
if (!history || history.length === 0) return null
const rounds = history.length
const last = history[history.length - 1]
const lastState = last.state || last
const models = (lastState.agents || []).map((a: any) => ({ id: a.model_name, display_name: a.display_name || a.model_name }))
const per = models.map((m: any) => {
const positions = history.map(h => {
const state = h.state || h
const a = (state.agents || []).find((x: any) => x.model_name === m.id)
return a ? { x: a.x, y: a.y, water: a.water_collected, status: a.status, extinguish_score: a.extinguish_score || 0, last_message: a.last_message } : null
}).filter(Boolean) as Array<{ x: number, y: number, water: number, status: string, extinguish_score: number, last_message: string }>
let distance = 0
for (let i = 1; i < positions.length; i++) {
const p0 = positions[i-1]
const p1 = positions[i]
if (p0 && p1) {
const dx = p1.x - p0.x
const dy = p1.y - p0.y
distance += Math.sqrt(dx*dx + dy*dy)
}
}
let water_picks = 0
let logical_moves = 0
let logical_checks = 0
const messages: Record<string, number> = {}
// Count events (decisions/messages, water pickups) across ticks
for (let i = 0; i < history.length; i++) {
const tick = history[i]
const events = tick.events || []
for (const ev of events) {
if (ev.type === 'message' && ev.model === m.id) {
messages[ev.content] = (messages[ev.content] || 0) + 1
}
if (ev.type === 'water_collected' && ev.model === m.id) {
water_picks++
}
}
// logical move checks based on consecutive positions
if (i > 0) {
const prevState = history[i-1].state || history[i-1]
const curState = history[i].state || history[i]
const prevA = (prevState.agents || []).find((x: any) => x.model_name === m.id)
const curA = (curState.agents || []).find((x: any) => x.model_name === m.id)
if (prevA && curA) {
if (curA.status === 'searching') {
const ws = curState.water_sources || []
if (ws.length > 0) {
const distPrev = Math.min(...ws.map((w: any)=> Math.hypot(prevA.x - w.x, prevA.y - w.y)))
const distCur = Math.min(...ws.map((w: any)=> Math.hypot(curA.x - w.x, curA.y - w.y)))
logical_checks++
if (distCur <= distPrev) logical_moves++
}
}
}
}
}
const sortedMessages = Object.entries(messages).sort((a:any,b:any)=>b[1]-a[1]).slice(0,6).map((x:any)=>x[0])
return {
id: m.id,
display_name: m.display_name,
decisions: positions.length,
distance,
water_picks,
extinguish_score: positions.length>0 ? positions[positions.length-1].extinguish_score||0 : 0,
logical_pct: logical_checks>0 ? (logical_moves / logical_checks)*100 : 0,
top_messages: sortedMessages,
}
})
return { rounds, models: per }
}
function tickHistoryToTracks(history: any[]) {
const tracks: Record<string, { x: number; y: number }[]> = {}
if (!history || history.length === 0) return tracks
for (const h of history) {
const state = h.state || h
for (const a of state.agents || []) {
if (!tracks[a.model_name]) tracks[a.model_name] = []
tracks[a.model_name].push({ x: a.x, y: a.y })
}
}
return tracks
}
const [appState, setAppState] = useState<AppState>("loading")
const [models, setModels] = useState<string[]>([])
const [simState, setSimState] = useState<any>(null)
const [chatMessages, setChatMessages] = useState<any[]>([])
const [winnerLabel, setWinnerLabel] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [tickHistory, setTickHistory] = useState<any[]>([])
const [reportOpen, setReportOpen] = useState(false)
const [reportData, setReportData] = useState<any | null>(null)
const [fireStatus, setFireStatus] = useState<any | null>(null)
const [mapSize, setMapSize] = useState({ width: 1200, height: 800 })
const wsRef = useRef<WebSocket | null>(null)
const mapDivRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const el = mapDivRef.current
if (!el) return
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect
setMapSize({ width, height })
})
observer.observe(el)
return () => observer.disconnect()
}, [appState])
async function handleStart() {
if (models.length < 2) return
setLoading(true)
try {
const data = await startSimulation(models)
setSimState(data.state)
setWinnerLabel(null)
setChatMessages([])
setTickHistory([])
setAppState("placing")
} catch (err) {
console.error(err)
} finally {
setLoading(false)
}
}
async function handleMapClick(x: number, y: number) {
if (appState !== "placing" || !simState) return
try {
const data = await placeVolcano(simState.simulation_id, x, y)
setSimState(data)
setAppState("running")
const ws = createSimulationSocket(
simState.simulation_id,
(msg) => {
// store the full tick message (state + events) for richer analysis
setTickHistory(prev => {
const next = [...prev, msg]
// update sim state from the incoming message
if (msg.state) setSimState(msg.state)
// convert events into chat messages for UI
if (msg.events) {
const newMsgs = msg.events.map((e: any) => {
if (e.type === 'message') return { agent_id: e.model, text: e.content, type: 'message' }
if (e.type === 'death') return { agent_id: e.model, text: '', type: 'death' }
if (e.type === 'alliance_proposal') return { agent_id: e.from_model, text: '', type: 'alliance_proposal', to_model: e.to_model }
if (e.type === 'alliance_accept') return { agent_id: e.model_a, text: '', type: 'alliance_accept', to_model: e.model_b }
if (e.type === 'alliance_reject') return { agent_id: e.from_model, text: '', type: 'alliance_reject', to_model: e.to_model }
if (e.type === 'fire_status') {
setFireStatus({ intensity: e.intensity, radius: e.radius, ticks: e.ticks_to_extinguish, secs: e.secs_to_extinguish })
return null
}
return null
}).filter(Boolean)
setChatMessages(prevMsgs => [...prevMsgs, ...newMsgs])
}
if (msg.chat) {
const chatMsgs = msg.chat.map((entry: any) => ({
agent_id: entry.agent_id,
text: entry.message,
type: 'message',
}))
setChatMessages(prevMsgs => [...prevMsgs, ...chatMsgs])
}
// If simulation finished, mark gameover and compute final report using full tick history
if (msg.type === 'finished' || msg.state?.status === 'finished') {
const finalState = msg.state || (msg.type === 'finished' ? msg.state : null)
if (finalState) {
setWinnerLabel(finalState.winner_model || null)
}
setAppState('gameover')
const report = buildReport(next)
setReportData(report)
}
return next
})
// if the message was an immediate finished payload without state (rare), still handle
if (msg.type === "finished" && !msg.state) {
setAppState("gameover")
return
}
},
() => setAppState("gameover")
)
wsRef.current = ws
} catch (err) {
console.error(err)
}
}
return (
<main className="flex h-screen w-screen overflow-hidden bg-[#0a0a0a]">
{appState === "loading" && <LoadingScreen onReady={() => setAppState("selecting")} />}
{/* Map Area */}
<div ref={mapDivRef} className="flex-[7] h-full relative">
<MapCanvas
agents={simState?.agents ?? []}
fire={simState?.fire ?? null}
waterSources={simState?.water_sources ?? []}
waitingForScenario={appState === "placing"}
gameOver={appState === "gameover"}
winnerLabel={winnerLabel}
mapSize={mapSize}
onMapClick={handleMapClick}
tracks={tickHistoryToTracks(tickHistory)}
/>
</div>
{/* Sidebar */}
<aside className="flex-[3] h-full border-l border-white/5 flex flex-col bg-[#111] z-20">
<div className="p-6">
<h1 className="text-2xl font-bold text-white tracking-tighter">RUSHH <span className="text-red-500">AGENT</span> RUSHHH !!</h1>
<p className="text-[10px] text-white/30 uppercase tracking-[0.3em] mt-1">Survival Intelligence Test</p>
</div>
<div className="flex-1 flex flex-col min-h-0">
{appState === "selecting" ? (
<ModelSelector
models={models}
onAdd={id => setModels(p => [...p, id])}
onRemove={id => setModels(p => p.filter(m => m !== id))}
/>
) : (
<ChatFeed messages={chatMessages} />
)}
</div>
<div className="p-6 border-t border-white/5">
{simState?.fire && (
<div className="mb-4 text-xs font-mono text-white/60">
<div>Fire Intensity: <span className="font-bold text-white">{Math.round(simState.fire.intensity)}%</span></div>
{fireStatus && fireStatus.secs != null ? (
<div>ETA to extinguish: <span className="font-bold text-white">{Math.round(fireStatus.secs)}s</span></div>
) : (
<div>ETA to extinguish: <span className="text-white/30"></span></div>
)}
</div>
)}
{appState === "selecting" && (
<button
onClick={handleStart}
disabled={models.length < 2 || loading}
className="w-full bg-white text-black font-mono text-xs font-bold py-4 rounded-xl hover:bg-white/90 disabled:opacity-20 transition-all uppercase tracking-widest"
>
{loading ? "Initializing..." : "Start Simulation"}
</button>
)}
{appState === "placing" && (
<div className="text-center py-4 bg-red-500/10 border border-red-500/20 rounded-xl">
<span className="text-red-500 font-mono text-[10px] uppercase tracking-widest animate-pulse">
Click on the map to ignite the fire
</span>
</div>
)}
{(appState === "running" || appState === "gameover") && (
<div className="space-y-2">
{appState === 'gameover' && reportData && (
<button onClick={() => setReportOpen(true)} className="w-full bg-white text-black font-mono text-xs font-bold py-3 rounded-xl">View Report</button>
)}
<button
onClick={() => window.location.reload()}
className="w-full bg-white/5 text-white/50 font-mono text-[10px] py-3 rounded-lg hover:bg-white/10 transition-all uppercase tracking-widest"
>
Reset Arena
</button>
</div>
)}
</div>
</aside>
{reportOpen && reportData && (
<ReportPanel report={reportData} onClose={() => setReportOpen(false)} />
)}
</main>
)
}