Skip to content

Add SM-2 spaced repetition coaching with user-adjustable settings and "Never Seen" card action#58

Merged
coreysreid merged 3 commits into
mainfrom
copilot/add-revision-recommendation-algorithm
Mar 13, 2026
Merged

Add SM-2 spaced repetition coaching with user-adjustable settings and "Never Seen" card action#58
coreysreid merged 3 commits into
mainfrom
copilot/add-revision-recommendation-algorithm

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 12, 2026

Adds an evidence-based review scheduling system (SuperMemo SM-2) that tells students exactly when to revisit each card, surfaces overdue cards prominently, and lets users flag knowledge gaps with a "Never seen this before" action.

Algorithm — _apply_sm2(progress, quality, settings)

  • Quality 0–5 per-card rating replaces the old correct/incorrect boolean (legacy correct=true/false still maps to quality 4/1)
  • EF (easiness factor) per card adjusts automatically: repeated failures shrink intervals, easy recalls extend them
  • New FlashcardProgress fields: easiness_factor, interval_days, sm2_repetitions, next_review_date

User-Adjustable Settings — SpacedRepetitionSettings model + /settings/spaced-repetition/

  • Learning speed (ease_modifier −2→+2): shifts the EF floor (1.1–1.5), directly controlling how aggressively intervals grow
  • Max interval (30–730 days) and daily new cards (1–100) caps
  • Defaults defined once as DEFAULT_* class constants; view and get_or_create both reference them

Study Session UI

  • Result buttons replaced: Again / Hard / Good / Easy → quality 1 / 2 / 4 / 5
  • "Again" cards re-queued 3 positions ahead for same-session re-exposure
  • Post-rating badge shows next scheduled review date ("📅 Next review: in ~2 weeks")
  • "🔄 Never seen this before" button (POST /flashcard/<id>/never-seen/) resets all SM-2 state to blank, re-queuing as new; blocked to non-enrolled users (403)
  • Prerequisites panel surfaces linked topic prerequisites when a card is shown

Topic Detail

  • Due-card count badge + "📅 Review Due N" button when cards are overdue
  • GET /study/<topic_id>/review/ redirects to study session with ?review=1; shows info message when nothing is due

Migration

0041_spaced_repetition.py — adds SM-2 fields to FlashcardProgress; creates SpacedRepetitionSettings.


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

…ver Seen card action

Co-authored-by: coreysreid <111491611+coreysreid@users.noreply.github.com>
Copilot AI changed the title [WIP] Add algorithm for card revision recommendations Add SM-2 spaced repetition coaching with user-adjustable settings and "Never Seen" card action Mar 12, 2026
Copilot AI requested a review from coreysreid March 12, 2026 22:21
@coreysreid coreysreid marked this pull request as ready for review March 12, 2026 22:27
Copilot AI review requested due to automatic review settings March 12, 2026 22:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces SM-2–style spaced repetition to the study flow, adding “review due” entry points, per-card scheduling metadata, and a user-configurable spaced repetition settings page.

Changes:

  • Add SM-2 scheduling fields to FlashcardProgress and a new per-user SpacedRepetitionSettings model (with migration + admin support).
  • Extend study/review UX: “Review due” CTA on topic pages, review-only study sessions, and quality rating (“Again/Hard/Good/Easy”) buttons in the study session UI.
  • Update progress API to accept an SM-2 quality score (0–5) while preserving the legacy correct=true/false path; add endpoints for “never seen” reset and SR settings UI.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
study/views.py Adds due-count display, review-mode filtering, SM-2 logic + progress endpoints, review redirect, and SR settings view.
study/urls.py Wires new review/reset/settings routes.
study/tests.py Adds unit tests for SM-2 and view tests for new endpoints/settings.
study/templates/study/topic_detail.html Adds “Review Due” CTA and due-count banner.
study/templates/study/study_session.html Replaces legacy result buttons with SM-2 quality buttons; adds “Never seen” + prerequisites panel and related JS.
study/templates/study/spaced_repetition_settings.html New SR settings page UI and client-side interactions.
study/templates/study/base.html Adds nav link to SR settings.
study/models.py Adds SM-2 fields to FlashcardProgress and introduces SpacedRepetitionSettings.
study/migrations/0041_spaced_repetition.py Schema migration for new SM-2 fields and settings model.
study/admin.py Registers SR settings in Django admin.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread study/models.py Outdated
Comment on lines +398 to +399
help_text="SM-2 easiness factor (≥1.3). Higher = longer intervals between reviews.",
validators=[MinValueValidator(1.3)],
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SM-2 speed settings allow a minimum easiness factor down to 1.1 (ease_modifier=-2), but FlashcardProgress.easiness_factor is constrained with MinValueValidator(1.3) and help text says “≥1.3”. This is internally inconsistent with get_min_easiness() and can cause validation/admin form issues or misleading docs. Align the validator/help text with the intended min EF (e.g., lower validator to 1.1 or clamp get_min_easiness() to ≥1.3).

Suggested change
help_text="SM-2 easiness factor (≥1.3). Higher = longer intervals between reviews.",
validators=[MinValueValidator(1.3)],
help_text="SM-2 easiness factor (≥1.1). Higher = longer intervals between reviews.",
validators=[MinValueValidator(1.1)],

Copilot uses AI. Check for mistakes.
Comment thread study/views.py
user=request.user,
flashcard__topic=topic,
next_review_date__lte=today,
step_index=-1,
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due-count calculation filters FlashcardProgress with step_index=-1, but step_by_step cards post progress using their per-step step_index values. This means due_count (and the “Review Due” CTA) will ignore due step cards. Consider including step progress in the due calculation or changing step_by_step progress updates to consistently use step_index=-1 for scheduling.

Suggested change
step_index=-1,

Copilot uses AI. Check for mistakes.
Comment thread study/views.py
Comment on lines +366 to +376
if is_review_mode:
# Only cards due today
due_ids = set(
FlashcardProgress.objects.filter(
user=request.user,
flashcard__topic=topic,
next_review_date__lte=today,
step_index=-1,
).values_list('flashcard_id', flat=True)
)
)

base_qs = topic.flashcards.filter(id__in=due_ids).prefetch_related('skills')
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review mode only selects due cards from FlashcardProgress where step_index=-1, but step_by_step virtual cards send their own step_index when posting progress. As written, step progress can be scheduled by SM-2 yet never be surfaced by review mode (and all steps of a due flashcard may be included even if only some steps are due). Decide whether SM-2 should schedule per-card or per-step and make the due selection logic match that choice.

Copilot uses AI. Check for mistakes.
Comment on lines +677 to +682
})
.catch(function(err) {
console.warn('markNeverSeen failed:', err);
});
// Treat the same as "Again" (quality 1) for session flow
markQuality(1);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

markNeverSeen() fires the “never-seen” reset request and then immediately calls markQuality(1) without waiting. Because both endpoints mutate the same FlashcardProgress, request ordering is nondeterministic and can leave progress partially reset or overwrite the newly computed schedule. Chain the calls (await reset success before posting quality) or combine into a single endpoint.

Suggested change
})
.catch(function(err) {
console.warn('markNeverSeen failed:', err);
});
// Treat the same as "Again" (quality 1) for session flow
markQuality(1);
// Treat the same as "Again" (quality 1) for session flow,
// but only after the reset has completed successfully.
markQuality(1);
})
.catch(function(err) {
console.warn('markNeverSeen failed:', err);
});

Copilot uses AI. Check for mistakes.
Comment on lines +635 to +665
function markQuality(quality) {
if (!isFlipped) { revealCard(); return; }
const card = cards[currentIndex];
postProgress(card.id, card.step_index, correct === true);
postQuality(card.id, card.step_index, quality).then(function(data) {
if (data.next_review_date) {
const badge = document.getElementById('nextReviewBadge');
const days = data.interval_days;
let msg;
if (days <= 1) {
msg = '📅 Next review: tomorrow';
} else if (days < 7) {
msg = '📅 Next review: in ' + days + ' days';
} else if (days < 30) {
msg = '📅 Next review: in ~' + Math.round(days / 7) + ' week' + (days >= 14 ? 's' : '');
} else {
msg = '📅 Next review: in ~' + Math.round(days / 30) + ' month' + (days >= 60 ? 's' : '');
}
badge.textContent = msg;
badge.style.display = 'block';
}
});
cardsStudied++;
if (correct === false) {
// Re-queue missed card 3 positions ahead so the user sees it again soon
if (quality < 3) {
// Forgot — re-queue card 3 positions ahead so user sees it again soon
const insertAt = Math.min(currentIndex + 3, cards.length);
cards.splice(insertAt, 0, Object.assign({}, card));
document.getElementById('cardTotal').textContent = cards.length;
}
moveToNext();
// Small delay so the user can read the next-review badge, then advance
setTimeout(moveToNext, quality < 3 ? 800 : 300);
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

markQuality() advances to the next card on a fixed timeout without waiting for postQuality() to resolve. On slower networks, the postQuality() callback can update #nextReviewBadge after moveToNext()/loadCard(), showing the previous card’s schedule on the next card. Consider awaiting the response before advancing, or capture the currentIndex/cardId and ignore late responses when the card has changed.

Copilot uses AI. Check for mistakes.
Comment thread study/views.py
Comment on lines +724 to +747
@login_required
def spaced_repetition_settings(request):
"""View and update the user's SM-2 spaced repetition settings."""
settings_obj, _ = SpacedRepetitionSettings.objects.get_or_create(user=request.user)

if request.method == 'POST':
try:
ease_modifier = int(request.POST.get(
'ease_modifier', SpacedRepetitionSettings.DEFAULT_EASE_MODIFIER))
max_interval = int(request.POST.get(
'max_interval_days', SpacedRepetitionSettings.DEFAULT_MAX_INTERVAL_DAYS))
daily_new = int(request.POST.get(
'daily_new_cards', SpacedRepetitionSettings.DEFAULT_DAILY_NEW_CARDS))
except (ValueError, TypeError):
messages.error(request, 'Invalid values — please use the sliders.')
return redirect('spaced_repetition_settings')

# Clamp to model validator bounds
settings_obj.ease_modifier = max(-2, min(2, ease_modifier))
settings_obj.max_interval_days = max(30, min(730, max_interval))
settings_obj.daily_new_cards = max(1, min(100, daily_new))
settings_obj.save()
messages.success(request, 'Spaced repetition settings saved.')
return redirect('spaced_repetition_settings')
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SpacedRepetitionSettings.daily_new_cards is exposed in the UI and persisted here, but it is not referenced anywhere in the study/review flow (search shows no usage outside settings/admin/models/tests). This makes the setting misleading. Either implement enforcement in review mode, or remove/rename it until it’s supported.

Copilot uses AI. Check for mistakes.
@coreysreid
Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

Co-authored-by: coreysreid <111491611+coreysreid@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 12, 2026

@copilot apply changes based on the comments in this thread

Applied all 6 review comments in commit d01d28a:

  1. Validator/help-text alignmentMinValueValidator on easiness_factor lowered from 1.3 → 1.1 with matching help text; new migration 0042 created.
  2. SM-2 always on step_index=-1update_flashcard_progress now always writes SM-2 scheduling to the whole-card (step_index=-1) record. Step records still track per-step stats. due_count and review-mode filtering (already step_index=-1) now correctly surface step-by-step cards.
  3. mark_card_never_seen resets all records — The step_index filter was removed; the update now resets ALL FlashcardProgress rows for the card (every step + whole card).
  4. markNeverSeen chainingmarkQuality(1) is now called only inside the .then() callback after the reset succeeds, eliminating the nondeterministic ordering.
  5. Stale badge guard in markQualityconst capturedIndex = currentIndex is captured before the async call; the badge update is skipped if currentIndex !== capturedIndex when the response arrives.
  6. daily_new_cards enforcementstudy_session now caps new (never SM-2 reviewed) cards to settings.daily_new_cards using the already-queried progress_map. The settings template description updated to reflect this. Review mode is unaffected.

Copilot AI requested a review from coreysreid March 12, 2026 23:17
@coreysreid coreysreid merged commit 2cbffd3 into main Mar 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants