TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1// Examens blancs : liste, démarrage d'une tentative, soumission, correction et analyse.2import { NextResponse } from "next/server";3import { z } from "zod";4import { apiError, parseBody, requireEnrollment } from "@/lib/api.ts";5import { assertSameOrigin, requireUser } from "@/lib/auth/session.ts";6import { all, get, run } from "@/lib/db/index.ts";7import { gradeAnswer, type QuizQuestion } from "@/lib/learning/quiz.ts";8import { recordMasteryEvent } from "@/lib/learning/mastery.ts";9import { logActivity } from "@/lib/usage.ts";1011export async function GET(req: Request) {12 try {13 const user = await requireUser();14 const url = new URL(req.url);15 const course = url.searchParams.get("course")?.toUpperCase();16 if (course !== "IMM1003" && course !== "IMM1033") return NextResponse.json({ error: "Cours requis." }, { status: 400 });17 requireEnrollment(user.id, course);18 const exams = all<{ id: number; kind: string; title: string; description: string; duration_minutes: number; question_ids: string }>(19 "SELECT id, kind, title, description, duration_minutes, question_ids FROM mock_exams WHERE course_code = ? ORDER BY kind, id",20 course21 );22 const attempts = all(23 `SELECT ea.id, ea.exam_id, ea.mode, ea.started_at, ea.finished_at, ea.score, ea.total24 FROM exam_attempts ea JOIN mock_exams me ON me.id = ea.exam_id25 WHERE ea.user_id = ? AND me.course_code = ? ORDER BY ea.id DESC LIMIT 50`,26 user.id, course27 );28 return NextResponse.json({29 exams: exams.map((e) => ({ ...e, questionCount: (JSON.parse(e.question_ids) as number[]).length, question_ids: undefined })),30 attempts,31 });32 } catch (e) {33 return apiError(e);34 }35}3637const startSchema = z.object({38 action: z.literal("start"),39 examId: z.number().int().positive(),40 mode: z.enum(["timed", "practice"]),41});42const submitSchema = z.object({43 action: z.literal("submit"),44 attemptId: z.number().int().positive(),45 answers: z.record(z.string(), z.string().max(4000)),46});4748export async function POST(req: Request) {49 try {50 await assertSameOrigin();51 const user = await requireUser();52 const body = await parseBody(req, z.discriminatedUnion("action", [startSchema, submitSchema]));5354 if (body.action === "start") {55 const exam = get<{ id: number; course_code: string; question_ids: string; duration_minutes: number; title: string }>(56 "SELECT id, course_code, question_ids, duration_minutes, title FROM mock_exams WHERE id = ?", body.examId57 );58 if (!exam) return NextResponse.json({ error: "Examen introuvable." }, { status: 404 });59 requireEnrollment(user.id, exam.course_code);60 const ids = JSON.parse(exam.question_ids) as number[];61 const questions = ids.length62 ? all<QuizQuestion>(`SELECT * FROM quiz_questions WHERE id IN (${ids.map(() => "?").join(",")})`, ...ids)63 : [];64 const ordered = ids.map((id) => questions.find((q) => q.id === id)).filter((q): q is QuizQuestion => !!q);65 const r = run(66 "INSERT INTO exam_attempts (user_id, exam_id, mode) VALUES (?, ?, ?)",67 user.id, exam.id, body.mode68 );69 return NextResponse.json({70 attemptId: Number(r.lastInsertRowid),71 durationMinutes: exam.duration_minutes,72 title: exam.title,73 questions: ordered.map((q) => ({74 id: q.id, type: q.type, difficulty: q.difficulty, question: q.question,75 options: JSON.parse(q.options || "[]"),76 })),77 });78 }7980 // submit81 const attempt = get<{ id: number; user_id: number; exam_id: number; finished_at: string | null; mode: string; started_at: string }>(82 "SELECT * FROM exam_attempts WHERE id = ?", body.attemptId83 );84 if (!attempt || attempt.user_id !== user.id) return NextResponse.json({ error: "Tentative introuvable." }, { status: 404 });85 if (attempt.finished_at) return NextResponse.json({ error: "Tentative déjà soumise." }, { status: 409 });86 const exam = get<{ course_code: string; question_ids: string; title: string }>(87 "SELECT course_code, question_ids, title FROM mock_exams WHERE id = ?", attempt.exam_id88 )!;89 const ids = JSON.parse(exam.question_ids) as number[];90 const questions = all<QuizQuestion>(`SELECT * FROM quiz_questions WHERE id IN (${ids.map(() => "?").join(",")})`, ...ids);9192 let score = 0;93 const detail: Record<string, { answer: string; correct: boolean; expected: string; explanation: string }> = {};94 const byConcept = new Map<string, { name: string; correct: number; total: number; conceptId: number | null }>();95 const byAxis = new Map<string, { correct: number; total: number }>();96 for (const q of questions) {97 const ua = body.answers[String(q.id)] ?? "";98 const correct = ua ? gradeAnswer(q, ua) : false;99 if (correct) score++;100 detail[String(q.id)] = { answer: ua, correct, expected: q.answer, explanation: q.explanation };101 const c = q.concept_id102 ? get<{ name: string; axis: string }>("SELECT name, axis FROM concepts WHERE id = ?", q.concept_id)103 : null;104 const key = c?.name ?? "Divers";105 const e = byConcept.get(key) ?? { name: key, correct: 0, total: 0, conceptId: q.concept_id };106 e.total++;107 if (correct) e.correct++;108 byConcept.set(key, e);109 const axisKey = c?.axis ?? "connaissances";110 const ax = byAxis.get(axisKey) ?? { correct: 0, total: 0 };111 ax.total++;112 if (correct) ax.correct++;113 byAxis.set(axisKey, ax);114 if (q.concept_id) {115 recordMasteryEvent({ userId: user.id, conceptId: q.concept_id, kind: "exam", correct, difficulty: q.difficulty });116 }117 if (!correct && ua) {118 run(119 `INSERT INTO error_notebook (user_id, course_code, concept_id, question, given_answer, correction, explanation, source)120 VALUES (?, ?, ?, ?, ?, ?, ?, 'exam')`,121 user.id, exam.course_code, q.concept_id, q.question, ua.slice(0, 2000), q.answer, q.explanation122 );123 }124 }125126 const weakest = [...byConcept.values()].filter((c) => c.total >= 1).sort((a, b) => a.correct / a.total - b.correct / b.total).slice(0, 3);127 const analysis = {128 byConcept: [...byConcept.values()],129 byAxis: [...byAxis.entries()].map(([axis, v]) => ({ axis, ...v })),130 weakest: weakest.map((w) => w.name),131 recommendation: weakest.length132 ? `Priorités de révision : ${weakest.map((w) => w.name).join(", ")}. Refaites un quiz ciblé sur chacune, puis retentez un examen thématique.`133 : "Excellente performance — passez au niveau supérieur avec le mode Défi du chat.",134 };135 run(136 "UPDATE exam_attempts SET finished_at = datetime('now'), answers = ?, score = ?, total = ?, analysis = ? WHERE id = ?",137 JSON.stringify(detail), score, questions.length, JSON.stringify(analysis), attempt.id138 );139 logActivity(user.id, "exam", exam.course_code, questions.length * 90, { attemptId: attempt.id, score, total: questions.length });140141 const previous = all<{ score: number; total: number; finished_at: string }>(142 "SELECT score, total, finished_at FROM exam_attempts WHERE user_id = ? AND exam_id = ? AND finished_at IS NOT NULL AND id != ? ORDER BY id DESC LIMIT 5",143 user.id, attempt.exam_id, attempt.id144 );145 return NextResponse.json({ ok: true, score, total: questions.length, detail, analysis, previous });146 } catch (e) {147 return apiError(e);148 }149}150