Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions database.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -716,5 +716,16 @@ <h3 style="margin:0 0 12px; font-size:18px; font-weight:600;">New task</h3>
</div>
</div>
</div>
<div id="revision-modal" class="modal-backdrop" style="display:none; position:fixed; inset:0; z-index:9999; align-items:center; justify-content:center;">
<div class="modal-card" style="border-radius:12px; padding:20px; width:340px; box-shadow:0 12px 40px rgba(0,0,0,0.25); text-align:center;">
<h3 style="margin:0 0 8px; font-size:18px; font-weight:600;">Revision Complete! 🎉</h3>
<p style="font-size:14px; margin-bottom:16px; color:var(--color-text-secondary);">How difficult was it to recall this concept?</p>
<div style="display:flex; justify-content:space-between; gap:8px;">
<button id="rev-btn-easy" class="btn" style="flex:1; background:var(--color-text-success); color:white; border:none;">Easy</button>
<button id="rev-btn-medium" class="btn" style="flex:1; background:var(--color-text-warning); color:black; border:none;">Medium</button>
<button id="rev-btn-hard" class="btn" style="flex:1; background:var(--color-text-danger); color:white; border:none;">Hard</button>
</div>
</div>
</div>
</body>
</html>
63 changes: 62 additions & 1 deletion js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1168,7 +1168,12 @@ function renderTasks() {
labelsHtml = t.labels.map(l => `<span class="task-pill" style="background:${getLabelColor(l)}; color:white;">${l}</span>`).join(' ');
}

html += `
let revisionHtml = '';
if (t.revision_stage > 0) {
revisionHtml = `<span class="task-pill pill-purple" title="Spaced Repetition Stage ${t.revision_stage}">🔁 Stage ${t.revision_stage}</span>`;
}

html += `
<div class="task-item
${isUrgent ? 'urgent' : ''}
${isHighPriority ? 'high-priority' : ''}
Expand Down Expand Up @@ -1203,6 +1208,8 @@ function renderTasks() {
<span class="task-pill ${pillClass}">
${sub.short_code}
</span>

${revisionHtml}

${labelsHtml}
</div>
Expand Down Expand Up @@ -2280,3 +2287,57 @@ if (calendarDownloadBtn) {
downloadCalendar();
});
}


// ================= 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'));
};

21 changes: 21 additions & 0 deletions js/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,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 {
Expand Down
8 changes: 5 additions & 3 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,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;
Expand Down Expand Up @@ -439,6 +439,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 });
Expand Down Expand Up @@ -481,7 +482,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 ';
Expand All @@ -498,6 +499,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' });
Expand Down