Spaces:
Sleeping
Sleeping
| 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 ( | |
| <div className="loading-screen"> | |
| <div className="loading-spinner" /> | |
| <div className="loading-text">Initializing NovaTech AI — CEO Simulation</div> | |
| </div> | |
| ); | |
| } | |
| const inbox = obs?.inbox || []; | |
| const goals = obs?.goals || []; | |
| const calendar = obs?.calendar || []; | |
| return ( | |
| <div className={`app ${isAutoRunning ? 'app--autorunning' : ''}`}> | |
| {/* Floating reward popups */} | |
| <div className="reward-popups"> | |
| {rewardPopups.map(p => ( | |
| <div key={p.id} className={`reward-popup ${p.reward >= 0 ? 'positive' : 'negative'}`}> | |
| <span className="popup-reward">{p.reward >= 0 ? '+' : ''}{p.reward.toFixed(3)}</span> | |
| <span className="popup-label">{p.label}</span> | |
| </div> | |
| ))} | |
| </div> | |
| {error && ( | |
| <div className="error-banner"> | |
| ⚠ {error} | |
| <button onClick={() => setError(null)}>✕</button> | |
| </div> | |
| )} | |
| <SimulationControls | |
| taskId={taskId} | |
| currentTime={obs?.time || '9:00 AM'} | |
| currentStep={obs?.step || 0} | |
| maxSteps={obs?.max_steps || 6} | |
| totalReward={obs?.total_reward || 0} | |
| done={obs?.done || false} | |
| taskName={obs?.task_name || ''} | |
| onReset={() => handleReset(taskId)} | |
| onTaskChange={handleTaskChange} | |
| loading={loading} | |
| isAutoRunning={isAutoRunning} | |
| onAutoRun={handleAutoRun} | |
| /> | |
| <div className="main-grid"> | |
| <div className="left-col"> | |
| <InboxPanel | |
| emails={inbox} | |
| selectedEmailId={selectedEmail?.id} | |
| onSelect={setSelectedEmail} | |
| isAutoRunning={isAutoRunning} | |
| /> | |
| </div> | |
| <div className="center-col"> | |
| <ActionPanel | |
| selectedEmail={selectedEmail} | |
| onAction={handleAction} | |
| done={obs?.done || false} | |
| loading={loading || isAutoRunning} | |
| /> | |
| {calendar.length > 0 && ( | |
| <div className="panel calendar-panel"> | |
| <div className="panel-header"> | |
| <span className="panel-title">📅 Calendar</span> | |
| </div> | |
| <div className="calendar-list"> | |
| {[...calendar] | |
| .sort((a, b) => a.time_slot - b.time_slot) | |
| .map((ev, i) => ( | |
| <div key={i} className={`calendar-event ${ev.locked ? 'locked' : 'scheduled'}`}> | |
| <span className="cal-time">Slot {ev.time_slot + 1}</span> | |
| <span className="cal-title">{ev.title}</span> | |
| {ev.locked && <span className="cal-locked">🔒</span>} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="right-col"> | |
| <ScorePanel | |
| goals={goals} | |
| score={score} | |
| done={obs?.done || false} | |
| totalReward={obs?.total_reward || 0} | |
| currentStep={obs?.step || 0} | |
| maxSteps={obs?.max_steps || 6} | |
| /> | |
| </div> | |
| </div> | |
| <div className="bottom-row"> | |
| <TimelinePanel history={history} /> | |
| </div> | |
| </div> | |
| ); | |
| } | |