summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2025-07-18 18:21:00 -0400
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2025-07-18 18:21:00 -0400
commitb9909fc19efb4874a2fb43d8603809a18995a781 (patch)
treedb2ab8ec99e44c3ec8de82652fa57a238a89867f
Initial commit
-rw-r--r--cards.json12
-rw-r--r--flashcards.html33
-rw-r--r--script.js107
-rw-r--r--script.js.bak200
-rw-r--r--style.css87
5 files changed, 439 insertions, 0 deletions
diff --git a/cards.json b/cards.json
new file mode 100644
index 0000000..8c3bd15
--- /dev/null
+++ b/cards.json
@@ -0,0 +1,12 @@
+[
+ {
+ "front": "This is the front page!",
+ "back": "This is the back page!",
+ "scheduled": "2025-07-17T00:00:00.000Z"
+ },
+ {
+ "front": "Another front page",
+ "back": "Another back page",
+ "scheduled": "2025-07-20T00:00:00.000Z"
+ }
+]
diff --git a/flashcards.html b/flashcards.html
new file mode 100644
index 0000000..1dba5e8
--- /dev/null
+++ b/flashcards.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>Flashcard</title>
+ <link rel="stylesheet" href="style.css">
+ </head>
+ <body>
+ <div id="flashcard">
+ <div id="flashcard-front"></div>
+ <div id="flashcard-back"></div>
+ </div>
+
+ <div class="status-section">
+ <div id="status-info">
+ No cards loaded!
+ </div>
+ <hr>
+ <input type="file" id="cards-input" accept=".json" />
+ </div>
+
+ <div class="button-container">
+ <button class="rating-button">Forgot</button>
+ <button class="rating-button">Hard</button>
+ <button class="rating-button">Medium</button>
+ <button class="rating-button">Easy</button>
+ </div>
+
+ <div id="toast"></div>
+
+ <script src="script.js"></script>
+ </body>
+</html>
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..94a2fa3
--- /dev/null
+++ b/script.js
@@ -0,0 +1,107 @@
+/***********************************************************************
+ * State file processing *
+ ***********************************************************************/
+
+function makeDeck(cards) {
+ let today = new Date();
+ // Granularity only at the day level.
+ today.setHours(0, 0, 0, 0);
+ return cards.filter((card) => {
+ return (new Date(card.scheduled)) <= today;
+ });
+}
+
+function pad(n) {
+ return n < 10 ? '0' + n : n;
+}
+
+function statusString() {
+ const now = Date.now();
+ const elapsedSeconds = Math.floor((now - window.started) / 1000);
+ const minutes = Math.floor(elapsedSeconds / 60);
+ const seconds = elapsedSeconds % 60;
+ return `Card ${window.index + 1} of ${window.deck.length} scheduled for review, ${window.cards.length} total
+Current session: ${pad(minutes)}:${pad(seconds)}`;
+}
+
+function showCard(card) {
+ const front = document.getElementById('flashcard-front');
+ const back = document.getElementById('flashcard-back');
+
+ front.innerHTML = card.front;
+ back.innerHTML = card.back;
+}
+
+function toggleCard() {
+ const front = document.getElementById('flashcard-front');
+ const back = document.getElementById('flashcard-back');
+
+ if (front.style.display === "block") {
+ front.style.display = "none";
+ back.style.display = "block";
+ } else {
+ back.style.display = "none";
+ front.style.display = "block";
+ }
+}
+
+function loadCards(data) {
+ const statusInfo = document.getElementById('status-info');
+
+ window.cards = data;
+ window.deck = makeDeck(window.cards);
+ window.index = 0;
+ window.started = Date.now();
+
+ // Temporary
+ showCard(window.cards[0]);
+
+ setInterval(() => { statusInfo.innerText = statusString() }, 1000);
+ // Add an event listener for the spacebar press
+ window.addEventListener('keydown', (event) => {
+ if (event.code === 'Space') {
+ toggleCard();
+ }
+ });
+}
+
+/***********************************************************************
+ * Status display *
+ ***********************************************************************/
+
+function showToast(message, duration = 3000) {
+ const toast = document.getElementById("toast");
+ toast.textContent = 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 (error) {
+ console.error("JSON Parsing Error:", error);
+ }
+ };
+
+ reader.onerror = function(error) {
+ document.getElementById('result').textContent = "Error reading file: " + error;
+ console.error("File Reading Error:", error);
+ };
+
+ reader.readAsText(file); // Read the file as text
+ });
+});
diff --git a/script.js.bak b/script.js.bak
new file mode 100644
index 0000000..c81f2e5
--- /dev/null
+++ b/script.js.bak
@@ -0,0 +1,200 @@
+// Define a list of objects with "front" and "back" HTML content
+const pages = [
+ { front: "This is the front page!", back: "This is the back page!" },
+ { front: "Another front page", back: "Another back page" }
+];
+
+// Select the first page from the list
+let currentPage = pages[0];
+let showFront = true;
+
+// Replace the page with the front HTML initially
+document.body.innerHTML = currentPage.front;
+
+// Add an event listener for the spacebar press
+window.addEventListener('keydown', (event) => {
+ if (event.code === 'Space') {
+ // Toggle between front and back content
+ if (showFront) {
+ document.body.innerHTML = currentPage.back;
+ } else {
+ document.body.innerHTML = currentPage.front;
+ }
+ showFront = !showFront;
+ }
+});
+
+// Constants: W as an array of f64 values
+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
+];
+
+// Grade enum representation
+const Grade = {
+ Forgot: 1.0,
+ Hard: 2.0,
+ Good: 3.0,
+ Easy: 4.0
+};
+
+// Helper to convert Grade to f64 (as in From<Grade> for f64)
+function gradeToNumber(g) {
+ return Grade[g];
+}
+
+// Constants
+const F = 19.0 / 81.0;
+const C = -0.5;
+
+// retrievability(t: T, s: S) -> R
+function retrievability(t, s) {
+ return Math.pow(1.0 + F * (t / s), C);
+}
+
+// interval(r_d: R, s: S) -> T
+function interval(rD, s) {
+ return (s / F) * (Math.pow(rD, 1.0 / C) - 1.0);
+}
+
+// s_0(g: Grade) -> S
+function s0(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];
+ default: throw new Error("Invalid grade");
+ }
+}
+
+// s_success(d: D, s: S, r: R, g: Grade) -> S
+function sSuccess(d, s, r, g) {
+ const tD = 11.0 - d;
+ const tS = Math.pow(s, -W[9]);
+ const tR = 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 + tD * tS * tR * h * b * c;
+ return s * alpha;
+}
+
+// s_fail(d: D, s: S, r: R) => number
+function sFail(d, s, r) {
+ return Math.exp(W[8]) - 1.0;
+}
+
+// Main function
+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
new file mode 100644
index 0000000..bb2a135
--- /dev/null
+++ b/style.css
@@ -0,0 +1,87 @@
+body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ margin: 0;
+ background: #f0f0f0;
+ font-family: Arial, sans-serif;
+}
+
+.button-container {
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+ padding: 1rem;
+ background-color: #f5f5f5;
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.rating-button {
+ padding: 0.6rem 1.2rem;
+ font-size: 1rem;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+}
+
+.rating-button:hover {
+ background-color: #e0e0e0;
+}
+
+.status-section {
+ position: fixed;
+ top: 0;
+ right: 0;
+ background-color: #f0f8ff; /* Light blue for subtle color coding */
+ padding: 10px 16px;
+ font-size: 14px;
+ color: #000;
+ border-bottom-left-radius: 8px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+}
+
+.status-section div {
+ margin: 2px 0;
+}
+
+/* Basic styling for the toast */
+#toast {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background-color: #333;
+ color: white;
+ padding: 12px 24px;
+ border-radius: 6px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
+ z-index: 1000;
+}
+
+#toast.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+#flashcard {
+ padding: 16px;
+ background: #fff;
+ border-radius: 4px;
+ filter: drop-shadow(5px 5px 10px gray);
+ box-shadow: 5px 5px 10px gray;
+}
+
+#flashcard-front {
+ display: block;
+}
+#flashcard-back {
+ display: none;
+}