"use client"; // Examens blancs : liste avec tentatives passées, choix du mode (chronométré / // pratique), interface d'examen (navigation, compte à rebours, remise auto), // écran de résultats avec analyse par concept et par axe. import { useCallback, useEffect, useRef, useState } from "react"; import { AlertTriangle, CheckCircle2, ChevronLeft, ChevronRight, Clock3, FileCheck2, GraduationCap, Send, Timer, XCircle, } from "lucide-react"; import { Badge, Button, Card, EmptyState, Modal, ProgressBar, Skeleton, Spinner, Textarea, cn } from "@/components/ui"; import { Markdown } from "@/components/chat/markdown"; import { DifficultyDots, ErrorBanner, fetchJson, fmtDateTime, postJson } from "./shared"; type Exam = { id: number; kind: string; title: string; description: string; duration_minutes: number; questionCount: number }; type Attempt = { id: number; exam_id: number; mode: string; started_at: string; finished_at: string | null; score: number | null; total: number | null }; type ExamQuestion = { id: number; type: string; difficulty: number; question: string; options: string[] }; type SubmitResult = { score: number; total: number; detail: Record; analysis: { byConcept: { name: string; correct: number; total: number }[]; byAxis: { axis: string; correct: number; total: number }[]; weakest: string[]; recommendation: string; }; previous: { score: number; total: number; finished_at: string }[]; }; const KIND_LABELS: Record = { intra: { label: "Intra", tone: "brand" }, final: { label: "Final", tone: "gold" }, thematic: { label: "Thématique", tone: "neutral" }, }; const AXIS_LABELS: Record = { connaissances: "Connaissances", calcul: "Calcul", interpretation: "Interprétation", jugement: "Jugement professionnel", communication: "Communication", }; export function ExamsApp({ course }: { course: string }) { const upper = course.toUpperCase(); const [exams, setExams] = useState(null); const [attempts, setAttempts] = useState([]); const [error, setError] = useState(null); // Choix du mode const [modeFor, setModeFor] = useState(null); const [busy, setBusy] = useState(false); // Tentative en cours const [attempt, setAttempt] = useState<{ attemptId: number; title: string; mode: "timed" | "practice"; durationMinutes: number; questions: ExamQuestion[] } | null>(null); const [answers, setAnswers] = useState>({}); const [idx, setIdx] = useState(0); const [deadline, setDeadline] = useState(null); const [remaining, setRemaining] = useState(null); const [confirmOpen, setConfirmOpen] = useState(false); const submittedRef = useRef(false); // Résultats const [result, setResult] = useState(null); const [openDetail, setOpenDetail] = useState>({}); const load = useCallback(() => { setError(null); fetchJson<{ exams: Exam[]; attempts: Attempt[] }>(`/api/learning/exams?course=${upper}`) .then((d) => { setExams(d.exams); setAttempts(d.attempts); }) .catch((e) => { setExams([]); setError(e instanceof Error ? e.message : "Erreur de chargement."); }); }, [upper]); useEffect(() => { load(); }, [load]); async function start(exam: Exam, mode: "timed" | "practice") { setBusy(true); setError(null); try { const d = await postJson<{ attemptId: number; durationMinutes: number; title: string; questions: ExamQuestion[] }>( "/api/learning/exams", { action: "start", examId: exam.id, mode } ); submittedRef.current = false; setAttempt({ ...d, mode }); setAnswers({}); setIdx(0); setResult(null); setModeFor(null); setDeadline(mode === "timed" ? Date.now() + d.durationMinutes * 60_000 : null); } catch (e) { setError(e instanceof Error ? e.message : "Impossible de démarrer l'examen."); } finally { setBusy(false); } } const submit = useCallback(async (auto = false) => { if (!attempt || submittedRef.current) return; submittedRef.current = true; setBusy(true); setConfirmOpen(false); setError(null); try { const d = await postJson("/api/learning/exams", { action: "submit", attemptId: attempt.attemptId, answers, }); setResult(d); setAttempt(null); setDeadline(null); load(); if (auto) setError(null); } catch (e) { submittedRef.current = false; setError(e instanceof Error ? e.message : "Erreur lors de la remise."); } finally { setBusy(false); } }, [attempt, answers, load]); // Compte à rebours + remise automatique à 0:00 useEffect(() => { if (!deadline) { setRemaining(null); return; } const tick = () => { const r = Math.max(0, Math.round((deadline - Date.now()) / 1000)); setRemaining(r); if (r <= 0) submit(true); }; tick(); const t = setInterval(tick, 1000); return () => clearInterval(t); }, [deadline, submit]); const answeredCount = attempt ? attempt.questions.filter((q) => (answers[String(q.id)] ?? "").trim() !== "").length : 0; // ---------- Interface d'examen ---------- if (attempt) { const q = attempt.questions[idx]; const total = attempt.questions.length; const mins = remaining != null ? Math.floor(remaining / 60) : null; const secs = remaining != null ? remaining % 60 : null; const low = remaining != null && remaining < 300; return (

{attempt.title}

{attempt.mode === "timed" ? "Chronométré" : "Mode pratique"} {remaining != null && ( {mins}:{String(secs).padStart(2, "0")} )}
{/* Grille de navigation */}
{attempt.questions.map((qq, i) => { const done = (answers[String(qq.id)] ?? "").trim() !== ""; return ( ); })}
Question {idx + 1} / {total}
{q.type === "mcq" ? (
{q.options.map((opt, i) => { const chosen = answers[String(q.id)] === opt; return ( ); })}
) : (