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.
114 lines
6.9 KiB
Markdown
114 lines
6.9 KiB
Markdown
# 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](https://www.nuget.org/packages/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`).
|