From a842d52cb3ecf6bab142b635103c78f068d4044b Mon Sep 17 00:00:00 2001 From: anushkagupta200615-jpg Date: Wed, 24 Jun 2026 03:23:24 +0530 Subject: [PATCH] feat: implement spaced repetition scheduler (#1178) --- database.js | 5 +++++ index.html | 11 ++++++++++ js/app.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++- js/store.js | 21 +++++++++++++++++++ server.js | 8 ++++--- 5 files changed, 101 insertions(+), 4 deletions(-) diff --git a/database.js b/database.js index d2a34f89..afd172d3 100644 --- a/database.js +++ b/database.js @@ -78,6 +78,11 @@ db.all("PRAGMA table_info(tasks)", (err, rows) => { if (!columnNames.includes("labels")) { db.run("ALTER TABLE tasks ADD COLUMN labels TEXT DEFAULT '[]'"); } + + // Revision stage + if (!columnNames.includes("revision_stage")) { + db.run("ALTER TABLE tasks ADD COLUMN revision_stage INTEGER DEFAULT 0"); + } }); // Pre-populate some subjects if empty diff --git a/index.html b/index.html index aa1d4645..9a2391cf 100644 --- a/index.html +++ b/index.html @@ -584,5 +584,16 @@

New task

+ diff --git a/js/app.js b/js/app.js index ccec3408..3b665795 100644 --- a/js/app.js +++ b/js/app.js @@ -808,6 +808,11 @@ function renderTasks() { labelsHtml = t.labels.map(l => `${l}`).join(' '); } + let revisionHtml = ''; + if (t.revision_stage > 0) { + revisionHtml = `🔁 Stage ${t.revision_stage}`; + } + html += `
@@ -816,6 +821,7 @@ function renderTasks() {
${isDone ? 'Done' : 'Due ' + formatDate(t.due_at)} ${sub.short_code} + ${revisionHtml} ${labelsHtml}
@@ -1829,4 +1835,56 @@ if (calendarDownloadBtn) { calendarDownloadBtn.addEventListener('click', () => { downloadCalendar(); }); -} \ No newline at end of file +} + +// ================= SPACED REPETITION (Issue 1178) ================= +window.showRevisionModal = function(task) { + const modal = document.getElementById('revision-modal'); + if (!modal) return; + modal.style.display = 'flex'; + + const btnEasy = document.getElementById('rev-btn-easy'); + const btnMedium = document.getElementById('rev-btn-medium'); + const btnHard = document.getElementById('rev-btn-hard'); + + // Clone to remove old event listeners + const newEasy = btnEasy.cloneNode(true); + const newMedium = btnMedium.cloneNode(true); + const newHard = btnHard.cloneNode(true); + btnEasy.parentNode.replaceChild(newEasy, btnEasy); + btnMedium.parentNode.replaceChild(newMedium, btnMedium); + btnHard.parentNode.replaceChild(newHard, btnHard); + + const handleFeedback = (difficulty) => { + modal.style.display = 'none'; + let nextStage = task.revision_stage || 1; + if (difficulty === 'easy') nextStage += 1; + else if (difficulty === 'hard') nextStage = Math.max(1, nextStage - 1); + + let daysToAdd = 1; + if (nextStage === 2) daysToAdd = 3; + else if (nextStage === 3) daysToAdd = 7; + else if (nextStage >= 4) daysToAdd = 14; + + const revDate = new Date(); + revDate.setDate(revDate.getDate() + daysToAdd); + + let baseTitle = task.title; + if (baseTitle.startsWith('Revision: ')) { + baseTitle = baseTitle.replace('Revision: ', ''); + } + + store.addTasks([{ + title: `Revision: ${baseTitle}`, + subject_id: task.subject_id, + due_at: revDate.toISOString(), + status: 'Not Started', + priority: task.priority, + revision_stage: nextStage + }]); + }; + + newEasy.addEventListener('click', () => handleFeedback('easy')); + newMedium.addEventListener('click', () => handleFeedback('medium')); + newHard.addEventListener('click', () => handleFeedback('hard')); +}; \ No newline at end of file diff --git a/js/store.js b/js/store.js index b78dd42e..85ede321 100644 --- a/js/store.js +++ b/js/store.js @@ -203,6 +203,27 @@ export const store = { if (newStatus === 'Done') { triggerConfetti(); + + // SPACED REPETITION LOGIC (Issue 1178) + if (!task.revision_stage || task.revision_stage === 0) { + // Schedule first revision 1 day later + const revDate = new Date(); + revDate.setDate(revDate.getDate() + 1); + + this.addTasks([{ + title: `Revision: ${task.title}`, + subject_id: task.subject_id, + due_at: revDate.toISOString(), + status: 'Not Started', + priority: task.priority, + revision_stage: 1 + }]); + } else { + // It's already a revision task, ask for feedback + if (window.showRevisionModal) { + window.showRevisionModal(task); + } + } } try { diff --git a/server.js b/server.js index 13aab53f..40fe6b8a 100644 --- a/server.js +++ b/server.js @@ -370,8 +370,8 @@ app.post('/api/tasks', (req, res) => { let errors = []; const stmt = db.prepare(`INSERT INTO tasks - (id, subject_id, title, due_at, status, priority, confidence_score, notes, estimated_duration, is_estimated_duration_min, labels) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); + (id, subject_id, title, due_at, status, priority, confidence_score, notes, estimated_duration, is_estimated_duration_min, labels, revision_stage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); let pending = tasks.length; @@ -432,6 +432,7 @@ app.post('/api/tasks', (req, res) => { Number.isFinite(Number(t.estimated_duration)) ? Number(t.estimated_duration) : null, t.is_estimated_duration_min === 0 ? 0 : 1, typeof t.labels === 'string' ? t.labels : JSON.stringify(t.labels || []), + t.revision_stage || 0, function (insertErr) { if (insertErr) { errors.push({ task: t, error: insertErr.message }); @@ -474,7 +475,7 @@ app.post('/api/tasks', (req, res) => { // ================= UPDATE ================= app.put('/api/tasks/:id', (req, res) => { - const { status, archived, title, subject_id, due_at, notes, priority, estimated_duration, is_estimated_duration_min,labels } = req.body; + const { status, archived, title, subject_id, due_at, notes, priority, estimated_duration, is_estimated_duration_min,labels, revision_stage } = req.body; let query = 'UPDATE tasks SET '; @@ -491,6 +492,7 @@ app.put('/api/tasks/:id', (req, res) => { if (estimated_duration !== undefined) { updates.push('estimated_duration = ?'); params.push(Number.isFinite(Number(estimated_duration)) ? Number(estimated_duration) : null); } if (is_estimated_duration_min !== undefined) { updates.push('is_estimated_duration_min = ?'); params.push(is_estimated_duration_min === 0 ? 0 : 1); } if (labels !== undefined) { updates.push('labels = ?'); params.push(typeof labels === 'string' ? labels : JSON.stringify(labels)); } + if (revision_stage !== undefined) { updates.push('revision_stage = ?'); params.push(revision_stage); } if (updates.length === 0) { return res.status(400).json({ error: 'No fields to update' });