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.
48 lines
1.9 KiB
React
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>
|
|
)
|
|
}
|