diff --git a/src/pages/exam/setup/ExamTemplateSetupPage.tsx b/src/pages/exam/setup/ExamTemplateSetupPage.tsx index be9b7a4..648de6c 100644 --- a/src/pages/exam/setup/ExamTemplateSetupPage.tsx +++ b/src/pages/exam/setup/ExamTemplateSetupPage.tsx @@ -1,7 +1,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' -import { Alert, Box, Button, Chip, CircularProgress, Collapse, Divider, IconButton, Paper, Snackbar, Stack, Tooltip, Typography, useTheme } from '@mui/material' +import { Alert, Box, Button, Chip, CircularProgress, Collapse, Dialog, DialogContent, DialogTitle, Divider, IconButton, Paper, Snackbar, Stack, Tooltip, Typography, useTheme } from '@mui/material' +import DescriptionIcon from '@mui/icons-material/Description' import ArrowBackIcon from '@mui/icons-material/ArrowBack' import HelpOutlineIcon from '@mui/icons-material/HelpOutline' import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh' @@ -17,7 +18,7 @@ import axios from 'axios' import { ErrorBoundary } from '../../../components/ErrorBoundary' import { logger } from '../../../debugConfig' import { examRepository } from '../../../services/exam/examRepository' -import type { AutoMapJobStatus, ExamTemplateDetail } from '../../../types/exam.types' +import type { AutoMapJobStatus, DigitalTextResponse, ExamTemplateDetail } from '../../../types/exam.types' import { CanvasPageGeometry, ExamCanvasShapeModel, PAGE_HEIGHT, PAGE_WIDTH, isUuid, newDomainId, serializeCanvasShapes, shapesFromTemplate } from '../../../utils/exam-canvas/model' import { PDF_PAGE_SHAPE_TYPE, canvasShapePalette, examCanvasShapeUtils, examCanvasTools, ExamCanvasTLShape, SHAPE_TYPES, isPdfPageShape, shapeTypeToKind } from './examCanvasShapes' import { loadPdfPageImages, PdfPageImage } from './pdfLoader' @@ -224,8 +225,25 @@ const ExamTemplateSetupInner: React.FC = () => { const [autoMapStatus, setAutoMapStatus] = useState(null) const [autoMapBusy, setAutoMapBusy] = useState(false) const [liveReview, setLiveReview] = useState({ ai: 0, unconfirmed: 0, lowConfidence: 0 }) + const [digitalText, setDigitalText] = useState(null) + const [digitalOpen, setDigitalOpen] = useState(false) + const [digitalBusy, setDigitalBusy] = useState(false) const autoMapPollRef = useRef(null) + const openDigitalText = useCallback(async () => { + if (!template) return + setDigitalBusy(true); setDigitalOpen(true) + try { + setDigitalText(await examRepository.getDigitalText(template.id)) + } catch (e) { + setDigitalText(null) + setError(e instanceof Error ? e.message : 'No digital text yet — run auto-map first.') + setDigitalOpen(false) + } finally { + setDigitalBusy(false) + } + }, [template]) + const refreshReview = useCallback(() => setLiveReview(reviewFromShapes(editorRef.current)), []) const applyTemplateToCanvas = useCallback((detail: ExamTemplateDetail) => { @@ -439,6 +457,11 @@ const ExamTemplateSetupInner: React.FC = () => { {(autoMapBusy || autoMapStatus) && } + {(() => { + const a = (template as (ExamTemplateDetail & { extraction_meta?: { audit?: { status?: string; cover_total?: number; detected_total?: number } } }) | null)?.extraction_meta?.audit + return a?.status ? : null + })()} + @@ -552,6 +575,32 @@ const ExamTemplateSetupInner: React.FC = () => { {loading && } + setDigitalOpen(false)} maxWidth="md" fullWidth> + + + Digital text + {digitalText && — {digitalText.title} · {digitalText.n_questions}Q · {digitalText.total_marks} marks} + + {digitalText && ( + + { + const blob = new Blob([digitalText.markdown], { type: 'text/markdown' }) + const url = URL.createObjectURL(blob); const a = document.createElement('a') + a.href = url; a.download = `${digitalText.slug}.md`; a.click(); URL.revokeObjectURL(url) + }}> + + )} + + + {digitalBusy ? ( + + ) : digitalText ? ( + {digitalText.markdown} + ) : ( + No digital text available. + )} + + setError(null)}> setError(null)}>{error} ) diff --git a/src/services/exam/examRepository.ts b/src/services/exam/examRepository.ts index 47fd69a..e96670f 100644 --- a/src/services/exam/examRepository.ts +++ b/src/services/exam/examRepository.ts @@ -15,6 +15,7 @@ import type { AutoMapResponse, BankResponse, CorpusResponse, + DigitalTextResponse, BatchQueueResponse, BatchResultsResponse, CreateBatchPayload, @@ -164,6 +165,12 @@ export const examRepository = { return res.data.templates ?? []; }, + async getDigitalText(templateId: string): Promise { + const headers = await authHeaders(); + const res = await axios.get(`${EXAM_BASE}/templates/${templateId}/digital-text`, { headers }); + return res.data; + }, + async getTemplate(templateId: string): Promise { const headers = await authHeaders(); const res = await axios.get(`${EXAM_BASE}/templates/${templateId}`, { headers }); diff --git a/src/types/exam.types.ts b/src/types/exam.types.ts index 42231ca..3018a01 100644 --- a/src/types/exam.types.ts +++ b/src/types/exam.types.ts @@ -396,3 +396,13 @@ export interface CorpusResponse { totals: { specs: number; papers: number; sessions: number; QP: number; MS: number; ER: number }; boards: CorpusBoard[]; } + +// ── Digital-replica markdown (P4) ───────────────────────────────────────────── +export interface DigitalTextResponse { + slug: string; + title: string; + n_questions: number; + total_marks: number; + markdown: string; + questions: { label: string; marks: number | null; markdown: string }[]; +}