Add flashcards feature with spaced repetition scheduling
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.
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace QuestionsApi.Models;
|
||||
|
||||
public static class FlashcardTypes
|
||||
{
|
||||
public const string Basic = "basic";
|
||||
public const string Cloze = "cloze";
|
||||
public static readonly HashSet<string> All = [Basic, Cloze];
|
||||
}
|
||||
|
||||
public static class FlashcardRatings
|
||||
{
|
||||
public const string Again = "again";
|
||||
public const string Hard = "hard";
|
||||
public const string Good = "good";
|
||||
public const string Easy = "easy";
|
||||
public static readonly string[] All = [Again, Hard, Good, Easy];
|
||||
}
|
||||
|
||||
public static class FlashcardScheduleStates
|
||||
{
|
||||
public const string New = "new";
|
||||
public const string Learning = "learning";
|
||||
public const string Review = "review";
|
||||
public const string Relearning = "relearning";
|
||||
}
|
||||
|
||||
public sealed class FlashcardDeck
|
||||
{
|
||||
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
|
||||
public string UserId { get; set; } = "";
|
||||
public string StableId { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Name { get; set; } = "";
|
||||
public string NormalizedName { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public bool IsDefault { get; set; }
|
||||
public int? NewCardsPerDay { get; set; }
|
||||
public int? ReviewsPerDay { get; set; }
|
||||
public double? DesiredRetention { get; set; }
|
||||
public long Revision { get; set; } = 1;
|
||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ArchivedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardImageReference
|
||||
{
|
||||
public string Kind { get; set; } = "owned";
|
||||
public string AssetId { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class FlashcardNote
|
||||
{
|
||||
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
|
||||
public string UserId { get; set; } = "";
|
||||
public string StableId { get; set; } = Guid.NewGuid().ToString();
|
||||
public string DeckId { get; set; } = "";
|
||||
public string Type { get; set; } = FlashcardTypes.Basic;
|
||||
public string Prompt { get; set; } = "";
|
||||
public string Answer { get; set; } = "";
|
||||
public string Extra { get; set; } = "";
|
||||
public List<string> Tags { get; set; } = [];
|
||||
public List<FlashcardImageReference> Images { get; set; } = [];
|
||||
public string SourceQuestionExternalId { get; set; } = "";
|
||||
public string ContentFingerprint { get; set; } = "";
|
||||
public long Revision { get; set; } = 1;
|
||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ArchivedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardSchedule
|
||||
{
|
||||
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
|
||||
public string UserId { get; set; } = "";
|
||||
public string NoteId { get; set; } = "";
|
||||
public int Variant { get; set; }
|
||||
public string State { get; set; } = FlashcardScheduleStates.New;
|
||||
public int? Step { get; set; }
|
||||
public double? Stability { get; set; }
|
||||
public double? Difficulty { get; set; }
|
||||
public DateTime DueUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? LastReviewUtc { get; set; }
|
||||
public DateTime? BuriedUntilUtc { get; set; }
|
||||
public bool IsSuspended { get; set; }
|
||||
public long Revision { get; set; } = 1;
|
||||
public string AlgorithmVersion { get; set; } = "fsrs-core-1.0.7";
|
||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class FlashcardReviewLog
|
||||
{
|
||||
[BsonId] public string Id { get; set; } = "";
|
||||
public string UserId { get; set; } = "";
|
||||
public string CardId { get; set; } = "";
|
||||
public string Rating { get; set; } = "";
|
||||
public DateTime ReviewedUtc { get; set; }
|
||||
public int DurationSeconds { get; set; }
|
||||
public FlashcardScheduleSnapshot Before { get; set; } = new();
|
||||
public FlashcardScheduleSnapshot After { get; set; } = new();
|
||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? UndoneUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardScheduleSnapshot
|
||||
{
|
||||
public string State { get; set; } = FlashcardScheduleStates.New;
|
||||
public int? Step { get; set; }
|
||||
public double? Stability { get; set; }
|
||||
public double? Difficulty { get; set; }
|
||||
public DateTime DueUtc { get; set; }
|
||||
public DateTime? LastReviewUtc { get; set; }
|
||||
public long Revision { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardMediaAsset
|
||||
{
|
||||
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
|
||||
public string UserId { get; set; } = "";
|
||||
public string FileName { get; set; } = "";
|
||||
public string ContentType { get; set; } = "";
|
||||
public byte[] Bytes { get; set; } = [];
|
||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class FlashcardSettings
|
||||
{
|
||||
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
|
||||
public string UserId { get; set; } = "";
|
||||
public int NewCardsPerDay { get; set; } = 20;
|
||||
public int ReviewsPerDay { get; set; } = 200;
|
||||
public double DesiredRetention { get; set; } = 0.90;
|
||||
public long Revision { get; set; } = 1;
|
||||
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class UpsertFlashcardDeckRequest
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public int? NewCardsPerDay { get; set; }
|
||||
public int? ReviewsPerDay { get; set; }
|
||||
public double? DesiredRetention { get; set; }
|
||||
public long? ExpectedRevision { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpsertFlashcardNoteRequest
|
||||
{
|
||||
public string DeckId { get; set; } = "";
|
||||
public string StableId { get; set; } = "";
|
||||
public string Type { get; set; } = FlashcardTypes.Basic;
|
||||
public string Prompt { get; set; } = "";
|
||||
public string Answer { get; set; } = "";
|
||||
public string Extra { get; set; } = "";
|
||||
public List<string> Tags { get; set; } = [];
|
||||
public List<FlashcardImageReference> Images { get; set; } = [];
|
||||
public string SourceQuestionExternalId { get; set; } = "";
|
||||
public long? ExpectedRevision { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateFlashcardSettingsRequest
|
||||
{
|
||||
public int NewCardsPerDay { get; set; } = 20;
|
||||
public int ReviewsPerDay { get; set; } = 200;
|
||||
public double DesiredRetention { get; set; } = 0.90;
|
||||
public long? ExpectedRevision { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardDeckSummary
|
||||
{
|
||||
public FlashcardDeck Deck { get; set; } = new();
|
||||
public int Total { get; set; }
|
||||
public int Due { get; set; }
|
||||
public int New { get; set; }
|
||||
public int Suspended { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardLibraryResponse
|
||||
{
|
||||
public List<FlashcardNote> Items { get; set; } = [];
|
||||
public int Total { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardLibraryQuery
|
||||
{
|
||||
public string? DeckId { get; set; }
|
||||
public string? Tag { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? Search { get; set; }
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 50;
|
||||
}
|
||||
|
||||
public sealed class FlashcardStudyRequest
|
||||
{
|
||||
public string DeckId { get; set; } = "";
|
||||
public List<string> ExcludeCardIds { get; set; } = [];
|
||||
public DateTime StudyDayStartUtc { get; set; } = DateTime.UtcNow.Date;
|
||||
}
|
||||
|
||||
public sealed class FlashcardBulkNoteIdsRequest
|
||||
{
|
||||
public List<string> NoteIds { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class FlashcardMoveRequest
|
||||
{
|
||||
public List<string> NoteIds { get; set; } = [];
|
||||
public string TargetDeckId { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class FlashcardBatchReviewRequest
|
||||
{
|
||||
public List<FlashcardReviewRequest> Reviews { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class FlashcardBatchReviewResponse
|
||||
{
|
||||
public List<FlashcardReviewResponse> Results { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class FlashcardUndoRequest
|
||||
{
|
||||
public string ReviewLogId { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class FlashcardRatingOption
|
||||
{
|
||||
public string Rating { get; set; } = "";
|
||||
public string IntervalLabel { get; set; } = "";
|
||||
public DateTime DueUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardStudyCardResponse
|
||||
{
|
||||
public FlashcardNote? Note { get; set; }
|
||||
public FlashcardSchedule? Card { get; set; }
|
||||
public List<FlashcardRatingOption> RatingOptions { get; set; } = [];
|
||||
public int DueRemaining { get; set; }
|
||||
public int NewRemaining { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardReviewRequest
|
||||
{
|
||||
public string ClientReviewId { get; set; } = "";
|
||||
public string CardId { get; set; } = "";
|
||||
public string Rating { get; set; } = "";
|
||||
public long ExpectedRevision { get; set; }
|
||||
public DateTime ReviewedUtc { get; set; }
|
||||
public int DurationSeconds { get; set; }
|
||||
public DateTime StudyDayStartUtc { get; set; } = DateTime.UtcNow.Date;
|
||||
}
|
||||
|
||||
public sealed class FlashcardReviewResponse
|
||||
{
|
||||
public FlashcardSchedule Card { get; set; } = new();
|
||||
public List<FlashcardRatingOption> RatingOptions { get; set; } = [];
|
||||
public bool Duplicate { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FlashcardDashboardCounts
|
||||
{
|
||||
public int Due { get; set; }
|
||||
public int NewAvailable { get; set; }
|
||||
public int ReviewedThisWeek { get; set; }
|
||||
}
|
||||
|
||||
public static class FlashcardImportFormats
|
||||
{
|
||||
public const string Csv = "csv";
|
||||
public const string Json = "json";
|
||||
}
|
||||
|
||||
public static class FlashcardImportRowActions
|
||||
{
|
||||
public const string Create = "create";
|
||||
public const string Update = "update";
|
||||
public const string Skip = "skip";
|
||||
public const string Error = "error";
|
||||
}
|
||||
|
||||
public sealed class FlashcardImportPendingRecord
|
||||
{
|
||||
public int RowNumber { get; set; }
|
||||
public string Action { get; set; } = FlashcardImportRowActions.Create;
|
||||
public string? Error { get; set; }
|
||||
public string StableId { get; set; } = "";
|
||||
public string DeckName { get; set; } = "";
|
||||
public string Type { get; set; } = FlashcardTypes.Basic;
|
||||
public string Prompt { get; set; } = "";
|
||||
public string Answer { get; set; } = "";
|
||||
public string Extra { get; set; } = "";
|
||||
public List<string> Tags { get; set; } = [];
|
||||
public string SourceQuestionExternalId { get; set; } = "";
|
||||
public List<FlashcardSchedule> Cards { get; set; } = [];
|
||||
public List<FlashcardReviewLog> ReviewLogs { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class FlashcardImportPreview
|
||||
{
|
||||
[BsonId] public string Id { get; set; } = "";
|
||||
public string UserId { get; set; } = "";
|
||||
public string Format { get; set; } = FlashcardImportFormats.Csv;
|
||||
public List<FlashcardImportPendingRecord> Records { get; set; } = [];
|
||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime ExpiresUtc { get; set; } = DateTime.UtcNow.AddHours(1);
|
||||
}
|
||||
|
||||
public sealed class FlashcardImportPreviewRow
|
||||
{
|
||||
public int RowNumber { get; set; }
|
||||
public string Action { get; set; } = FlashcardImportRowActions.Create;
|
||||
public string? Error { get; set; }
|
||||
public string StableId { get; set; } = "";
|
||||
public string Deck { get; set; } = "";
|
||||
public string Type { get; set; } = FlashcardTypes.Basic;
|
||||
public string Prompt { get; set; } = "";
|
||||
public string Answer { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class FlashcardImportPreviewResponse
|
||||
{
|
||||
public string ImportToken { get; set; } = "";
|
||||
public int CreateCount { get; set; }
|
||||
public int UpdateCount { get; set; }
|
||||
public int SkipCount { get; set; }
|
||||
public int ErrorCount { get; set; }
|
||||
public List<FlashcardImportPreviewRow> Rows { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class FlashcardImportCommitRequest
|
||||
{
|
||||
public string ImportToken { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class FlashcardImportCommitResponse
|
||||
{
|
||||
public int Created { get; set; }
|
||||
public int Updated { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
public int Errored { get; set; }
|
||||
public List<FlashcardImportPreviewRow> Errors { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class FlashcardExportOmission
|
||||
{
|
||||
public string Kind { get; set; } = "";
|
||||
public string Reason { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class FlashcardJsonExport
|
||||
{
|
||||
public int SchemaVersion { get; set; } = 1;
|
||||
public DateTime ExportedUtc { get; set; } = DateTime.UtcNow;
|
||||
public FlashcardSettings Settings { get; set; } = new();
|
||||
public List<FlashcardDeck> Decks { get; set; } = [];
|
||||
public List<FlashcardJsonExportNote> Notes { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class FlashcardJsonExportNote
|
||||
{
|
||||
public FlashcardNote Note { get; set; } = new();
|
||||
public List<FlashcardSchedule> Cards { get; set; } = [];
|
||||
public List<FlashcardReviewLog> ReviewLogs { get; set; } = [];
|
||||
public List<FlashcardJsonExportImage> Images { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class FlashcardJsonExportImage
|
||||
{
|
||||
public string AssetId { get; set; } = "";
|
||||
public string FileName { get; set; } = "";
|
||||
public string ContentType { get; set; } = "";
|
||||
public string Base64Bytes { get; set; } = "";
|
||||
}
|
||||
Reference in New Issue
Block a user