"use client"; // Quiz adaptatif : écran de départ (concept optionnel), question (mcq / réponse // ouverte + confiance), rétroaction riche, synthèse de fin de session. import Link from "next/link"; import { useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; import { ArrowRight, CheckCircle2, Flag, ListChecks, NotebookPen, XCircle } from "lucide-react"; import { Badge, Button, Card, ProgressBar, Skeleton, Spinner, Textarea, cn } from "@/components/ui"; import { Markdown } from "@/components/chat/markdown"; import { DifficultyDots, ErrorBanner, fetchJson, postJson } from "./shared"; type Question = { id: number; type: string; difficulty: number; question: string; options: string[]; conceptId: number | null }; type Feedback = { correct: boolean; expected: string; explanation: string; score: { correct: number; total: number } }; type Summary = { correct: number; total: number; byConcept: { name: string; correct: number; total: number }[] }; type ConceptOpt = { slug: string; name: string }; const TYPE_LABELS: Record = { mcq: "Choix multiple", short: "Réponse courte", calc: "Calcul", "error-detect": "Détection d'erreur", case: "Étude de cas", order: "Mise en ordre", match: "Association", }; export function QuizApp({ course }: { course: string }) { const searchParams = useSearchParams(); const upper = course.toUpperCase(); const [phase, setPhase] = useState<"start" | "question" | "feedback" | "summary">("start"); const [concepts, setConcepts] = useState(null); const [conceptSlug, setConceptSlug] = useState(searchParams.get("concept") ?? ""); const [sessionId, setSessionId] = useState(null); const [question, setQuestion] = useState(null); const [answer, setAnswer] = useState(""); const [confidence, setConfidence] = useState(3); const [feedback, setFeedback] = useState(null); const [summary, setSummary] = useState(null); const [answered, setAnswered] = useState(0); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [exhaustedMsg, setExhaustedMsg] = useState(null); useEffect(() => { fetchJson<{ nodes: ConceptOpt[] }>(`/api/learning/${course}/concepts`) .then((d) => setConcepts(d.nodes)) .catch(() => setConcepts([])); }, [course]); async function start() { setBusy(true); setError(null); try { const d = await postJson<{ sessionId: number; question: Question }>("/api/learning/quiz", { action: "start", course: upper, conceptSlug: conceptSlug || null, }); setSessionId(d.sessionId); setQuestion(d.question); setAnswer(""); setConfidence(3); setAnswered(0); setExhaustedMsg(null); setPhase("question"); } catch (e) { setError(e instanceof Error ? e.message : "Impossible de démarrer le quiz."); } finally { setBusy(false); } } async function submitAnswer(value: string) { if (!sessionId || !question || busy) return; setBusy(true); setError(null); try { const d = await postJson("/api/learning/quiz", { action: "answer", sessionId, questionId: question.id, answer: value, confidence: question.type === "mcq" ? undefined : confidence, hintsUsed: 0, }); setFeedback(d); setAnswered((n) => n + 1); setPhase("feedback"); } catch (e) { setError(e instanceof Error ? e.message : "Erreur d'envoi de la réponse."); } finally { setBusy(false); } } async function nextQuestion() { if (!sessionId || busy) return; setBusy(true); setError(null); try { const d = await postJson<{ question: Question | null; message?: string }>("/api/learning/quiz", { action: "next", sessionId, }); if (!d.question) { setExhaustedMsg(d.message ?? "Banque de questions épuisée pour ce ciblage."); await finish(); return; } setQuestion(d.question); setAnswer(""); setConfidence(3); setFeedback(null); setPhase("question"); } catch (e) { setError(e instanceof Error ? e.message : "Erreur de chargement de la question."); } finally { setBusy(false); } } async function finish() { if (!sessionId) return; setBusy(true); setError(null); try { const d = await postJson<{ summary: Summary }>("/api/learning/quiz", { action: "finish", sessionId }); setSummary(d.summary); setPhase("summary"); } catch (e) { setError(e instanceof Error ? e.message : "Erreur de clôture de la session."); } finally { setBusy(false); } } // ---------- Écran de départ ---------- if (phase === "start") { return (

Quiz adaptatif

La difficulté s'ajuste à vos réponses (deux bonnes de suite → plus dur ; une erreur → plus facile). Chaque erreur est ajoutée à votre cahier d'erreurs.

{concepts === null ? ( ) : ( )} {error && }
); } // ---------- Synthèse ---------- if (phase === "summary") { const pct = summary && summary.total > 0 ? Math.round((summary.correct / summary.total) * 100) : 0; return (

Session terminée

{exhaustedMsg &&

{exhaustedMsg}

} {summary && summary.total > 0 ? ( <>

= 70 ? "text-emerald-500" : pct >= 50 ? "text-amber-500" : "text-red-500")}> {pct} %

{summary.correct} bonne{summary.correct > 1 ? "s" : ""} réponse{summary.correct > 1 ? "s" : ""} sur {summary.total}

) : (

Aucune question répondue dans cette session.

)}
{summary && summary.byConcept.length > 0 && (

Résultat par concept

{summary.byConcept.map((c) => (
{c.name} {c.correct}/{c.total}
))}
)} {error && }
{summary && summary.correct < summary.total && ( )}
); } // ---------- Question / rétroaction ---------- if (!question) return null; const isMcq = question.type === "mcq"; return (
{TYPE_LABELS[question.type] ?? question.type} Question {answered + (phase === "question" ? 1 : 0)}{feedback ? ` · ${feedback.score.correct}/${feedback.score.total} réussie${feedback.score.correct > 1 ? "s" : ""}` : ""}
{phase === "question" ? ( <> {isMcq ? (
{question.options.map((opt, i) => ( ))}
) : (
{ e.preventDefault(); if (answer.trim()) submitAnswer(answer); }} className="space-y-3" >