saaheerpurav's picture
first commit
1bb18a0
Raw
History Blame Contribute Delete
9.75 kB
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>
);
}