Add interactive learning modules MVP (Phases 1-3.1)
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
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
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>
This commit is contained in:
126
studio-v2/components/learn/QuizQuestion.tsx
Normal file
126
studio-v2/components/learn/QuizQuestion.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user