diff options
| author | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2025-07-20 11:07:37 -0400 |
|---|---|---|
| committer | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2025-07-20 11:07:37 -0400 |
| commit | 413e921582d4a58c7329c70f599d2163f82db026 (patch) | |
| tree | 69088c452d5ca7525a41413f9e7f715de66498bd /script.js | |
| parent | b2f9eccdd70af1123d10d3d6e70576af46002af2 (diff) | |
Docs, branding
Diffstat (limited to 'script.js')
| -rw-r--r-- | script.js | 402 |
1 files changed, 0 insertions, 402 deletions
diff --git a/script.js b/script.js deleted file mode 100644 index ad58179..0000000 --- a/script.js +++ /dev/null @@ -1,402 +0,0 @@ -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(); - downloadJSON(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 downloadJSON(obj) { - // Convert the object to a JSON string (with default formatting) - const jsonString = JSON.stringify(obj, null, 2); // 'null, 2' adds indentation for readability - - // Create a Blob with the JSON content - const blob = new Blob([jsonString], { type: 'application/json' }); - - // Generate a URL for the Blob - const url = URL.createObjectURL(blob); - - // Create an anchor element - const a = document.createElement('a'); - a.href = url; - a.download = 'data.json'; // Default filename - - // Append the anchor to the body (required for some browsers) - document.body.appendChild(a); - - // Simulate a click on the anchor to trigger the download - a.click(); - - // Clean up - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -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 shuffleArray(array) { - let currentIndex = array.length; - let randomIndex; - - // While there remain elements to shuffle. - while (currentIndex !== 0) { - // Pick a remaining element. - randomIndex = Math.floor(Math.random() * currentIndex); - currentIndex--; - - // And swap it with the current element. - [array[currentIndex], array[randomIndex]] = [ - array[randomIndex], - array[currentIndex], - ]; - } - - return array; -} - -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; - }); - shuffleArray(window.cards); - - 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 = "<pre>" + message + "</pre>"; - - // 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 - }); - - document.getElementById('flashcard').addEventListener('click', function(event) { - flipCard(); - }); -}); - -window.addEventListener('keydown', (event) => { - 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'); - } else if (event.code === 'KeyQ') { - endReview(); - } -}); |