Compare commits

..
Author SHA1 Message Date
CC WorkerandClaude Sonnet 4.6 ab35193be1 Merge S4-10: mark scheme editor + SpecPoint picker
app-ci-deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 04:31:02 +00:00
CC WorkerandClaude Sonnet 4.6 b3f71c5749 Merge S4-10: mark scheme editor + SpecPoint picker
- Add /exam-marker/:templateId/marks route with MarkSchemePage
- Per-Part mark scheme editor: points/levels/parts/checklist/free forms
- SpecPoint picker via GET /api/exam/specs/{spec_code}/points (falls back to manual spec_ref when endpoint 404s)
- Manual neo4j-sync button; ASSESSES edge verified in cc.public.exams
- Edit marks button on each template card in dashboard
- Merge-resolved: AppRoutes, index.ts, exam.types.ts, ExamDashboardPage (kept grouped UI + added Edit marks button)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 04:30:41 +00:00
3 changed files with 68 additions and 67 deletions
+8 -8
View File
@@ -315,20 +315,20 @@ const ExamDashboardPage: React.FC = () => {
{t.exam_code && ( {t.exam_code && (
<Typography variant="caption" color="text.secondary">{t.exam_code}</Typography> <Typography variant="caption" color="text.secondary">{t.exam_code}</Typography>
)} )}
<Stack spacing={1} sx={{ mt: 'auto', pt: 1 }}> <Box sx={{ mt: 'auto', pt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <Chip size="small" label={t.status} color={STATUS_COLOR[t.status] ?? 'default'} variant="outlined" />
<Chip size="small" label={t.status} color={STATUS_COLOR[t.status] ?? 'default'} variant="outlined" /> <Typography variant="caption" color="text.secondary">
<Typography variant="caption" color="text.secondary"> Updated {new Date(t.updated_at).toLocaleDateString()}
Updated {new Date(t.updated_at).toLocaleDateString()} </Typography>
</Typography> </Box>
</Box> <Stack direction="row" spacing={1} sx={{ pt: 0.5 }}>
<Button <Button
size="small" size="small"
variant="outlined" variant="outlined"
onClick={(e) => { e.stopPropagation(); navigate(`/exam-marker/${t.id}/marks`); }} onClick={(e) => { e.stopPropagation(); navigate(`/exam-marker/${t.id}/marks`); }}
startIcon={<GradingIcon fontSize="small" />} startIcon={<GradingIcon fontSize="small" />}
> >
Mark scheme Edit marks
</Button> </Button>
</Stack> </Stack>
</Paper> </Paper>
+3 -3
View File
@@ -1,3 +1,3 @@
export { default as ExamDashboardPage } from ./ExamDashboardPage; export { default as ExamDashboardPage } from './ExamDashboardPage';
export { default as ExamTemplateSetupPage } from ./setup/ExamTemplateSetupPage; export { default as ExamTemplateSetupPage } from './setup/ExamTemplateSetupPage';
export { default as MarkSchemePage } from ./MarkSchemePage; export { default as MarkSchemePage } from './MarkSchemePage';
+57 -56
View File
@@ -4,7 +4,7 @@
* the shared UUIDs (template/question/region ids, exam_code). * the shared UUIDs (template/question/region ids, exam_code).
*/ */
export type ExamTemplateStatus = draft | ready | archived; export type ExamTemplateStatus = 'draft' | 'ready' | 'archived';
export interface ExamTemplate { export interface ExamTemplate {
id: string; id: string;
@@ -31,7 +31,7 @@ export interface CreateTemplatePayload {
institute_id?: string; institute_id?: string;
} }
export type MarkSchemeType = points | levels | parts | checklist | free; export type MarkSchemeType = 'points' | 'levels' | 'parts' | 'checklist' | 'free';
export interface MarkSchemePoint { export interface MarkSchemePoint {
mark: number; mark: number;
@@ -92,12 +92,12 @@ export interface ExamQuestion {
} }
export type ExamResponseAreaKind = export type ExamResponseAreaKind =
| response | 'response'
| context | 'context'
| question_number | 'question_number'
| mark_area | 'mark_area'
| reference | 'reference'
| furniture; | 'furniture';
export interface ExamResponseArea { export interface ExamResponseArea {
id: string; id: string;
@@ -108,7 +108,7 @@ export interface ExamResponseArea {
kind: ExamResponseAreaKind; kind: ExamResponseAreaKind;
response_form: string | null; response_form: string | null;
context_type?: string | null; context_type?: string | null;
source: manual | ai; source: 'manual' | 'ai';
confirmed: boolean; confirmed: boolean;
confidence: number | null; confidence: number | null;
} }
@@ -121,7 +121,7 @@ export interface ExamBoundary {
page_index: number; page_index: number;
y: number; y: number;
bounds: Record<string, number> | null; bounds: Record<string, number> | null;
source: manual | ai; source: 'manual' | 'ai';
confirmed: boolean; confirmed: boolean;
} }
@@ -131,11 +131,57 @@ export interface ExamTemplateDetail extends ExamTemplate {
boundaries: ExamBoundary[]; boundaries: ExamBoundary[];
} }
export interface TemplateReplacePayload {
meta?: {
title?: string;
subject?: string;
page_count?: number;
status?: ExamTemplateStatus;
};
questions: Array<{
id?: string;
parent_id?: string | null;
label: string;
order?: number;
max_marks?: number;
answer_type?: 'written' | 'mcq' | 'short' | 'diagram' | null;
mcq_options?: unknown | null;
mark_scheme?: Record<string, unknown>;
is_container?: boolean;
spec_ref?: string | null;
bounds?: Record<string, number> | null;
page?: number | null;
}>;
response_areas: Array<{
id?: string;
question_id: string;
page: number;
bounds: Record<string, number>;
kind: ExamResponseArea['kind'];
response_form?: string | null;
context_type?: string | null;
source?: 'manual' | 'ai';
confirmed?: boolean;
confidence?: number | null;
}>;
boundaries: Array<{
id?: string;
question_id?: string | null;
label?: string | null;
page_index: number;
y: number;
bounds?: Record<string, number> | null;
source?: 'manual' | 'ai';
confirmed?: boolean;
}>;
}
export interface PatchQuestionPayload { export interface PatchQuestionPayload {
label?: string; label?: string;
order?: number; order?: number;
max_marks?: number; max_marks?: number;
answer_type?: written | mcq | short | diagram | null; answer_type?: 'written' | 'mcq' | 'short' | 'diagram' | null;
mcq_options?: unknown; mcq_options?: unknown;
mark_scheme?: MarkScheme; mark_scheme?: MarkScheme;
is_container?: boolean; is_container?: boolean;
@@ -155,48 +201,3 @@ export interface Neo4jSyncResult {
status: string; status: string;
projection?: Record<string, unknown>; projection?: Record<string, unknown>;
} }
export interface TemplateReplacePayload {
meta?: {
title?: string;
subject?: string;
page_count?: number;
status?: ExamTemplateStatus;
};
questions: Array<{
id?: string;
parent_id?: string | null;
label: string;
order?: number;
max_marks?: number;
answer_type?: written | mcq | short | diagram | null;
mcq_options?: unknown | null;
mark_scheme?: MarkScheme;
is_container?: boolean;
spec_ref?: string | null;
bounds?: Record<string, number> | null;
page?: number | null;
}>;
response_areas: Array<{
id?: string;
question_id: string;
page: number;
bounds: Record<string, number>;
kind: ExamResponseAreaKind;
response_form?: string | null;
context_type?: string | null;
source?: manual | ai;
confirmed?: boolean;
confidence?: number | null;
}>;
boundaries: Array<{
id?: string;
question_id?: string | null;
label?: string | null;
page_index: number;
y: number;
bounds?: Record<string, number> | null;
source?: manual | ai;
confirmed?: boolean;
}>;
}