Shreeraj Mummidivarapu commited on
Commit
28d5087
·
unverified ·
1 Parent(s): ad01980

Update Dashboard.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/components/Dashboard.jsx +223 -756
frontend/src/components/Dashboard.jsx CHANGED
@@ -1,802 +1,269 @@
1
- // import React, { useState, useEffect, useRef } from 'react';
2
- // import { RefreshCw, Briefcase, Coffee, Clock } from 'lucide-react';
3
 
4
- // const API_BASE = 'http://localhost:8000';
5
 
6
- // export default function Dashboard() {
7
- // const [level, setLevel] = useState('medium');
8
- // const [sessionId, setSessionId] = useState(null);
9
- // const [obs, setObs] = useState(null);
10
- // const [stateData, setStateData] = useState(null);
11
- // const [logs, setLogs] = useState([]);
12
- // const [loading, setLoading] = useState(false);
13
- // const [error, setError] = useState(null);
14
- // const scrollRef = useRef(null);
15
-
16
- // const fetchState = async (sid) => {
17
- // try {
18
- // const res = await fetch(`${API_BASE}/state?session_id=${sid}`);
19
- // if (res.ok) {
20
- // const data = await res.json();
21
- // setStateData(data);
22
- // }
23
- // } catch(e) { console.error("State fetch error", e); }
24
- // };
25
-
26
- // const handleReset = async () => {
27
- // setLoading(true);
28
- // setError(null);
29
- // try {
30
- // const res = await fetch(`${API_BASE}/reset`, {
31
- // method: 'POST',
32
- // headers: { 'Content-Type': 'application/json' },
33
- // body: JSON.stringify({ level })
34
- // });
35
- // const data = await res.json();
36
- // setSessionId(data.session_id);
37
- // setObs(data.observation);
38
- // setLogs([{ type: 'system', msg: `Environment reset: ${level} level` }]);
39
- // await fetchState(data.session_id);
40
- // } catch (err) {
41
- // setError(err.message || "Failed to connect to backend");
42
- // } finally {
43
- // setLoading(false);
44
- // }
45
- // };
46
-
47
- // const handleAction = async (actionType, taskId = null) => {
48
- // if (!sessionId) return;
49
- // setLoading(true);
50
-
51
- // const action = { type: actionType };
52
- // if (taskId) action.task_id = taskId;
53
-
54
- // try {
55
- // const res = await fetch(`${API_BASE}/step`, {
56
- // method: 'POST',
57
- // headers: { 'Content-Type': 'application/json' },
58
- // body: JSON.stringify({ session_id: sessionId, action })
59
- // });
60
- // const data = await res.json();
61
- // setObs(data.observation);
62
-
63
- // let logMsg = `Action: ${actionType}${taskId ? ' ('+taskId+')' : ''} | Reward: ${data.reward.toFixed(2)}`;
64
- // if (data.done) {
65
- // logMsg += ` | DONE. Final Score: ${data.info?.final_score?.toFixed(2) || 'N/A'}`;
66
- // }
67
-
68
- // setLogs(prev => [...prev, { type: 'action', msg: logMsg, reward: data.reward }]);
69
- // await fetchState(sessionId);
70
-
71
- // setTimeout(() => {
72
- // if(scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
73
- // }, 50);
74
-
75
- // } catch (err) {
76
- // setError(err.message);
77
- // } finally {
78
- // setLoading(false);
79
- // }
80
- // };
81
-
82
- // useEffect(() => {
83
- // handleReset();
84
- // }, [level]);
85
-
86
- // return (
87
- // <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
88
- // <div className="lg:col-span-2 space-y-6">
89
- // <div className="bg-slate-800 p-4 rounded-xl border border-slate-700 flex items-center gap-4">
90
- // <select
91
- // value={level}
92
- // onChange={e => setLevel(e.target.value)}
93
- // className="bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 outline-none"
94
- // >
95
- // <option value="easy">Easy</option>
96
- // <option value="medium">Medium</option>
97
- // <option value="hard">Hard</option>
98
- // </select>
99
- // <button
100
- // onClick={handleReset}
101
- // disabled={loading}
102
- // className="flex items-center gap-2 bg-slate-700 hover:bg-slate-600 transition-colors px-4 py-2 rounded-lg text-sm font-medium"
103
- // >
104
- // <RefreshCw size={16} className={loading ? "animate-spin" : ""} /> Reset Env
105
- // </button>
106
- // <div className="ml-auto text-sm text-slate-400">
107
- // Time Step: <span className="font-mono text-white bg-slate-900 px-2 py-1 rounded">{obs?.time_step || 0}</span>
108
- // </div>
109
- // {error && <span className="text-red-400 text-sm ml-4">{error}</span>}
110
- // </div>
111
-
112
- // <div className="grid grid-cols-2 gap-4">
113
- // <div className="bg-slate-800 p-5 rounded-xl border border-slate-700 hover:border-slate-600 transition-colors">
114
- // <div className="flex justify-between items-center mb-2">
115
- // <span className="text-slate-400 text-sm">Energy</span>
116
- // <span className="font-bold">{stateData ? (stateData.energy * 100).toFixed(0) : 0}%</span>
117
- // </div>
118
- // <div className="w-full bg-slate-900 rounded-full h-3">
119
- // <div
120
- // className={`h-3 rounded-full transition-all duration-500 ease-out ${stateData?.energy > 0.5 ? 'bg-emerald-500' : stateData?.energy > 0.2 ? 'bg-amber-500' : 'bg-red-500'}`}
121
- // style={{ width: `${stateData ? stateData.energy * 100 : 0}%` }}
122
- // ></div>
123
- // </div>
124
- // <div className="mt-3 text-xs text-slate-500 text-right">
125
- // Obs: <span className="text-slate-300 capitalize">{obs?.visible_state?.fatigue_level || 'N/A'}</span>
126
- // </div>
127
- // </div>
128
-
129
- // <div className="bg-slate-800 p-5 rounded-xl border border-slate-700 hover:border-slate-600 transition-colors">
130
- // <div className="flex justify-between items-center mb-2">
131
- // <span className="text-slate-400 text-sm">Stress</span>
132
- // <span className="font-bold">{stateData ? (stateData.stress * 100).toFixed(0) : 0}%</span>
133
- // </div>
134
- // <div className="w-full bg-slate-900 rounded-full h-3">
135
- // <div
136
- // className={`h-3 rounded-full transition-all duration-500 ease-out ${stateData?.stress > 0.7 ? 'bg-red-500 w-full animate-pulse' : stateData?.stress > 0.4 ? 'bg-amber-500' : 'bg-emerald-500'}`}
137
- // style={{ width: `${stateData ? stateData.stress * 100 : 0}%` }}
138
- // ></div>
139
- // </div>
140
- // <div className="mt-3 text-xs text-slate-500 text-right">
141
- // Warning: {obs?.visible_state?.stress_warning ? <span className="text-red-400 font-bold">YES</span> : <span className="text-emerald-400">NO</span>}
142
- // </div>
143
- // </div>
144
- // </div>
145
-
146
- // <div className="bg-slate-800 p-5 rounded-xl border border-slate-700">
147
- // <h3 className="text-slate-400 text-sm mb-4">Environment Actions</h3>
148
- // <div className="flex gap-4">
149
- // <button disabled={loading} onClick={() => handleAction('break')} className="flex-1 flex flex-col items-center justify-center p-4 rounded-xl bg-indigo-500/10 hover:bg-indigo-500/20 border border-indigo-500/30 text-indigo-400 transition-all hover:scale-105 active:scale-95">
150
- // <Coffee size={24} className="mb-2" />
151
- // <span className="text-sm font-medium">Take Break</span>
152
- // </button>
153
- // <button disabled={loading} onClick={() => handleAction('delay')} className="flex-1 flex flex-col items-center justify-center p-4 rounded-xl bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 text-emerald-400 transition-all hover:scale-105 active:scale-95">
154
- // <Clock size={24} className="mb-2" />
155
- // <span className="text-sm font-medium">Delay / Idle</span>
156
- // </button>
157
- // </div>
158
- // </div>
159
-
160
- // <div className="space-y-4">
161
- // <h2 className="text-lg font-bold flex items-center gap-2 px-1">
162
- // <Briefcase size={20} className="text-indigo-400" /> Active Tasks
163
- // </h2>
164
- // <div className="space-y-3">
165
- // {obs?.tasks?.map(t => {
166
- // const isCurrent = stateData?.current_task_id === t.id;
167
- // const isDone = t.progress >= 1.0;
168
- // const isLate = !isDone && t.deadline && obs.time_step > t.deadline;
169
- // const isUrgent = !isDone && t.deadline && (t.deadline - obs.time_step <= 3) && (t.deadline - obs.time_step >= 0);
170
-
171
- // return (
172
- // <div key={t.id} className={`p-4 rounded-xl border transition-all ${isCurrent && !isDone ? 'bg-indigo-900/40 border-indigo-500/50 shadow-[0_0_15px_rgba(99,102,241,0.15)]' : 'bg-slate-800 border-slate-700 hover:border-slate-500'} ${isDone ? 'opacity-50' : ''}`}>
173
- // <div className="flex justify-between items-start mb-3">
174
- // <div>
175
- // <h4 className="font-semibold flex items-center gap-2">
176
- // {t.id}
177
- // {isDone && <span className="text-xs bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded-full">Done</span>}
178
- // {isLate && <span className="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded-full">Late</span>}
179
- // {isUrgent && <span className="text-xs bg-amber-500/20 text-amber-400 px-2 py-0.5 rounded-full">Urgent</span>}
180
- // </h4>
181
- // <div className="text-xs text-slate-400 mt-1 flex gap-3">
182
- // <span>Diff: <span className="capitalize text-slate-300">{t.difficulty}</span></span>
183
- // {t.deadline && <span>Deadline: <span className="font-mono text-slate-300">{t.deadline}</span></span>}
184
- // </div>
185
- // </div>
186
- // <div className="flex gap-2">
187
- // <button
188
- // onClick={() => handleAction('work', t.id)}
189
- // disabled={loading || isDone}
190
- // className="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:hover:bg-indigo-600 rounded text-sm font-medium transition-colors shadow-sm"
191
- // >
192
- // Work
193
- // </button>
194
- // {!isCurrent && (
195
- // <button
196
- // onClick={() => handleAction('switch', t.id)}
197
- // disabled={loading || isDone}
198
- // className="px-4 py-1.5 bg-slate-700 hover:bg-slate-600 disabled:opacity-50 disabled:hover:bg-slate-700 rounded text-sm font-medium transition-colors shadow-sm"
199
- // >
200
- // Switch
201
- // </button>
202
- // )}
203
- // </div>
204
- // </div>
205
- // <div className="w-full bg-slate-900 mb-1 rounded-full h-2 overflow-hidden shadow-inner">
206
- // <div
207
- // className={`h-2 rounded-full transition-all duration-300 ease-out ${isDone ? 'bg-emerald-500' : 'bg-indigo-500'}`}
208
- // style={{ width: `${Math.min(100, t.progress * 100)}%` }}
209
- // ></div>
210
- // </div>
211
- // </div>
212
- // );
213
- // })}
214
- // </div>
215
- // </div>
216
- // </div>
217
-
218
- // <div className="bg-slate-800 rounded-xl border border-slate-700 flex flex-col h-[calc(100vh-6rem)] sticky top-6 shadow-xl">
219
- // <div className="p-4 border-b border-slate-700 bg-slate-900/50 rounded-t-xl">
220
- // <h3 className="font-bold text-slate-200">Activity Log</h3>
221
- // </div>
222
- // <div className="p-4 overflow-y-auto flex-1 space-y-3 font-mono text-xs" ref={scrollRef}>
223
- // {logs.length === 0 && <div className="text-slate-500 text-center mt-10">No activity yet.</div>}
224
- // {logs.map((log, i) => (
225
- // <div key={i} className={`p-2.5 rounded border ${log.type === 'system' ? 'text-slate-400 border-slate-700/50 bg-slate-800/50' : log.reward > 0 ? 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20' : log.reward < 0 ? 'text-red-400 bg-red-500/10 border-red-500/20' : 'text-slate-300 border-slate-700 bg-slate-800/80'}`}>
226
- // <span className="opacity-40 mr-2">[{i.toString().padStart(3, '0')}]</span>
227
- // {log.msg}
228
- // </div>
229
- // ))}
230
- // </div>
231
- // </div>
232
- // </div>
233
- // );
234
- // }
235
-
236
-
237
- import React, { useState, useEffect, useRef, useCallback } from 'react';
238
-
239
- const API_BASE = 'http://localhost:7860';
240
-
241
- /* ── helpers ─────────────────────────────────────────────── */
242
- const fmt2 = n => (+(n ?? 0)).toFixed(2);
243
- const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
244
-
245
- /* ── seed data (shown before backend connects) ───────────── */
246
- /* ── empty starting constants ───────────── */
247
- const SEED_TASKS = [];
248
- const SEED_TRAINED = [0.30, 0.31, 0.35, 0.39, 0.45, 0.51, 0.60, 0.66, 0.73, 0.78, 0.82, 0.85, 0.86, 0.87, 0.88];
249
- const SEED_EPISODE = 15;
250
- const AGENT_MSGS = [
251
- { from: 'manager', text: 'Simulating multi-agent layer. Manager checks stress levels and issues system prompts dynamically to keep the LLM worker aligned.' },
252
- { from: 'env', text: 'This demo environment is connected to the fully functional FastAPI backend. You can manually execute steps.' }
253
- ];
254
- const DRIFT_EVENTS = [];
255
- const ACTION_LOG = [];
256
-
257
- /* ── priority badge colours ──────────────────────────────── */
258
- const PRIORITY_STYLE = {
259
- critical: { bg: '#fef2f2', color: '#dc2626', border: '#fecaca' },
260
- high: { bg: '#fff7ed', color: '#c2410c', border: '#fed7aa' },
261
- blocked: { bg: '#f1f5f9', color: '#64748b', border: '#cbd5e1' },
262
- normal: { bg: '#f0fdf4', color: '#15803d', border: '#bbf7d0' },
263
- medium: { bg: '#fff7ed', color: '#b45309', border: '#fde68a' },
264
- };
265
-
266
- const PROGRESS_COLOR = {
267
- critical: '#dc2626', high: '#f97316', blocked: '#94a3b8', normal: '#22c55e', medium: '#f59e0b',
268
- };
269
-
270
- /* ── reward curve SVG ────────────────────────────────────── */
271
- function RewardCurve({ trained = SEED_TRAINED, episode = SEED_EPISODE }) {
272
- const W = 560, H = 160, pL = 36, pB = 28, pR = 16, pT = 12;
273
- const cW = W - pL - pR, cH = H - pT - pB;
274
- const BASELINE = 0.30;
275
- const yS = v => pT + cH - clamp((v / 1.0) * cH, 0, cH);
276
- const xS = (i, len) => pL + (i / Math.max(len - 1, 1)) * cW;
277
- const pts = trained.map((v, i) => `${xS(i, trained.length)},${yS(v)}`).join(' ');
278
- const ticks = [0, 0.2, 0.4, 0.6, 0.8, 1.0];
279
- const epLabels = ['ep 1', `ep ${Math.round(episode / 2)}`, `ep ${episode}`];
280
-
281
- return (
282
- <div>
283
- <svg width="100%" viewBox={`0 0 ${W} ${H}`} style={{ display: 'block' }}>
284
- {/* grid lines */}
285
- {ticks.map(v => (
286
- <g key={v}>
287
- <line x1={pL} y1={yS(v)} x2={W - pR} y2={yS(v)} stroke="#e2e8f0" strokeWidth={1} />
288
- <text x={pL - 4} y={yS(v) + 3.5} fill="#94a3b8" fontSize={9} textAnchor="end">{v.toFixed(1)}</text>
289
- </g>
290
- ))}
291
- {/* baseline dashed */}
292
- <line x1={pL} y1={yS(BASELINE)} x2={W - pR} y2={yS(BASELINE)}
293
- stroke="#f87171" strokeWidth={1.5} strokeDasharray="5 4" />
294
- {/* baseline end label */}
295
- <circle cx={W - pR} cy={yS(BASELINE)} r={4} fill="#f87171" />
296
-
297
- {/* trained area */}
298
- {trained.length > 1 && <>
299
- <defs>
300
- <linearGradient id="tGrad" x1="0" y1="0" x2="0" y2="1">
301
- <stop offset="0%" stopColor="#22c55e" stopOpacity="0.18" />
302
- <stop offset="100%" stopColor="#22c55e" stopOpacity="0.02" />
303
- </linearGradient>
304
- </defs>
305
- <polygon
306
- points={`${pL},${yS(0)} ${pts} ${xS(trained.length - 1, trained.length)},${yS(0)}`}
307
- fill="url(#tGrad)" />
308
- <polyline points={pts} fill="none" stroke="#22c55e" strokeWidth={2.5}
309
- strokeLinecap="round" strokeLinejoin="round" />
310
- <circle cx={xS(trained.length - 1, trained.length)} cy={yS(trained[trained.length - 1])} r={5}
311
- fill="#22c55e" stroke="#fff" strokeWidth={2} />
312
- </>}
313
-
314
- {/* x axis labels */}
315
- {epLabels.map((label, i) => {
316
- const x = pL + (i / 2) * cW;
317
- return <text key={i} x={x} y={H - 4} fill="#94a3b8" fontSize={9} textAnchor="middle">{label}</text>;
318
- })}
319
- </svg>
320
- {/* legend */}
321
- <div style={{ display: 'flex', gap: 20, marginTop: 4, paddingLeft: pL }}>
322
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#64748b' }}>
323
- <svg width={24} height={8}><line x1={0} y1={4} x2={24} y2={4} stroke="#f87171" strokeWidth={1.5} strokeDasharray="4 3" /></svg>
324
- Baseline (untrained)
325
- </div>
326
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#64748b' }}>
327
- <svg width={24} height={8}><line x1={0} y1={4} x2={24} y2={4} stroke="#22c55e" strokeWidth={2.5} /></svg>
328
- GRPO trained agent
329
- </div>
330
- </div>
331
- </div>
332
- );
333
- }
334
-
335
- /* ── main dashboard ──────────────────────────────────────── */
336
  export default function Dashboard() {
337
- const [level, setLevel] = useState('hard');
338
- const [targetWorker, setTargetWorker] = useState('w1');
339
- const [episode, setEpisode] = useState(SEED_EPISODE);
340
- const [step, setStep] = useState(0);
341
- const [maxStep, setMaxStep] = useState(50);
342
- const [workers, setWorkers] = useState([
343
- { id: 'w1', energy: 1.0, stress: 0.0, expertise: 'analytical' },
344
- { id: 'w2', energy: 1.0, stress: 0.0, expertise: 'social' },
345
- { id: 'w3', energy: 1.0, stress: 0.0, expertise: 'analytical' }
346
- ]);
347
- const [epReward, setEpReward] = useState(0.0);
348
- const [tasks, setTasks] = useState(SEED_TASKS);
349
- const [trained, setTrained] = useState(SEED_TRAINED);
350
- const [agentMsgs, setAgentMsgs] = useState(AGENT_MSGS);
351
- const [actionLog, setActionLog] = useState(ACTION_LOG);
352
- const [schemaDrifts, setSchemaDrifts] = useState(DRIFT_EVENTS);
353
  const [sessionId, setSessionId] = useState(null);
 
 
 
354
  const [loading, setLoading] = useState(false);
355
- const [liveMode, setLiveMode] = useState(false);
356
  const [error, setError] = useState(null);
357
- const logRef = useRef(null);
 
358
 
359
- const doneTasks = tasks.filter(t => t.progress >= 1).length;
360
- const blockedCount = tasks.filter(t => t.priority === 'blocked').length;
361
- const overdueCount = tasks.filter(t => t.priority === 'critical' && t.progress < 1).length;
 
 
 
362
 
363
- /* ── backend integration ── */
364
- const handleReset = useCallback(async () => {
365
- setLoading(true); setError(null);
 
366
  try {
367
  const res = await fetch(`${API_BASE}/reset`, {
368
- method: 'POST', headers: { 'Content-Type': 'application/json' },
369
- body: JSON.stringify({ task_id: level }),
 
370
  });
371
- if (!res.ok) throw new Error('Server error');
372
  const data = await res.json();
373
- const obs = data.observation || data;
374
- setSessionId('active');
375
- setStep(obs.time_step ?? 0);
376
- setMaxStep(level === 'expert' ? 60 : 50);
377
- if (obs.workers) setWorkers(obs.workers);
378
- setEpReward(0.0);
379
- setEpisode(e => e + 1);
380
- setLiveMode(true);
381
- setAgentMsgs([{ from: 'env', text: `Episode reset · ${level} difficulty · Oracle Manager managing 3 FTEs` }]);
382
- setSchemaDrifts([]);
383
- setActionLog([]);
384
- if (obs.tasks) {
385
- setTasks(obs.tasks.map(t => ({
386
- id: t.id, name: t.task_type || t.id, deadline: t.deadline ? `step ${t.deadline}` : 'None',
387
- deps: t.depends_on ? `deps on ${t.depends_on}` : 'no deps', priority: t.priority || 'normal', progress: t.progress || 0, icon: '📋'
388
- })));
389
- }
390
- } catch (e) {
391
- setError('Backend offline');
392
- setLiveMode(false);
393
  } finally { setLoading(false); }
394
- }, [level]);
395
 
396
- const doAction = useCallback(async (type, taskId = null) => {
397
- if (!sessionId) { setError('Reset first'); return; }
398
  setLoading(true);
399
- const action = { type, worker_id: targetWorker, ...(taskId ? { task_id: taskId } : {}) };
 
400
  try {
401
  const res = await fetch(`${API_BASE}/step`, {
402
- method: 'POST', headers: { 'Content-Type': 'application/json' },
403
- body: JSON.stringify({ action }),
 
404
  });
405
  const data = await res.json();
406
- const r = data.reward ?? 0;
407
- const obs = data.observation || data;
408
- const newStep = obs.time_step ?? step + 1;
409
-
410
- setStep(newStep);
411
- setEpReward(prev => +(prev + r).toFixed(3));
412
-
413
- if (obs.workers) setWorkers(obs.workers);
414
-
415
- if (obs.schema_drift) {
416
- setSchemaDrifts(prev => [...prev, obs.schema_drift]);
417
- }
418
-
419
- if (obs.tasks) {
420
- setTasks(obs.tasks.map(t => ({
421
- id: t.id, name: t.task_type || t.id, deadline: t.deadline ? `step ${t.deadline}` : 'None',
422
- deps: t.depends_on ? `deps on ${t.depends_on}` : 'no deps', priority: t.priority || 'normal', progress: t.progress || 0, icon: '📋'
423
- })));
424
- }
425
-
426
- const logEntry = {
427
- step: `s${newStep}`, action: type, detail: taskId ?? '—',
428
- reward: (r >= 0 ? '+' : '') + fmt2(r), pos: r >= 0
429
- };
430
- setActionLog(prev => [logEntry, ...prev].slice(0, 30));
431
-
432
  if (data.done) {
433
- const fs = obs.final_score ?? 0;
434
- setTrained(prev => [...prev, fs]);
435
- setAgentMsgs(prev => [...prev, { from: 'env', text: `Episode done · final score ${fmt2(fs)}` }]);
 
436
  }
437
- } catch (e) { setError(e.message); }
438
- finally { setLoading(false); }
439
- }, [sessionId, step, workers, targetWorker]);
440
-
441
- useEffect(() => {
442
- handleReset();
443
- }, [handleReset]);
444
-
445
- /* ── level badge colour ── */
446
- const LEVEL_STYLE = {
447
- easy: { bg: '#dcfce7', c: '#15803d' }, medium: { bg: '#fef3c7', c: '#b45309' },
448
- hard: { bg: '#fee2e2', c: '#dc2626' }, expert: { bg: '#f3e8ff', c: '#7c3aed' }
449
  };
450
- const lvl = LEVEL_STYLE[level] || LEVEL_STYLE.hard;
451
 
452
- return (
453
- <div style={{
454
- fontFamily: "'DM Sans', 'Helvetica Neue', Arial, sans-serif",
455
- background: '#f8fafc', minHeight: '100vh', padding: '0 0 32px 0',
456
- color: '#1e293b',
457
- }}>
458
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
459
 
460
- {/* ── TOP NAV ── */}
461
- <div style={{
462
- background: '#fff', borderBottom: '1px solid #e2e8f0',
463
- padding: '0 24px', height: 48, display: 'flex', alignItems: 'center', gap: 16,
464
- position: 'sticky', top: 0, zIndex: 10,
465
- }}>
466
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
467
- <div style={{ width: 20, height: 20, borderRadius: 6, background: 'linear-gradient(135deg,#6366f1,#8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
468
- <span style={{ fontSize: 11 }}>🧠</span>
469
- </div>
470
- <span style={{ fontWeight: 700, fontSize: 15, color: '#0f172a', letterSpacing: '-0.02em' }}>StressTest</span>
 
471
  </div>
472
-
473
- <Pill color="#22c55e" label="Live" />
474
- <Pill color="#6366f1" label="Training" />
475
- <Pill color="#f59e0b" label={`Episode ${episode}`} />
476
-
477
- {error && <span style={{ fontSize: 11, color: '#ef4444', marginLeft: 4 }}>{error}</span>}
478
-
479
- <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 12 }}>
480
- <select value={targetWorker} onChange={e => setTargetWorker(e.target.value)}
481
- style={{
482
- fontSize: 12, border: '1px solid #e2e8f0', borderRadius: 6, padding: '4px 10px',
483
- background: '#f8fafc', color: '#1e293b', outline: 'none', cursor: 'pointer', fontWeight: 600
484
- }}>
485
- <option value="w1">🎯 Assign to Employee 1</option>
486
- <option value="w2">🎯 Assign to Employee 2</option>
487
- <option value="w3">🎯 Assign to Employee 3</option>
488
- </select>
489
- <select value={level} onChange={e => { setLevel(e.target.value) }}
490
- style={{
491
- fontSize: 12, border: '1px solid #e2e8f0', borderRadius: 6, padding: '4px 10px',
492
- background: '#fff', color: '#1e293b', outline: 'none', cursor: 'pointer'
493
- }}>
494
- {['easy', 'medium', 'hard', 'expert'].map(l => <option key={l}>{l}</option>)}
495
  </select>
496
- <button onClick={handleReset} disabled={loading} style={{
497
- fontSize: 12, border: '1px solid #e2e8f0', borderRadius: 6, padding: '4px 12px',
498
- background: loading ? '#f1f5f9' : '#fff', color: '#64748b', cursor: 'pointer',
499
- display: 'flex', alignItems: 'center', gap: 5,
500
- }}>
501
- <span style={{ display: 'inline-block', animation: loading ? 'spin 1s linear infinite' : 'none' }}>↻</span> Reset
502
  </button>
503
- <span style={{ fontSize: 12, color: '#64748b' }}>
504
- Step <b style={{ color: '#0f172a', fontFamily: 'DM Mono,monospace' }}>{step} / {maxStep}</b>
505
- </span>
506
- <div style={{
507
- background: lvl.bg, color: lvl.c, fontSize: 11, fontWeight: 700,
508
- padding: '3px 10px', borderRadius: 6, letterSpacing: '0.04em', textTransform: 'capitalize',
509
- }}>{level}</div>
510
  </div>
511
  </div>
512
 
513
- <div style={{ maxWidth: 1200, margin: '0 auto', padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
514
-
515
- {/* ── ROW 1: 3 FTEs + overall stats ── */}
516
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 14 }}>
517
- {(workers || []).map(w => {
518
- const wid = w?.id || 'w?';
519
- const wexp = w?.expertise || 'none';
520
- const weng = w?.energy ?? 0;
521
- const wstress = w?.stress ?? 0;
522
- return (
523
- <StatCard key={wid}
524
- label={`Employee ${wid.replace('w','')} (${wexp.charAt(0).toUpperCase() + wexp.slice(1)})`}
525
- value={`Energy: ${(weng * 100).toFixed(0)}%`}
526
- sub={wstress > 0.65 ? 'Elevated Stress Level' : (weng < 0.35 ? 'High Fatigue' : `Stress: ${(wstress * 100).toFixed(0)}%`)}
527
- bar={weng} barColor={weng > 0.5 ? '#22c55e' : weng > 0.25 ? '#f59e0b' : '#ef4444'}
528
- />
529
- );
530
- })}
531
- <StatCard
532
- label="Episode reward"
533
- value={(epReward >= 0 ? '+' : '') + epReward.toFixed(2)}
534
- valueColor={epReward >= 0 ? '#22c55e' : '#ef4444'}
535
- sub={`vs baseline 0.30`}
536
- />
537
- <StatCard
538
- label="Tasks done"
539
- value={`${doneTasks} / ${tasks.length}`}
540
- sub={`${blockedCount} blocked, ${overdueCount} overdue`}
541
- bar={doneTasks / Math.max(tasks.length, 1)} barColor="#6366f1"
542
- />
543
  </div>
 
544
 
545
- {/* ── ROW 2: task queue + reward curve ── */}
546
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
547
-
548
- {/* task queue */}
549
- <Card label="TASK QUEUE">
550
- <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
551
- {tasks.map(t => {
552
- const ps = PRIORITY_STYLE[t.priority] || PRIORITY_STYLE.normal;
553
- const pc = PROGRESS_COLOR[t.priority] || '#6366f1';
554
- return (
555
- <div key={t.id} style={{
556
- display: 'flex', alignItems: 'center', gap: 10,
557
- padding: '9px 4px', borderBottom: '1px solid #f1f5f9',
558
- }}>
559
- {/* icon */}
560
- <div style={{
561
- width: 30, height: 30, borderRadius: 8, background: '#f8fafc', border: '1px solid #e2e8f0',
562
- display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, flexShrink: 0
563
- }}>
564
- {t.icon}
565
- </div>
566
- {/* name + sub */}
567
- <div style={{ flex: 1, minWidth: 0 }}>
568
- <div style={{ fontSize: 12, fontWeight: 600, color: '#0f172a', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
569
- {t.name}
570
- </div>
571
- <div style={{ fontSize: 10, color: '#94a3b8', marginTop: 1 }}>
572
- {t.deadline && <span>{t.deadline} · </span>}
573
- {t.deps || ''}
574
- </div>
575
- </div>
576
- {/* priority badge */}
577
- <div style={{
578
- background: ps.bg, color: ps.color, border: `1px solid ${ps.border}`,
579
- fontSize: 10, fontWeight: 600, padding: '2px 8px', borderRadius: 5,
580
- flexShrink: 0, textTransform: 'capitalize',
581
- }}>{t.priority}</div>
582
- {/* progress bar + pct */}
583
- <div style={{ width: 80, flexShrink: 0 }}>
584
- <div style={{ height: 4, background: '#e2e8f0', borderRadius: 99, overflow: 'hidden', marginBottom: 3 }}>
585
- <div style={{
586
- width: `${clamp(t.progress * 100, 0, 100)}%`, height: '100%',
587
- background: pc, borderRadius: 99, transition: 'width 0.4s ease'
588
- }} />
589
- </div>
590
- <div style={{ fontSize: 10, color: '#94a3b8', textAlign: 'right' }}>
591
- {(t.progress * 100).toFixed(0)}%
592
- </div>
593
- </div>
594
- {/* action buttons */}
595
- {t.priority !== 'blocked' && t.progress < 1 && (
596
- <div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
597
- <TinyBtn label="Work" onClick={() => doAction('work', t.id)} disabled={loading} color="#6366f1" />
598
- <TinyBtn label="Focus" onClick={() => doAction('focus', t.id)} disabled={loading} color="#8b5cf6" />
599
- </div>
600
- )}
601
- </div>
602
- );
603
- })}
604
  </div>
605
- {/* global actions */}
606
- <div style={{ display: 'flex', gap: 8, marginTop: 12, paddingTop: 12, borderTop: '1px solid #f1f5f9' }}>
607
- <TinyBtn label="☕ Break" onClick={() => doAction('break')} disabled={loading} color="#0891b2" wide />
608
- <TinyBtn label="⏸ Idle" onClick={() => doAction('delay')} disabled={loading} color="#64748b" wide />
609
- </div>
610
- </Card>
611
-
612
- {/* reward curve */}
613
- <Card label="REWARD CURVE — TRAINED VS BASELINE">
614
- <RewardCurve trained={trained} episode={episode} />
615
- </Card>
616
  </div>
 
617
 
618
- {/* ── ROW 3: multi-agent + schema drift + action log ── */}
619
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
620
-
621
- {/* multi-agent comms */}
622
- <Card label="MULTI-AGENT COMMUNICATION">
623
- {/* agent pills */}
624
- <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
625
- <AgentPill color="#6366f1" label="Manager agent" />
626
- <AgentPill color="#22c55e" label="Worker agent" />
627
- </div>
628
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
629
- {agentMsgs.map((m, i) => {
630
- const isManager = m.from === 'manager';
631
- const isEnv = m.from === 'env';
632
- return (
633
- <div key={i} style={{
634
- background: isManager ? '#eff6ff' : isEnv ? '#f8fafc' : '#f0fdf4',
635
- border: `1px solid ${isManager ? '#bfdbfe' : isEnv ? '#e2e8f0' : '#bbf7d0'}`,
636
- borderRadius: 8, padding: '8px 12px',
637
- }}>
638
- <div style={{
639
- fontSize: 9, fontWeight: 700, color: isManager ? '#6366f1' : isEnv ? '#94a3b8' : '#22c55e',
640
- marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.06em'
641
- }}>
642
- {isManager ? 'Manager → Worker' : isEnv ? 'Env → Both' : 'Worker → Env'}
643
- </div>
644
- <div style={{ fontSize: 11, color: '#334155', lineHeight: 1.5 }}>{m.text}</div>
645
- </div>
646
- );
647
- })}
648
  </div>
649
- </Card>
650
-
651
- {/* schema drift + action log stacked */}
652
- <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
653
- <Card label="SCHEMA DRIFT EVENTS">
654
- <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
655
- {schemaDrifts.map((e, i) => (
656
- <div key={i} style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
657
- <div style={{
658
- width: 10, height: 10, borderRadius: '50%', flexShrink: 0, marginTop: 3,
659
- background: e.dot === 'green' ? '#22c55e' : e.dot === 'orange' ? '#f59e0b' : '#cbd5e1',
660
- }} />
661
- <div>
662
- <div style={{ fontSize: 12, fontWeight: 600, color: '#0f172a' }}>{e.title}</div>
663
- <div style={{ fontSize: 10, color: '#94a3b8', marginTop: 1 }}>triggered at step {e.step}</div>
664
- </div>
665
- </div>
666
- ))}
667
- {schemaDrifts.length === 0 && (
668
- <div style={{ fontSize: 11, color: '#cbd5e1', textAlign: 'center', padding: '10px 0' }}>No drift events yet</div>
 
 
 
 
 
 
 
 
 
 
 
669
  )}
670
  </div>
671
- </Card>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
 
673
- <Card label="STEP ACTION LOG" style={{ flex: 1 }}>
674
- <div ref={logRef} style={{ maxHeight: 200, overflowY: 'auto' }}>
675
- <table style={{ width: '100%', borderCollapse: 'collapse' }}>
676
- <tbody>
677
- {actionLog.map((row, i) => (
678
- <tr key={i} style={{ borderBottom: '1px solid #f1f5f9' }}>
679
- <td style={{ padding: '5px 6px', fontSize: 10, fontFamily: 'DM Mono,monospace', color: '#94a3b8', width: 28 }}>{row.step}</td>
680
- <td style={{
681
- padding: '5px 6px', fontSize: 10, fontWeight: 600,
682
- color: row.action === 'focus' ? '#6366f1' : row.action === 'work' ? '#0891b2' :
683
- row.action === 'break' ? '#22c55e' : row.action === 'switch' ? '#f59e0b' : '#94a3b8',
684
- width: 44
685
- }}>{row.action}</td>
686
- <td style={{ padding: '5px 6px', fontSize: 10, color: '#64748b', flex: 1 }}>{row.detail}</td>
687
- <td style={{
688
- padding: '5px 6px', fontSize: 10, fontFamily: 'DM Mono,monospace', fontWeight: 600,
689
- color: row.pos ? '#22c55e' : '#ef4444', textAlign: 'right', width: 44
690
- }}>{row.reward}</td>
691
- </tr>
692
- ))}
693
- {actionLog.length === 0 && (
694
- <tr><td colSpan={4} style={{ padding: '16px 0', textAlign: 'center', fontSize: 11, color: '#cbd5e1' }}>No actions yet</td></tr>
695
- )}
696
- </tbody>
697
- </table>
698
- </div>
699
- </Card>
700
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  </div>
702
  </div>
703
 
704
- <style>{`
705
- @keyframes spin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
706
- * { box-sizing: border-box; }
707
- ::-webkit-scrollbar { width:4px; height:4px; }
708
- ::-webkit-scrollbar-track { background:#f1f5f9; }
709
- ::-webkit-scrollbar-thumb { background:#cbd5e1; border-radius:99px; }
710
- `}</style>
711
- </div>
712
- );
713
- }
714
-
715
- /* ── small atoms ─────────────────────────────────────────── */
716
- function Pill({ color, label }) {
717
- return (
718
- <div style={{
719
- display: 'flex', alignItems: 'center', gap: 5,
720
- fontSize: 12, color, fontWeight: 500,
721
- }}>
722
- <div style={{ width: 7, height: 7, borderRadius: '50%', background: color }} />
723
- {label}
724
- </div>
725
- );
726
- }
727
-
728
- function AgentPill({ color, label }) {
729
- return (
730
- <div style={{
731
- display: 'flex', alignItems: 'center', gap: 6,
732
- border: '1px solid #e2e8f0', borderRadius: 99,
733
- padding: '4px 10px', fontSize: 11, color: '#334155',
734
- }}>
735
- <div style={{ width: 8, height: 8, borderRadius: '50%', background: color }} />
736
- {label}
737
- </div>
738
- );
739
- }
740
-
741
- function TinyBtn({ label, onClick, disabled, color, wide }) {
742
- return (
743
- <button onClick={onClick} disabled={disabled} style={{
744
- fontSize: 11, fontWeight: 600,
745
- padding: wide ? '5px 14px' : '4px 9px',
746
- background: `${color}10`,
747
- border: `1px solid ${color}30`,
748
- borderRadius: 6, color,
749
- cursor: disabled ? 'not-allowed' : 'pointer',
750
- opacity: disabled ? 0.5 : 1,
751
- transition: 'all 0.15s',
752
- whiteSpace: 'nowrap',
753
- }}>{label}</button>
754
- );
755
- }
756
 
757
- function Card({ label, children, style = {} }) {
758
- return (
759
- <div style={{
760
- background: '#fff',
761
- border: '1px solid #e8ecf0',
762
- borderRadius: 12,
763
- padding: '16px 18px',
764
- ...style,
765
- }}>
766
- <div style={{
767
- fontSize: 10, fontWeight: 700, color: '#94a3b8',
768
- letterSpacing: '0.1em', textTransform: 'uppercase',
769
- marginBottom: 14,
770
- }}>{label}</div>
771
- {children}
772
  </div>
773
  );
774
  }
775
-
776
- function StatCard({ label, value, sub, bar, barColor, valueColor }) {
777
- return (
778
- <div style={{
779
- background: '#fff', border: '1px solid #e8ecf0', borderRadius: 12, padding: '16px 18px',
780
- }}>
781
- <div style={{
782
- fontSize: 10, fontWeight: 700, color: '#94a3b8', letterSpacing: '0.1em',
783
- textTransform: 'uppercase', marginBottom: 8
784
- }}>{label}</div>
785
- <div style={{
786
- fontSize: 28, fontWeight: 700, color: valueColor || '#0f172a',
787
- letterSpacing: '-0.03em', lineHeight: 1, marginBottom: 8, fontFamily: 'DM Mono,monospace'
788
- }}>
789
- {value}
790
- </div>
791
- {bar !== undefined && (
792
- <div style={{ height: 4, background: '#f1f5f9', borderRadius: 99, overflow: 'hidden', marginBottom: 6 }}>
793
- <div style={{
794
- width: `${clamp(bar * 100, 0, 100)}%`, height: '100%',
795
- background: barColor, borderRadius: 99, transition: 'width 0.5s ease'
796
- }} />
797
- </div>
798
- )}
799
- <div style={{ fontSize: 11, color: '#94a3b8' }}>{sub}</div>
800
- </div>
801
- );
802
- }
 
1
+ import React, { useState, useEffect, useRef } from 'react';
 
2
 
3
+ const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:7860';
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  export default function Dashboard() {
6
+ const [level, setLevel] = useState('medium');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  const [sessionId, setSessionId] = useState(null);
8
+ const [obs, setObs] = useState(null);
9
+ const [stateData, setStateData] = useState(null);
10
+ const [logs, setLogs] = useState([]);
11
  const [loading, setLoading] = useState(false);
 
12
  const [error, setError] = useState(null);
13
+ const [rewardHistory, setRewardHistory] = useState([]);
14
+ const scrollRef = useRef(null);
15
 
16
+ const fetchState = async (sid) => {
17
+ try {
18
+ const res = await fetch(`${API_BASE}/state?session_id=${sid}`);
19
+ if (res.ok) setStateData(await res.json());
20
+ } catch(e) { console.error(e); }
21
+ };
22
 
23
+ const handleReset = async () => {
24
+ setLoading(true);
25
+ setError(null);
26
+ setRewardHistory([]);
27
  try {
28
  const res = await fetch(`${API_BASE}/reset`, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({ task_id: level })
32
  });
 
33
  const data = await res.json();
34
+ setSessionId(data.session_id);
35
+ setObs(data.observation);
36
+ setLogs([{ type: 'system', msg: `Episode started: ${level}` }]);
37
+ await fetchState(data.session_id);
38
+ } catch (err) {
39
+ setError('Cannot reach backend at ' + API_BASE);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  } finally { setLoading(false); }
41
+ };
42
 
43
+ const handleAction = async (actionType, taskId = null) => {
44
+ if (!sessionId) return;
45
  setLoading(true);
46
+ const action = { type: actionType };
47
+ if (taskId) action.task_id = taskId;
48
  try {
49
  const res = await fetch(`${API_BASE}/step`, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({ session_id: sessionId, action })
53
  });
54
  const data = await res.json();
55
+ setObs(data.observation);
56
+ setRewardHistory(prev => [...prev, {
57
+ step: prev.length + 1,
58
+ reward: data.reward
59
+ }]);
60
+ setLogs(prev => [...prev, {
61
+ type: data.reward >= 0 ? 'positive' : 'negative',
62
+ msg: `${actionType}${taskId ? ' '+taskId : ''} → reward: ${data.reward?.toFixed(3)}`,
63
+ reward: data.reward
64
+ }]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  if (data.done) {
66
+ setLogs(prev => [...prev, {
67
+ type: 'system',
68
+ msg: `DONE. Final score: ${data.info?.final_score?.toFixed(3) || 'N/A'}`
69
+ }]);
70
  }
71
+ await fetchState(sessionId);
72
+ setTimeout(() => {
73
+ if(scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
74
+ }, 50);
75
+ } catch (err) {
76
+ setError(err.message);
77
+ } finally { setLoading(false); }
 
 
 
 
 
78
  };
 
79
 
80
+ const workers = obs?.visible_state?.workers || [];
81
+ const tasks = obs?.tasks || [];
82
+ const firstWorker = workers[0] || {};
 
 
 
 
83
 
84
+ return (
85
+ <div style={{ minHeight: '100vh', background: '#f8fafc', fontFamily: 'system-ui, sans-serif', padding: 24 }}>
86
+
87
+ {/* HEADER */}
88
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
89
+ <div>
90
+ <h1 style={{ fontSize: 22, fontWeight: 700, color: '#0f172a', margin: 0 }}>
91
+ 🧠 StressTest Cognitive Load Manager
92
+ </h1>
93
+ <p style={{ fontSize: 12, color: '#64748b', margin: '4px 0 0' }}>
94
+ Multi-Agent RL Environment · Meta OpenEnv Hackathon
95
+ </p>
96
  </div>
97
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
98
+ <select value={level} onChange={e => setLevel(e.target.value)}
99
+ style={{ border: '1px solid #e2e8f0', borderRadius: 8, padding: '6px 10px', fontSize: 13 }}>
100
+ <option value="easy">Easy</option>
101
+ <option value="medium">Medium</option>
102
+ <option value="hard">Hard</option>
103
+ <option value="expert">Expert</option>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  </select>
105
+ <button onClick={handleReset} disabled={loading}
106
+ style={{ background: '#6366f1', color: '#fff', border: 'none', borderRadius: 8,
107
+ padding: '8px 18px', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
108
+ {loading ? 'Loading...' : sessionId ? '↺ Reset' : '▶ Start'}
 
 
109
  </button>
 
 
 
 
 
 
 
110
  </div>
111
  </div>
112
 
113
+ {error && (
114
+ <div style={{ background: '#fef2f2', border: '1px solid #fca5a5', borderRadius: 8,
115
+ padding: '10px 14px', marginBottom: 16, fontSize: 13, color: '#dc2626' }}>
116
+ ⚠️ {error}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  </div>
118
+ )}
119
 
120
+ {/* WORKER METRICS */}
121
+ {workers.length > 0 && (
122
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 16 }}>
123
+ {[
124
+ { label: 'Fatigue', value: firstWorker.fatigue_level || '—',
125
+ color: firstWorker.fatigue_level === 'high' ? '#ef4444' : firstWorker.fatigue_level === 'medium' ? '#f59e0b' : '#22c55e' },
126
+ { label: 'Stress', value: firstWorker.stress_level || '—',
127
+ color: firstWorker.stress_level === 'critical' ? '#ef4444' : firstWorker.stress_level === 'elevated' ? '#f59e0b' : '#22c55e' },
128
+ { label: 'Step', value: obs?.time_step ?? '—', color: '#6366f1' },
129
+ { label: 'Tasks Done', value: tasks.filter(t => t.progress >= 1.0).length + '/' + tasks.length, color: '#0ea5e9' },
130
+ ].map(m => (
131
+ <div key={m.label} style={{ background: '#fff', border: '1px solid #e2e8f0',
132
+ borderRadius: 12, padding: '14px 16px', textAlign: 'center' }}>
133
+ <div style={{ fontSize: 10, color: '#94a3b8', textTransform: 'uppercase',
134
+ letterSpacing: '0.08em', marginBottom: 6 }}>{m.label}</div>
135
+ <div style={{ fontSize: 22, fontWeight: 700, color: m.color }}>{m.value}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  </div>
137
+ ))}
 
 
 
 
 
 
 
 
 
 
138
  </div>
139
+ )}
140
 
141
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
142
+
143
+ {/* TASK LIST */}
144
+ <div style={{ background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12, padding: 16 }}>
145
+ <div style={{ fontSize: 11, fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase',
146
+ letterSpacing: '0.08em', marginBottom: 12 }}>Task Queue</div>
147
+ {tasks.length === 0 && (
148
+ <div style={{ color: '#cbd5e1', fontSize: 13, textAlign: 'center', padding: 20 }}>
149
+ Press Start to begin episode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  </div>
151
+ )}
152
+ {tasks.map(task => (
153
+ <div key={task.id} style={{ display: 'flex', alignItems: 'center', gap: 10,
154
+ padding: '8px 0', borderBottom: '1px solid #f1f5f9' }}>
155
+ <div style={{ flex: 1 }}>
156
+ <div style={{ fontSize: 13, fontWeight: 500, color: '#0f172a' }}>
157
+ {task.task_type} <span style={{ fontSize: 10, color: '#94a3b8' }}>#{task.id}</span>
158
+ </div>
159
+ <div style={{ fontSize: 11, color: '#94a3b8' }}>
160
+ deadline: {task.deadline ?? ''} · {task.depends_on ? `depends: ${task.depends_on}` : 'no dep'}
161
+ </div>
162
+ <div style={{ height: 3, background: '#f1f5f9', borderRadius: 99, marginTop: 4, overflow: 'hidden' }}>
163
+ <div style={{ width: `${task.progress * 100}%`, height: '100%',
164
+ background: task.progress >= 1 ? '#22c55e' : '#6366f1', borderRadius: 99 }} />
165
+ </div>
166
+ </div>
167
+ <span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 99, fontWeight: 600,
168
+ background: task.priority === 'critical' ? '#fef2f2' : task.priority === 'high' ? '#fffbeb' : '#f0fdf4',
169
+ color: task.priority === 'critical' ? '#dc2626' : task.priority === 'high' ? '#d97706' : '#16a34a' }}>
170
+ {task.priority}
171
+ </span>
172
+ <div style={{ display: 'flex', gap: 4 }}>
173
+ {sessionId && task.progress < 1.0 && (
174
+ <>
175
+ <button onClick={() => handleAction('work', task.id)}
176
+ style={{ fontSize: 10, padding: '3px 8px', borderRadius: 6, border: '1px solid #e2e8f0',
177
+ background: '#f8fafc', cursor: 'pointer' }}>work</button>
178
+ <button onClick={() => handleAction('focus', task.id)}
179
+ style={{ fontSize: 10, padding: '3px 8px', borderRadius: 6, border: '1px solid #6366f1',
180
+ background: '#eef2ff', color: '#6366f1', cursor: 'pointer' }}>focus</button>
181
+ </>
182
  )}
183
  </div>
184
+ </div>
185
+ ))}
186
+ {sessionId && (
187
+ <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
188
+ <button onClick={() => handleAction('break')}
189
+ style={{ flex: 1, padding: '8px', borderRadius: 8, border: '1px solid #e2e8f0',
190
+ background: '#f0fdf4', color: '#16a34a', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>
191
+ ☕ Break
192
+ </button>
193
+ <button onClick={() => handleAction('delay')}
194
+ style={{ flex: 1, padding: '8px', borderRadius: 8, border: '1px solid #e2e8f0',
195
+ background: '#f8fafc', color: '#64748b', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>
196
+ ⏸ Delay
197
+ </button>
198
+ </div>
199
+ )}
200
+ </div>
201
 
202
+ {/* REWARD CURVE */}
203
+ <div style={{ background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12, padding: 16 }}>
204
+ <div style={{ fontSize: 11, fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase',
205
+ letterSpacing: '0.08em', marginBottom: 12 }}>Reward Per Step</div>
206
+ {rewardHistory.length === 0 ? (
207
+ <div style={{ color: '#cbd5e1', fontSize: 13, textAlign: 'center', padding: 40 }}>
208
+ Rewards will appear here as the agent acts
209
+ </div>
210
+ ) : (
211
+ <div style={{ position: 'relative', height: 180 }}>
212
+ <svg width="100%" height="180" viewBox={`0 0 ${Math.max(rewardHistory.length * 20, 300)} 180`}
213
+ preserveAspectRatio="none">
214
+ <line x1="0" y1="90" x2="10000" y2="90" stroke="#f1f5f9" strokeWidth="1" />
215
+ {rewardHistory.map((d, i) => {
216
+ const x = i * 20 + 10;
217
+ const y = 90 - (d.reward * 70);
218
+ const prev = rewardHistory[i - 1];
219
+ return (
220
+ <g key={i}>
221
+ {prev && (
222
+ <line x1={(i-1)*20+10} y1={90-(prev.reward*70)} x2={x} y2={y}
223
+ stroke={d.reward >= 0 ? '#6366f1' : '#ef4444'} strokeWidth="2" />
224
+ )}
225
+ <circle cx={x} cy={y} r="3"
226
+ fill={d.reward >= 0 ? '#6366f1' : '#ef4444'} />
227
+ </g>
228
+ );
229
+ })}
230
+ </svg>
231
+ </div>
232
+ )}
233
+ {rewardHistory.length > 0 && (
234
+ <div style={{ display: 'flex', gap: 16, marginTop: 8 }}>
235
+ {[
236
+ { label: 'Total', val: rewardHistory.reduce((s,d) => s+d.reward, 0).toFixed(3) },
237
+ { label: 'Mean', val: (rewardHistory.reduce((s,d) => s+d.reward, 0)/rewardHistory.length).toFixed(3) },
238
+ { label: 'Steps', val: rewardHistory.length },
239
+ ].map(s => (
240
+ <div key={s.label} style={{ textAlign: 'center' }}>
241
+ <div style={{ fontSize: 10, color: '#94a3b8' }}>{s.label}</div>
242
+ <div style={{ fontSize: 14, fontWeight: 700, color: '#0f172a' }}>{s.val}</div>
243
+ </div>
244
+ ))}
245
+ </div>
246
+ )}
247
  </div>
248
  </div>
249
 
250
+ {/* ACTION LOG */}
251
+ <div style={{ background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12, padding: 16 }}>
252
+ <div style={{ fontSize: 11, fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase',
253
+ letterSpacing: '0.08em', marginBottom: 10 }}>Action Log</div>
254
+ <div ref={scrollRef} style={{ height: 120, overflowY: 'auto', fontFamily: 'monospace', fontSize: 12 }}>
255
+ {logs.length === 0 && (
256
+ <span style={{ color: '#cbd5e1' }}>No actions yet...</span>
257
+ )}
258
+ {logs.map((log, i) => (
259
+ <div key={i} style={{ padding: '2px 0',
260
+ color: log.type === 'positive' ? '#16a34a' : log.type === 'negative' ? '#dc2626' : '#64748b' }}>
261
+ [{i}] {log.msg}
262
+ </div>
263
+ ))}
264
+ </div>
265
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  </div>
268
  );
269
  }