53 lines
1.6 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|