Adds a full flashcards system: FSRS-based scheduling, cloze/basic card types, CSV import/export, media storage, and study UI, wired into navigation and the dashboard alongside existing question flows.
4.8 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 oneIsDefaultdeck, created lazily on first use.FlashcardNotes- authored content (Prompt/Answer/Extra/Tags/Images). Cloze notes store their{{cN::answer::hint}}markup inPrompt.FlashcardSchedules- one row per generated card, keyed by(NoteId, Variant). Basic notes always have variant0; 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).
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.
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).