diff options
| author | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2025-07-18 18:21:00 -0400 |
|---|---|---|
| committer | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2025-07-18 18:21:00 -0400 |
| commit | b9909fc19efb4874a2fb43d8603809a18995a781 (patch) | |
| tree | db2ab8ec99e44c3ec8de82652fa57a238a89867f | |
Initial commit
| -rw-r--r-- | cards.json | 12 | ||||
| -rw-r--r-- | flashcards.html | 33 | ||||
| -rw-r--r-- | script.js | 107 | ||||
| -rw-r--r-- | script.js.bak | 200 | ||||
| -rw-r--r-- | style.css | 87 |
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; +} |