AE-Shree
updated with the model before and after
6783f47
import React, { useState, useEffect, useRef, useCallback } from 'react'
// Relative URL = same origin. Works on HF Spaces (frontend+backend on :7860)
// and locally via the Vite proxy defined in vite.config.js.
const API = ''
// ── Tiny SVG line chart ────────────────────────────────────────────────────────
function LineChart({ data, color = '#6366f1', height = 130, label }) {
if (!data.length) return (
<div style={{ height, display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#cbd5e1', fontSize: 12 }}>
{label || 'Waiting for data…'}
</div>
)
const W = Math.max(data.length * 18, 260)
const lo = Math.min(...data)
const hi = Math.max(...data)
const span = hi === lo ? 1 : hi - lo
const py = v => (height - 16) - ((v - lo) / span) * (height - 28) + 8
const pts = data.map((v, i) => `${i * 18 + 9},${py(v)}`).join(' ')
return (
<svg width="100%" height={height} viewBox={`0 0 ${W} ${height}`}
preserveAspectRatio="none" style={{ display: 'block' }}>
{/* zero line */}
<line x1="0" y1={py(0)} x2={W} y2={py(0)}
stroke="#e2e8f0" strokeWidth="1" strokeDasharray="4 3" />
{/* fill area */}
<polyline
points={[`0,${height}`, ...data.map((v, i) => `${i * 18 + 9},${py(v)}`),
`${(data.length - 1) * 18 + 9},${height}`].join(' ')}
fill={color + '18'} stroke="none"
/>
{/* line */}
<polyline points={pts} fill="none" stroke={color} strokeWidth="2"
strokeLinejoin="round" strokeLinecap="round" />
{/* last point dot */}
{data.length > 0 && (
<circle cx={(data.length - 1) * 18 + 9} cy={py(data[data.length - 1])}
r="4" fill={color} />
)}
</svg>
)
}
// Dual line chart (energy + stress)
function DualChart({ energy, stress, height = 130 }) {
const data = energy.length ? energy : []
if (!data.length) return (
<div style={{ height, display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#cbd5e1', fontSize: 12 }}>Waiting for data…</div>
)
const W = Math.max(data.length * 18, 260)
const py = (v, lo, hi) => {
const span = hi === lo ? 1 : hi - lo
return (height - 16) - ((v - lo) / span) * (height - 28) + 8
}
const loE = Math.min(...energy, 0), hiE = Math.max(...energy, 1)
const loS = Math.min(...stress, 0), hiS = Math.max(...stress, 1)
const ePts = energy.map((v, i) => `${i * 18 + 9},${py(v, loE, hiE)}`).join(' ')
const sPts = stress.map((v, i) => `${i * 18 + 9},${py(v, loS, hiS)}`).join(' ')
return (
<svg width="100%" height={height} viewBox={`0 0 ${W} ${height}`}
preserveAspectRatio="none" style={{ display: 'block' }}>
<polyline points={ePts} fill="none" stroke="#22c55e" strokeWidth="2"
strokeLinejoin="round" strokeLinecap="round" />
<polyline points={sPts} fill="none" stroke="#f59e0b" strokeWidth="2"
strokeLinejoin="round" strokeLinecap="round" strokeDasharray="5 3" />
</svg>
)
}
// ── Tiny components ────────────────────────────────────────────────────────────
function Chip({ label, value, color, bg }) {
return (
<div style={{ background: bg || '#fff', border: '1px solid #e2e8f0',
borderRadius: 12, padding: '12px 14px', textAlign: 'center', minWidth: 80 }}>
<div style={{ fontSize: 10, color: '#94a3b8', textTransform: 'uppercase',
letterSpacing: '.07em', marginBottom: 4 }}>{label}</div>
<div style={{ fontSize: 20, fontWeight: 800, color: color || '#0f172a',
lineHeight: 1.1 }}>{value}</div>
</div>
)
}
function Tag({ children, color, bg }) {
return (
<span style={{ fontSize: 10, fontWeight: 700, padding: '2px 7px',
borderRadius: 99, background: bg, color, whiteSpace: 'nowrap' }}>
{children}
</span>
)
}
const PRI_STYLE = {
critical: { color: '#dc2626', bg: '#fef2f2' },
high: { color: '#d97706', bg: '#fffbeb' },
normal: { color: '#16a34a', bg: '#f0fdf4' },
low: { color: '#64748b', bg: '#f8fafc' },
}
// ── Scoring Formula Card ───────────────────────────────────────────────────────
const FORMULA_ITEMS = [
{ key:'completion', label:'Task Completion', weight:0.60, color:'#6366f1',
desc:'Fraction of tasks fully completed, weighted by priority' },
{ key:'deadline', label:'Deadline Adherence', weight:0.22, color:'#0ea5e9',
desc:'Bonus for finishing before deadline; penalty for missing it' },
{ key:'energy', label:'Energy Efficiency', weight:0.10, color:'#22c55e',
desc:'Penalises high worker fatigue and stress spikes' },
{ key:'dependency', label:'Dependency Bonus', weight:0.05, color:'#f59e0b',
desc:'Reward for respecting task dependency order' },
{ key:'interrupt', label:'Interruption Bonus', weight:0.03, color:'#f43f5e',
desc:'Reward for minimising context-switching interruptions' },
]
function ScoringFormulaCard() {
return (
<div style={{ background:'#fff', border:'1px solid #e2e8f0',
borderRadius:14, padding:'20px 20px 16px', marginBottom:16 }}>
<div style={{ marginBottom:16 }}>
<div style={{ fontSize:14, fontWeight:800, color:'#0f172a', marginBottom:4 }}>
Reward Scoring Formula
</div>
<div style={{ fontSize:11, color:'#64748b' }}>
Each action is scored on 5 dimensions. Weights reflect cognitive-load research priorities.
</div>
</div>
{/* Stacked weight bar */}
<div style={{ marginBottom:16 }}>
<div style={{ fontSize:10, color:'#94a3b8', fontWeight:700, textTransform:'uppercase',
letterSpacing:'.07em', marginBottom:6 }}>Weight distribution</div>
<div style={{ display:'flex', height:18, borderRadius:99, overflow:'hidden',
boxShadow:'0 1px 4px #0001' }}>
{FORMULA_ITEMS.map(it => (
<div key={it.key} title={`${it.label}: ×${it.weight}`}
style={{ width:`${it.weight * 100}%`, background:it.color,
display:'flex', alignItems:'center', justifyContent:'center' }}>
{it.weight >= 0.10 && (
<span style={{ fontSize:9, color:'#fff', fontWeight:800 }}>
{(it.weight * 100).toFixed(0)}%
</span>
)}
</div>
))}
</div>
<div style={{ display:'flex', marginTop:5 }}>
{FORMULA_ITEMS.map(it => (
<div key={it.key} style={{ width:`${it.weight * 100}%`,
textAlign:'center', fontSize:8.5, color:it.color, fontWeight:700,
overflow:'hidden', whiteSpace:'nowrap' }}>
{it.weight >= 0.08 ? it.label.split(' ')[0] : ''}
</div>
))}
</div>
</div>
{/* Component cards */}
<div style={{ display:'grid', gridTemplateColumns:'repeat(5,1fr)', gap:10,
marginBottom:14 }}>
{FORMULA_ITEMS.map(it => (
<div key={it.key} style={{
background:`${it.color}08`,
border:`1.5px solid ${it.color}30`,
borderRadius:12, padding:'12px 12px 10px',
position:'relative', overflow:'hidden',
}}>
<div style={{ position:'absolute', top:0, left:0, right:0,
height:4, background:it.color, borderRadius:'12px 12px 0 0' }}/>
<div style={{ display:'inline-flex', alignItems:'center',
background:it.color, color:'#fff', borderRadius:99,
padding:'2px 9px', fontSize:13, fontWeight:900,
marginBottom:8, marginTop:2 }}>
×{it.weight.toFixed(2)}
</div>
<div style={{ fontSize:11, fontWeight:700, color:'#1e293b',
marginBottom:4, lineHeight:1.3 }}>
{it.label}
</div>
<div style={{ fontSize:10, color:'#64748b', lineHeight:1.4 }}>
{it.desc}
</div>
</div>
))}
</div>
{/* Formula expression */}
<div style={{ background:'#f8fafc', borderRadius:10,
padding:'12px 16px', border:'1px solid #e2e8f0' }}>
<div style={{ fontSize:10, color:'#94a3b8', fontWeight:700,
textTransform:'uppercase', letterSpacing:'.07em', marginBottom:6 }}>
Formula
</div>
<div style={{ fontFamily:'monospace', fontSize:12, lineHeight:1.8,
display:'flex', flexWrap:'wrap', gap:'0 4px', alignItems:'center' }}>
<span style={{ color:'#0f172a', fontWeight:700 }}>score =</span>
{FORMULA_ITEMS.map((it, idx) => (
<span key={it.key}>
<span style={{ color:it.color, fontWeight:800 }}>
{it.key === 'completion' ? 'completion' :
it.key === 'deadline' ? 'deadline' :
it.key === 'energy' ? 'energy' :
it.key === 'dependency' ? 'dep' : 'interrupt'}×{it.weight}
</span>
{idx < FORMULA_ITEMS.length - 1 &&
<span style={{ color:'#94a3b8' }}> + </span>}
</span>
))}
<span style={{ color:'#94a3b8', marginLeft:6 }}>∈ (0.01, 0.99)</span>
</div>
</div>
</div>
)
}
// ── Training Performance Card (data from CLM_GRPO_Training.ipynb) ──────────────
// Mean reward sampled at training checkpoints from the GRPO training run.
const TRAINING_CURVE = [
{ step: 1, reward: 0.1006 },
{ step: 50, reward: 0.0882 },
{ step: 100, reward: 0.1192 },
{ step: 200, reward: 0.1010 },
{ step: 300, reward: 0.1383 },
{ step: 400, reward: 0.1700 },
{ step: 500, reward: 0.2201 },
{ step: 600, reward: 0.2542 },
{ step: 700, reward: 0.2595 },
{ step: 800, reward: 0.2648 },
{ step: 900, reward: 0.2345 },
{ step: 1000, reward: 0.2648 },
]
// Per-action mean reward measured before training (heuristic baseline) and
// after 1000 steps of GRPO training.
const ACTION_COMPARISON = [
{ action: 'Work', before: 0.045, after: 0.102 },
{ action: 'Focus', before: 0.124, after: 0.249 },
{ action: 'Break', before: 0.030, after: 0.030 },
{ action: 'Delay', before: 0.010, after: 0.010 },
]
function TrainingLineChart({ data, height = 160 }) {
const W = 720
const padL = 36, padR = 16, padT = 18, padB = 28
const innerW = W - padL - padR
const innerH = height - padT - padB
const xs = data.map(d => d.step)
const ys = data.map(d => d.reward)
const xLo = Math.min(...xs), xHi = Math.max(...xs)
const yLo = Math.min(0, ...ys), yHi = Math.max(...ys, 0.3)
const px = v => padL + ((v - xLo) / (xHi - xLo || 1)) * innerW
const py = v => padT + innerH - ((v - yLo) / (yHi - yLo || 1)) * innerH
const pts = data.map(d => `${px(d.step)},${py(d.reward)}`).join(' ')
return (
<svg viewBox={`0 0 ${W} ${height}`} width="100%" height={height}
preserveAspectRatio="none" style={{ display: 'block' }}>
{/* Y axis grid */}
{[0, 0.1, 0.2, 0.3].map(g => (
<g key={g}>
<line x1={padL} y1={py(g)} x2={W - padR} y2={py(g)}
stroke="#e2e8f0" strokeWidth="1" />
<text x={padL - 6} y={py(g) + 3} fontSize="10" fill="#94a3b8" textAnchor="end">
{g.toFixed(2)}
</text>
</g>
))}
{/* X axis labels */}
{[0, 250, 500, 750, 1000].map(s => (
<text key={s} x={px(s)} y={height - 8} fontSize="10" fill="#94a3b8"
textAnchor="middle">step {s}</text>
))}
{/* Area + line */}
<polyline
points={[`${px(xLo)},${py(yLo)}`, ...data.map(d => `${px(d.step)},${py(d.reward)}`),
`${px(xHi)},${py(yLo)}`].join(' ')}
fill="#6366f122" stroke="none"
/>
<polyline points={pts} fill="none" stroke="#6366f1" strokeWidth="2.5"
strokeLinejoin="round" strokeLinecap="round" />
{data.map(d => (
<circle key={d.step} cx={px(d.step)} cy={py(d.reward)} r="3" fill="#6366f1" />
))}
</svg>
)
}
function ActionComparisonChart({ data, height = 200 }) {
const W = 520
const padL = 60, padR = 16, padT = 18, padB = 36
const innerW = W - padL - padR
const innerH = height - padT - padB
const yMax = Math.max(...data.flatMap(d => [d.before, d.after]), 0.3) * 1.1
const groupW = innerW / data.length
const barW = (groupW - 18) / 2
return (
<svg viewBox={`0 0 ${W} ${height}`} width="100%" height={height}
preserveAspectRatio="none" style={{ display: 'block' }}>
{/* Y grid */}
{[0, 0.1, 0.2, 0.3].map(g => {
const y = padT + innerH - (g / yMax) * innerH
return (
<g key={g}>
<line x1={padL} y1={y} x2={W - padR} y2={y}
stroke="#e2e8f0" strokeWidth="1" />
<text x={padL - 6} y={y + 3} fontSize="10" fill="#94a3b8" textAnchor="end">
{g.toFixed(2)}
</text>
</g>
)
})}
{data.map((d, i) => {
const gx = padL + i * groupW + 9
const hBefore = (d.before / yMax) * innerH
const hAfter = (d.after / yMax) * innerH
return (
<g key={d.action}>
<rect x={gx} y={padT + innerH - hBefore} width={barW} height={hBefore}
fill="#94a3b8" rx="3" />
<rect x={gx + barW + 4} y={padT + innerH - hAfter} width={barW} height={hAfter}
fill="#6366f1" rx="3" />
<text x={gx + barW + 2} y={padT + innerH - hBefore - 4}
fontSize="9" fill="#64748b" textAnchor="middle">{d.before.toFixed(3)}</text>
<text x={gx + barW + barW + 6} y={padT + innerH - hAfter - 4}
fontSize="9" fontWeight="700" fill="#4338ca" textAnchor="middle">
{d.after.toFixed(3)}
</text>
<text x={gx + barW + 2} y={height - 14} fontSize="11" fontWeight="600"
fill="#334155" textAnchor="middle">{d.action}</text>
</g>
)
})}
</svg>
)
}
function TrainingPerformanceCard() {
const first = TRAINING_CURVE[0].reward
const last = TRAINING_CURVE[TRAINING_CURVE.length - 1].reward
const lift = (((last - first) / (Math.abs(first) || 1)) * 100).toFixed(0)
return (
<div style={{ background: '#fff', border: '1px solid #e2e8f0',
borderRadius: 14, padding: '20px 20px 16px', marginBottom: 16 }}>
<div style={{ marginBottom: 14, display: 'flex', alignItems: 'center',
justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
<div>
<div style={{ fontSize: 14, fontWeight: 800, color: '#0f172a', marginBottom: 4 }}>
Training Performance — Before vs After GRPO
</div>
<div style={{ fontSize: 11, color: '#64748b' }}>
Measured during 1000-step GRPO fine-tuning of the policy on the CLM environment
</div>
</div>
<div style={{ display: 'flex', gap: 10 }}>
<div style={{ background: '#f1f5f9', borderRadius: 10, padding: '8px 14px',
textAlign: 'center', minWidth: 90 }}>
<div style={{ fontSize: 9, color: '#94a3b8', fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '.06em' }}>Before</div>
<div style={{ fontSize: 18, fontWeight: 800, color: '#475569' }}>
{first.toFixed(3)}
</div>
</div>
<div style={{ background: '#eef2ff', borderRadius: 10, padding: '8px 14px',
textAlign: 'center', minWidth: 90 }}>
<div style={{ fontSize: 9, color: '#6366f1', fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '.06em' }}>After</div>
<div style={{ fontSize: 18, fontWeight: 800, color: '#4338ca' }}>
{last.toFixed(3)}
</div>
</div>
<div style={{ background: '#f0fdf4', borderRadius: 10, padding: '8px 14px',
textAlign: 'center', minWidth: 90 }}>
<div style={{ fontSize: 9, color: '#16a34a', fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '.06em' }}>Lift</div>
<div style={{ fontSize: 18, fontWeight: 800, color: '#16a34a' }}>
+{lift}%
</div>
</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 14 }}>
<div style={{ background: '#f8fafc', borderRadius: 10, padding: 12,
border: '1px solid #e2e8f0' }}>
<div style={{ fontSize: 10, fontWeight: 700, color: '#94a3b8',
textTransform: 'uppercase', letterSpacing: '.07em', marginBottom: 8 }}>
Mean Reward Across Training Steps
</div>
<TrainingLineChart data={TRAINING_CURVE} height={170} />
</div>
<div style={{ background: '#f8fafc', borderRadius: 10, padding: 12,
border: '1px solid #e2e8f0' }}>
<div style={{ fontSize: 10, fontWeight: 700, color: '#94a3b8',
textTransform: 'uppercase', letterSpacing: '.07em', marginBottom: 8,
display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Per-Action Reward</span>
<span style={{ fontSize: 10, fontWeight: 600, color: '#64748b' }}>
<span style={{ display: 'inline-block', width: 9, height: 9, background: '#94a3b8',
borderRadius: 2, verticalAlign: 'middle', marginRight: 4 }} />Before
&ensp;
<span style={{ display: 'inline-block', width: 9, height: 9, background: '#6366f1',
borderRadius: 2, verticalAlign: 'middle', marginRight: 4 }} />After
</span>
</div>
<ActionComparisonChart data={ACTION_COMPARISON} height={170} />
</div>
</div>
</div>
)
}
// ── Main Dashboard ─────────────────────────────────────────────────────────────
export default function Dashboard() {
// ── mode: 'stream' (auto-play via SSE) | 'manual' (interactive) ────────────
const [mode, setMode] = useState('stream')
const [difficulty, setDiff] = useState('medium')
const diffRef = useRef('medium')
useEffect(() => { diffRef.current = difficulty }, [difficulty])
// ── stream state ──────────────────────────────────────────────────────────
const [streaming, setStreaming] = useState(false)
const [streamDone, setStreamDone] = useState(false)
const [currentStep, setCurrentStep] = useState(null)
const [rewardTrace, setRwTrace] = useState([])
const [energyTrace, setEnTrace] = useState([])
const [stressTrace, setStTrace] = useState([])
const [tasks, setTasks] = useState([])
const [driftAlerts, setDrift] = useState([])
const [finalScore, setFinal] = useState(null)
const [currentAction, setAction] = useState(null)
const [episodeCount, setEpCount] = useState(0)
// history of scores across replays: [{ep, score, difficulty, steps}]
const [history, setHistory] = useState([])
const esRef = useRef(null)
const replayTimer = useRef(null)
// ── manual state ──────────────────────────────────────────────────────────
const [sessionId, setSession] = useState(null)
const [obs, setObs] = useState(null)
const [manLogs, setManLogs] = useState([])
const [manRewards, setManRw] = useState([])
const [manDone, setManDone] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const logRef = useRef(null)
// ── SSE streaming ─────────────────────────────────────────────────────────
const startStream = useCallback((diff) => {
const d = (typeof diff === 'string' && diff) ? diff : diffRef.current
if (esRef.current) { esRef.current.close(); esRef.current = null }
if (replayTimer.current) { clearTimeout(replayTimer.current); replayTimer.current = null }
// Reset per-episode state (keep history)
setStreaming(true); setStreamDone(false); setFinal(null)
setCurrentStep(null); setAction(null)
setRwTrace([]); setEnTrace([]); setStTrace([])
setTasks([]); setDrift([])
setEpCount(prev => prev + 1)
// Tracks whether this episode finished cleanly so onerror can ignore
// the connection-close the browser fires after the server ends the stream.
const episodeDone = { current: false }
const es = new EventSource(`${API}/stream/run?difficulty=${d}&delay_ms=350`)
esRef.current = es
es.onmessage = (ev) => {
let msg
try { msg = JSON.parse(ev.data) } catch { return }
if (!msg || typeof msg !== 'object') return
const num = (v, fallback = 0) => (typeof v === 'number' && !isNaN(v) ? v : fallback)
if (msg.type === 'reset') {
setTasks(Array.isArray(msg.tasks) ? msg.tasks : [])
setEnTrace([num(msg.energy, 1)])
setStTrace([num(msg.stress, 0)])
}
if (msg.type === 'step') {
setCurrentStep(num(msg.step, 0))
setAction(msg.action || null)
setTasks(Array.isArray(msg.tasks) ? msg.tasks : [])
setRwTrace(prev => [...prev, num(msg.reward)])
setEnTrace(prev => [...prev, num(msg.energy, 1)])
setStTrace(prev => [...prev, num(msg.stress, 0)])
if (msg.schema_drift) setDrift(prev => [...prev, msg.schema_drift])
if (msg.done) {
episodeDone.current = true
const score = typeof msg.final_score === 'number' ? msg.final_score : null
setFinal(score)
setStreamDone(true)
setStreaming(false)
setHistory(prev => [
{ ep: prev.length + 1, score, difficulty: d, steps: num(msg.step, 0) },
...prev.slice(0, 9),
])
es.close(); esRef.current = null
}
}
if (msg.type === 'error') {
setError(msg.message || 'Unknown error')
setStreaming(false)
es.close(); esRef.current = null
}
}
es.onerror = () => {
// When the server closes the stream after a clean episode end, the browser
// fires onerror. Ignore it — only show an error for genuine disconnects.
if (episodeDone.current) return
setError('Stream disconnected. Check backend is running, then press Play again.')
setStreaming(false)
es.close(); esRef.current = null
}
}, []) // stable — reads difficulty through diffRef, never needs to be recreated
const stopStream = () => {
if (esRef.current) { esRef.current.close(); esRef.current = null }
if (replayTimer.current) { clearTimeout(replayTimer.current); replayTimer.current = null }
setStreaming(false)
}
// Cleanup on unmount only
useEffect(() => () => {
if (esRef.current) esRef.current.close()
if (replayTimer.current) clearTimeout(replayTimer.current)
}, [])
// ── Manual episode helpers ────────────────────────────────────────────────
const handleReset = async () => {
setLoading(true); setError(null); setManRw([]); setManLogs([]); setManDone(false)
try {
const r = await fetch(`${API}/reset`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: difficulty }),
})
if (!r.ok) throw new Error(`HTTP ${r.status}`)
const d = await r.json()
setSession(d.session_id); setObs(d.observation)
setManLogs([{ type: 'system', msg: `Episode started (${difficulty})` }])
} catch (err) {
setError(err.message)
} finally { setLoading(false) }
}
const handleAction = async (type, taskId = null) => {
if (!sessionId) return
setLoading(true)
const action = { type }; if (taskId) action.task_id = taskId
try {
const r = await fetch(`${API}/step`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, action }),
})
if (!r.ok) throw new Error(`HTTP ${r.status}`)
const d = await r.json()
setObs(d.observation)
setManRw(prev => [...prev, d.reward])
setManLogs(prev => [...prev, {
type: d.reward >= 0 ? 'pos' : 'neg',
msg: `${type}${taskId ? ' ' + taskId : ''}${d.reward?.toFixed(3)}`,
}])
if (d.done) {
setManLogs(prev => [...prev, {
type: 'system',
msg: `Done. Final score: ${d.info?.final_score?.toFixed(4) ?? 'N/A'}`,
}])
setSession(null)
setManDone(true)
}
setTimeout(() => {
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight
}, 50)
} catch (err) {
setError(err.message)
} finally { setLoading(false) }
}
// ── Derived stream metrics ────────────────────────────────────────────────
const totalReward = rewardTrace.reduce((s, v) => s + v, 0)
const lastEnergy = energyTrace[energyTrace.length - 1] ?? null
const lastStress = stressTrace[stressTrace.length - 1] ?? null
const tasksDone = tasks.filter(t => t.progress >= 1.0).length
const manTasks = obs?.tasks || []
const manWorkers = obs?.visible_state?.workers || []
const manW0 = manWorkers[0] || {}
// ── card ──────────────────────────────────────────────────────────────────
const card = (extra = {}) => ({
background: '#fff', border: '1px solid #e2e8f0',
borderRadius: 14, padding: 16, ...extra,
})
const section = { fontSize: 10, fontWeight: 700, color: '#94a3b8',
textTransform: 'uppercase', letterSpacing: '.08em', marginBottom: 10 }
return (
<div>
{/* ── Top controls ───────────────────────────────────────────────────── */}
<div style={{ display: 'flex', gap: 10, alignItems: 'center',
marginBottom: 16, flexWrap: 'wrap' }}>
{/* Mode toggle */}
<div style={{ display: 'flex', background: '#e2e8f0', borderRadius: 10, padding: 3 }}>
{[['stream', 'Auto-Play'], ['manual', 'Manual']].map(([id, lbl]) => (
<button key={id} onClick={() => setMode(id)}
style={{ padding: '7px 14px', borderRadius: 8, border: 'none',
background: mode === id ? '#fff' : 'transparent',
fontWeight: 600, fontSize: 13, cursor: 'pointer',
boxShadow: mode === id ? '0 1px 3px rgba(0,0,0,.15)' : 'none',
color: mode === id ? '#0f172a' : '#64748b',
}}>{lbl}</button>
))}
</div>
{/* Difficulty */}
<select value={difficulty}
onChange={e => setDiff(e.target.value)}
style={{ border: '1px solid #e2e8f0', borderRadius: 8,
padding: '8px 12px', fontSize: 13, background: '#fff' }}>
{['easy','medium','hard','expert'].map(l => (
<option key={l} value={l}>{l.charAt(0).toUpperCase()+l.slice(1)}</option>
))}
</select>
{/* Action button */}
{mode === 'stream' ? (
streaming
? <button onClick={stopStream}
style={{ background: '#ef4444', color: '#fff', border: 'none',
borderRadius: 8, padding: '8px 20px', fontWeight: 700,
fontSize: 13, cursor: 'pointer' }}>Stop</button>
: !streamDone && (
<button onClick={() => startStream()}
style={{ background: '#6366f1', color: '#fff', border: 'none',
borderRadius: 8, padding: '8px 20px', fontWeight: 700,
fontSize: 13, cursor: 'pointer' }}>
Play Episode
</button>
)
) : (
!manDone && (
<button onClick={handleReset} disabled={loading}
style={{ background: loading ? '#94a3b8' : '#6366f1', color: '#fff',
border: 'none', borderRadius: 8, padding: '8px 20px', fontWeight: 700,
fontSize: 13, cursor: loading ? 'not-allowed' : 'pointer' }}>
{loading ? 'Loading…' : sessionId ? 'Reset' : 'Start'}
</button>
)
)}
{streaming && (
<span style={{ fontSize: 12, color: '#6366f1', fontWeight: 700,
background: '#eef2ff', border: '1px solid #c7d2fe', borderRadius: 8,
padding: '5px 12px', animation: 'pulse 1.2s ease-in-out infinite' }}>
● LIVE · Episode #{episodeCount}
</span>
)}
{streamDone && (
<span style={{ fontSize: 13, fontWeight: 700, color: '#16a34a',
background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 8,
padding: '6px 14px' }}>
Episode #{episodeCount} Complete
{typeof finalScore === 'number' ? ` · Score: ${finalScore.toFixed(4)}` : ''}
</span>
)}
</div>
{/* ── Error banner ───────────────────────────────────────────────────── */}
{error && (
<div style={{ background: '#fef2f2', border: '1px solid #fca5a5',
borderRadius: 10, padding: '10px 14px', marginBottom: 14,
fontSize: 13, color: '#dc2626' }}>
{error}&ensp;
<button onClick={() => setError(null)}
style={{ background: 'none', border: 'none', cursor: 'pointer',
color: '#dc2626', fontWeight: 700 }}>x</button>
</div>
)}
{/* ── Schema drift alerts ────────────────────────────────────────────── */}
{driftAlerts.map((d, i) => (
<div key={i} style={{ background: '#fffbeb', border: '1px solid #fcd34d',
borderRadius: 10, padding: '10px 14px', marginBottom: 10,
fontSize: 13, color: '#92400e', fontWeight: 600 }}>
Schema Drift @ step {d.step}: {d.message}
</div>
))}
{/* ═══════════════════ STREAM MODE ═══════════════════════════════════ */}
{mode === 'stream' && (
<>
{/* Episode complete banner */}
{streamDone && (
<div style={{
background: 'linear-gradient(135deg,#16a34a 0%,#0ea5e9 100%)',
borderRadius: 14, padding: '18px 24px', marginBottom: 16,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
flexWrap: 'wrap', gap: 12,
}}>
<div>
<div style={{ fontSize: 18, fontWeight: 900, color: '#fff', letterSpacing: '-0.3px' }}>
Episode #{episodeCount} Complete
</div>
<div style={{ fontSize: 12, color: '#d1fae5', marginTop: 2 }}>
Final results frozen below — all charts and task data preserved
</div>
</div>
{typeof finalScore === 'number' && (
<div style={{ background: 'rgba(255,255,255,0.2)', borderRadius: 12,
padding: '10px 20px', textAlign: 'center' }}>
<div style={{ fontSize: 11, color: '#d1fae5', fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '.06em' }}>Final Score</div>
<div style={{ fontSize: 28, fontWeight: 900, color: '#fff',
lineHeight: 1.1 }}>{finalScore.toFixed(4)}</div>
</div>
)}
</div>
)}
{/* Metric chips */}
{(streaming || streamDone) && (
<div style={{ display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(90px,1fr))',
gap: 10, marginBottom: 16 }}>
<Chip label="Step" value={currentStep ?? '—'} color="#6366f1" />
<Chip label="∑ Reward" value={totalReward.toFixed(2)}
color={totalReward >= 0 ? '#16a34a' : '#ef4444'} />
<Chip label="Energy" value={lastEnergy !== null ? lastEnergy.toFixed(2) : '—'}
color={lastEnergy < 0.3 ? '#ef4444' : lastEnergy < 0.6 ? '#f59e0b' : '#22c55e'} />
<Chip label="Stress" value={lastStress !== null ? lastStress.toFixed(2) : '—'}
color={lastStress > 0.7 ? '#ef4444' : lastStress > 0.4 ? '#f59e0b' : '#22c55e'} />
<Chip label="Done" value={`${tasksDone}/${tasks.length}`} color="#0ea5e9" />
{currentAction && (
<Chip label="Action"
value={currentAction.type + (currentAction.task_id ? ' ' + currentAction.task_id : '')}
color="#6366f1"
bg={currentAction.type === 'focus' ? '#eef2ff' : '#f8fafc'}
/>
)}
</div>
)}
{/* Charts row */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr',
gap: 14, marginBottom: 14 }}>
{/* Reward curve */}
<div style={card()}>
<div style={section}>Reward / Step</div>
<LineChart data={rewardTrace} color="#6366f1" height={130}
label="Press Play Episode to start" />
{rewardTrace.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between',
fontSize: 11, color: '#64748b', marginTop: 6 }}>
<span>Min: {Math.min(...rewardTrace).toFixed(3)}</span>
<span>Max: {Math.max(...rewardTrace).toFixed(3)}</span>
<span>Steps: {rewardTrace.length}</span>
</div>
)}
</div>
{/* Energy + Stress dual chart */}
<div style={card()}>
<div style={{ ...section, display: 'flex', justifyContent: 'space-between',
alignItems: 'center' }}>
<span>Energy & Stress</span>
<span style={{ fontSize: 10, fontWeight: 400 }}>
<span style={{ color: '#22c55e', fontWeight: 700 }}></span> Energy&ensp;
<span style={{ color: '#f59e0b', fontWeight: 700 }}></span> Stress
</span>
</div>
<DualChart energy={energyTrace} stress={stressTrace} height={130} />
{energyTrace.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between',
fontSize: 11, color: '#64748b', marginTop: 6 }}>
<span>Min E: {Math.min(...energyTrace).toFixed(2)}</span>
<span>Max S: {Math.max(...stressTrace).toFixed(2)}</span>
</div>
)}
</div>
{/* Task completion */}
<div style={card({ overflow: 'auto', maxHeight: 200 })}>
<div style={section}>Task Progress</div>
{tasks.length === 0 && (
<div style={{ color: '#cbd5e1', fontSize: 12, padding: '20px 0',
textAlign: 'center' }}>
Episode not started
</div>
)}
{tasks.map(t => {
const ps = PRI_STYLE[t.priority] || PRI_STYLE.normal
return (
<div key={t.id} style={{ marginBottom: 8 }}>
<div style={{ display: 'flex', justifyContent: 'space-between',
alignItems: 'center', marginBottom: 3 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: '#334155' }}>
{t.task_type} <span style={{ color: '#94a3b8', fontWeight: 400 }}>#{t.id}</span>
</span>
<Tag color={ps.color} bg={ps.bg}>{t.priority}</Tag>
</div>
<div style={{ height: 5, background: '#f1f5f9', borderRadius: 99 }}>
<div style={{
height: 5, borderRadius: 99,
width: `${t.progress * 100}%`,
background: t.progress >= 1 ? '#22c55e' : '#6366f1',
transition: 'width .3s ease',
}} />
</div>
<div style={{ fontSize: 10, color: '#94a3b8', marginTop: 2 }}>
{(t.progress * 100).toFixed(0)}%
{t.deadline ? ` · deadline: step ${t.deadline}` : ''}
{t.is_interrupted ? ' (interrupted)' : ''}
</div>
</div>
)
})}
</div>
</div>
{/* Cumulative reward + episode history */}
{rewardTrace.length > 0 && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 14 }}>
<div style={card({ marginBottom: 0 })}>
<div style={section}>Cumulative Reward — Episode #{episodeCount}</div>
<LineChart
data={rewardTrace.reduce((acc, v) => {
acc.push((acc[acc.length - 1] || 0) + v); return acc
}, [])}
color="#0ea5e9" height={80}
/>
</div>
{history.length > 0 && (
<div style={{ ...card({ marginBottom: 0 }), minWidth: 220 }}>
<div style={section}>Episode History</div>
<div style={{ fontFamily: 'monospace', fontSize: 11 }}>
{history.map(h => {
const sc = typeof h.score === 'number' ? h.score : null
const col = sc == null ? '#64748b'
: sc >= 0.5 ? '#16a34a' : sc >= 0.3 ? '#f59e0b' : '#ef4444'
const diff = typeof h.difficulty === 'string' ? h.difficulty : '—'
const steps = typeof h.steps === 'number' ? h.steps : 0
return (
<div key={h.ep} style={{ display: 'flex', gap: 8,
padding: '3px 0', borderBottom: '1px solid #f8fafc', color: col }}>
<span style={{ color: '#94a3b8', minWidth: 24 }}>#{h.ep}</span>
<span style={{ textTransform: 'capitalize', minWidth: 52,
color: '#475569' }}>{diff}</span>
<span style={{ fontWeight: 700 }}>{sc != null ? sc.toFixed(4) : '—'}</span>
<span style={{ color: '#94a3b8' }}>{steps}s</span>
</div>
)
})}
</div>
</div>
)}
</div>
)}
</>
)}
{/* ═══════════════════ MANUAL MODE ════════════════════════════════════ */}
{mode === 'manual' && (
<>
{/* Manual episode complete banner */}
{manDone && (
<div style={{
background: 'linear-gradient(135deg,#6366f1 0%,#0ea5e9 100%)',
borderRadius: 14, padding: '18px 24px', marginBottom: 16,
display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
}}>
<div style={{ fontSize: 18, fontWeight: 900, color: '#fff' }}>
Manual Episode Complete
</div>
<div style={{ fontSize: 12, color: '#c7d2fe' }}>
All results frozen — task log and reward chart preserved below
</div>
</div>
)}
{/* Worker metric chips */}
{(manWorkers.length > 0 || manDone) && obs && (
<div style={{ display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(90px,1fr))',
gap: 10, marginBottom: 16 }}>
<Chip label="Fatigue" value={manW0.fatigue_level || '—'}
color={manW0.fatigue_level === 'high' ? '#ef4444'
: manW0.fatigue_level === 'medium' ? '#f59e0b' : '#22c55e'} />
<Chip label="Stress" value={manW0.stress_level || '—'}
color={manW0.stress_level === 'critical' ? '#ef4444'
: manW0.stress_level === 'elevated' ? '#f59e0b' : '#22c55e'} />
<Chip label="Step" value={obs?.time_step ?? '—'} color="#6366f1" />
<Chip label="Done"
value={`${manTasks.filter(t => t.progress >= 1).length}/${manTasks.length}`}
color="#0ea5e9" />
<Chip label="∑ Reward"
value={manRewards.reduce((s, v) => s + v, 0).toFixed(2)}
color={manRewards.reduce((s, v) => s + v, 0) >= 0 ? '#16a34a' : '#ef4444'} />
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginBottom: 14 }}>
{/* Task list */}
<div style={card()}>
<div style={section}>Task Queue</div>
{manTasks.length === 0 && (
<div style={{ color: '#cbd5e1', fontSize: 13, textAlign: 'center', padding: 24 }}>
Press Start to begin
</div>
)}
{manTasks.map(task => {
const ps = PRI_STYLE[task.priority] || PRI_STYLE.normal
return (
<div key={task.id} style={{ display: 'flex', alignItems: 'center',
gap: 8, padding: '8px 0', borderBottom: '1px solid #f1f5f9' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a' }}>
{task.task_type}&ensp;
<span style={{ fontSize: 10, color: '#94a3b8' }}>#{task.id}</span>
</div>
<div style={{ fontSize: 11, color: '#94a3b8' }}>
{task.deadline ? `deadline: ${task.deadline}` : 'no deadline'}
{task.depends_on ? ` · depends: ${task.depends_on}` : ''}
</div>
<div style={{ height: 4, background: '#f1f5f9', borderRadius: 99, marginTop: 4 }}>
<div style={{ height: 4, borderRadius: 99,
width: `${task.progress * 100}%`,
background: task.progress >= 1 ? '#22c55e' : '#6366f1',
transition: 'width .25s' }} />
</div>
</div>
<Tag color={ps.color} bg={ps.bg}>{task.priority}</Tag>
{sessionId && task.progress < 1.0 && (
<div style={{ display: 'flex', gap: 4 }}>
<button onClick={() => handleAction('work', task.id)}
style={{ fontSize: 11, padding: '4px 9px', borderRadius: 6,
border: '1px solid #e2e8f0', background: '#f8fafc',
cursor: 'pointer', fontWeight: 600 }}>work</button>
<button onClick={() => handleAction('focus', task.id)}
style={{ fontSize: 11, padding: '4px 9px', borderRadius: 6,
border: '1px solid #6366f1', background: '#eef2ff',
color: '#6366f1', cursor: 'pointer', fontWeight: 600 }}>focus</button>
</div>
)}
</div>
)
})}
{sessionId && (
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
<button onClick={() => handleAction('break')}
style={{ flex: 1, padding: 9, borderRadius: 8,
border: '1px solid #e2e8f0', background: '#f0fdf4',
color: '#16a34a', fontWeight: 700, cursor: 'pointer', fontSize: 13 }}>
Break</button>
<button onClick={() => handleAction('delay')}
style={{ flex: 1, padding: 9, borderRadius: 8,
border: '1px solid #e2e8f0', background: '#f8fafc',
color: '#64748b', fontWeight: 700, cursor: 'pointer', fontSize: 13 }}>
Delay</button>
</div>
)}
</div>
{/* Reward chart */}
<div style={card()}>
<div style={section}>Reward / Step</div>
<LineChart data={manRewards} color="#6366f1" height={160}
label="Rewards appear here as you act" />
{manRewards.length > 0 && (
<div style={{ display: 'flex', gap: 20, marginTop: 8 }}>
{[
{ l: 'Total', v: manRewards.reduce((s,v)=>s+v,0).toFixed(3) },
{ l: 'Mean', v: (manRewards.reduce((s,v)=>s+v,0)/manRewards.length).toFixed(3) },
{ l: 'Steps', v: manRewards.length },
].map(s => (
<div key={s.l} style={{ textAlign: 'center' }}>
<div style={{ fontSize: 10, color: '#94a3b8' }}>{s.l}</div>
<div style={{ fontSize: 15, fontWeight: 700, color: '#0f172a' }}>{s.v}</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Action log */}
<div style={card()}>
<div style={section}>Action Log</div>
<div ref={logRef} style={{ height: 110, overflowY: 'auto',
fontFamily: 'monospace', fontSize: 12 }}>
{manLogs.length === 0 && (
<span style={{ color: '#cbd5e1' }}>No actions yet…</span>
)}
{manLogs.map((l, i) => (
<div key={i} style={{ padding: '1px 0',
color: l.type === 'pos' ? '#16a34a'
: l.type === 'neg' ? '#ef4444' : '#64748b' }}>
[{i}] {l.msg}
</div>
))}
</div>
</div>
</>
)}
{/* ── Training performance — before vs after GRPO ── */}
<div style={{ marginTop: 20 }}>
<TrainingPerformanceCard />
</div>
{/* ── Reward scoring formula — always visible ── */}
<div>
<ScoringFormulaCard />
</div>
<style>{`
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
`}</style>
</div>
)
}