Files
step3/docs/FLASHCARDS.md
hz 70aa425fde Rebuild flashcard study screen from mockup, with real backend-backed stats
Study screen now matches the provided mockup's layout: a "Current
Deck" panel with a circular mastery-progress ring, and a "Spaced
Repetition" panel (New/Due/Learning/Review counts, streak, daily
goal) on either side of the card. Kept the app's real global sidebar
rather than the mockup's own nav, per direction.

Mastery/Streak/Daily Goal weren't tracked anywhere, so this adds real
backend support rather than showing fabricated numbers:
- Mastered = graduated to review state AND FSRS stability >= 21 days
  (FlashcardScheduleStore.MasteryStabilityDaysThreshold), stricter
  than the plain Review-state count
- CurrentStreakDays = consecutive UTC days with a review, computed
  from review-log timestamps in the new pure FlashcardScheduling
  .ComputeStreakDays (unit tested, habit-streak semantics: doesn't
  reset to 0 the instant a new day starts before today's first review)
- DailyGoal/ReviewsToday reuse the account's existing ReviewsPerDay
  setting rather than inventing a new one
Also removes the exit "X" button in favor of the same Escape/pause
menu the question-player flow uses (main.gd's shared app_menu, with
an "Exit Study Session" action), and adds a new circular_progress.gd
reusable ring control plus a real "Options" menu (bury/suspend the
current card) wired to schedule endpoints that had no UI before.
2026-07-21 23:20:14 -05:00

6.9 KiB

Flashcards

A separate spaced-repetition system alongside the missed-question review queue. Users author basic and cloze flashcards (or generate a basic card from a resolved question), organize them in flat decks, and study them on an FSRS schedule.

Scheduling

Reviews are scored by FSRS.Core 1.0.7, wrapped behind IFlashcardScheduler (api/QuestionsApi/Services/Flashcards/FlashcardScheduler.cs) so the rest of the API never touches the library directly. Pinned, non-configurable defaults:

  • Desired retention: 90%
  • Maximum interval: 36,500 days
  • Interval fuzzing: disabled, so a review's outcome is a deterministic function of stored state (needed for idempotent replay from the offline outbox, and for golden-vector testing)
  • Algorithm/parameter version is recorded per card (FlashcardSchedule.AlgorithmVersion) so a future FSRS upgrade or personalized-parameter feature can be introduced without losing the ability to tell which cards were scheduled under which version

There is no per-user parameter optimization yet; FlashcardScheduleStore stores complete before/after snapshots on every FlashcardReviewLog entry specifically so that a future optimizer can be trained from full history without a data migration.

api/QuestionsApi.Tests/FlashcardSchedulerTests.cs validates the wrapper against the official FSRS formulas (initial stability/difficulty per rating, and the interval-equals-stability identity that holds at exactly 90% desired retention) computed from the library's own shipped default parameters, rather than against hand-copied constants - this stays correct even if FSRS.Core's defaults are retrained upstream.

Data model

Six user-owned Mongo collections, all indexed on (UserId, ...):

  • FlashcardDecks - flat (no nesting); every user has exactly one IsDefault deck, created lazily on first use.
  • FlashcardNotes - authored content (Prompt/Answer/Extra/Tags/Images). Cloze notes store their {{cN::answer::hint}} markup in Prompt.
  • FlashcardSchedules - one row per generated card, keyed by (NoteId, Variant). Basic notes always have variant 0; cloze notes get one row per distinct cloze number found in the prompt. Kept in a separate collection from notes so editing a note's content never discards its review history.
  • FlashcardReviewLogs - immutable, keyed by the client-generated review UUID for idempotent submission; carries full before/after schedule snapshots.
  • FlashcardMediaAssets - user-uploaded images (PNG/JPEG/WebP, 5 MB/4-per-note limits).
  • FlashcardSettings - one row per user (global new/review daily limits).

Attaching a question's own image to a flashcard does not copy bytes: FlashcardImageReference has a Kind ("question" vs "owned") so the client can resolve "question" references against the existing /api/questions/media/{id} endpoint instead of duplicating the asset.

Sibling cloze cards are buried (FlashcardSchedule.BuriedUntilUtc) until the next local study day whenever any one sibling is reviewed. Daily new/review limits reset at the same boundary. That boundary is supplied by the client as studyDayStartUtc on study/review requests (the local calendar day at session start, expressed as a UTC instant) rather than computed server-side, per the spec's client-local-day requirement.

API

All endpoints live under /api/flashcards (FlashcardsController.cs) and require USER/SUPER auth. Deck/note mutations use the same optimistic-concurrency (ExpectedRevision + 409 Conflict) pattern as the rest of the API. Review submission is idempotent on ClientReviewId; undo only allows reverting the single most recent review on a card (it refuses if the card's revision has moved since).

Progress metrics

GET /decks (FlashcardDeckSummary) and GET /counts (FlashcardDashboardCounts) expose a few derived stats the client can't compute itself:

  • Mastered (per-deck, in FlashcardDeckSummary.Mastered) - a card counts once it's graduated to review state and its FSRS stability has reached FlashcardScheduleStore.MasteryStabilityDaysThreshold (21 days), the same rough "won't need attention again soon" cutoff several SRS apps use. Deliberately stricter than the plain Review-state count, which includes cards that just graduated and haven't proven stable yet.
  • CurrentStreakDays (global, in FlashcardDashboardCounts) - consecutive UTC calendar days with at least one non-undone review, computed in FlashcardScheduling.ComputeStreakDays from a 400-day lookback of review timestamps. The streak counts through today even before today's first review (it only breaks once yesterday has no review either), matching typical habit-streak semantics rather than resetting to 0 at midnight.
  • DailyGoal / ReviewsToday (global) - DailyGoal is simply the account's configured FlashcardSettings.ReviewsPerDay; ReviewsToday is reviews logged since the client's studyDayStartUtc for /study/next calls, but since /counts has no per-request client-day concept it uses the server's own UTC calendar day instead - a day-boundary edge case is possible right around midnight, unlike the study-session endpoints which always trust the client's local day.

Import/export

CSV is UTF-8, text-only, headers stable_id,deck,type,prompt,answer,extra,tags, source_question_external_id (tags semicolon-separated); it omits images, schedule state, and review history and reports those omissions via the X-Flashcard-Export-Omissions response header. App JSON is full-fidelity (decks, settings, notes, schedules, review logs, base64 images) and merges on import by the note's stable UUID. Both formats go through a create/update/skip/error preview (FlashcardImportPreviews, TTL-expired after an hour) before import/commit applies it; commit never silently drops a row - everything is created, updated, skipped as an exact-content duplicate, or reported as a row-level error. Limits: 25 MB and 10,000 notes per import.

The Godot client (scripts/autoload/flashcard_service.gd) picks a file, uploads it to the matching preview endpoint as multipart/form-data, and shows the create/update/skip/error counts before letting the user commit or cancel. Export streams the response body and saves it client side. File selection and download both branch on OS.has_feature("web"): on web, a hidden <input type="file">/FileReader pair (bridged back into GDScript via JavaScriptBridge.create_callback) and an anchor-click data-URL download stand in for a native picker/save dialog; on desktop, a FileDialog and the OS downloads folder are used instead.

Deployment

No new secrets. FlashcardsController/stores register the same way as the rest of the API (Program.cs, AddSingleton). The Godot client resolves its API base URL from step3/flashcards_api_url in project.godot, following the same same-origin-on-web resolution as every other autoload service (api_endpoint_config.gd).