import { useState } from 'react' import type { Exam, ExamState } from '../types' import { gradeQuestion } from '../utils/grading' import { formatTime } from '../utils/formatting' import Tex from './Tex' import QuestionMedia from './QuestionMedia' import QuestionRenderer from './questions/QuestionRenderer' interface Props { exam: Exam state: ExamState onRestart: () => void onExit: () => void reviewOnly?: boolean } export default function Results({ exam, state, onRestart, onExit, reviewOnly }: Props) { const { answers, startedAt, completedAt } = state const elapsedMs = (completedAt ?? Date.now()) - startedAt const graded = exam.questions.map((q) => ({ q, userAnswer: answers[q.id], result: gradeQuestion(q, answers[q.id]), })) const totalPoints = exam.questions.reduce((a, q) => a + (q.points ?? 1), 0) const scorePoints = graded.reduce((a, g) => a + g.result.points, 0) const gradedCount = graded.filter((g) => g.result.status !== 'ungraded').length const correctCount = graded.filter((g) => g.result.correct).length const pct = totalPoints ? Math.round((scorePoints / totalPoints) * 100) : 0 const passing = exam.passingScore ?? 60 const passed = pct >= passing const byCat: Record = {} graded.forEach((g) => { const cat = g.q.category ?? 'Uncategorized' if (!byCat[cat]) byCat[cat] = { earned: 0, possible: 0, correct: 0, total: 0 } byCat[cat].possible += g.q.points ?? 1 byCat[cat].earned += g.result.points byCat[cat].total += 1 if (g.result.correct) byCat[cat].correct += 1 }) const [openIdx, setOpenIdx] = useState(null) return (
{reviewOnly ? 'Reviewing past attempt' : 'Exam complete'} · {exam.title}

You scored {pct}%
{passed ? `Above the ${passing}% passing mark.` : `Below the ${passing}% passing mark. Keep practicing.`}

Points
{scorePoints}/ {totalPoints}
Correct
{correctCount}/ {gradedCount}
Time
{formatTime(elapsedMs)}
{Object.keys(byCat).length > 1 && (
By category
{Object.entries(byCat).map(([cat, c]) => { const catPct = c.possible ? Math.round((c.earned / c.possible) * 100) : 0 return (
{cat}
{catPct}%
) })}
)}

Review

{graded.map((g, i) => { const promptText = g.q.prompt .replace(/\$\$([^$]+)\$\$/g, '[math]') .replace(/\$([^$]+)\$/g, '[math]') return (
setOpenIdx(openIdx === i ? null : i)}> {String(i + 1).padStart(2, '0')} {promptText.slice(0, 90)} {promptText.length > 90 ? '…' : ''} {g.result.status} {openIdx === i ? '−' : '+'}
{openIdx === i && (
{g.q.prompt}
{}} locked graded /> {g.q.explanation && (
Explanation
{g.q.explanation}
)} {g.q.rubric && (
Rubric (self-grade)
    {g.q.rubric.map((r, j) =>
  • {r}
  • )}
)}
)}
) })}
{!reviewOnly && }
) }