Files
rootnode-academy/frontend/src/components/AssessmentBanner.jsx
T
ced4568 d4db8f99f5 feat(student): add learning tabs and Lab hint/assessment UX
StudentDashboard is now tabbed: My Learning (the original Phase 2A/2B flow, extracted
unchanged into MyLearningTab and extended with an overall percent-complete bar), Practice,
Assessments (available/active-with-live-countdown/history), Capstone (locked checklist ->
unlocked -> start/resume -> completed), Achievements (a flat dated list, not a leaderboard).

Inside the Lab UI: ProgressiveHints replaces the static hint array render when the Gateway has
stripped hints (detected via scenario.hints being undefined but hint_count present - standalone
dev mode never sees this, so it keeps the original behavior); a new top-level AssessmentBanner
shows a live countdown and completion count whenever a Platform assessment is running,
rendered alongside (not replacing) the pre-existing Lab-local exam-mode UI, which is untouched.
2026-08-18 14:35:59 -05:00

48 lines
1.9 KiB
React

import { useState, useEffect } from 'react'
// Phase 3B — shows when the student has an active Platform-level assessment (exam or capstone)
// running, distinct from the pre-existing Lab-local exam mode (ExamTimer/ExamSession above,
// which predates this and stays untouched). Polls /api/assessments/active, which 404s/errors
// harmlessly in standalone/single-container dev mode (no Platform API there at all) — so this
// component renders nothing in that mode, by design.
export default function AssessmentBanner() {
const [active, setActive] = useState(null)
const [remaining, setRemaining] = useState(null)
useEffect(() => {
let cancelled = false
function poll() {
fetch('/api/assessments/active').then((r) => r.json()).then((d) => {
if (!cancelled) setActive(d.active ? d : null)
}).catch(() => { if (!cancelled) setActive(null) })
}
poll()
const id = setInterval(poll, 15000)
return () => { cancelled = true; clearInterval(id) }
}, [])
useEffect(() => {
if (!active) return
const tick = () => setRemaining(Math.max(0, new Date(active.expires_at) - Date.now()))
tick()
const id = setInterval(tick, 1000)
return () => clearInterval(id)
}, [active])
if (!active) return null
const m = Math.floor(remaining / 60000), s = Math.floor((remaining % 60000) / 1000)
const completedCount = active.scenarios.filter((sc) => sc.status === 'completed').length
return (
<div style={{
display: 'flex', alignItems: 'center', gap: '1rem', padding: '0.5rem 1rem',
background: 'var(--amber-dim)', color: 'var(--amber)', fontSize: '0.85rem', fontFamily: 'var(--mono)',
borderBottom: '1px solid var(--border)',
}}>
<span>🎯 Assessment Mode: {active.title}</span>
<span>{completedCount}/{active.scenarios.length} scenarios complete</span>
<span>⏱ {m}m {s}s remaining</span>
</div>
)
}