Implement FSRS-4.5 17-parameter DSR spaced repetition scheduler with algorithm toggle and live metrics

This commit is contained in:
harald committed 2026-09-28 17:19:24 +02:00
1 parent eb0c464112
commit 53ecd61666
2 files changed
+537 -57

No files matched your search

+536 -56
View File
@@ -240,24 +240,42 @@
<!-- VIEW 1: FLASHCARDS (Leitner 5-Box-System) --> <!-- VIEW 1: FLASHCARDS (Leitner 5-Box-System) -->
<!-- ========================================== --> <!-- ========================================== -->
<section id="view-cards" class="space-y-4 sm:space-y-5"> <section id="view-cards" class="space-y-4 sm:space-y-5">
<!-- Filter Bar: Categories & Leitner Boxes --> <!-- Filter Bar: Categories & Algorithm Selectors -->
<div class="bg-white p-4 sm:p-5 rounded-2xl border border-slate-200/80 shadow-sm space-y-3.5"> <div class="bg-white p-4 sm:p-5 rounded-2xl border border-slate-200/80 shadow-sm space-y-3.5">
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-3 sm:gap-4"> <div class="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-3 sm:gap-4">
<div> <div>
<h2 class="text-base sm:text-xl font-extrabold text-slate-900 flex items-center gap-2"> <div class="flex items-center gap-2.5 flex-wrap">
<span>Leitner-Karteikarten (5-Kasten-System)</span> <h2 id="cardSectionTitle" class="text-base sm:text-xl font-extrabold text-slate-900 flex items-center gap-2">
</h2> <i class="fa-solid fa-brain text-indigo-600"></i>
<p class="text-xs sm:text-sm text-slate-500 mt-0.5">Wische nach links (wiederholen) oder rechts (gekonnt) oder tippe zum Wenden.</p> <span>FSRS-4.5 Karteikasten</span>
</h2>
<span id="algoHeaderBadge" class="px-2.5 py-0.5 rounded-full bg-indigo-50 border border-indigo-200 text-indigo-700 font-extrabold text-[11px] uppercase tracking-wider">Spaced Repetition</span>
</div>
<p id="cardSectionSubtitle" class="text-xs sm:text-sm text-slate-500 mt-1">
Modernes 17-Parameter DSR-Modell (Retrievability, Stability, Difficulty) – 20–30 % weniger Wiederholungen bei 90 % Behaltungsquote.
</p>
</div> </div>
<div class="flex flex-wrap items-center gap-2 w-full lg:w-auto justify-between lg:justify-end"> <div class="flex flex-wrap items-center gap-2 w-full lg:w-auto justify-between lg:justify-end">
<!-- Algorithm Toggle -->
<div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold shadow-inner">
<button id="algoFsrsBtn" onclick="setSpacedRepetitionAlgo('fsrs')" class="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">
<i class="fa-solid fa-bolt text-indigo-500"></i>
<span>FSRS (KI)</span>
</button>
<button id="algoLeitnerBtn" onclick="setSpacedRepetitionAlgo('leitner')" class="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">
<i class="fa-solid fa-boxes-stacked"></i>
<span>5-Kasten</span>
</button>
</div>
<!-- Card Face Toggle --> <!-- Card Face Toggle -->
<div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold"> <div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold">
<button id="cardModeTermBtn" onclick="setCardFaceMode('term')" class="px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold"> <button id="cardModeTermBtn" onclick="setCardFaceMode('term')" class="px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold">
Begriff zuerst Begriff
</button> </button>
<button id="cardModeExampleBtn" onclick="setCardFaceMode('example')" class="px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold"> <button id="cardModeExampleBtn" onclick="setCardFaceMode('example')" class="px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">
Beispiel zuerst Beispiel
</button> </button>
</div> </div>
@@ -266,7 +284,7 @@
<button onclick="shuffleCards()" title="Karten mischen" class="p-2 sm:px-3 sm:py-2 rounded-xl border border-slate-200 hover:bg-slate-100 text-slate-600 text-xs sm:text-sm font-bold transition flex items-center gap-1"> <button onclick="shuffleCards()" title="Karten mischen" class="p-2 sm:px-3 sm:py-2 rounded-xl border border-slate-200 hover:bg-slate-100 text-slate-600 text-xs sm:text-sm font-bold transition flex items-center gap-1">
<i class="fa-solid fa-shuffle"></i> <i class="fa-solid fa-shuffle"></i>
</button> </button>
<button onclick="resetLeitnerProgress()" title="Lernstand für Karten zurücksetzen" class="p-2 sm:px-3 sm:py-2 rounded-xl border border-slate-200 hover:bg-rose-50 hover:text-rose-600 text-slate-500 text-xs sm:text-sm font-bold transition flex items-center gap-1"> <button onclick="resetCardProgressPrompt()" title="Lernstand für Karten zurücksetzen" class="p-2 sm:px-3 sm:py-2 rounded-xl border border-slate-200 hover:bg-rose-50 hover:text-rose-600 text-slate-500 text-xs sm:text-sm font-bold transition flex items-center gap-1">
<i class="fa-solid fa-rotate-left"></i> <i class="fa-solid fa-rotate-left"></i>
</button> </button>
</div> </div>
@@ -288,8 +306,8 @@
<button onclick="setCardCategoryFilter('gedanke')" id="cardCatBtn-gedanke" class="card-cat-pill px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200">Gedanken</button> <button onclick="setCardCategoryFilter('gedanke')" id="cardCatBtn-gedanke" class="card-cat-pill px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200">Gedanken</button>
</div> </div>
<!-- Leitner Box Filter --> <!-- Leitner Box Filter (Active in Leitner Mode) -->
<div class="flex items-center gap-1.5"> <div id="leitnerFilterWrap" class="flex items-center gap-1.5 hidden">
<span class="font-bold text-slate-400 uppercase text-[11px] mr-1">Kasten:</span> <span class="font-bold text-slate-400 uppercase text-[11px] mr-1">Kasten:</span>
<select id="leitnerBoxFilter" onchange="filterByLeitnerBox(this.value)" class="bg-slate-100 border border-slate-200 text-slate-700 text-xs sm:text-sm rounded-xl px-3 py-1.5 font-bold focus:outline-none focus:ring-1 focus:ring-indigo-500"> <select id="leitnerBoxFilter" onchange="filterByLeitnerBox(this.value)" class="bg-slate-100 border border-slate-200 text-slate-700 text-xs sm:text-sm rounded-xl px-3 py-1.5 font-bold focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="all">Alle Kästen (1-5)</option> <option value="all">Alle Kästen (1-5)</option>
@@ -300,14 +318,27 @@
<option value="5">Kasten 5 (Gemeistert)</option> <option value="5">Kasten 5 (Gemeistert)</option>
</select> </select>
</div> </div>
<!-- FSRS Queue Filter (Active in FSRS Mode) -->
<div id="fsrsFilterWrap" class="flex items-center gap-1.5">
<span class="font-bold text-slate-400 uppercase text-[11px] mr-1">Intervall:</span>
<select id="fsrsQueueFilter" onchange="filterByFsrsQueue(this.value)" class="bg-indigo-50/70 border border-indigo-200 text-indigo-900 text-xs sm:text-sm rounded-xl px-3 py-1.5 font-extrabold focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="all">Alle Karten</option>
<option value="due" selected>📅 Jetzt fällig</option>
<option value="learning">📈 In Wiederholung</option>
<option value="new">🌱 Noch unberührt</option>
</select>
</div>
</div> </div>
</div> </div>
<!-- Progress bar & Leitner Box Indicators --> <!-- Progress bar & Algorithm Indicators -->
<div class="space-y-1.5"> <div class="space-y-1.5">
<div class="flex justify-between items-center text-xs sm:text-sm text-slate-600 font-bold px-1"> <div class="flex flex-wrap justify-between items-center text-xs sm:text-sm text-slate-600 font-bold px-1 gap-2">
<span id="cardProgressLabel">Karte 1 von 17 (🌟 Kernstoff)</span> <span id="cardProgressLabel">Karte 1 von 17 (🌟 Kernstoff)</span>
<div class="flex items-center gap-2">
<!-- Leitner Box Visual Indicators -->
<div id="leitnerMetricsContainer" class="flex items-center gap-2 hidden">
<span id="cardBoxBadge" class="px-2.5 py-0.5 rounded-lg bg-amber-50 text-amber-800 border border-amber-200 font-bold text-xs">Kasten 1</span> <span id="cardBoxBadge" class="px-2.5 py-0.5 rounded-lg bg-amber-50 text-amber-800 border border-amber-200 font-bold text-xs">Kasten 1</span>
<div id="leitnerBoxesVisual" class="flex gap-1"> <div id="leitnerBoxesVisual" class="flex gap-1">
<span class="w-2.5 h-2.5 rounded-full bg-indigo-600"></span> <span class="w-2.5 h-2.5 rounded-full bg-indigo-600"></span>
@@ -317,6 +348,26 @@
<span class="w-2.5 h-2.5 rounded-full bg-slate-200"></span> <span class="w-2.5 h-2.5 rounded-full bg-slate-200"></span>
</div> </div>
</div> </div>
<!-- FSRS DSR Live Metrics -->
<div id="fsrsMetricsContainer" class="flex flex-wrap items-center gap-1.5 text-[11px] sm:text-xs">
<span id="fsrsDueBadge" class="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">
<i class="fa-solid fa-clock"></i>
<span>Fällig</span>
</span>
<span id="fsrsRetrievabilityBadge" title="Retrievability: Geschätzte Abrufwahrscheinlichkeit" class="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">
<i class="fa-solid fa-gauge-high text-emerald-600"></i>
<span>R: 90%</span>
</span>
<span id="fsrsStabilityBadge" title="Stability: Gedächtnisstabilität in Tagen (Intervall bis R=90%)" class="px-2.5 py-0.5 rounded-lg bg-blue-50 text-blue-800 border border-blue-200 font-bold flex items-center gap-1">
<i class="fa-solid fa-calendar-day text-blue-500"></i>
<span>S: 3.1 T</span>
</span>
<span id="fsrsDifficultyBadge" title="Difficulty: Berechnete Kartenschwierigkeit von 1 bis 10" class="px-2.5 py-0.5 rounded-lg bg-amber-50 text-amber-800 border border-amber-200 font-bold flex items-center gap-1">
<i class="fa-solid fa-weight-scale text-amber-500"></i>
<span>D: 5.3</span>
</span>
</div>
</div> </div>
<div class="w-full bg-slate-200 h-2.5 rounded-full overflow-hidden"> <div class="w-full bg-slate-200 h-2.5 rounded-full overflow-hidden">
<div id="cardProgressBar" class="bg-indigo-600 h-full transition-all duration-300 w-0"></div> <div id="cardProgressBar" class="bg-indigo-600 h-full transition-all duration-300 w-0"></div>
@@ -410,13 +461,15 @@
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex items-center justify-between gap-2.5 sm:gap-4 mt-6"> <!-- Action Buttons -->
<button onclick="prevCard()" class="px-4 py-3 sm:py-3.5 rounded-2xl bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 font-bold text-sm sm:text-base shadow-sm transition flex items-center gap-1.5"> <div class="flex items-center justify-between gap-2 sm:gap-4 mt-6">
<button onclick="prevCard()" class="px-3.5 sm:px-4 py-3 sm:py-3.5 rounded-2xl bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 font-bold text-sm sm:text-base shadow-sm transition flex items-center gap-1.5 flex-shrink-0">
<i class="fa-solid fa-chevron-left"></i> <i class="fa-solid fa-chevron-left"></i>
<span class="hidden sm:inline">Zurück</span> <span class="hidden sm:inline">Zurück</span>
</button> </button>
<div class="flex items-center gap-2.5 sm:gap-3"> <!-- LEITNER ACTION BUTTONS (2 Options) -->
<div id="leitnerActionButtons" class="flex items-center gap-2.5 sm:gap-3 hidden">
<button onclick="markCardMastered(false)" class="px-4 sm:px-6 py-3 sm:py-3.5 rounded-2xl bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 font-extrabold text-sm sm:text-base transition flex items-center gap-2 shadow-sm"> <button onclick="markCardMastered(false)" class="px-4 sm:px-6 py-3 sm:py-3.5 rounded-2xl bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 font-extrabold text-sm sm:text-base transition flex items-center gap-2 shadow-sm">
<span class="hidden md:inline-block px-1.5 py-0.5 bg-rose-200/80 rounded font-mono text-xs">1</span> <span class="hidden md:inline-block px-1.5 py-0.5 bg-rose-200/80 rounded font-mono text-xs">1</span>
<i class="fa-solid fa-rotate-left"></i> <i class="fa-solid fa-rotate-left"></i>
@@ -429,7 +482,50 @@
</button> </button>
</div> </div>
<button onclick="nextCard()" class="px-4 py-3 sm:py-3.5 rounded-2xl bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 font-bold text-sm sm:text-base shadow-sm transition flex items-center gap-1.5"> <!-- FSRS ACTION BUTTONS (4 Ratings with Predicted Next Interval Badges) -->
<div id="fsrsActionButtons" class="flex flex-wrap items-center justify-center gap-1.5 sm:gap-2.5">
<!-- 1: Nochmal / Again -->
<button onclick="rateFsrsCard(1)" class="px-2.5 sm:px-4 py-2 sm:py-2.5 rounded-2xl bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 font-extrabold text-xs sm:text-sm transition flex flex-col items-center shadow-sm hover:scale-[1.02] active:scale-95">
<div class="flex items-center gap-1">
<span class="hidden md:inline-block px-1 py-0.2 bg-rose-200/80 rounded font-mono text-[10px]">1</span>
<i class="fa-solid fa-rotate-left"></i>
<span>Nochmal</span>
</div>
<span id="fsrsIntervalAgain" class="text-[10px] text-rose-600 font-semibold mt-0.5">&lt; 10 Min</span>
</button>
<!-- 2: Schwer / Hard -->
<button onclick="rateFsrsCard(2)" class="px-2.5 sm:px-4 py-2 sm:py-2.5 rounded-2xl bg-amber-50 hover:bg-amber-100 text-amber-800 border border-amber-200 font-extrabold text-xs sm:text-sm transition flex flex-col items-center shadow-sm hover:scale-[1.02] active:scale-95">
<div class="flex items-center gap-1">
<span class="hidden md:inline-block px-1 py-0.2 bg-amber-200/80 rounded font-mono text-[10px]">2</span>
<i class="fa-solid fa-triangle-exclamation"></i>
<span>Schwer</span>
</div>
<span id="fsrsIntervalHard" class="text-[10px] text-amber-700 font-semibold mt-0.5">+1 Tag</span>
</button>
<!-- 3: Gut / Good -->
<button onclick="rateFsrsCard(3)" class="px-2.5 sm:px-4 py-2 sm:py-2.5 rounded-2xl bg-emerald-50 hover:bg-emerald-100 text-emerald-800 border border-emerald-300 font-extrabold text-xs sm:text-sm transition flex flex-col items-center shadow-sm hover:scale-[1.02] active:scale-95">
<div class="flex items-center gap-1">
<span class="hidden md:inline-block px-1 py-0.2 bg-emerald-200/80 rounded font-mono text-[10px]">3</span>
<i class="fa-solid fa-check"></i>
<span>Gut</span>
</div>
<span id="fsrsIntervalGood" class="text-[10px] text-emerald-700 font-semibold mt-0.5">+3 Tage</span>
</button>
<!-- 4: Einfach / Easy -->
<button onclick="rateFsrsCard(4)" class="px-2.5 sm:px-4 py-2 sm:py-2.5 rounded-2xl bg-indigo-50 hover:bg-indigo-100 text-indigo-700 border border-indigo-200 font-extrabold text-xs sm:text-sm transition flex flex-col items-center shadow-sm hover:scale-[1.02] active:scale-95">
<div class="flex items-center gap-1">
<span class="hidden md:inline-block px-1 py-0.2 bg-indigo-200/80 rounded font-mono text-[10px]">4</span>
<i class="fa-solid fa-wand-magic-sparkles"></i>
<span>Einfach</span>
</div>
<span id="fsrsIntervalEasy" class="text-[10px] text-indigo-600 font-semibold mt-0.5">+15 Tage</span>
</button>
</div>
<button onclick="nextCard()" class="px-3.5 sm:px-4 py-3 sm:py-3.5 rounded-2xl bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 font-bold text-sm sm:text-base shadow-sm transition flex items-center gap-1.5 flex-shrink-0">
<span class="hidden sm:inline">Weiter</span> <span class="hidden sm:inline">Weiter</span>
<i class="fa-solid fa-chevron-right"></i> <i class="fa-solid fa-chevron-right"></i>
</button> </button>
@@ -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 cardLevels = JSON.parse(localStorage.getItem('rhetorik_leitner_levels') || '{}');
let activeCardCategory = 'highlighted'; let activeCardCategory = 'highlighted';
let activeLeitnerBox = 'all'; let activeLeitnerBox = 'all';
let activeFsrsQueue = 'all';
let filteredCards = [...STILMITTEL_DATA]; let filteredCards = [...STILMITTEL_DATA];
let currentTabIndex = 'cards'; let currentTabIndex = 'cards';
@@ -1909,6 +2024,87 @@
// Glossary State // Glossary State
let currentGlossaryFilter = 'all'; 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) { function getItemLevel(id) {
return cardLevels[id] || 1; return cardLevels[id] || 1;
} }
@@ -1920,9 +2116,41 @@
} }
function updateMasteredHeader() { function updateMasteredHeader() {
const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length; const isFsrs = cardAlgo === 'fsrs';
document.getElementById('globalMasteredStat').innerText = `${masteredCount} / ${STILMITTEL_DATA.length} gelernt`; 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(); 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() { 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() { 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 = {}; cardLevels = {};
localStorage.removeItem('rhetorik_leitner_levels'); localStorage.removeItem('rhetorik_leitner_levels');
applyCardFilters(); applyCardFilters();
updateMasteredHeader(); 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 = '<i class="fa-solid fa-brain text-indigo-600"></i><span>FSRS-4.5 Karteikasten</span>';
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 = '<i class="fa-solid fa-boxes-stacked text-indigo-600"></i><span>Leitner-Karteikarten (5-Kasten)</span>';
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() { function applyCardFilters() {
const isFsrs = cardAlgo === 'fsrs';
filteredCards = STILMITTEL_DATA.filter(item => { filteredCards = STILMITTEL_DATA.filter(item => {
if (activeCardCategory === 'highlighted') { if (activeCardCategory === 'highlighted') {
if (!item.highlighted) return false; if (!item.highlighted) return false;
@@ -1962,14 +2264,33 @@
return false; return false;
} }
if (activeLeitnerBox !== 'all') { if (isFsrs) {
const lvl = getItemLevel(item.id); const cardData = fsrsCards[item.id];
if (lvl.toString() !== activeLeitnerBox.toString()) return false; 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 (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]; filteredCards = [...STILMITTEL_DATA];
} }
@@ -1989,17 +2310,12 @@
applyCardFilters(); applyCardFilters();
} }
function filterByLeitnerBox(box) {
activeLeitnerBox = box;
applyCardFilters();
}
function renderCurrentCard() { function renderCurrentCard() {
if (filteredCards.length === 0) return; if (filteredCards.length === 0) return;
if (currentCardIndex >= filteredCards.length) currentCardIndex = 0; if (currentCardIndex >= filteredCards.length) currentCardIndex = 0;
const item = filteredCards[currentCardIndex]; const item = filteredCards[currentCardIndex];
const level = getItemLevel(item.id); const isFsrs = cardAlgo === 'fsrs';
cardIsFlipped = false; cardIsFlipped = false;
const cardEl = document.getElementById('flashcardElement'); const cardEl = document.getElementById('flashcardElement');
@@ -2010,14 +2326,82 @@
document.getElementById('cardCounter').innerText = `Karte ${currentCardIndex + 1} von ${filteredCards.length}`; document.getElementById('cardCounter').innerText = `Karte ${currentCardIndex + 1} von ${filteredCards.length}`;
document.getElementById('cardProgressLabel').innerText = `Karte ${currentCardIndex + 1} von ${filteredCards.length} (${item.categoryLabel})`; 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)']; if (isFsrs) {
document.getElementById('cardBoxBadge').innerText = boxNames[level] || `Kasten ${level}`; 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 => { // Update DSR Badges
const bg = i <= level ? 'bg-indigo-600' : 'bg-slate-200'; const rBadge = document.getElementById('fsrsRetrievabilityBadge');
return `<span class="w-2.5 h-2.5 rounded-full ${bg} transition-colors"></span>`; if (rBadge) {
}).join(''); rBadge.innerHTML = `<i class="fa-solid fa-gauge-high text-emerald-600"></i><span>R: ${rVal}%</span>`;
document.getElementById('leitnerBoxesVisual').innerHTML = dotsHtml; 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 = `<i class="fa-solid fa-calendar-day text-blue-500"></i><span>S: ${sVal} T</span>`;
}
const dBadge = document.getElementById('fsrsDifficultyBadge');
if (dBadge) {
dBadge.innerHTML = `<i class="fa-solid fa-weight-scale text-amber-500"></i><span>D: ${dVal}</span>`;
}
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 = '<i class="fa-solid fa-seedling text-indigo-500"></i><span>Neu</span>';
} 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 = '<i class="fa-solid fa-clock text-rose-500"></i><span>Fällig</span>';
} 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 = '<i class="fa-solid fa-check text-emerald-500"></i><span>Bereit</span>';
}
}
// 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 `<span class="w-2.5 h-2.5 rounded-full ${bg} transition-colors"></span>`;
}).join('');
document.getElementById('leitnerBoxesVisual').innerHTML = dotsHtml;
}
document.getElementById('cardBadgeFront').innerText = item.categoryLabel; document.getElementById('cardBadgeFront').innerText = item.categoryLabel;
document.getElementById('cardBackCategory').innerText = item.categoryLabel; document.getElementById('cardBackCategory').innerText = item.categoryLabel;
@@ -2067,6 +2451,60 @@
playTone('flip'); 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) { function markCardMastered(isMastered) {
if (filteredCards.length === 0) return; if (filteredCards.length === 0) return;
const item = filteredCards[currentCardIndex]; const item = filteredCards[currentCardIndex];
@@ -2125,7 +2563,7 @@
renderCurrentCard(); renderCurrentCard();
} }
// Touch Swipe Gestures // Touch Swipe Gestures (supports both FSRS and Leitner)
let touchStartX = 0; let touchStartX = 0;
let touchStartY = 0; let touchStartY = 0;
const cardTouchContainer = document.getElementById('flashcardContainer'); const cardTouchContainer = document.getElementById('flashcardContainer');
@@ -2142,10 +2580,18 @@
const diffY = touchEndY - touchStartY; const diffY = touchEndY - touchStartY;
if (Math.abs(diffX) > 50 && Math.abs(diffX) > Math.abs(diffY)) { if (Math.abs(diffX) > 50 && Math.abs(diffX) > Math.abs(diffY)) {
if (diffX > 0) { if (cardAlgo === 'fsrs') {
markCardMastered(true); if (diffX > 0) {
rateFsrsCard(3); // Gut
} else {
rateFsrsCard(1); // Nochmal
}
} else { } else {
markCardMastered(false); if (diffX > 0) {
markCardMastered(true);
} else {
markCardMastered(false);
}
} }
} else if (Math.abs(diffX) < 15 && Math.abs(diffY) < 15) { } else if (Math.abs(diffX) < 15 && Math.abs(diffY) < 15) {
flipCard(); flipCard();
@@ -3424,9 +3870,11 @@
function getSyncPayload() { function getSyncPayload() {
return { return {
v: 1, v: 2,
timestamp: Date.now(), timestamp: Date.now(),
algo: cardAlgo,
levels: cardLevels, levels: cardLevels,
fsrs: fsrsCards,
mistakes: quizMistakes, mistakes: quizMistakes,
stats: { stats: {
maxStreak: quizMaxStreak maxStreak: quizMaxStreak
@@ -3453,8 +3901,18 @@
const payload = getSyncPayload(); const payload = getSyncPayload();
const token = encodeSyncPayload(payload); const token = encodeSyncPayload(payload);
const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length; const isFsrs = cardAlgo === 'fsrs';
document.getElementById('syncStatMastered').innerText = `${masteredCount} / ${STILMITTEL_DATA.length}`; 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('syncStatMistakes').innerText = `${quizMistakes.length}`;
document.getElementById('syncStatStreak').innerText = `${quizMaxStreak}`; document.getElementById('syncStatStreak').innerText = `${quizMaxStreak}`;
@@ -3589,6 +4047,14 @@
cardLevels = payload.levels; cardLevels = payload.levels;
localStorage.setItem('rhetorik_leitner_levels', JSON.stringify(cardLevels)); 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)) { if (Array.isArray(payload.mistakes)) {
quizMistakes = payload.mistakes; quizMistakes = payload.mistakes;
localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes)); localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes));
@@ -3597,7 +4063,7 @@
quizMaxStreak = Math.max(quizMaxStreak, payload.stats.maxStreak); quizMaxStreak = Math.max(quizMaxStreak, payload.stats.maxStreak);
} }
applyCardFilters(); setSpacedRepetitionAlgo(cardAlgo);
updateMasteredHeader(); updateMasteredHeader();
renderCurrentCard(); renderCurrentCard();
showToast('Lernstand erfolgreich synchronisiert!', 'fa-arrows-rotate'); showToast('Lernstand erfolgreich synchronisiert!', 'fa-arrows-rotate');
@@ -3608,9 +4074,14 @@
if (hash && hash.startsWith('#sync=')) { if (hash && hash.startsWith('#sync=')) {
const token = hash.replace('#sync=', '').trim(); const token = hash.replace('#sync=', '').trim();
const payload = decodeSyncPayload(token); const payload = decodeSyncPayload(token);
if (payload && payload.levels) { if (payload && (payload.levels || payload.fsrs)) {
pendingIncomingSync = payload; 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('incomingSyncMastered').innerText = `${masteredCount} von ${STILMITTEL_DATA.length}`;
document.getElementById('incomingSyncMistakes').innerText = `${(payload.mistakes || []).length}`; document.getElementById('incomingSyncMistakes').innerText = `${(payload.mistakes || []).length}`;
document.getElementById('incomingSyncStreak').innerText = `${(payload.stats && payload.stats.maxStreak) || 0}`; document.getElementById('incomingSyncStreak').innerText = `${(payload.stats && payload.stats.maxStreak) || 0}`;
@@ -3791,8 +4262,13 @@
} }
function checkAchievements() { function checkAchievements() {
const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length; function isMasteredCard(item) {
const klangMastered = STILMITTEL_DATA.filter(item => item.category === 'klang' && (cardLevels[item.id] || 1) >= 5).length; 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; const totalKlang = STILMITTEL_DATA.filter(item => item.category === 'klang').length;
// 1. Erster Schritt // 1. Erster Schritt
@@ -3992,10 +4468,14 @@
nextCard(); nextCard();
} else if (e.code === 'ArrowLeft') { } else if (e.code === 'ArrowLeft') {
prevCard(); prevCard();
} else if (e.key === '1') { } else if (cardAlgo === 'fsrs') {
markCardMastered(false); if (e.key === '1') rateFsrsCard(1);
} else if (e.key === '2') { else if (e.key === '2') rateFsrsCard(2);
markCardMastered(true); 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') { } else if (currentTabIndex === 'quiz') {
if (['1', '2', '3', '4'].includes(e.key)) { if (['1', '2', '3', '4'].includes(e.key)) {
@@ -4075,7 +4555,7 @@
window.onload = function() { window.onload = function() {
updateSoundUI(); updateSoundUI();
applyCardFilters(); setSpacedRepetitionAlgo(cardAlgo);
updateMasteredHeader(); updateMasteredHeader();
renderPrintCheatsheet(); renderPrintCheatsheet();
updateGamificationHeader(); updateGamificationHeader();
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'rhetorikmaster-v10'; const CACHE_NAME = 'rhetorikmaster-v11';
const STATIC_ASSETS = [ const STATIC_ASSETS = [
'/', '/',
'/index.html', '/index.html',