Implement FSRS-4.5 17-parameter DSR spaced repetition scheduler with algorithm toggle and live metrics
This commit is contained in:
1 parent
eb0c464112
commit
53ecd61666
2 files changed
+537
-57
No files matched your search
+536
-56
@@ -240,24 +240,42 @@
|
||||
<!-- VIEW 1: FLASHCARDS (Leitner 5-Box-System) -->
|
||||
<!-- ========================================== -->
|
||||
<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="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-3 sm:gap-4">
|
||||
<div>
|
||||
<h2 class="text-base sm:text-xl font-extrabold text-slate-900 flex items-center gap-2">
|
||||
<span>Leitner-Karteikarten (5-Kasten-System)</span>
|
||||
</h2>
|
||||
<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>
|
||||
<div class="flex items-center gap-2.5 flex-wrap">
|
||||
<h2 id="cardSectionTitle" class="text-base sm:text-xl font-extrabold text-slate-900 flex items-center gap-2">
|
||||
<i class="fa-solid fa-brain text-indigo-600"></i>
|
||||
<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 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 -->
|
||||
<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">
|
||||
Begriff zuerst
|
||||
Begriff
|
||||
</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">
|
||||
Beispiel zuerst
|
||||
Beispiel
|
||||
</button>
|
||||
</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">
|
||||
<i class="fa-solid fa-shuffle"></i>
|
||||
</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>
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- Leitner Box Filter -->
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- Leitner Box Filter (Active in Leitner Mode) -->
|
||||
<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>
|
||||
<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>
|
||||
@@ -300,14 +318,27 @@
|
||||
<option value="5">Kasten 5 (Gemeistert)</option>
|
||||
</select>
|
||||
</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>
|
||||
|
||||
<!-- Progress bar & Leitner Box Indicators -->
|
||||
<!-- Progress bar & Algorithm Indicators -->
|
||||
<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>
|
||||
<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>
|
||||
<div id="leitnerBoxesVisual" class="flex gap-1">
|
||||
<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>
|
||||
</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 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>
|
||||
@@ -410,13 +461,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center justify-between gap-2.5 sm:gap-4 mt-6">
|
||||
<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">
|
||||
<!-- Action Buttons -->
|
||||
<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>
|
||||
<span class="hidden sm:inline">Zurück</span>
|
||||
</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">
|
||||
<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>
|
||||
@@ -429,7 +482,50 @@
|
||||
</button>
|
||||
</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">< 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>
|
||||
<i class="fa-solid fa-chevron-right"></i>
|
||||
</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 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 = '<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() {
|
||||
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 `<span class="w-2.5 h-2.5 rounded-full ${bg} transition-colors"></span>`;
|
||||
}).join('');
|
||||
document.getElementById('leitnerBoxesVisual').innerHTML = dotsHtml;
|
||||
// Update DSR Badges
|
||||
const rBadge = document.getElementById('fsrsRetrievabilityBadge');
|
||||
if (rBadge) {
|
||||
rBadge.innerHTML = `<i class="fa-solid fa-gauge-high text-emerald-600"></i><span>R: ${rVal}%</span>`;
|
||||
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('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();
|
||||
|
||||
Reference in new issue
Block a user