Files
breakpilot-lehrer/studio-v2/components/learn/QuizQuestion.tsx
Benjamin Admin 20a0585eb1
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 44s
CI / test-go-edu-search (push) Successful in 51s
CI / test-python-klausur (push) Failing after 2m44s
CI / test-python-agent-core (push) Successful in 33s
CI / test-nodejs-website (push) Successful in 34s
Add interactive learning modules MVP (Phases 1-3.1)
New feature: After OCR vocabulary extraction, users can generate interactive
learning modules (flashcards, quiz, type trainer) with one click.

Frontend (studio-v2):
- Fortune Sheet spreadsheet editor tab in vocab-worksheet
- "Lernmodule generieren" button in ExportTab
- /learn page with unit overview and exercise type cards
- /learn/[unitId]/flashcards — Flip-card trainer with Leitner spaced repetition
- /learn/[unitId]/quiz — Multiple choice quiz with explanations
- /learn/[unitId]/type — Type-in trainer with Levenshtein distance feedback
- AudioButton component using Web Speech API for EN+DE TTS

Backend (klausur-service):
- vocab_learn_bridge.py: Converts VocabularyEntry[] to analysis_data format
- POST /sessions/{id}/generate-learning-unit endpoint

Backend (backend-lehrer):
- generate-qa, generate-mc, generate-cloze endpoints on learning units
- get-qa/mc/cloze data retrieval endpoints
- Leitner progress update + next review items endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:13:23 +02:00

127 lines
3.9 KiB
TypeScript

'use client'
import React, { useState, useCallback } from 'react'
interface Option {
id: string
text: string
}
interface QuizQuestionProps {
question: string
options: Option[]
correctAnswer: string
explanation?: string
questionNumber: number
totalQuestions: number
onAnswer: (correct: boolean) => void
isDark: boolean
}
export function QuizQuestion({
question,
options,
correctAnswer,
explanation,
questionNumber,
totalQuestions,
onAnswer,
isDark,
}: QuizQuestionProps) {
const [selected, setSelected] = useState<string | null>(null)
const [revealed, setRevealed] = useState(false)
const handleSelect = useCallback((optionId: string) => {
if (revealed) return
setSelected(optionId)
setRevealed(true)
const isCorrect = optionId === correctAnswer
setTimeout(() => {
onAnswer(isCorrect)
setSelected(null)
setRevealed(false)
}, isCorrect ? 1000 : 2500)
}, [revealed, correctAnswer, onAnswer])
const getOptionStyle = (optionId: string) => {
if (!revealed) {
return isDark
? 'bg-white/10 border-white/20 hover:bg-white/20 hover:border-white/30 text-white'
: 'bg-white border-slate-200 hover:bg-slate-50 hover:border-slate-300 text-slate-900'
}
if (optionId === correctAnswer) {
return isDark
? 'bg-green-500/20 border-green-400 text-green-200'
: 'bg-green-50 border-green-500 text-green-800'
}
if (optionId === selected && optionId !== correctAnswer) {
return isDark
? 'bg-red-500/20 border-red-400 text-red-200'
: 'bg-red-50 border-red-500 text-red-800'
}
return isDark
? 'bg-white/5 border-white/10 text-white/40'
: 'bg-slate-50 border-slate-200 text-slate-400'
}
return (
<div className="w-full max-w-lg mx-auto space-y-6">
{/* Progress */}
<div className="flex items-center justify-between">
<span className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
Frage {questionNumber} / {totalQuestions}
</span>
<div className="w-32 h-1.5 rounded-full bg-white/10 overflow-hidden">
<div
className="h-full rounded-full bg-gradient-to-r from-purple-500 to-pink-500 transition-all"
style={{ width: `${(questionNumber / totalQuestions) * 100}%` }}
/>
</div>
</div>
{/* Question */}
<div className={`p-6 rounded-2xl ${isDark ? 'bg-white/10 backdrop-blur-xl border border-white/20' : 'bg-white shadow-lg border border-slate-200'}`}>
<p className={`text-xl font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{question}
</p>
</div>
{/* Options */}
<div className="space-y-3">
{options.map((opt, idx) => (
<button
key={opt.id}
onClick={() => handleSelect(opt.id)}
disabled={revealed}
className={`w-full p-4 rounded-xl border-2 text-left transition-all flex items-center gap-3 ${getOptionStyle(opt.id)}`}
>
<span className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0 ${
revealed && opt.id === correctAnswer
? 'bg-green-500 text-white'
: revealed && opt.id === selected
? 'bg-red-500 text-white'
: isDark ? 'bg-white/10' : 'bg-slate-100'
}`}>
{String.fromCharCode(65 + idx)}
</span>
<span className="text-base">{opt.text}</span>
</button>
))}
</div>
{/* Explanation */}
{revealed && explanation && (
<div className={`p-4 rounded-xl ${isDark ? 'bg-blue-500/10 border border-blue-500/20' : 'bg-blue-50 border border-blue-200'}`}>
<p className={`text-sm ${isDark ? 'text-blue-200' : 'text-blue-700'}`}>
{explanation}
</p>
</div>
)}
</div>
)
}