Python 64.6%
TypeScript 33.7%
CSS 0.8%
1import { useState } from 'react';2import { Check, RotateCcw, X } from 'lucide-react';3import type { QuizPayload, QuizResult, ToolCallView } from '@/lib/types';4import { api } from '@/lib/api';5import { Button } from '@/components/ui/button';6import { cn } from '@/lib/cn';7import { StreamingText } from '@/components/chat/streaming-text';89export function QuizCard({ call }: { call: ToolCallView }) {10 const quiz = (call.payload as { quiz?: QuizPayload }).quiz;11 const [answers, setAnswers] = useState<Record<string, unknown>>({});12 const [result, setResult] = useState<QuizResult | null>(null);13 const [busy, setBusy] = useState(false);14 if (!quiz) return null;1516 const submit = async () => {17 setBusy(true);18 try {19 setResult(await api<QuizResult>(`/quiz/${quiz.quiz_id}/answers`, { method: 'POST', body: JSON.stringify({ answers }) }));20 } finally {21 setBusy(false);22 }23 };24 const reset = () => {25 setAnswers({});26 setResult(null);27 };28 const res = (id: string) => result?.results.find((r) => r.id === id);29 const answered = quiz.questions.every((q) => answers[q.id] !== undefined && answers[q.id] !== '');3031 return (32 <div className="space-y-4">33 <div className="flex items-center justify-between gap-2">34 <div>35 <div className="font-semibold text-uqo-blue-dark">{quiz.title}</div>36 <div className="text-xs text-neutral-muted">{quiz.course} · {quiz.questions.length} questions · {quiz.difficulty}</div>37 </div>38 {result && (39 <div className={cn('rounded-xl px-3 py-1.5 text-sm font-semibold', result.score >= 0.7 ? 'bg-[#e9f6d9] text-[#3f7a0a]' : 'bg-amber-50 text-amber-700')}>40 {result.correct}/{result.total} · {Math.round(result.score * 100)} %41 </div>42 )}43 </div>44 <ol className="space-y-4">45 {quiz.questions.map((q, i) => {46 const r = res(q.id);47 return (48 <li key={q.id} className={cn('rounded-xl border p-3 bg-white', r ? (r.correct ? 'border-uqo-green' : 'border-semantic-error') : 'border-neutral-line')}>49 <div className="flex gap-2">50 <span className="text-sm font-semibold text-uqo-blue">{i + 1}.</span>51 <div className="md text-sm flex-1"><StreamingText text={q.prompt} /></div>52 {r && (r.correct ? <Check className="text-uqo-green shrink-0" size={18} /> : <X className="text-semantic-error shrink-0" size={18} />)}53 </div>54 <div className="mt-2 pl-5 space-y-1.5">55 {q.type === 'mcq' && q.choices?.map((c, idx) => (56 <label key={idx} className={cn('flex items-start gap-2 rounded-lg px-2 py-2 cursor-pointer min-h-[44px] hover:bg-neutral-surface', r && Number(r.answer) === idx && 'bg-[#e9f6d9]')}>57 <input type="radio" name={q.id} className="mt-1 accent-uqo-blue" disabled={!!result} checked={answers[q.id] === idx} onChange={() => setAnswers((a) => ({ ...a, [q.id]: idx }))} />58 <span className="text-sm">{c}</span>59 </label>60 ))}61 {q.type === 'true_false' && (62 <div className="flex gap-2">63 {[true, false].map((v) => (64 <button key={String(v)} disabled={!!result} onClick={() => setAnswers((a) => ({ ...a, [q.id]: v }))}65 className={cn('h-11 px-4 rounded-xl border text-sm font-medium', answers[q.id] === v ? 'bg-uqo-blue text-white border-uqo-blue' : 'border-neutral-line bg-white', r && r.answer === v && 'ring-2 ring-uqo-green')}>66 {v ? 'Vrai' : 'Faux'}67 </button>68 ))}69 </div>70 )}71 {q.type === 'numeric' && (72 <div className="flex items-center gap-2">73 <input type="text" inputMode="decimal" disabled={!!result} placeholder="Réponse" value={(answers[q.id] as string) ?? ''}74 onChange={(e) => setAnswers((a) => ({ ...a, [q.id]: e.target.value }))}75 className="h-11 w-44 rounded-xl border border-neutral-line px-3 text-sm" />76 {q.unit && <span className="text-sm text-neutral-muted">{q.unit}</span>}77 {r && !r.correct && <span className="text-sm text-neutral-muted">Réponse : <b>{String(r.answer)}</b> {r.unit}</span>}78 </div>79 )}80 {r && r.explanation && <div className="md text-sm mt-2 rounded-lg bg-uqo-blue-light/60 p-2.5"><StreamingText text={r.explanation} /></div>}81 </div>82 </li>83 );84 })}85 </ol>86 <div className="flex gap-2">87 {!result ? (88 <Button onClick={submit} disabled={!answered || busy}>{busy ? 'Correction…' : 'Corriger'}</Button>89 ) : (90 <Button variant="secondary" onClick={reset}><RotateCcw size={16} /> Refaire</Button>91 )}92 </div>93 </div>94 );95}96