import { useState, useEffect, useCallback } from 'react' import { adminApi } from './api' import styles from './Admin.module.css' // Instructor learning analytics — cohort summary, "needs attention," per-scenario analytics, // and a student drill-down. Deliberately no leaderboards or scoring language — this is // instructional visibility, not a ranking (docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §13). export default function LearningView() { const [cohorts, setCohorts] = useState(null) const [cohortId, setCohortId] = useState(null) const [summary, setSummary] = useState(null) const [needsAttention, setNeedsAttention] = useState(null) const [scenarioStats, setScenarioStats] = useState(null) const [selectedStudent, setSelectedStudent] = useState(null) const [error, setError] = useState(null) useEffect(() => { adminApi.cohorts().then((c) => { setCohorts(c); if (c[0]) setCohortId(c[0].cohort_id) }).catch((e) => setError(e.message)) }, []) const reload = useCallback(() => { if (!cohortId) return adminApi.cohortLearning(cohortId).then(setSummary).catch((e) => setError(e.message)) adminApi.needsAttention(cohortId).then(setNeedsAttention).catch((e) => setError(e.message)) adminApi.scenarioAnalytics(cohortId).then(setScenarioStats).catch((e) => setError(e.message)) }, [cohortId]) useEffect(reload, [reload]) if (selectedStudent) return setSelectedStudent(null)} /> return ( <> {error &&
{error}
}
{summary && (

Cohort Summary

{summary.student_count}
Students
{summary.active_learners}
Active (7d)
{summary.overall_completion_pct}%
Avg. Completion
{summary.stages.map((s) => ( ))}
StageStudents Complete
{s.name}{s.completedBy}/{summary.student_count} ({s.pct}%)
)}

Needs Attention

{!needsAttention ?

Loading…

: needsAttention.length === 0 ?

No signals right now.

: ( {needsAttention.map((s, i) => ( ))}
StudentSignalDetail
{s.display_name} {s.label} {s.detail}
)}

Scenario Analytics

{!scenarioStats ?

Loading…

: ( {scenarioStats.map((s) => ( ))}
ScenarioCompletionsAvg AttemptsFailuresHints UsedAvg Time
{s.scenario_id}{s.completions}{s.avg_attempts} {s.total_failures}{s.total_hints_used} {s.avg_duration_seconds ? `${Math.round(s.avg_duration_seconds / 60)}m` : '—'}
)}
) } function StudentLearningDetail({ studentId, onBack }) { const [data, setData] = useState(null) const [error, setError] = useState(null) useEffect(() => { adminApi.studentLearning(studentId).then(setData).catch((e) => setError(e.message)) }, [studentId]) if (error) return
{error}
if (!data) return

Loading…

return ( <>

{studentId}

{data.curriculum && (

Curriculum ({data.curriculum.percent_complete}%)

{data.curriculum.stages.map((s) => (
{s.complete ? '✅' : s.locked ? '🔒' : '🟡'} {s.name} — {s.completed}/{s.total}
))}
)}

Attempts

{data.attempts.map((a, i) => ( ))}
ScenarioStatusValidationsHints (max level)Duration
{a.scenario_id} {a.status} {a.validation_pass_count} passed, {a.validation_fail_count} failed {a.hint_count} ({a.highest_hint_level}) {a.duration_seconds ? `${Math.round(a.duration_seconds / 60)}m` : '—'}

Assessment History

{data.assessment_sessions.length === 0 ?

None yet.

: ( {data.assessment_sessions.map((s) => ( ))}
AssessmentStatusResult
{s.title} {s.status} {s.result ? `${s.result.scenarios_passed}/${s.result.scenarios_total}` : '—'}
)}
) }