function reportException(e) { const error = e instanceof Error ? e : new Error(e.toString()); console.log(JSON.stringify(error.stack)); showToast(`An error has occurred!\n\n${error}\n\n${error.stack}`); } /*********************************************************************** * Actual FSRS5 algorithm * ***********************************************************************/ const W = [ 0.40255, 1.18385, 3.173, 15.69105, 7.1949, 0.5345, 1.4604, 0.0046, 1.54575, 0.1192, 1.01925, 1.9395, 0.11, 0.29605, 2.2698, 0.2315, 2.9898, 0.51655, 0.6621, ]; const F = 19.0 / 81.0; const C = -0.5; const Grade = { forgot: 1.0, hard: 2.0, good: 3.0, easy: 4.0 }; function retrievability(t, s) { return Math.pow(1.0 + F * (t / s), C); } function interval(r_d, s) { return (s / F) * (Math.pow(r_d, 1.0 / C) - 1.0); } function s_0(g) { switch (g) { case Grade.forgot: return W[0]; case Grade.hard: return W[1]; case Grade.good: return W[2]; case Grade.easy: return W[3]; } } function s_success(d, s, r, g) { const t_d = 11.0 - d; const t_s = Math.pow(s, -W[9]); const t_r = Math.exp(W[10] * (1.0 - r)) - 1.0; const h = g === Grade.hard ? W[15] : 1.0; const b = g === Grade.easy ? W[16] : 1.0; const c = Math.exp(W[8]); const alpha = 1.0 + t_d * t_s * t_r * h * b * c; return s * alpha; } function s_fail(d, s, r) { const d_f = Math.pow(d, -W[12]); const s_f = Math.pow(s + 1.0, W[13]) - 1.0; const r_f = Math.exp(W[14] * (1.0 - r)); const c_f = W[11]; const result = d_f * s_f * r_f * c_f; return Math.min(result, s); } function stability(d, s, r, g) { return g === Grade.forgot ? s_fail(d, s, r) : s_success(d, s, r, g); } function clamp_d(d) { return Math.min(10.0, Math.max(1.0, d)); } function d_0(g) { return clamp_d(W[4] - Math.exp(W[5] * (g - 1.0)) + 1.0); } function difficulty(d, g) { return clamp_d(W[7] * d_0(Grade.easy) + (1.0 - W[7]) * dp(d, g)); } function dp(d, g) { return d + delta_d(g) * ((10.0 - d) / 9.0); } function delta_d(g) { return -W[6] * (g - 3.0); } function sim(grades) { let t = 0.0; const r_d = 0.9; const steps = []; // Initial review if (grades.length === 0) { throw new Error("Grades cannot be empty"); } let gradesCopy = [...grades]; let g = gradesCopy.shift(); // Remove the first grade let s = s_0(g); let d = d_0(g); let i = Math.max(Math.round(interval(r_d, s)), 1.0); steps.push({ t, s, d, i }); // n-th review for (let g of gradesCopy) { t += i; const r = retrievability(i, s); s = stability(d, s, r, g); d = difficulty(d, g); i = Math.max(Math.round(interval(r_d, s)), 1.0); steps.push({ t, s, d, i }); } return steps; } /*********************************************************************** * State file processing * ***********************************************************************/ function statusString() { const now = Date.now(); const elapsedSeconds = Math.floor((now - window.started) / 1000); const minutes = Math.floor(elapsedSeconds / 60); const seconds = elapsedSeconds % 60; const pad = (n) => n < 10 ? '0' + n : n; return `Card ${window.index + 1} of ${window.cards.length} scheduled for review, ${window.allCards.length} total Current session: ${pad(minutes)}:${pad(seconds)}`; } function loadCard(card) { window.card.revealed = false; window.card.front.innerHTML = card.front; window.card.front.style.display = "block"; window.card.back.innerHTML = card.back; window.card.back.style.display = "none"; window.buttons.style.display = "none"; } function flipCard() { if (window.index >= window.cards.length) { return; } window.card.revealed = true; // Reveal the "back". if (window.card.front.style.display !== "block") { window.card.front.style.display = "block"; window.card.back.style.display = "none"; } else { window.card.front.style.display = "none"; window.card.back.style.display = "block"; } // Reveal the buttons. window.buttons.style.display = "flex"; } function endReview() { clearInterval(window.updateInterval); window.statusInfo.innerHTML = "Session complete!"; window.flashcard.style.display = "none"; window.buttons.style.display = "none"; updateAllCards(); console.log(window.allCards); } function rateCard(difficulty) { if (!window.card.revealed) { return; } const card = window.cards[window.index]; const r_d = 0.9; const s = card.scheduler.s; const d = card.scheduler.d; const i = card.scheduler.i; const g = card.flagged ? Grade.forgot : Grade[difficulty]; let s_n = undefined, d_n = undefined, i_n = undefined; if (s == null || d == null || i == null) { s_n = s_0(g); d_n = d_0(g); } else { const r = retrievability(i, s); s_n = stability(d, s, r, g); d_n = difficulty(d, g); } i_n = Math.max(Math.round(interval(r_d, s)), 1.0); let scheduled = today(); scheduled.setDate(scheduled.getDate() + i_n); if (difficulty === "forgot") { // Shift forgotten card to end. window.cards.push(card); window.cards.splice(window.index, 1); loadCard(window.cards[window.index]); } else { // Unflag and update scheduler parameters. delete card.flagged; card.scheduler.s = s_n; card.scheduler.d = d_n; card.scheduler.i = i_n; card.scheduled = scheduled; if (++window.index < window.cards.length) { loadCard(window.cards[window.index]); } else { endReview(); } } } // Update `window.allCards` with new scheduler parameters. function updateAllCards() { for (let i = 0; i < window.allCards.length; i++) { const updated = window.cards.find((card) => card.id == window.allCards[i].id); if (updated) { window.allCards[i] = updated; } } } function today() { let today = new Date(); today.setHours(0, 0, 0, 0); return today; } function checkCards(cards) { for (let i = 0; i < cards.length; i++) { const card = cards[i]; if (!card.id) { throw new Error("malformed card: missing id") } else if (!card.front) { throw new Error(`malformed card ${card.id}: missing front`) } else if (!card.back) { throw new Error(`malformed card ${card.id}: missing bacak`) } if (!card.scheduled) { card.scheduled = today().toJSON(); } if (!card.scheduler) { card.scheduler = { s: null, d: null, i: null } } } } function loadCards(data) { checkCards(data); document.getElementById('flashcard').style.display = "block"; window.allCards = data; window.index = 0; window.started = Date.now(); window.statusInfo = document.getElementById('status-info'); window.buttons = document.getElementById('button-container'); window.card = { front: document.getElementById('flashcard-front'), back: document.getElementById('flashcard-back') }; document.getElementById('rating-button-forgot') .addEventListener('click', () => rateCard('forgot')); document.getElementById('rating-button-hard') .addEventListener('click', () => rateCard('hard')); document.getElementById('rating-button-good') .addEventListener('click', () => rateCard('good')); document.getElementById('rating-button-easy') .addEventListener('click', () => rateCard('easy')); let today = new Date(); // Granularity only at the day level. today.setHours(0, 0, 0, 0); window.cards = data.filter((card) => { return (new Date(card.scheduled)) <= today; }); loadCard(window.cards[0]); window.updateInterval = setInterval(() => { window.statusInfo.innerText = statusString() }, 1000); } /*********************************************************************** * Status display * ***********************************************************************/ function showToast(message, duration = 3000) { const toast = document.getElementById("toast"); toast.innerHTML = "
" + message + ""; // Show the toast toast.classList.add("show"); // Hide the toast after specified time setTimeout(() => { toast.classList.remove("show"); }, duration); } window.addEventListener("load", (event) => { document.getElementById('cards-input').addEventListener('change', function(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const content = e.target.result; loadCards(JSON.parse(content)); } catch (e) { reportException(e); } }; reader.onerror = function(error) { document.getElementById('result').textContent = "Error reading file: " + error; reportException(error); }; reader.readAsText(file); // Read the file as text }); }); window.addEventListener('keydown', (event) => { // console.log(event.code); if (event.code === 'Space') { flipCard(); } else if (event.code === 'Digit1' || event.code === "Numpad1") { rateCard('forgot') } else if (event.code === 'Digit2' || event.code === "Numpad2") { rateCard('easy') } else if (event.code === 'Digit3' || event.code === "Numpad3") { rateCard('good') } else if (event.code === 'Digit4' || event.code === "Numpad4") { rateCard('hard') } });