diff --git a/www/rhetorik_trainer_quiz.html b/www/rhetorik_trainer_quiz.html index 18155be..afea2cd 100644 --- a/www/rhetorik_trainer_quiz.html +++ b/www/rhetorik_trainer_quiz.html @@ -240,24 +240,42 @@ - + - - Leitner-Karteikarten (5-Kasten-System) - - Wische nach links (wiederholen) oder rechts (gekonnt) oder tippe zum Wenden. + + + + FSRS-4.5 Karteikasten + + Spaced Repetition + + + Modernes 17-Parameter DSR-Modell (Retrievability, Stability, Difficulty) – 20–30 % weniger Wiederholungen bei 90 % Behaltungsquote. + + + + + + FSRS (KI) + + + + 5-Kasten + + + - Begriff zuerst + Begriff - Beispiel zuerst + Beispiel @@ -266,7 +284,7 @@ - + @@ -288,8 +306,8 @@ Gedanken - - + + Kasten: Alle Kästen (1-5) @@ -300,14 +318,27 @@ Kasten 5 (Gemeistert) + + + + Intervall: + + Alle Karten + 📅 Jetzt fällig + 📈 In Wiederholung + 🌱 Noch unberührt + + - + - + Karte 1 von 17 (🌟 Kernstoff) - + + + Kasten 1 @@ -317,6 +348,26 @@ + + + + + + Fällig + + + + R: 90% + + + + S: 3.1 T + + + + D: 5.3 + + @@ -410,13 +461,15 @@ - - + + + Zurück - + + 1 @@ -429,7 +482,50 @@ - + + + + + + 1 + + Nochmal + + < 10 Min + + + + + + 2 + + Schwer + + +1 Tag + + + + + + 3 + + Gut + + +3 Tage + + + + + + 4 + + Einfach + + +15 Tage + + + + Weiter @@ -1883,10 +1979,29 @@ } } - // Leitner 5-Box State + // ========================================== + // FSRS-4.5 (FREE SPACED REPETITION SCHEDULER) ENGINE + // 17-Parameter DSR (Difficulty, Stability, Retrievability) Model + // ========================================== + const FSRS_WEIGHTS = [ + 0.4072, 1.1829, 3.1262, 15.4722, // w0-w3: Initial stability for ratings 1 (Again), 2 (Hard), 3 (Good), 4 (Easy) + 7.2102, 0.5316, // w4, w5: Initial difficulty parameters + 1.0651, 0.0234, // w6, w7: Next difficulty shift & mean reversion to D0(3) + 1.616, 0.1544, 1.0824, // w8, w9, w10: Stability after recall + 1.9813, 0.0953, 0.2975, 0.472, // w11, w12, w13, w14: Stability after forgetting + 0.2655, 1.0 // w15: Hard penalty factor, w16: Easy bonus factor + ]; + const FSRS_FACTOR = 19 / 81; // ~0.2345679 (Power forgetting factor) + const FSRS_DESIRED_RETENTION = 0.90; // 90 % Behaltensquote + + // Algorithm & Card States + let cardAlgo = localStorage.getItem('rhetorik_card_algo') || 'fsrs'; + let fsrsCards = JSON.parse(localStorage.getItem('rhetorik_fsrs_cards') || '{}'); let cardLevels = JSON.parse(localStorage.getItem('rhetorik_leitner_levels') || '{}'); + let activeCardCategory = 'highlighted'; let activeLeitnerBox = 'all'; + let activeFsrsQueue = 'all'; let filteredCards = [...STILMITTEL_DATA]; let currentTabIndex = 'cards'; @@ -1909,6 +2024,87 @@ // Glossary State let currentGlossaryFilter = 'all'; + // ========================================== + // FSRS MATH CALCULATIONS + // ========================================== + function getFsrsCard(id) { + return fsrsCards[id] || { s: 0, d: 0, reps: 0, lapses: 0, lastReview: 0, dueDate: 0, state: 0 }; + } + + function getElapsedDays(lastReview) { + if (!lastReview || lastReview <= 0) return 0; + return Math.max(0, (Date.now() - lastReview) / 86400000); + } + + function calcRetrievability(tDays, s) { + if (!s || s <= 0) return 0; + return Math.pow(1 + FSRS_FACTOR * (tDays / s), -0.5); + } + + function calcInitDifficulty(grade) { + const d = FSRS_WEIGHTS[4] - Math.exp(FSRS_WEIGHTS[5] * (grade - 1)) + 1; + return Math.min(10, Math.max(1, d)); + } + + function calcNextDifficulty(d, grade) { + const d0_3 = calcInitDifficulty(3); + const dPrime = d - FSRS_WEIGHTS[6] * (grade - 3); + const nextD = FSRS_WEIGHTS[7] * d0_3 + (1 - FSRS_WEIGHTS[7]) * dPrime; + return Math.min(10, Math.max(1, nextD)); + } + + function calcNextStability(cardData, grade, tDays) { + const w = FSRS_WEIGHTS; + if (!cardData || !cardData.reps || cardData.reps === 0) { + return Math.max(0.1, w[grade - 1]); + } + const r = Math.min(1, Math.max(0, calcRetrievability(tDays, cardData.s))); + const d = cardData.d || calcInitDifficulty(3); + + if (grade === 1) { + const sForget = w[11] * Math.pow(d, -w[12]) * (Math.pow(cardData.s + 1, w[13]) - 1) * Math.exp((1 - r) * w[14]); + return Math.max(0.1, Math.min(cardData.s, sForget)); + } else { + let h = 1; + if (grade === 2) h = w[15]; + if (grade === 4) h = w[16]; + const sRecall = cardData.s * (1 + Math.exp(w[8]) * (11 - d) * Math.pow(cardData.s, -w[9]) * (Math.exp((1 - r) * w[10]) - 1) * h); + return Math.max(0.1, sRecall); + } + } + + function calcNextInterval(s, grade) { + if (grade === 1) return 0.007; // ca. 10 Minuten + if (grade === 2) return Math.max(1, Math.round(s * 0.85)); + if (grade === 3) return Math.max(1, Math.round(s)); + if (grade === 4) return Math.max(Math.round(s * 1.3), Math.round(s) + 1); + return 1; + } + + function formatIntervalText(days) { + if (days <= 0.02) return '< 10 Min'; + if (days < 1) return 'heute'; + const roundDays = Math.round(days); + if (roundDays === 1) return '1 Tag'; + if (roundDays < 30) return `${roundDays} Tage`; + if (roundDays < 365) { + const months = (days / 30).toFixed(days % 30 === 0 ? 0 : 1); + return `${months} Mon.`; + } + return `${(days / 365).toFixed(1)} J.`; + } + + function isFsrsDue(cardData) { + if (!cardData || !cardData.reps || cardData.reps === 0) return true; + if (cardData.dueDate && cardData.dueDate <= Date.now()) return true; + const t = getElapsedDays(cardData.lastReview); + const r = calcRetrievability(t, cardData.s); + return r < FSRS_DESIRED_RETENTION; + } + + // ========================================== + // LEITNER 5-BOX HELPERS + // ========================================== function getItemLevel(id) { return cardLevels[id] || 1; } @@ -1920,9 +2116,41 @@ } function updateMasteredHeader() { - const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length; - document.getElementById('globalMasteredStat').innerText = `${masteredCount} / ${STILMITTEL_DATA.length} gelernt`; + const isFsrs = cardAlgo === 'fsrs'; + const headerStat = document.getElementById('globalMasteredStat'); + + if (isFsrs) { + const fsrsItems = Object.values(fsrsCards); + const stableCount = fsrsItems.filter(c => c && c.reps > 0 && c.s >= 14).length; + const dueCount = STILMITTEL_DATA.filter(item => isFsrsDue(fsrsCards[item.id])).length; + if (headerStat) { + headerStat.innerText = `${stableCount} / ${STILMITTEL_DATA.length} stabil (${dueCount} fällig)`; + } + } else { + const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length; + if (headerStat) { + headerStat.innerText = `${masteredCount} / ${STILMITTEL_DATA.length} gelernt`; + } + } + updateQuizMistakesBadge(); + updateQueueFilterOptions(); + } + + function updateQueueFilterOptions() { + const qSelect = document.getElementById('fsrsQueueFilter'); + if (!qSelect) return; + const dueCount = STILMITTEL_DATA.filter(item => isFsrsDue(fsrsCards[item.id])).length; + const learningCount = STILMITTEL_DATA.filter(item => fsrsCards[item.id] && fsrsCards[item.id].reps > 0).length; + const newCount = STILMITTEL_DATA.filter(item => !fsrsCards[item.id] || !fsrsCards[item.id].reps).length; + + const optDue = qSelect.querySelector('option[value="due"]'); + const optLearning = qSelect.querySelector('option[value="learning"]'); + const optNew = qSelect.querySelector('option[value="new"]'); + + if (optDue) optDue.innerText = `📅 Jetzt fällig (${dueCount})`; + if (optLearning) optLearning.innerText = `📈 In Wiederholung (${learningCount})`; + if (optNew) optNew.innerText = `🌱 Noch unberührt (${newCount})`; } function updateQuizMistakesBadge() { @@ -1942,17 +2170,91 @@ } } + function resetCardProgressPrompt() { + if (cardAlgo === 'fsrs') { + if (confirm('Möchtest du den FSRS-Lernverlauf (Stabilität, Schwierigkeit & Fälligkeiten) für alle Karten zurücksetzen?')) { + fsrsCards = {}; + localStorage.removeItem('rhetorik_fsrs_cards'); + applyCardFilters(); + updateMasteredHeader(); + showToast('FSRS-Lernverlauf zurückgesetzt!', 'fa-rotate-left'); + } + } else { + resetLeitnerProgress(); + } + } + function resetLeitnerProgress() { - if (confirm('Möchtest du den Lernfortschritt für alle Karteikarten wirklich auf Kasten 1 zurücksetzen?')) { + if (confirm('Möchtest du den Leitner-Lernfortschritt für alle Karteikarten wirklich auf Kasten 1 zurücksetzen?')) { cardLevels = {}; localStorage.removeItem('rhetorik_leitner_levels'); applyCardFilters(); updateMasteredHeader(); - showToast('Lernstand zurückgesetzt!', 'fa-rotate-left'); + showToast('Leitner-Lernstand zurückgesetzt!', 'fa-rotate-left'); } } + function setSpacedRepetitionAlgo(algo) { + cardAlgo = algo; + localStorage.setItem('rhetorik_card_algo', cardAlgo); + + const isFsrs = cardAlgo === 'fsrs'; + const fsrsBtn = document.getElementById('algoFsrsBtn'); + const leitnerBtn = document.getElementById('algoLeitnerBtn'); + const title = document.getElementById('cardSectionTitle'); + const badge = document.getElementById('algoHeaderBadge'); + const subtitle = document.getElementById('cardSectionSubtitle'); + + const fsrsFilterWrap = document.getElementById('fsrsFilterWrap'); + const leitnerFilterWrap = document.getElementById('leitnerFilterWrap'); + const fsrsMetrics = document.getElementById('fsrsMetricsContainer'); + const leitnerMetrics = document.getElementById('leitnerMetricsContainer'); + const fsrsActions = document.getElementById('fsrsActionButtons'); + const leitnerActions = document.getElementById('leitnerActionButtons'); + + if (isFsrs) { + if (fsrsBtn) fsrsBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold flex items-center gap-1.5'; + if (leitnerBtn) leitnerBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1.5'; + if (title) title.innerHTML = 'FSRS-4.5 Karteikasten'; + if (badge) badge.innerText = 'Spaced Repetition'; + if (subtitle) subtitle.innerText = 'Modernes 17-Parameter DSR-Modell (Retrievability, Stability, Difficulty) – 20–30 % weniger Wiederholungen bei 90 % Behaltungsquote.'; + if (fsrsFilterWrap) fsrsFilterWrap.classList.remove('hidden'); + if (leitnerFilterWrap) leitnerFilterWrap.classList.add('hidden'); + if (fsrsMetrics) fsrsMetrics.classList.remove('hidden'); + if (leitnerMetrics) leitnerMetrics.classList.add('hidden'); + if (fsrsActions) fsrsActions.classList.remove('hidden'); + if (leitnerActions) leitnerActions.classList.add('hidden'); + } else { + if (fsrsBtn) fsrsBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1.5'; + if (leitnerBtn) leitnerBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold flex items-center gap-1.5'; + if (title) title.innerHTML = 'Leitner-Karteikarten (5-Kasten)'; + if (badge) badge.innerText = 'Klassisch'; + if (subtitle) subtitle.innerText = 'Klassisches 5-Kasten-Lernsystem: Wische nach links (wiederholen) oder rechts (gekonnt).'; + if (fsrsFilterWrap) fsrsFilterWrap.classList.add('hidden'); + if (leitnerFilterWrap) leitnerFilterWrap.classList.remove('hidden'); + if (fsrsMetrics) fsrsMetrics.classList.add('hidden'); + if (leitnerMetrics) leitnerMetrics.classList.remove('hidden'); + if (fsrsActions) fsrsActions.classList.add('hidden'); + if (leitnerActions) leitnerActions.classList.remove('hidden'); + } + + applyCardFilters(); + updateMasteredHeader(); + } + + function filterByFsrsQueue(queue) { + activeFsrsQueue = queue; + applyCardFilters(); + } + + function filterByLeitnerBox(box) { + activeLeitnerBox = box; + applyCardFilters(); + } + function applyCardFilters() { + const isFsrs = cardAlgo === 'fsrs'; + filteredCards = STILMITTEL_DATA.filter(item => { if (activeCardCategory === 'highlighted') { if (!item.highlighted) return false; @@ -1962,14 +2264,33 @@ return false; } - if (activeLeitnerBox !== 'all') { - const lvl = getItemLevel(item.id); - if (lvl.toString() !== activeLeitnerBox.toString()) return false; + if (isFsrs) { + const cardData = fsrsCards[item.id]; + if (activeFsrsQueue === 'due') { + return isFsrsDue(cardData); + } else if (activeFsrsQueue === 'learning') { + return cardData && cardData.reps > 0; + } else if (activeFsrsQueue === 'new') { + return !cardData || !cardData.reps || cardData.reps === 0; + } + return true; + } else { + if (activeLeitnerBox !== 'all') { + const lvl = getItemLevel(item.id); + if (lvl.toString() !== activeLeitnerBox.toString()) return false; + } + return true; } - return true; }); if (filteredCards.length === 0) { + if (isFsrs && activeFsrsQueue === 'due') { + showToast('Großartig! Alle fälligen Karten wurden wiederholt. Zeige alle.', 'fa-sparkles'); + activeFsrsQueue = 'all'; + const qSelect = document.getElementById('fsrsQueueFilter'); + if (qSelect) qSelect.value = 'all'; + return applyCardFilters(); + } filteredCards = [...STILMITTEL_DATA]; } @@ -1989,17 +2310,12 @@ applyCardFilters(); } - function filterByLeitnerBox(box) { - activeLeitnerBox = box; - applyCardFilters(); - } - function renderCurrentCard() { if (filteredCards.length === 0) return; if (currentCardIndex >= filteredCards.length) currentCardIndex = 0; const item = filteredCards[currentCardIndex]; - const level = getItemLevel(item.id); + const isFsrs = cardAlgo === 'fsrs'; cardIsFlipped = false; const cardEl = document.getElementById('flashcardElement'); @@ -2010,14 +2326,82 @@ document.getElementById('cardCounter').innerText = `Karte ${currentCardIndex + 1} von ${filteredCards.length}`; document.getElementById('cardProgressLabel').innerText = `Karte ${currentCardIndex + 1} von ${filteredCards.length} (${item.categoryLabel})`; - const boxNames = ['', 'Kasten 1 (Neu)', 'Kasten 2 (Aufbau)', 'Kasten 3 (Vertieft)', 'Kasten 4 (Fast sicher)', 'Kasten 5 (Gemeistert)']; - document.getElementById('cardBoxBadge').innerText = boxNames[level] || `Kasten ${level}`; + if (isFsrs) { + const cardData = getFsrsCard(item.id); + const t = getElapsedDays(cardData.lastReview); + const rVal = cardData.reps > 0 ? Math.round(calcRetrievability(t, cardData.s) * 100) : 100; + const sVal = cardData.s ? (cardData.s >= 10 ? cardData.s.toFixed(0) : cardData.s.toFixed(1)) : '0'; + const dVal = cardData.d ? cardData.d.toFixed(1) : '-'; + const due = isFsrsDue(cardData); - const dotsHtml = [1, 2, 3, 4, 5].map(i => { - const bg = i <= level ? 'bg-indigo-600' : 'bg-slate-200'; - return ``; - }).join(''); - document.getElementById('leitnerBoxesVisual').innerHTML = dotsHtml; + // Update DSR Badges + const rBadge = document.getElementById('fsrsRetrievabilityBadge'); + if (rBadge) { + rBadge.innerHTML = `R: ${rVal}%`; + if (rVal >= 90) { + rBadge.className = 'px-2.5 py-0.5 rounded-lg bg-emerald-50 text-emerald-800 border border-emerald-200 font-extrabold flex items-center gap-1'; + } else if (rVal >= 75) { + rBadge.className = 'px-2.5 py-0.5 rounded-lg bg-amber-50 text-amber-800 border border-amber-200 font-extrabold flex items-center gap-1'; + } else { + rBadge.className = 'px-2.5 py-0.5 rounded-lg bg-rose-50 text-rose-800 border border-rose-200 font-extrabold flex items-center gap-1'; + } + } + + const sBadge = document.getElementById('fsrsStabilityBadge'); + if (sBadge) { + sBadge.innerHTML = `S: ${sVal} T`; + } + + const dBadge = document.getElementById('fsrsDifficultyBadge'); + if (dBadge) { + dBadge.innerHTML = `D: ${dVal}`; + } + + const dueBadge = document.getElementById('fsrsDueBadge'); + if (dueBadge) { + if (!cardData.reps || cardData.reps === 0) { + dueBadge.className = 'px-2.5 py-0.5 rounded-lg bg-indigo-50 text-indigo-700 border border-indigo-200 font-extrabold flex items-center gap-1'; + dueBadge.innerHTML = 'Neu'; + } else if (due) { + dueBadge.className = 'px-2.5 py-0.5 rounded-lg bg-rose-50 text-rose-700 border border-rose-200 font-extrabold flex items-center gap-1'; + dueBadge.innerHTML = 'Fällig'; + } else { + dueBadge.className = 'px-2.5 py-0.5 rounded-lg bg-emerald-50 text-emerald-700 border border-emerald-200 font-extrabold flex items-center gap-1'; + dueBadge.innerHTML = 'Bereit'; + } + } + + // Live calculation of next predicted intervals for all 4 ratings + const nextS1 = calcNextStability(cardData, 1, t); + const nextS2 = calcNextStability(cardData, 2, t); + const nextS3 = calcNextStability(cardData, 3, t); + const nextS4 = calcNextStability(cardData, 4, t); + + const i1 = calcNextInterval(nextS1, 1); + const i2 = calcNextInterval(nextS2, 2); + const i3 = calcNextInterval(nextS3, 3); + const i4 = calcNextInterval(nextS4, 4); + + const lbl1 = document.getElementById('fsrsIntervalAgain'); + const lbl2 = document.getElementById('fsrsIntervalHard'); + const lbl3 = document.getElementById('fsrsIntervalGood'); + const lbl4 = document.getElementById('fsrsIntervalEasy'); + + if (lbl1) lbl1.innerText = formatIntervalText(i1); + if (lbl2) lbl2.innerText = `+${formatIntervalText(i2)}`; + if (lbl3) lbl3.innerText = `+${formatIntervalText(i3)}`; + if (lbl4) lbl4.innerText = `+${formatIntervalText(i4)}`; + } else { + const level = getItemLevel(item.id); + const boxNames = ['', 'Kasten 1 (Neu)', 'Kasten 2 (Aufbau)', 'Kasten 3 (Vertieft)', 'Kasten 4 (Fast sicher)', 'Kasten 5 (Gemeistert)']; + document.getElementById('cardBoxBadge').innerText = boxNames[level] || `Kasten ${level}`; + + const dotsHtml = [1, 2, 3, 4, 5].map(i => { + const bg = i <= level ? 'bg-indigo-600' : 'bg-slate-200'; + return ``; + }).join(''); + document.getElementById('leitnerBoxesVisual').innerHTML = dotsHtml; + } document.getElementById('cardBadgeFront').innerText = item.categoryLabel; document.getElementById('cardBackCategory').innerText = item.categoryLabel; @@ -2067,6 +2451,60 @@ playTone('flip'); } + function rateFsrsCard(grade) { + if (filteredCards.length === 0) return; + const item = filteredCards[currentCardIndex]; + const cardData = getFsrsCard(item.id); + const t = getElapsedDays(cardData.lastReview); + + const nextS = calcNextStability(cardData, grade, t); + const nextD = (!cardData.reps || cardData.reps === 0) ? calcInitDifficulty(grade) : calcNextDifficulty(cardData.d, grade); + const intervalDays = calcNextInterval(nextS, grade); + const now = Date.now(); + const nextDueDate = now + Math.round(intervalDays * 86400000); + + const updatedCard = { + s: parseFloat(nextS.toFixed(4)), + d: parseFloat(nextD.toFixed(4)), + reps: (cardData.reps || 0) + 1, + lapses: grade === 1 ? (cardData.lapses || 0) + 1 : (cardData.lapses || 0), + lastReview: now, + dueDate: nextDueDate, + state: grade === 1 ? 3 : 2 + }; + + fsrsCards[item.id] = updatedCard; + localStorage.setItem('rhetorik_fsrs_cards', JSON.stringify(fsrsCards)); + + const cardEl = document.getElementById('flashcardElement'); + if (grade === 1) { + playTone('error'); + addXP(5, 'FSRS: Noch üben'); + cardEl.classList.add('swipe-left'); + showToast(`„${item.term}“: Wiederholung ${formatIntervalText(intervalDays)}`, 'fa-rotate-left'); + } else if (grade === 2) { + playTone('flip'); + addXP(10, 'FSRS: Schwer'); + showToast(`„${item.term}“: Wiederholung in ${formatIntervalText(intervalDays)}`, 'fa-clock'); + } else if (grade === 3) { + playTone('success'); + addXP(15, 'FSRS: Gut'); + cardEl.classList.add('swipe-right'); + showToast(`„${item.term}“: Wiederholung in ${formatIntervalText(intervalDays)}`, 'fa-check'); + } else if (grade === 4) { + playTone('win'); + addXP(20, 'FSRS: Perfekt!'); + cardEl.classList.add('swipe-right'); + showToast(`„${item.term}“: Wiederholung in ${formatIntervalText(intervalDays)}`, 'fa-wand-magic-sparkles'); + } + + updateMasteredHeader(); + + setTimeout(() => { + nextCard(); + }, 250); + } + function markCardMastered(isMastered) { if (filteredCards.length === 0) return; const item = filteredCards[currentCardIndex]; @@ -2125,7 +2563,7 @@ renderCurrentCard(); } - // Touch Swipe Gestures + // Touch Swipe Gestures (supports both FSRS and Leitner) let touchStartX = 0; let touchStartY = 0; const cardTouchContainer = document.getElementById('flashcardContainer'); @@ -2142,10 +2580,18 @@ const diffY = touchEndY - touchStartY; if (Math.abs(diffX) > 50 && Math.abs(diffX) > Math.abs(diffY)) { - if (diffX > 0) { - markCardMastered(true); + if (cardAlgo === 'fsrs') { + if (diffX > 0) { + rateFsrsCard(3); // Gut + } else { + rateFsrsCard(1); // Nochmal + } } else { - markCardMastered(false); + if (diffX > 0) { + markCardMastered(true); + } else { + markCardMastered(false); + } } } else if (Math.abs(diffX) < 15 && Math.abs(diffY) < 15) { flipCard(); @@ -3424,9 +3870,11 @@ function getSyncPayload() { return { - v: 1, + v: 2, timestamp: Date.now(), + algo: cardAlgo, levels: cardLevels, + fsrs: fsrsCards, mistakes: quizMistakes, stats: { maxStreak: quizMaxStreak @@ -3453,8 +3901,18 @@ const payload = getSyncPayload(); const token = encodeSyncPayload(payload); - const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length; - document.getElementById('syncStatMastered').innerText = `${masteredCount} / ${STILMITTEL_DATA.length}`; + const isFsrs = cardAlgo === 'fsrs'; + let masteredDisplay = ''; + if (isFsrs) { + const fsrsItems = Object.values(fsrsCards); + const stableCount = fsrsItems.filter(c => c && c.reps > 0 && c.s >= 14).length; + masteredDisplay = `${stableCount} / ${STILMITTEL_DATA.length} stabil`; + } else { + const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length; + masteredDisplay = `${masteredCount} / ${STILMITTEL_DATA.length} (Kasten 5)`; + } + + document.getElementById('syncStatMastered').innerText = masteredDisplay; document.getElementById('syncStatMistakes').innerText = `${quizMistakes.length}`; document.getElementById('syncStatStreak').innerText = `${quizMaxStreak}`; @@ -3589,6 +4047,14 @@ cardLevels = payload.levels; localStorage.setItem('rhetorik_leitner_levels', JSON.stringify(cardLevels)); } + if (payload.fsrs && typeof payload.fsrs === 'object') { + fsrsCards = payload.fsrs; + localStorage.setItem('rhetorik_fsrs_cards', JSON.stringify(fsrsCards)); + } + if (payload.algo && (payload.algo === 'leitner' || payload.algo === 'fsrs')) { + cardAlgo = payload.algo; + localStorage.setItem('rhetorik_card_algo', cardAlgo); + } if (Array.isArray(payload.mistakes)) { quizMistakes = payload.mistakes; localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes)); @@ -3597,7 +4063,7 @@ quizMaxStreak = Math.max(quizMaxStreak, payload.stats.maxStreak); } - applyCardFilters(); + setSpacedRepetitionAlgo(cardAlgo); updateMasteredHeader(); renderCurrentCard(); showToast('Lernstand erfolgreich synchronisiert!', 'fa-arrows-rotate'); @@ -3608,9 +4074,14 @@ if (hash && hash.startsWith('#sync=')) { const token = hash.replace('#sync=', '').trim(); const payload = decodeSyncPayload(token); - if (payload && payload.levels) { + if (payload && (payload.levels || payload.fsrs)) { pendingIncomingSync = payload; - const masteredCount = STILMITTEL_DATA.filter(item => (payload.levels[item.id] || 1) >= 5).length; + let masteredCount = 0; + if (payload.algo === 'fsrs' && payload.fsrs) { + masteredCount = Object.values(payload.fsrs).filter(c => c && c.reps > 0 && c.s >= 14).length; + } else if (payload.levels) { + masteredCount = STILMITTEL_DATA.filter(item => (payload.levels[item.id] || 1) >= 5).length; + } document.getElementById('incomingSyncMastered').innerText = `${masteredCount} von ${STILMITTEL_DATA.length}`; document.getElementById('incomingSyncMistakes').innerText = `${(payload.mistakes || []).length}`; document.getElementById('incomingSyncStreak').innerText = `${(payload.stats && payload.stats.maxStreak) || 0}`; @@ -3791,8 +4262,13 @@ } function checkAchievements() { - const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length; - const klangMastered = STILMITTEL_DATA.filter(item => item.category === 'klang' && (cardLevels[item.id] || 1) >= 5).length; + function isMasteredCard(item) { + if ((cardLevels[item.id] || 1) >= 5) return true; + const c = fsrsCards[item.id]; + return !!(c && c.reps >= 2 && c.s >= 14); + } + const masteredCount = STILMITTEL_DATA.filter(item => isMasteredCard(item)).length; + const klangMastered = STILMITTEL_DATA.filter(item => item.category === 'klang' && isMasteredCard(item)).length; const totalKlang = STILMITTEL_DATA.filter(item => item.category === 'klang').length; // 1. Erster Schritt @@ -3992,10 +4468,14 @@ nextCard(); } else if (e.code === 'ArrowLeft') { prevCard(); - } else if (e.key === '1') { - markCardMastered(false); - } else if (e.key === '2') { - markCardMastered(true); + } else if (cardAlgo === 'fsrs') { + if (e.key === '1') rateFsrsCard(1); + else if (e.key === '2') rateFsrsCard(2); + else if (e.key === '3') rateFsrsCard(3); + else if (e.key === '4') rateFsrsCard(4); + } else { + if (e.key === '1') markCardMastered(false); + else if (e.key === '2') markCardMastered(true); } } else if (currentTabIndex === 'quiz') { if (['1', '2', '3', '4'].includes(e.key)) { @@ -4075,7 +4555,7 @@ window.onload = function() { updateSoundUI(); - applyCardFilters(); + setSpacedRepetitionAlgo(cardAlgo); updateMasteredHeader(); renderPrintCheatsheet(); updateGamificationHeader(); diff --git a/www/sw.js b/www/sw.js index 7082007..5481a56 100644 --- a/www/sw.js +++ b/www/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'rhetorikmaster-v10'; +const CACHE_NAME = 'rhetorikmaster-v11'; const STATIC_ASSETS = [ '/', '/index.html',
Wische nach links (wiederholen) oder rechts (gekonnt) oder tippe zum Wenden.
+ Modernes 17-Parameter DSR-Modell (Retrievability, Stability, Difficulty) – 20–30 % weniger Wiederholungen bei 90 % Behaltungsquote. +