summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--flashcards.html5
-rw-r--r--script.js235
-rw-r--r--script.js.bak108
-rw-r--r--style.css2
4 files changed, 223 insertions, 127 deletions
diff --git a/flashcards.html b/flashcards.html
index 50f3f4d..4727889 100644
--- a/flashcards.html
+++ b/flashcards.html
@@ -22,11 +22,12 @@
<div id="button-container">
<button id="rating-button-forgot" class="rating-button">Forgot [1]</button>
<button id="rating-button-hard" class="rating-button">Hard [2]</button>
- <button id="rating-button-medium" class="rating-button">Medium [3]</button>
+ <button id="rating-button-good" class="rating-button">Good [3]</button>
<button id="rating-button-easy" class="rating-button">Easy [4]</button>
</div>
- <div id="toast"></div>
+ <div id="toast">
+ </div>
<script src="script.js"></script>
</body>
diff --git a/script.js b/script.js
index d6f3415..d2004ad 100644
--- a/script.js
+++ b/script.js
@@ -1,3 +1,122 @@
+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) {
+ const g_value = Grade[g];
+ return clamp_d(W[4] - Math.exp(W[5] * (g_value - 1.0)) + 1.0);
+}
+
+function difficulty(d, g) {
+ return clamp_d(W[7] * d_0('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) {
+ const g_value = Grade[g];
+ return -W[6] * (g_value - 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 *
***********************************************************************/
@@ -22,6 +141,10 @@ function loadCard(card) {
}
function flipCard() {
+ if (window.index >= window.cards.length) {
+ return;
+ }
+
window.card.revealed = true;
// Reveal the "back".
@@ -37,26 +160,105 @@ function flipCard() {
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";
+
+ // TODO: Update cards.
+}
+
function rateCard(difficulty) {
if (!window.card.revealed) {
return;
}
- // if (difficulty === "forgot") {
- // }
- if (++window.index < window.cards.length) {
+ const card = window.cards[window.index];
+
+ const r_d = 0.9;
+ const s = card.s;
+ const d = card.d;
+ const i = card.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);
+
+ const scheduled = new Date(card.scheduled);
+ scheduled.setDate(scheduled + i);
+
+ if (difficulty === "forgot") {
+ // Shift forgotten card to end.
+ window.cards.push(card);
+ window.cards.splice(window.index, 1);
loadCard(window.cards[window.index]);
} else {
- clearInterval(window.updateInterval);
- window.statusInfo.innerHTML = "Session complete!";
- window.flashcard.style.display = "none";
- window.buttons.style.display = "none";
+ // Unflag and update scheduler parameters.
+ delete card.flagged;
+ card.s = s_n;
+ card.d = d_n;
+ card.i = i_n;
+
+ 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;
+ }
}
}
-// Todo: check if card is revealed before allowing button inpt.
+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;
@@ -73,8 +275,8 @@ function loadCards(data) {
.addEventListener('click', () => rateCard('forgot'));
document.getElementById('rating-button-hard')
.addEventListener('click', () => rateCard('hard'));
- document.getElementById('rating-button-medium')
- .addEventListener('click', () => rateCard('medium'));
+ document.getElementById('rating-button-good')
+ .addEventListener('click', () => rateCard('good'));
document.getElementById('rating-button-easy')
.addEventListener('click', () => rateCard('easy'));
@@ -85,7 +287,6 @@ function loadCards(data) {
return (new Date(card.scheduled)) <= today;
});
- // Temporary
loadCard(window.cards[0]);
window.updateInterval = setInterval(() => {
@@ -99,7 +300,7 @@ function loadCards(data) {
function showToast(message, duration = 3000) {
const toast = document.getElementById("toast");
- toast.textContent = message;
+ toast.innerHTML = "<pre>" + message + "</pre>";
// Show the toast
toast.classList.add("show");
@@ -111,6 +312,7 @@ function showToast(message, duration = 3000) {
}
window.addEventListener("load", (event) => {
+
document.getElementById('cards-input').addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) return;
@@ -120,18 +322,19 @@ window.addEventListener("load", (event) => {
try {
const content = e.target.result;
loadCards(JSON.parse(content));
- } catch (error) {
- console.error("JSON Parsing Error:", error);
+ } catch (e) {
+ reportException(e);
}
};
reader.onerror = function(error) {
document.getElementById('result').textContent = "Error reading file: " + error;
- console.error("File Reading Error:", error);
+ reportException(error);
};
reader.readAsText(file); // Read the file as text
});
+
});
window.addEventListener('keydown', (event) => {
@@ -143,7 +346,7 @@ window.addEventListener('keydown', (event) => {
} else if (event.code === 'Digit2' || event.code === "Numpad2") {
rateCard('easy')
} else if (event.code === 'Digit3' || event.code === "Numpad3") {
- rateCard('medium')
+ rateCard('good')
} else if (event.code === 'Digit4' || event.code === "Numpad4") {
rateCard('hard')
}
diff --git a/script.js.bak b/script.js.bak
index c81f2e5..e9b9378 100644
--- a/script.js.bak
+++ b/script.js.bak
@@ -90,111 +90,3 @@ function main() {
// All functions
}
-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) {
- const g_value = Grade[g];
- return clamp_d(W[4] - Math.exp(W[5] * (g_value - 1.0)) + 1.0);
-}
-
-function difficulty(d, g) {
- return clamp_d(W[7] * d_0('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) {
- const g_value = Grade[g];
- return -W[6] * (g_value - 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;
-}
diff --git a/style.css b/style.css
index 38c4c42..6c77a0c 100644
--- a/style.css
+++ b/style.css
@@ -54,7 +54,7 @@ body {
#toast {
position: fixed;
top: 20px;
- right: 20px;
+ left: 20px;
background-color: #333;
color: white;
padding: 12px 24px;