From f4be1330691c3c7583d3bf00d80af77ccf88bd3f Mon Sep 17 00:00:00 2001 From: "Kevin Carter (via Claude)" Date: Thu, 2 Jul 2026 22:35:35 +0000 Subject: [PATCH] Exam bank: corpus coverage dashboard (/exam-marker/corpus) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New "Exam bank" page displaying the state of the collected past-paper corpus: per-specification coverage of question papers / mark schemes / examiner reports, with global + filtered rollups, board/level/subject filters, a "sciences only" default, and an expandable per-session table (QP/MS/ER present ✓/–). Reads GET /api/exam/corpus. Dashboard gets an "Exam bank" entry point. examRepository.getCorpus + corpus types. tsc clean on the changed files. Co-Authored-By: Claude Opus 4.8 --- src/AppRoutes.tsx | 3 +- src/pages/exam/CorpusPage.tsx | 156 +++++++++++++++++++++++++++ src/pages/exam/ExamDashboardPage.tsx | 4 + src/pages/exam/index.ts | 1 + src/services/exam/examRepository.ts | 7 ++ src/types/exam.types.ts | 25 +++++ 6 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 src/pages/exam/CorpusPage.tsx diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index 807db01..89a5c4d 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -7,7 +7,7 @@ import LoginPage from './pages/auth/loginPage'; import SignupPage from './pages/auth/signupPage'; import SinglePlayerPage from './pages/tldraw/singlePlayerPage'; import MultiplayerUser from './pages/tldraw/multiplayerUser'; -import { ExamDashboardPage, ExamMarkingPage, ExamResultsPage, ExamTemplateSetupPage, MarkSchemePage, QuestionBankPage } from './pages/exam'; +import { ExamDashboardPage, ExamMarkingPage, ExamResultsPage, ExamTemplateSetupPage, MarkSchemePage, QuestionBankPage, CorpusPage } from './pages/exam'; import { ErrorBoundary } from './components/ErrorBoundary'; import CalendarPage from './pages/user/calendarPage'; import SettingsPage from './pages/user/settingsPage'; @@ -185,6 +185,7 @@ const AppRoutes: React.FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/exam/CorpusPage.tsx b/src/pages/exam/CorpusPage.tsx new file mode 100644 index 0000000..099b4cf --- /dev/null +++ b/src/pages/exam/CorpusPage.tsx @@ -0,0 +1,156 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Accordion, AccordionDetails, AccordionSummary, Alert, Box, Button, Chip, CircularProgress, Container, + Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography, +} from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import CheckIcon from '@mui/icons-material/Check'; +import RemoveIcon from '@mui/icons-material/Remove'; + +import { examRepository } from '../../services/exam/examRepository'; +import type { CorpusResponse, CorpusSpec, CorpusDocType } from '../../types/exam.types'; + +const DOCS: CorpusDocType[] = ['QP', 'MS', 'ER']; +const DOC_LABEL: Record = { QP: 'Question papers', MS: 'Mark schemes', ER: 'Examiner reports' }; +const SCIENCES = new Set(['Biology', 'Chemistry', 'Physics']); + +const CorpusPage: React.FC = () => { + const navigate = useNavigate(); + const [corpus, setCorpus] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [board, setBoard] = useState(null); + const [level, setLevel] = useState(null); + const [subject, setSubject] = useState(null); + const [sciencesOnly, setSciencesOnly] = useState(true); + + const load = useCallback(async () => { + setLoading(true); setError(null); + try { setCorpus(await examRepository.getCorpus()); } + catch (e) { setError(e instanceof Error ? e.message : String(e)); } + finally { setLoading(false); } + }, []); + useEffect(() => { void load(); }, [load]); + + const allSpecs = useMemo( + () => (corpus?.boards ?? []).flatMap((b) => b.specs), + [corpus], + ); + const boards = useMemo(() => Array.from(new Set(allSpecs.map((s) => s.board))).sort(), [allSpecs]); + const levels = useMemo(() => Array.from(new Set(allSpecs.map((s) => s.level))).sort(), [allSpecs]); + const subjects = useMemo(() => Array.from(new Set(allSpecs.map((s) => s.subject))).sort(), [allSpecs]); + + const specs = useMemo(() => allSpecs.filter((s) => + (!board || s.board === board) && (!level || s.level === level) && (!subject || s.subject === subject) + && (!sciencesOnly || SCIENCES.has(s.subject)) + ).sort((a, b) => (a.board + a.level + a.subject + a.spec_code).localeCompare(b.board + b.level + b.subject + b.spec_code)), + [allSpecs, board, level, subject, sciencesOnly]); + + const filteredTotals = useMemo(() => specs.reduce((acc, s) => { + acc.specs += 1; acc.papers += s.n_papers; + DOCS.forEach((d) => { acc[d] += s.counts[d] ?? 0; }); + return acc; + }, { specs: 0, papers: 0, QP: 0, MS: 0, ER: 0 } as Record), [specs]); + + return ( + + + Exam bank + + The collected past-paper corpus — question papers, mark schemes and examiner reports per specification. + + + {error && setError(null)}>{error}} + + {loading ? ( + + ) : ( + <> + {/* Summary of the current selection */} + + + + + + + + + {/* Filters */} + + + setSciencesOnly((v) => !v)} /> + + Board: + setBoard(null)} /> + {boards.map((b) => setBoard(b)} />)} + + + Level: + setLevel(null)} /> + {levels.map((l) => setLevel(l)} />)} + {!sciencesOnly && (<> + + Subject: + setSubject(null)} /> + {subjects.map((s) => setSubject(s)} />)} + )} + + + + {specs.length === 0 ? ( + No specifications match the current filters. + ) : specs.map((s) => ( + + }> + + + {s.level} {s.subject} · {s.spec_code} + + + + {DOCS.map((d) => ( + + ))} + + + + + + + + Session + Paper + Tier + {DOCS.map((d) => {DOC_LABEL[d]})} + + + + {s.papers.map((p, i) => ( + + {p.session ?? '—'} + {p.paper_code ?? '—'} + {p.tier ?? '—'} + {DOCS.map((d) => ( + + {p.docs[d] ? : } + + ))} + + ))} + +
+
+
+
+ ))} + + )} +
+ ); +}; + +export default CorpusPage; diff --git a/src/pages/exam/ExamDashboardPage.tsx b/src/pages/exam/ExamDashboardPage.tsx index b81c819..170b779 100644 --- a/src/pages/exam/ExamDashboardPage.tsx +++ b/src/pages/exam/ExamDashboardPage.tsx @@ -26,6 +26,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import EditIcon from '@mui/icons-material/Edit'; import GradingIcon from '@mui/icons-material/Grading'; import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck'; +import LibraryBooksIcon from '@mui/icons-material/LibraryBooks'; import { useAuth } from '../../contexts/AuthContext'; import { examRepository } from '../../services/exam/examRepository'; @@ -235,6 +236,9 @@ const ExamDashboardPage: React.FC = () => { + diff --git a/src/pages/exam/index.ts b/src/pages/exam/index.ts index 3489d49..94a95ab 100644 --- a/src/pages/exam/index.ts +++ b/src/pages/exam/index.ts @@ -1,5 +1,6 @@ export { default as ExamDashboardPage } from './ExamDashboardPage'; export { default as QuestionBankPage } from './QuestionBankPage'; +export { default as CorpusPage } from './CorpusPage'; export { default as ExamTemplateSetupPage } from './setup/ExamTemplateSetupPage'; export { default as MarkSchemePage } from './MarkSchemePage'; export { default as ExamMarkingPage } from './ExamMarkingPage'; diff --git a/src/services/exam/examRepository.ts b/src/services/exam/examRepository.ts index 6c5daef..47fd69a 100644 --- a/src/services/exam/examRepository.ts +++ b/src/services/exam/examRepository.ts @@ -14,6 +14,7 @@ import type { AutoMapJobStatus, AutoMapResponse, BankResponse, + CorpusResponse, BatchQueueResponse, BatchResultsResponse, CreateBatchPayload, @@ -303,6 +304,12 @@ export const examRepository = { return res.data; }, + async getCorpus(): Promise { + const headers = await authHeaders(); + const res = await axios.get(`${EXAM_BASE}/corpus`, { headers }); + return res.data; + }, + async getBank(params: { specRef?: string; subject?: string } = {}): Promise { const headers = await authHeaders(); const res = await axios.get(`${EXAM_BASE}/bank`, { diff --git a/src/types/exam.types.ts b/src/types/exam.types.ts index 4c83a1e..42231ca 100644 --- a/src/types/exam.types.ts +++ b/src/types/exam.types.ts @@ -371,3 +371,28 @@ export interface CreateCustomPaperResult { n_questions: number; n_response_areas: number; } + +// ── Exam-bank corpus coverage (state of the collected bank) ─────────────────── +export type CorpusDocType = 'QP' | 'MS' | 'ER'; +export interface CorpusPaper { + paper_code: string | null; + session: string | null; + tier: string | null; + docs: Partial>; + exam_codes: Partial>; +} +export interface CorpusSpec { + spec_code: string; + subject: string; + level: string; + board: string; + first_teach: string | null; + n_papers: number; + counts: Record; + papers: CorpusPaper[]; +} +export interface CorpusBoard { board: string; n_specs: number; specs: CorpusSpec[] } +export interface CorpusResponse { + totals: { specs: number; papers: number; sessions: number; QP: number; MS: number; ER: number }; + boards: CorpusBoard[]; +}