Implement Point 1: Exam Simulator with grading scale & timer, Weak-Spots trainer, Free-text input with fuzzy matching, and Web Speech audio

This commit is contained in:
harald committed 2026-09-28 15:16:07 +02:00
1 parent 4d59622149
commit c6615c0db9
2 files changed
+754 -67

No files matched your search

+753 -66
View File
@@ -319,6 +319,12 @@
<div id="cardFrontSub" class="text-sm sm:text-base text-slate-500 mt-4 font-medium italic">
Tippe oder drücke [Leertaste] zum Aufdecken
</div>
<div class="mt-3">
<button onclick="speakCurrentCard(event)" title="Laut vorlesen" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-indigo-50 text-slate-600 hover:text-indigo-600 border border-slate-200 text-xs font-bold transition">
<i class="fa-solid fa-volume-high text-indigo-500"></i>
<span>Vorlesen</span>
</button>
</div>
</div>
<div class="text-xs sm:text-sm text-slate-400 flex items-center gap-2 font-semibold">
@@ -348,8 +354,14 @@
</p>
</div>
<div class="bg-white/10 p-3.5 rounded-2xl border border-white/10 backdrop-blur-sm">
<span class="text-xs font-bold text-indigo-300 uppercase tracking-wider block">Typische Phrasen & Zitate</span>
<div class="bg-white/10 p-3.5 rounded-2xl border border-white/10 backdrop-blur-sm relative">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-indigo-300 uppercase tracking-wider block">Typische Phrasen & Zitate</span>
<button onclick="speakCurrentCard(event)" title="Beispiele vorlesen" class="px-2 py-1 rounded-lg bg-white/20 hover:bg-white/30 text-white text-xs font-bold transition flex items-center gap-1">
<i class="fa-solid fa-volume-high text-xs"></i>
<span>Vorlesen</span>
</button>
</div>
<p id="cardBackExamples" class="text-base sm:text-lg font-serif italic text-amber-200 mt-1">
„Fischers Fritz fischt frische Fische.“
</p>
@@ -418,20 +430,37 @@
</div>
</div>
<!-- Quiz Mode Selector -->
<div class="flex flex-wrap items-center gap-2 w-full lg:w-auto justify-between lg:justify-end">
<div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold">
<button id="quizModeKernBtn" onclick="startQuiz('highlighted')" class="px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition">🌟 Kernstoff (17)</button>
<button id="quizModeAllBtn" onclick="startQuiz('all')" class="px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">Alle (23)</button>
<button id="quizModeZusatzBtn" onclick="startQuiz('zusatz')" class="px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">🔍 Zusatz (6)</button>
<!-- Quiz Mode & Type Selectors -->
<div class="flex flex-col sm:flex-row flex-wrap items-start sm:items-center gap-2.5 w-full lg:w-auto justify-between lg:justify-end">
<!-- Modes: Kernstoff | Alle | Zusatz | Schwachstellen | Prüfung -->
<div class="flex flex-wrap items-center gap-1.5 p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold">
<button id="quizModeKernBtn" onclick="startQuiz('highlighted')" class="px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition">🌟 Kernstoff (17)</button>
<button id="quizModeAllBtn" onclick="startQuiz('all')" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">Alle (23)</button>
<button id="quizModeZusatzBtn" onclick="startQuiz('zusatz')" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">🔍 Zusatz (6)</button>
<button id="quizModeWeakBtn" onclick="startWeakSpotsQuiz()" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-rose-700 hover:bg-rose-50 transition font-bold flex items-center gap-1">
<i class="fa-solid fa-triangle-exclamation text-xs"></i>
<span>Schwachstellen (<span id="quizWeakBtnCount">0</span>)</span>
</button>
<button id="quizModeExamBtn" onclick="startQuiz('exam')" class="px-2.5 sm:px-3 py-1.5 rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-sm font-extrabold transition flex items-center gap-1">
<i class="fa-solid fa-stopwatch text-xs"></i>
<span>Prüfungssimulation</span>
</button>
</div>
<!-- Streak, Score & Mistakes counter -->
<div class="flex items-center gap-2 text-xs sm:text-sm font-bold">
<div id="quizMistakesBadge" onclick="startWeakSpotsQuiz()" title="Gezielt Schwachstellen trainieren" class="cursor-pointer hidden flex items-center gap-1.5 text-rose-700 bg-rose-50 px-3 py-1.5 rounded-xl border border-rose-200">
<i class="fa-solid fa-triangle-exclamation"></i>
<span><span id="quizMistakesCount">0</span> Fehler</span>
</div>
<!-- Input Format Toggle: Multiple Choice vs Freitext -->
<div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs font-bold">
<button id="quizInputChoiceBtn" onclick="setQuizInputType('choice')" class="px-2.5 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition flex items-center gap-1">
<i class="fa-solid fa-list-check"></i>
<span>Multiple-Choice</span>
</button>
<button id="quizInputTextBtn" onclick="setQuizInputType('text')" class="px-2.5 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1">
<i class="fa-solid fa-keyboard"></i>
<span>Freitext-Eingabe</span>
</button>
</div>
<!-- Streak & Score counters (hidden during Exam) -->
<div id="quizScoreStatsContainer" class="flex items-center gap-2 text-xs sm:text-sm font-bold">
<div class="flex items-center gap-1.5 text-amber-700 bg-amber-50 px-3 py-1.5 rounded-xl border border-amber-200/70">
<i class="fa-solid fa-fire text-amber-500"></i>
<span>Streak: <span id="quizStreak">0</span></span>
@@ -444,6 +473,30 @@
</div>
</div>
<!-- EXAM TIMER & STATUS BAR (Visible only during Exam Simulation) -->
<div id="examTimerBar" class="hidden bg-gradient-to-r from-slate-900 to-indigo-950 text-white rounded-2xl p-4 sm:p-5 shadow-lg max-w-2xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-3">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-amber-500 text-slate-950 flex items-center justify-center font-black text-xl shadow">
<i class="fa-solid fa-stopwatch"></i>
</div>
<div>
<div class="text-[11px] font-bold text-indigo-200 uppercase tracking-wider">Klausur-Simulation (15 Fragen)</div>
<div class="text-sm font-semibold text-slate-300">Kein Sofort-Feedback • Notenskala 1 bis 6</div>
</div>
</div>
<div class="flex items-center gap-4 w-full sm:w-auto justify-between sm:justify-end">
<div class="text-right">
<div class="text-[10px] uppercase font-bold text-slate-400">Verbleibende Zeit</div>
<div id="examTimerDisplay" class="font-mono text-2xl font-black text-amber-400 tracking-wider">10:00</div>
</div>
<button onclick="confirmSubmitExamEarly()" class="px-3.5 py-2 rounded-xl bg-rose-600 hover:bg-rose-700 text-white font-bold text-xs sm:text-sm shadow transition flex items-center gap-1.5 whitespace-nowrap">
<i class="fa-solid fa-paper-plane"></i>
<span>Abgeben</span>
</button>
</div>
</div>
<!-- Active Question Container -->
<div id="quizPlayArea" class="bg-white rounded-3xl border border-slate-200 shadow-xl p-5 sm:p-8 max-w-2xl mx-auto space-y-5 sm:space-y-6">
<div>
@@ -456,8 +509,14 @@
<h3 id="quizQuestionPrompt" class="text-xl sm:text-2xl font-black text-slate-900 mt-3 leading-snug">
Welches rhetorische Stilmittel liegt in diesem Zitat vor?
</h3>
<div id="quizPhraseBox" class="mt-4 p-5 sm:p-6 bg-slate-50 border-l-4 border-indigo-600 rounded-2xl font-serif text-xl sm:text-2xl text-slate-900 italic shadow-inner">
„Die Kunst ist lang, und kurz ist unser Leben.“
<div class="mt-4 relative group">
<div id="quizPhraseBox" class="p-5 sm:p-6 pr-14 bg-slate-50 border-l-4 border-indigo-600 rounded-2xl font-serif text-xl sm:text-2xl text-slate-900 italic shadow-inner leading-relaxed">
„Die Kunst ist lang, und kurz ist unser Leben.“
</div>
<!-- Audio Read Aloud Button -->
<button onclick="speakCurrentQuizPhrase(event)" title="Beispiel / Zitat laut vorlesen (Web Speech)" class="absolute right-3 top-3 sm:top-4 w-10 h-10 rounded-xl bg-white hover:bg-indigo-50 text-indigo-600 border border-slate-200 hover:border-indigo-300 shadow-sm flex items-center justify-center transition" aria-label="Audio Vorlesen">
<i class="fa-solid fa-volume-high text-base"></i>
</button>
</div>
</div>
@@ -466,6 +525,21 @@
<!-- Options dynamically generated here -->
</div>
<!-- Freitext Input Container (Active when quizInputType === 'text') -->
<div id="quizFreitextContainer" class="hidden space-y-3 pt-2">
<div class="relative">
<input type="text" id="quizFreitextInput" onkeydown="handleFreitextKeydown(event)" placeholder="Stilmittel eintippen (z. B. Alliteration, Chiasmus)..." class="w-full px-5 py-4 pr-32 rounded-2xl border-2 border-slate-300 focus:border-indigo-600 focus:outline-none text-base sm:text-lg font-bold text-slate-900 placeholder:text-slate-400 shadow-inner">
<button onclick="submitFreitextAnswer()" class="absolute right-2.5 top-2.5 bottom-2.5 px-6 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-extrabold text-sm sm:text-base shadow transition flex items-center gap-1.5">
<span>Prüfen</span>
<i class="fa-solid fa-arrow-right text-xs"></i>
</button>
</div>
<p class="text-xs text-slate-500 font-medium flex items-center gap-1">
<i class="fa-solid fa-wand-magic-sparkles text-indigo-500"></i>
<span>Aktives Abrufen trainiert das Langzeitgedächtnis. Tippfehler werden automatisch toleriert!</span>
</p>
</div>
<!-- Inline Explanation Feedback -->
<div id="quizFeedbackBox" class="hidden rounded-2xl p-4 sm:p-6 border transition-all">
<div class="flex items-start gap-3.5">
@@ -483,6 +557,83 @@
</button>
</div>
</div>
<!-- EXAM NAVIGATION BAR (Only visible during Exam Simulation) -->
<div id="examNavBar" class="hidden pt-3 border-t border-slate-200 flex items-center justify-between gap-3">
<button id="examPrevBtn" onclick="prevExamQuestion()" class="px-4 sm:px-5 py-2.5 rounded-xl border border-slate-300 hover:bg-slate-50 text-slate-700 font-bold text-xs sm:text-sm transition flex items-center gap-1.5">
<i class="fa-solid fa-chevron-left"></i>
<span>Zurück</span>
</button>
<div id="examQuestionTracker" class="text-xs sm:text-sm font-bold text-slate-500">
Frage 1 von 15
</div>
<button id="examNextBtn" onclick="nextExamQuestion()" class="px-5 sm:px-6 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">
<span>Weiter</span>
<i class="fa-solid fa-chevron-right"></i>
</button>
</div>
</div>
<!-- EXAM RESULT SCREEN (Klausur-Zeugnis mit Notenschlüssel) -->
<div id="examResultScreen" class="hidden bg-white rounded-3xl border border-slate-200 shadow-xl p-6 sm:p-8 max-w-2xl mx-auto space-y-6">
<div class="text-center space-y-2">
<div id="examGradeBadgeContainer" class="inline-flex items-center gap-2 px-5 py-2 rounded-2xl border-2 font-black text-lg sm:text-2xl shadow-sm">
<i id="examGradeIcon" class="fa-solid fa-crown text-amber-500"></i>
<span id="examGradeTitle">Note: Sehr gut (1)</span>
</div>
<h3 class="text-2xl sm:text-3xl font-black text-slate-900 mt-2">Prüfungsauswertung</h3>
<p id="examPedagogicFeedback" class="text-sm sm:text-base text-slate-600 font-medium max-w-lg mx-auto">
Hervorragende Leistung! Du beherrschst die rhetorischen Stilmittel souverän.
</p>
</div>
<!-- Exam Performance Stats -->
<div class="bg-slate-50 p-4 sm:p-5 rounded-2xl border border-slate-200 grid grid-cols-3 gap-2.5 text-center">
<div>
<div class="text-[11px] font-bold text-slate-400 uppercase">Richtig gelöst</div>
<div id="examScoreDisplay" class="text-xl sm:text-3xl font-black text-indigo-600 mt-1">14 / 15</div>
</div>
<div>
<div class="text-[11px] font-bold text-slate-400 uppercase">Quote</div>
<div id="examPercentDisplay" class="text-xl sm:text-3xl font-black text-emerald-600 mt-1">93%</div>
</div>
<div>
<div class="text-[11px] font-bold text-slate-400 uppercase">Zeit</div>
<div id="examTimeDisplay" class="text-xl sm:text-3xl font-black text-amber-600 mt-1">04:32 Min</div>
</div>
</div>
<!-- Itemized Exam Review (Alle 15 Fragen zur Nachbesprechung) -->
<div class="space-y-3 text-left">
<div class="flex items-center justify-between border-b border-slate-200 pb-2">
<h4 class="text-xs sm:text-sm font-extrabold text-slate-700 uppercase tracking-wider flex items-center gap-2">
<i class="fa-solid fa-list-check text-indigo-600"></i>
<span>Detaillierte Fragen-Nachbesprechung (15 Aufgaben)</span>
</h4>
<span class="text-xs text-slate-400">Klausur-Korrektur</span>
</div>
<div id="examReviewList" class="space-y-3 max-h-96 overflow-y-auto pr-1 text-xs sm:text-sm">
<!-- Populated dynamically -->
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-3 justify-center pt-2">
<button onclick="startQuiz('exam')" class="px-6 py-3.5 rounded-2xl bg-indigo-600 hover:bg-indigo-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-indigo-600/30 transition flex items-center justify-center gap-2">
<i class="fa-solid fa-rotate-right"></i>
<span>Prüfung wiederholen</span>
</button>
<button id="examRepeatMistakesBtn" onclick="startWeakSpotsQuiz()" class="hidden px-6 py-3.5 rounded-2xl bg-rose-600 hover:bg-rose-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-rose-600/20 transition flex items-center justify-center gap-2">
<i class="fa-solid fa-triangle-exclamation"></i>
<span>Fehler trainieren</span>
</button>
<button onclick="startQuiz('highlighted')" class="px-6 py-3.5 rounded-2xl bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold text-sm sm:text-base transition">
Übungs-Quiz
</button>
</div>
</div>
<!-- Quiz Results Screen -->
@@ -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 = `
<div class="flex items-center gap-3">
<span class="w-7 h-7 rounded-xl bg-slate-100 group-hover:bg-indigo-600 group-hover:text-white flex items-center justify-center text-xs font-mono text-slate-600 transition">${idx + 1}</span>
<span>${opt.term}</span>
</div>
<i class="fa-solid fa-chevron-right text-slate-300 group-hover:text-indigo-600 text-xs transition"></i>
`;
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 = '<span>Prüfung abgeben</span> <i class="fa-solid fa-paper-plane text-xs"></i>';
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 = '<span>Weiter</span> <i class="fa-solid fa-chevron-right text-xs"></i>';
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 = `
<div class="flex items-center gap-3">
<span class="w-7 h-7 rounded-xl ${isSelected ? 'bg-indigo-600 text-white' : 'bg-slate-100 group-hover:bg-indigo-600 group-hover:text-white'} flex items-center justify-center text-xs font-mono text-slate-600 transition">${idx + 1}</span>
<span>${opt.term}</span>
</div>
<i class="fa-solid ${isSelected ? 'fa-check text-indigo-600' : 'fa-chevron-right text-slate-300 group-hover:text-indigo-600'} text-xs transition"></i>
`;
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 += `<i class="fa-solid fa-check text-emerald-600 text-lg"></i>`;
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 += `<i class="fa-solid fa-check text-emerald-600 text-lg"></i>`;
}
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 = `<p>${q.correctItem.definition} (${q.correctItem.tip || ''})</p>`;
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 += `<i class="fa-solid fa-xmark text-rose-600 text-lg"></i>`;
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 += `<i class="fa-solid fa-xmark text-rose-600 text-lg"></i>`;
}
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') : '<Keine Antwort gegeben>');
itemEl.innerHTML = `
<div class="flex items-start justify-between gap-2">
<div>
<span class="font-extrabold text-xs uppercase tracking-wider ${q.isCorrect ? 'text-emerald-700' : 'text-rose-700'}">Frage ${idx + 1} (${q.correctItem.categoryLabel})</span>
<p class="font-serif italic text-sm sm:text-base text-slate-900 mt-0.5">„${q.example}“</p>
</div>
<span class="px-2.5 py-1 rounded-xl text-xs font-bold ${q.isCorrect ? 'bg-emerald-200 text-emerald-950' : 'bg-rose-200 text-rose-950'} flex items-center gap-1">
<i class="fa-solid ${q.isCorrect ? 'fa-check' : 'fa-xmark'}"></i>
<span>${q.isCorrect ? 'Richtig' : 'Falsch'}</span>
</span>
</div>
<div class="pt-1 text-xs border-t border-slate-200/60 flex flex-wrap items-center gap-x-4 gap-y-1">
<div><span class="text-slate-500 font-bold">Deine Antwort:</span> <span class="${q.isCorrect ? 'text-emerald-800' : 'text-rose-700'} font-bold">${userAns}</span></div>
<div><span class="text-slate-500 font-bold">Lösung:</span> <span class="text-slate-900 font-extrabold">${q.correctItem.term}</span></div>
</div>
<p class="text-xs text-slate-600 leading-snug">${q.correctItem.definition}</p>
${!q.isCorrect && q.correctItem.confusion ? `
<div class="p-2 bg-amber-50 rounded-xl border border-amber-200 text-[11px] text-amber-950">
<strong>⚠️ Verwechslungsgefahr (${q.correctItem.confusion.trap}):</strong> ${q.correctItem.confusion.text}
</div>
` : ''}
${!q.isCorrect && q.correctItem.analysisTemplate ? `
<div class="p-2 bg-indigo-50/80 rounded-xl border border-indigo-100 text-[11px] text-indigo-950 font-serif italic">
<strong>📝 Klausur-Muster:</strong> ${q.correctItem.analysisTemplate}
</div>
` : ''}
`;
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;
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'rhetorikmaster-v6';
const CACHE_NAME = 'rhetorikmaster-v7';
const STATIC_ASSETS = [
'/',
'/index.html',