Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

53 lines
1.6 KiB
TypeScript

import type { QuizQuestion } from '@/lib/types/stage';
export interface QuestionResult {
questionId: string;
correct: boolean | null;
status: 'correct' | 'incorrect';
earned: number;
aiComment?: string;
}
export function arraysEqual(a: string[], b: string[]): boolean {
if (a.length !== b.length) return false;
const sa = [...a].sort();
const sb = [...b].sort();
return sa.every((v, i) => v === sb[i]);
}
export function toArray(v: string | string[] | undefined): string[] {
if (!v) return [];
return Array.isArray(v) ? v : [v];
}
/**
* Whether a question is graded as open text (AI) rather than by exact
* answer-key match. Classification is by the explicit `type` only: an
* unanswered choice question (empty `answer`) is still a choice question and
* must not be re-routed to AI grading. `hasAnswer` does not override the type.
*/
export function isShortAnswer(q: QuizQuestion): boolean {
return q.type === 'short_answer';
}
/** Grade choice questions locally. Returns results only for non-short-answer questions. */
export function gradeChoiceQuestions(
questions: QuizQuestion[],
answers: Record<string, string | string[]>,
): QuestionResult[] {
return questions
.filter((q) => !isShortAnswer(q))
.map((q) => {
const pts = q.points ?? 1;
const userAnswer = toArray(answers[q.id]);
const correctAnswer = toArray(q.answer);
const correct = arraysEqual(userAnswer, correctAnswer);
return {
questionId: q.id,
correct,
status: correct ? ('correct' as const) : ('incorrect' as const),
earned: correct ? pts : 0,
};
});
}