import React, { useState, useEffect, useCallback, useRef } from 'react'; import './App.css'; import SimulationControls from './components/SimulationControls'; import InboxPanel from './components/InboxPanel'; import ActionPanel from './components/ActionPanel'; import TimelinePanel from './components/TimelinePanel'; import ScorePanel from './components/ScorePanel'; import { resetEnv, stepEnv, getGrade } from './api'; const STEP_DELAY = 1400; // ~0.5× — realistic LLM response cadence /* Greedy agent: match pending goals first, then highest priority*urgency */ function pickBestAction(obs) { if (!obs) return null; const unhandled = (obs.inbox || []).filter(e => !e.handled); if (!unhandled.length) return null; const pendingGoals = [...(obs.pending_goals || [])].sort((a, b) => b.priority - a.priority); for (const goal of pendingGoals) { const email = unhandled.find(e => e.id === goal.target_email_id); if (email) return { type: goal.required_action, email_id: email.id }; } const best = [...unhandled].sort( (a, b) => b.priority * b.urgency - a.priority * a.urgency )[0]; return { type: best.priority <= 2 ? 'delegate' : 'reply', email_id: best.id }; } const sleep = ms => new Promise(r => setTimeout(r, ms)); export default function App() { const [obs, setObs] = useState(null); const [history, setHistory] = useState([]); const [selectedEmail, setSelectedEmail] = useState(null); const [taskId, setTaskId] = useState('easy'); const [score, setScore] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isAutoRunning, setIsAutoRunning] = useState(false); const [rewardPopups, setRewardPopups] = useState([]); const [firstLoad, setFirstLoad] = useState(true); // eslint-disable-line no-unused-vars const obsRef = useRef(obs); const autoRunRef = useRef(false); const popupIdRef = useRef(0); useEffect(() => { obsRef.current = obs; }, [obs]); /* ── helpers ─────────────────────────────────────── */ const showRewardPopup = (reward, subject) => { const id = ++popupIdRef.current; setRewardPopups(prev => [...prev.slice(-4), { id, reward, label: subject?.slice(0, 32) }]); setTimeout(() => setRewardPopups(prev => prev.filter(p => p.id !== id)), 2200); }; /* ── reset ───────────────────────────────────────── */ const handleReset = useCallback(async (task = taskId) => { autoRunRef.current = false; setIsAutoRunning(false); setLoading(true); setError(null); try { const fresh = await resetEnv(task); setObs(fresh); setHistory([]); setSelectedEmail(null); setScore(0); setRewardPopups([]); setFirstLoad(false); } catch { setError('Cannot connect to server.'); } finally { setLoading(false); } }, [taskId]); const handleTaskChange = useCallback((t) => { setTaskId(t); handleReset(t); }, [handleReset]); useEffect(() => { handleReset('easy'); }, []); // eslint-disable-line /* ── single step (returns new obs or null) ────────── */ const executeStep = useCallback(async (action, currentObs) => { try { const result = await stepEnv(action); const newObs = result.observation || result; if (!('done' in newObs)) newObs.done = result.done ?? false; const entry = { step: currentObs.step ?? 0, time: currentObs.time, action: action.type, email_id: action.email_id, email_subject: result.info?.email_subject ?? action.email_id, email_priority: (currentObs.inbox || []).find(e => e.id === action.email_id)?.priority, reward: result.reward ?? 0, }; setObs(newObs); setHistory(h => [...h, entry]); showRewardPopup(result.reward, entry.email_subject); if (newObs.done) { const g = await getGrade().catch(() => null); if (g) setScore(g.score); } return newObs; } catch (e) { setError(e?.detail || 'Step failed.'); return null; } }, []); /* ── manual action ────────────────────────────────── */ const handleAction = useCallback(async (action) => { if (!obs || obs.done || loading || isAutoRunning) return; setLoading(true); setError(null); await executeStep(action, obs); setSelectedEmail(null); setLoading(false); }, [obs, loading, isAutoRunning, executeStep]); /* ── auto-run ─────────────────────────────────────── */ const handleAutoRun = useCallback(async () => { if (autoRunRef.current) { autoRunRef.current = false; setIsAutoRunning(false); return; } autoRunRef.current = true; setIsAutoRunning(true); setError(null); let current = obsRef.current; // Auto-reset if episode already finished if (current?.done) { setLoading(true); try { const fresh = await resetEnv(taskId); setObs(fresh); setHistory([]); setScore(0); setRewardPopups([]); setSelectedEmail(null); obsRef.current = fresh; current = fresh; await sleep(600); } finally { setLoading(false); } } while (autoRunRef.current && current && !current.done) { const action = pickBestAction(current); if (!action) break; // Highlight email briefly — looks like AI "thinking" const email = (current.inbox || []).find(e => e.id === action.email_id); if (email) { setSelectedEmail(email); await sleep(STEP_DELAY * 0.4); } if (!autoRunRef.current) break; current = await executeStep(action, current); if (!current) break; setSelectedEmail(null); await sleep(STEP_DELAY * 0.6); } autoRunRef.current = false; setIsAutoRunning(false); setSelectedEmail(null); }, [taskId, executeStep]); /* ── render ───────────────────────────────────────── */ if (!obs && !error) { return (