Shreeraj Mummidivarapu commited on
Update Dashboard.jsx
Browse files- frontend/src/components/Dashboard.jsx +223 -756
frontend/src/components/Dashboard.jsx
CHANGED
|
@@ -1,802 +1,269 @@
|
|
| 1 |
-
|
| 2 |
-
// import { RefreshCw, Briefcase, Coffee, Clock } from 'lucide-react';
|
| 3 |
|
| 4 |
-
|
| 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('
|
| 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
|
|
|
|
| 358 |
|
| 359 |
-
const
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
|
|
|
| 366 |
try {
|
| 367 |
const res = await fetch(`${API_BASE}/reset`, {
|
| 368 |
-
method: 'POST',
|
| 369 |
-
|
|
|
|
| 370 |
});
|
| 371 |
-
if (!res.ok) throw new Error('Server error');
|
| 372 |
const data = await res.json();
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 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 |
-
}
|
| 395 |
|
| 396 |
-
const
|
| 397 |
-
if (!sessionId)
|
| 398 |
setLoading(true);
|
| 399 |
-
const action = { type
|
|
|
|
| 400 |
try {
|
| 401 |
const res = await fetch(`${API_BASE}/step`, {
|
| 402 |
-
method: 'POST',
|
| 403 |
-
|
|
|
|
| 404 |
});
|
| 405 |
const data = await res.json();
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 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 |
-
|
| 434 |
-
|
| 435 |
-
|
|
|
|
| 436 |
}
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 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 |
-
|
| 453 |
-
|
| 454 |
-
|
| 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 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
<
|
| 470 |
-
|
|
|
|
| 471 |
</div>
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 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}
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 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 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 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 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
{
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 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 |
-
|
| 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 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
<
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 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 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
)}
|
| 670 |
</div>
|
| 671 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
</div>
|
| 702 |
</div>
|
| 703 |
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
::
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
}
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 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 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|