From c6615c0db93a104967f351be78014cf17817b0a9 Mon Sep 17 00:00:00 2001 From: Harald Pansi Date: Mon, 28 Sep 2026 15:16:07 +0200 Subject: [PATCH] Implement Point 1: Exam Simulator with grading scale & timer, Weak-Spots trainer, Free-text input with fuzzy matching, and Web Speech audio --- www/rhetorik_trainer_quiz.html | 819 ++++++++++++++++++++++++++++++--- www/sw.js | 2 +- 2 files changed, 754 insertions(+), 67 deletions(-) diff --git a/www/rhetorik_trainer_quiz.html b/www/rhetorik_trainer_quiz.html index 01c51e8..54d38ac 100644 --- a/www/rhetorik_trainer_quiz.html +++ b/www/rhetorik_trainer_quiz.html @@ -319,6 +319,12 @@
Tippe oder drücke [Leertaste] zum Aufdecken
+
+ +
@@ -348,8 +354,14 @@

-
- Typische Phrasen & Zitate +
+
+ Typische Phrasen & Zitate + +

„Fischers Fritz fischt frische Fische.“

@@ -418,20 +430,37 @@
- -
-
- - - + +
+ +
+ + + + +
- -
- + +
+ + +
+ + +
Streak: 0 @@ -444,6 +473,30 @@
+ + +
@@ -456,8 +509,14 @@

Welches rhetorische Stilmittel liegt in diesem Zitat vor?

-
- „Die Kunst ist lang, und kurz ist unser Leben.“ +
+
+ „Die Kunst ist lang, und kurz ist unser Leben.“ +
+ +
@@ -466,6 +525,21 @@
+ + + + + + +
+ + + + @@ -1518,12 +1669,17 @@ function updateQuizMistakesBadge() { const badge = document.getElementById('quizMistakesBadge'); const countEl = document.getElementById('quizMistakesCount'); - if (!badge || !countEl) return; - if (quizMistakes.length > 0) { - badge.classList.remove('hidden'); - countEl.innerText = quizMistakes.length; - } else { - badge.classList.add('hidden'); + if (badge && countEl) { + if (quizMistakes.length > 0) { + badge.classList.remove('hidden'); + countEl.innerText = quizMistakes.length; + } else { + badge.classList.add('hidden'); + } + } + const weakBtnCount = document.getElementById('quizWeakBtnCount'); + if (weakBtnCount) { + weakBtnCount.innerText = quizMistakes.length; } } @@ -1736,63 +1892,319 @@ } }, { passive: true }); - // QUIZ ENGINE + // ========================================== + // AUDIO & WEB SPEECH SYNTHESIS + // ========================================== + function speakText(text, event) { + if (event) event.stopPropagation(); + if (!('speechSynthesis' in window)) { + showToast('Sprachausgabe im Browser nicht verfügbar', 'fa-volume-xmark'); + return; + } + try { + window.speechSynthesis.cancel(); + // Remove surrounding quotes and context in brackets for fluid natural speech + let clean = text.replace(/„|“|"|'/g, '').replace(/\(.*?\)/g, '').trim(); + if (!clean) return; + const utterance = new SpeechSynthesisUtterance(clean); + utterance.lang = 'de-DE'; + utterance.rate = 0.95; + utterance.pitch = 1.0; + window.speechSynthesis.speak(utterance); + } catch (err) { + console.warn('SpeechSynthesis error:', err); + } + } + + function speakCurrentQuizPhrase(event) { + if (currentQuizIndex < quizQuestions.length) { + const q = quizQuestions[currentQuizIndex]; + speakText(q.example, event); + } + } + + function speakCurrentCard(event) { + if (filteredCards.length > 0 && currentCardIndex < filteredCards.length) { + const item = filteredCards[currentCardIndex]; + if (cardFaceMode === 'term' && !cardIsFlipped) { + speakText(item.term, event); + } else { + speakText(item.examples[0], event); + } + } + } + + // ========================================== + // FUZZY MATCHER & LEVENSHTEIN DISTANCE + // ========================================== + function levenshteinDistance(s1, s2) { + if (!s1) return s2 ? s2.length : 0; + if (!s2) return s1.length; + if (s1.length < s2.length) return levenshteinDistance(s2, s1); + + let prevRow = Array.from({ length: s2.length + 1 }, (_, i) => i); + for (let i = 0; i < s1.length; i++) { + let currRow = [i + 1]; + for (let j = 0; j < s2.length; j++) { + let cost = s1[i] === s2[j] ? 0 : 1; + currRow.push(Math.min( + currRow[j] + 1, // Insertion + prevRow[j + 1] + 1, // Deletion + prevRow[j] + cost // Substitution + )); + } + prevRow = currRow; + } + return prevRow[s2.length]; + } + + function checkFuzzyMatch(userInput, targetTerm) { + if (!userInput) return { isMatch: false, exact: false, dist: 99 }; + const normUser = userInput.toLowerCase().replace(/[^a-zäöüß0-9]/g, ''); + const normTarget = targetTerm.toLowerCase().replace(/[^a-zäöüß0-9]/g, ''); + + if (normUser === normTarget) { + return { isMatch: true, exact: true, dist: 0 }; + } + + // Special aliases and dual terms + if (normTarget.includes('tautologie') || normTarget.includes('pleonasmus')) { + if (normUser === 'tautologie' || normUser === 'pleonasmus') { + return { isMatch: true, exact: true, dist: 0 }; + } + } + if (normTarget.includes('parsprototo')) { + if (normUser === 'parsprototo' || normUser === 'synekdoche' || normUser === 'partsprototo') { + return { isMatch: true, exact: true, dist: 0 }; + } + } + + const dist = levenshteinDistance(normUser, normTarget); + const maxAllowed = normTarget.length > 7 ? 2 : (normTarget.length > 4 ? 1 : 0); + + if (dist <= maxAllowed) { + return { isMatch: true, exact: false, dist: dist }; + } + return { isMatch: false, exact: false, dist: dist }; + } + + // ========================================== + // NOTENSCHLÜSSEL & EXAM GRADING + // ========================================== + function calculateExamGrade(score, total) { + const percent = Math.round((score / total) * 100); + if (percent >= 90) { + return { + grade: 1, + name: "Sehr gut (1)", + points: "15 - 13 Notenpunkte", + badgeClass: "bg-emerald-100 text-emerald-900 border-emerald-300", + icon: "fa-crown", + iconColor: "text-amber-500", + feedback: "Ausgezeichnete Leistung! Du beherrschst die rhetorischen Stilmittel absolut souverän und bist bestens für jede Textanalyse gerüstet." + }; + } else if (percent >= 80) { + return { + grade: 2, + name: "Gut (2)", + points: "12 - 10 Notenpunkte", + badgeClass: "bg-blue-100 text-blue-900 border-blue-300", + icon: "fa-star", + iconColor: "text-blue-500", + feedback: "Sehr gute Textanalyse-Kenntnisse mit nur minimalen Unsicherheiten. Wiederhole kurz die Verwechslungsfallen." + }; + } else if (percent >= 65) { + return { + grade: 3, + name: "Befriedigend (3)", + points: "9 - 7 Notenpunkte", + badgeClass: "bg-amber-100 text-amber-900 border-amber-300", + icon: "fa-thumbs-up", + iconColor: "text-amber-600", + feedback: "Solides Grundlagenwissen. Ein gezieltes Schwachstellen-Training bringt dich sicher in den Bereich 'Gut'." + }; + } else if (percent >= 50) { + return { + grade: 4, + name: "Ausreichend (4)", + points: "6 - 5 Notenpunkte", + badgeClass: "bg-orange-100 text-orange-900 border-orange-300", + icon: "fa-triangle-exclamation", + iconColor: "text-orange-500", + feedback: "Bestanden, aber mit spürbaren Lücken. Nutze die Karteikarten und das Glossar, um typische Fallen zu vermeiden." + }; + } else if (percent >= 30) { + return { + grade: 5, + name: "Mangelhaft (5)", + points: "4 - 1 Notenpunkte", + badgeClass: "bg-rose-100 text-rose-900 border-rose-300", + icon: "fa-circle-xmark", + iconColor: "text-rose-500", + feedback: "Die Grundlagen sitzen noch nicht fest genug. Wiederhole systematisch den Kernstoff mit den Karteikarten." + }; + } else { + return { + grade: 6, + name: "Ungenügend (6)", + points: "0 Notenpunkte", + badgeClass: "bg-slate-200 text-slate-900 border-slate-300", + icon: "fa-skull-crossbones", + iconColor: "text-slate-600", + feedback: "Nicht bestanden. Keine Panik: Starte am besten im Glossar und arbeite dich durch die 17 Kernstoff-Figuren." + }; + } + } + + // ========================================== + // QUIZ ENGINE (CHOICE, FREITEXT, EXAM & WEAKSPOTS) + // ========================================== + let currentQuizInputType = 'choice'; // 'choice' | 'text' + let examTimeRemaining = 600; // 10 minutes in seconds + let examTimerInterval = null; + let examStartTime = null; + + function setQuizInputType(type) { + currentQuizInputType = type; + const choiceBtn = document.getElementById('quizInputChoiceBtn'); + const textBtn = document.getElementById('quizInputTextBtn'); + + if (type === 'choice') { + choiceBtn.className = 'px-2.5 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition flex items-center gap-1'; + textBtn.className = 'px-2.5 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1'; + } else { + textBtn.className = 'px-2.5 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition flex items-center gap-1'; + choiceBtn.className = 'px-2.5 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1'; + } + + renderQuizQuestion(); + } + function startQuiz(mode = 'highlighted') { currentQuizMode = mode; let pool = []; + // Clear any running exam timer + if (examTimerInterval) { + clearInterval(examTimerInterval); + examTimerInterval = null; + } + // Update Quiz Mode buttons UI const btnMap = { 'highlighted': 'quizModeKernBtn', 'all': 'quizModeAllBtn', - 'zusatz': 'quizModeZusatzBtn' + 'zusatz': 'quizModeZusatzBtn', + 'weakspots': 'quizModeWeakBtn', + 'exam': 'quizModeExamBtn' }; - ['quizModeKernBtn', 'quizModeAllBtn', 'quizModeZusatzBtn'].forEach(id => { + + ['quizModeKernBtn', 'quizModeAllBtn', 'quizModeZusatzBtn', 'quizModeWeakBtn', 'quizModeExamBtn'].forEach(id => { const b = document.getElementById(id); - if (b) b.className = 'px-2.5 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold'; + if (b) { + if (id === 'quizModeExamBtn') { + b.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-slate-200 text-slate-700 hover:bg-slate-300 transition font-bold flex items-center gap-1'; + } else { + b.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold'; + } + } }); + const activeBtn = document.getElementById(btnMap[mode]); if (activeBtn) { - activeBtn.className = 'px-2.5 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition'; + if (mode === 'exam') { + activeBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow font-extrabold transition flex items-center gap-1'; + } else { + activeBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition'; + } } - if (mode === 'weakspots' && quizMistakes.length > 0) { - pool = STILMITTEL_DATA.filter(item => quizMistakes.includes(item.id)); - document.getElementById('quizModeName').innerText = `Schwachstellen-Training (${pool.length} Begriffe)`; + const timerBar = document.getElementById('examTimerBar'); + const examNavBar = document.getElementById('examNavBar'); + const scoreStatsContainer = document.getElementById('quizScoreStatsContainer'); + + if (mode === 'exam') { + pool = [...STILMITTEL_DATA]; + document.getElementById('quizModeName').innerText = '⏱️ Klausursimulation (15 Fragen unter Zeitdruck)'; + timerBar.classList.remove('hidden'); + examNavBar.classList.remove('hidden'); + scoreStatsContainer.classList.add('hidden'); + + // Start 10-minute timer + examTimeRemaining = 600; + examStartTime = Date.now(); + updateExamTimerDisplay(); + examTimerInterval = setInterval(handleExamTimerTick, 1000); + } else if (mode === 'weakspots') { + if (quizMistakes.length > 0) { + pool = STILMITTEL_DATA.filter(item => quizMistakes.includes(item.id)); + } else { + pool = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) <= 2); + } + document.getElementById('quizModeName').innerText = `⚠️ Schwachstellen-Training (${pool.length} Begriffe)`; + timerBar.classList.add('hidden'); + examNavBar.classList.add('hidden'); + scoreStatsContainer.classList.remove('hidden'); } else if (mode === 'all') { pool = [...STILMITTEL_DATA]; document.getElementById('quizModeName').innerText = 'Gesamt-Challenge (Alle 23 Stilmittel)'; + timerBar.classList.add('hidden'); + examNavBar.classList.add('hidden'); + scoreStatsContainer.classList.remove('hidden'); } else if (mode === 'zusatz') { pool = STILMITTEL_DATA.filter(item => !item.highlighted); - document.getElementById('quizModeName').innerText = 'Zusatzinfo-Training (6 erweiterte Stilmittel)'; + document.getElementById('quizModeName').innerText = '🔍 Zusatzinfo-Training (6 erweiterte Stilmittel)'; + timerBar.classList.add('hidden'); + examNavBar.classList.add('hidden'); + scoreStatsContainer.classList.remove('hidden'); } else { // Default: highlighted / Kernstoff (17) currentQuizMode = 'highlighted'; pool = STILMITTEL_DATA.filter(item => item.highlighted); document.getElementById('quizModeName').innerText = '🌟 Kernstoff-Challenge (17 Stilmittel)'; + timerBar.classList.add('hidden'); + examNavBar.classList.add('hidden'); + scoreStatsContainer.classList.remove('hidden'); } if (pool.length === 0) pool = [...STILMITTEL_DATA]; pool.sort(() => Math.random() - 0.5); - const questionCount = Math.min(10, pool.length); + const questionCount = mode === 'exam' ? 15 : Math.min(10, pool.length); quizQuestions = []; for (let i = 0; i < questionCount; i++) { - const correct = pool[i]; + const correct = pool[i % pool.length]; const randomExample = correct.examples[Math.floor(Math.random() * correct.examples.length)]; - const distractors = STILMITTEL_DATA - .filter(x => x.id !== correct.id) - .sort(() => Math.random() - 0.5) - .slice(0, 3); + // Smart distractors: prioritize confusable term if available + let potentialDistractors = STILMITTEL_DATA.filter(x => x.id !== correct.id); + let priorityDistractor = null; + if (correct.confusion && correct.confusion.trap) { + priorityDistractor = potentialDistractors.find(x => correct.confusion.trap.toLowerCase().includes(x.term.toLowerCase())); + } + + let distractors = []; + if (priorityDistractor) { + distractors.push(priorityDistractor); + potentialDistractors = potentialDistractors.filter(x => x.id !== priorityDistractor.id); + } + + potentialDistractors.sort(() => Math.random() - 0.5); + while (distractors.length < 3 && potentialDistractors.length > 0) { + distractors.push(potentialDistractors.pop()); + } const options = [correct, ...distractors].sort(() => Math.random() - 0.5); quizQuestions.push({ correctItem: correct, example: randomExample, - options: options + options: options, + userAnswerId: null, + userAnswerText: '', + isAnswered: false, + isCorrect: false }); } @@ -1806,6 +2218,7 @@ document.getElementById('quizStreak').innerText = '0'; document.getElementById('quizPlayArea').classList.remove('hidden'); document.getElementById('quizResultScreen').classList.add('hidden'); + document.getElementById('examResultScreen').classList.add('hidden'); document.getElementById('quizFeedbackBox').classList.add('hidden'); renderQuizQuestion(); @@ -1813,52 +2226,200 @@ function startWeakSpotsQuiz() { if (quizMistakes.length === 0) { - showToast('Keine Schwachstellen vorhanden! Starte Kernstoff-Quiz.', 'fa-star'); - startQuiz('highlighted'); - return; + const lowCards = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) <= 2); + if (lowCards.length === 0) { + showToast('Keine Schwachstellen vorhanden! Starte Kernstoff-Quiz.', 'fa-star'); + startQuiz('highlighted'); + return; + } } startQuiz('weakspots'); } + function handleExamTimerTick() { + examTimeRemaining--; + updateExamTimerDisplay(); + if (examTimeRemaining <= 0) { + clearInterval(examTimerInterval); + examTimerInterval = null; + showToast('Zeit abgelaufen! Prüfung wird ausgewertet...', 'fa-stopwatch'); + finishExam(); + } + } + + function updateExamTimerDisplay() { + const minutes = Math.floor(Math.max(0, examTimeRemaining) / 60); + const seconds = Math.max(0, examTimeRemaining) % 60; + const displayStr = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + const timerEl = document.getElementById('examTimerDisplay'); + if (timerEl) { + timerEl.innerText = displayStr; + if (examTimeRemaining <= 30) { + timerEl.className = 'font-mono text-2xl font-black text-rose-500 animate-pulse tracking-wider'; + } else if (examTimeRemaining <= 120) { + timerEl.className = 'font-mono text-2xl font-black text-amber-400 tracking-wider'; + } else { + timerEl.className = 'font-mono text-2xl font-black text-emerald-400 tracking-wider'; + } + } + } + + function confirmSubmitExamEarly() { + const answeredCount = quizQuestions.filter(q => q.isAnswered).length; + const confirmMsg = `Du hast ${answeredCount} von ${quizQuestions.length} Fragen beantwortet.\n\nMöchtest du die Prüfung wirklich jetzt abgeben und auswerten lassen?`; + if (confirm(confirmMsg)) { + finishExam(); + } + } + function renderQuizQuestion() { if (currentQuizIndex >= quizQuestions.length) { - finishQuiz(); + if (currentQuizMode === 'exam') { + finishExam(); + } else { + finishQuiz(); + } return; } const q = quizQuestions[currentQuizIndex]; - isQuestionAnswered = false; + isQuestionAnswered = q.isAnswered || false; document.getElementById('quizQuestionNumber').innerText = `Frage ${currentQuizIndex + 1} von ${quizQuestions.length}`; document.getElementById('quizCategoryTag').innerText = q.correctItem.categoryLabel; document.getElementById('quizPhraseBox').innerText = `„${q.example}“`; document.getElementById('quizFeedbackBox').classList.add('hidden'); - const container = document.getElementById('quizOptionsContainer'); - container.innerHTML = ''; + const isExam = currentQuizMode === 'exam'; - q.options.forEach((opt, idx) => { - const btn = document.createElement('button'); - btn.id = `quiz-opt-btn-${idx}`; - btn.className = 'quiz-opt-btn w-full p-4 rounded-2xl bg-white border-2 border-slate-200 hover:border-indigo-500 hover:bg-indigo-50/50 font-bold text-slate-800 text-sm transition shadow-sm text-left flex items-center justify-between group'; - btn.innerHTML = ` -
- ${idx + 1} - ${opt.term} -
- - `; - btn.onclick = () => answerQuiz(opt.id, btn); - container.appendChild(btn); - }); + // Update Exam Nav Bar + const examTracker = document.getElementById('examQuestionTracker'); + if (examTracker) examTracker.innerText = `Frage ${currentQuizIndex + 1} von ${quizQuestions.length}`; + const prevBtn = document.getElementById('examPrevBtn'); + if (prevBtn) prevBtn.disabled = currentQuizIndex === 0; + const nextBtn = document.getElementById('examNextBtn'); + if (nextBtn) { + if (currentQuizIndex === quizQuestions.length - 1) { + nextBtn.innerHTML = 'Prüfung abgeben '; + nextBtn.className = 'px-5 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs sm:text-sm shadow transition flex items-center gap-1.5'; + } else { + nextBtn.innerHTML = 'Weiter '; + nextBtn.className = 'px-5 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs sm:text-sm shadow transition flex items-center gap-1.5'; + } + } + + const optionsContainer = document.getElementById('quizOptionsContainer'); + const freitextContainer = document.getElementById('quizFreitextContainer'); + + if (currentQuizInputType === 'text') { + optionsContainer.classList.add('hidden'); + freitextContainer.classList.remove('hidden'); + const input = document.getElementById('quizFreitextInput'); + input.value = q.userAnswerText || ''; + input.disabled = isExam ? false : isQuestionAnswered; + setTimeout(() => input.focus(), 50); + } else { + freitextContainer.classList.add('hidden'); + optionsContainer.classList.remove('hidden'); + optionsContainer.innerHTML = ''; + + q.options.forEach((opt, idx) => { + const btn = document.createElement('button'); + btn.id = `quiz-opt-btn-${idx}`; + const isSelected = q.userAnswerId === opt.id; + + let btnClass = 'quiz-opt-btn w-full p-4 rounded-2xl border-2 font-bold text-sm transition shadow-sm text-left flex items-center justify-between group '; + if (isExam) { + btnClass += isSelected + ? 'bg-indigo-50 border-indigo-600 text-indigo-950 ring-2 ring-indigo-300' + : 'bg-white border-slate-200 hover:border-indigo-400 text-slate-800'; + } else { + btnClass += 'bg-white border-slate-200 hover:border-indigo-500 hover:bg-indigo-50/50 text-slate-800'; + } + btn.className = btnClass; + + btn.innerHTML = ` +
+ ${idx + 1} + ${opt.term} +
+ + `; + btn.onclick = () => answerQuiz(opt.id, btn); + optionsContainer.appendChild(btn); + }); + } } function answerQuiz(selectedId, buttonEl) { + const q = quizQuestions[currentQuizIndex]; + const isExam = currentQuizMode === 'exam'; + + if (isExam) { + q.userAnswerId = selectedId; + const chosenOpt = q.options.find(o => o.id === selectedId); + q.userAnswerText = chosenOpt ? chosenOpt.term : ''; + q.isAnswered = true; + q.isCorrect = selectedId === q.correctItem.id; + playTone('tap'); + renderQuizQuestion(); + return; + } + if (isQuestionAnswered) return; isQuestionAnswered = true; + q.isAnswered = true; + q.userAnswerId = selectedId; + + const isCorrect = selectedId === q.correctItem.id; + q.isCorrect = isCorrect; + + handleAnswerEvaluation(isCorrect, buttonEl); + } + + function handleFreitextKeydown(e) { + if (e.key === 'Enter') { + e.preventDefault(); + submitFreitextAnswer(); + } + } + + function submitFreitextAnswer() { + const input = document.getElementById('quizFreitextInput'); + const val = input.value.trim(); + if (!val) { + input.focus(); + return; + } const q = quizQuestions[currentQuizIndex]; - const isCorrect = selectedId === q.correctItem.id; + const match = checkFuzzyMatch(val, q.correctItem.term); + const isExam = currentQuizMode === 'exam'; + + q.userAnswerText = val; + q.isAnswered = true; + q.isCorrect = match.isMatch; + + if (isExam) { + playTone('tap'); + showToast('Antwort gespeichert!', 'fa-check'); + if (currentQuizIndex < quizQuestions.length - 1) { + nextExamQuestion(); + } else { + renderQuizQuestion(); + } + return; + } + + if (isQuestionAnswered) return; + isQuestionAnswered = true; + input.disabled = true; + + handleAnswerEvaluation(match.isMatch, null, match); + } + + function handleAnswerEvaluation(isCorrect, buttonEl, fuzzyResult = null) { + const q = quizQuestions[currentQuizIndex]; const feedbackBox = document.getElementById('quizFeedbackBox'); const feedbackIcon = document.getElementById('quizFeedbackIcon'); const feedbackTitle = document.getElementById('quizFeedbackTitle'); @@ -1879,12 +2440,19 @@ quizMistakes = quizMistakes.filter(id => id !== q.correctItem.id); localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes)); - buttonEl.className = 'quiz-opt-btn w-full p-4 rounded-2xl bg-emerald-50 border-2 border-emerald-500 font-bold text-emerald-950 text-sm shadow-md text-left flex items-center justify-between'; - buttonEl.innerHTML += ``; + if (buttonEl) { + buttonEl.className = 'quiz-opt-btn w-full p-4 rounded-2xl bg-emerald-50 border-2 border-emerald-500 font-bold text-emerald-950 text-sm shadow-md text-left flex items-center justify-between'; + buttonEl.innerHTML += ``; + } feedbackBox.className = 'rounded-2xl p-4 sm:p-5 border bg-emerald-50 border-emerald-200 text-emerald-950 block'; feedbackIcon.className = 'fa-solid fa-circle-check text-emerald-600 text-2xl mt-0.5'; - feedbackTitle.innerText = `Exakt richtig! Es ist eine ${q.correctItem.term}.`; + + let title = `Exakt richtig! Es ist eine ${q.correctItem.term}.`; + if (fuzzyResult && !fuzzyResult.exact) { + title = `Richtig gelöst! (Tippfehler bemerkt: Korrekt schreibt man '${q.correctItem.term}')`; + } + feedbackTitle.innerText = title; let correctExtraHtml = `

${q.correctItem.definition} (${q.correctItem.tip || ''})

`; if (q.correctItem.confusion) { @@ -1908,8 +2476,10 @@ localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes)); } - buttonEl.className = 'quiz-opt-btn w-full p-4 rounded-2xl bg-rose-50 border-2 border-rose-500 font-bold text-rose-950 text-sm shadow-md text-left flex items-center justify-between'; - buttonEl.innerHTML += ``; + if (buttonEl) { + buttonEl.className = 'quiz-opt-btn w-full p-4 rounded-2xl bg-rose-50 border-2 border-rose-500 font-bold text-rose-950 text-sm shadow-md text-left flex items-center justify-between'; + buttonEl.innerHTML += ``; + } buttons.forEach((btn, idx) => { if (q.options[idx].id === q.correctItem.id) { @@ -1954,6 +2524,124 @@ renderQuizQuestion(); } + function prevExamQuestion() { + if (currentQuizIndex > 0) { + currentQuizIndex--; + renderQuizQuestion(); + } + } + + function nextExamQuestion() { + if (currentQuizIndex < quizQuestions.length - 1) { + currentQuizIndex++; + renderQuizQuestion(); + } else { + confirmSubmitExamEarly(); + } + } + + function finishExam() { + if (examTimerInterval) { + clearInterval(examTimerInterval); + examTimerInterval = null; + } + + const elapsedSec = Math.round((Date.now() - (examStartTime || Date.now())) / 1000); + const minutes = Math.floor(elapsedSec / 60); + const seconds = elapsedSec % 60; + const timeStr = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')} Min`; + + let correctCount = 0; + quizQuestions.forEach(q => { + let isRight = false; + if (currentQuizInputType === 'text') { + isRight = checkFuzzyMatch(q.userAnswerText || '', q.correctItem.term).isMatch; + } else { + isRight = q.userAnswerId === q.correctItem.id; + } + q.isCorrect = isRight; + if (isRight) correctCount++; + else { + if (!quizMistakes.includes(q.correctItem.id)) { + quizMistakes.push(q.correctItem.id); + } + } + }); + + localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes)); + updateQuizMistakesBadge(); + + const gradeInfo = calculateExamGrade(correctCount, quizQuestions.length); + playTone(gradeInfo.grade <= 3 ? 'win' : 'error'); + + // Populate Exam Screen + document.getElementById('quizPlayArea').classList.add('hidden'); + document.getElementById('examTimerBar').classList.add('hidden'); + document.getElementById('examResultScreen').classList.remove('hidden'); + + const badgeContainer = document.getElementById('examGradeBadgeContainer'); + badgeContainer.className = `inline-flex items-center gap-2 px-6 py-2.5 rounded-2xl border-2 font-black text-xl sm:text-2xl shadow-sm ${gradeInfo.badgeClass}`; + document.getElementById('examGradeTitle').innerText = `Note: ${gradeInfo.name}`; + const iconEl = document.getElementById('examGradeIcon'); + iconEl.className = `fa-solid ${gradeInfo.icon} ${gradeInfo.iconColor}`; + + document.getElementById('examPedagogicFeedback').innerText = gradeInfo.feedback; + document.getElementById('examScoreDisplay').innerText = `${correctCount} / ${quizQuestions.length}`; + document.getElementById('examPercentDisplay').innerText = `${Math.round((correctCount / quizQuestions.length) * 100)}%`; + document.getElementById('examTimeDisplay').innerText = timeStr; + + const repeatMistakesBtn = document.getElementById('examRepeatMistakesBtn'); + if (quizQuestions.some(q => !q.isCorrect)) { + repeatMistakesBtn.classList.remove('hidden'); + } else { + repeatMistakesBtn.classList.add('hidden'); + } + + // Render Itemized Review List + const reviewList = document.getElementById('examReviewList'); + reviewList.innerHTML = ''; + + quizQuestions.forEach((q, idx) => { + const itemEl = document.createElement('div'); + itemEl.className = `p-4 rounded-2xl border ${q.isCorrect ? 'bg-emerald-50/60 border-emerald-200' : 'bg-rose-50/60 border-rose-200'} space-y-2`; + + const userAns = q.userAnswerText || (q.userAnswerId ? (q.options.find(o => o.id === q.userAnswerId)?.term || 'Unbekannt') : ''); + + itemEl.innerHTML = ` +
+
+ Frage ${idx + 1} (${q.correctItem.categoryLabel}) +

„${q.example}“

+
+ + + ${q.isCorrect ? 'Richtig' : 'Falsch'} + +
+ +
+
Deine Antwort: ${userAns}
+
Lösung: ${q.correctItem.term}
+
+ +

${q.correctItem.definition}

+ + ${!q.isCorrect && q.correctItem.confusion ? ` +
+ ⚠️ Verwechslungsgefahr (${q.correctItem.confusion.trap}): ${q.correctItem.confusion.text} +
+ ` : ''} + + ${!q.isCorrect && q.correctItem.analysisTemplate ? ` +
+ 📝 Klausur-Muster: ${q.correctItem.analysisTemplate} +
+ ` : ''} + `; + reviewList.appendChild(itemEl); + }); + } + function finishQuiz() { playTone('win'); document.getElementById('quizPlayArea').classList.add('hidden'); @@ -1972,7 +2660,6 @@ repeatBtn.classList.add('hidden'); } } - // MATCHER GAME function initMatcherGame() { matcherSelectedPhrase = null; diff --git a/www/sw.js b/www/sw.js index c608cfd..2164e11 100644 --- a/www/sw.js +++ b/www/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'rhetorikmaster-v6'; +const CACHE_NAME = 'rhetorikmaster-v7'; const STATIC_ASSETS = [ '/', '/index.html',