summaryrefslogtreecommitdiff
path: root/script.js
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2025-07-20 11:07:37 -0400
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2025-07-20 11:07:37 -0400
commit413e921582d4a58c7329c70f599d2163f82db026 (patch)
tree69088c452d5ca7525a41413f9e7f715de66498bd /script.js
parentb2f9eccdd70af1123d10d3d6e70576af46002af2 (diff)
Docs, branding
Diffstat (limited to 'script.js')
-rw-r--r--script.js402
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();
- }
-});