From 589a22b89d03e9762b8c9bdb9a3e1152469ffc4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=84=AD=E6=B2=9B=E8=BB=92?= Date: Sat, 27 Sep 2025 23:36:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90CardSet=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=B8=85=E7=90=86=E5=92=8C=E6=B8=AC=E8=A9=A6=E8=B3=87?= =?UTF-8?q?=E6=96=99=E5=84=AA=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主要改動 ### 後端 CardSet 功能完全移除 - 刪除 CardSet.cs 實體模型 - 移除 Flashcard 中的 CardSetId 欄位和導航屬性 - 清理 User 實體中的 CardSets 導航屬性 - 更新 DbContext 移除 CardSet 相關配置 - 修復 FlashcardsController、StatsController、StudyController 中的 CardSet 引用 - 創建和執行資料庫 migration 移除 CardSet 表和相關約束 ### API 功能修復和優化 - 修復 FlashcardsController GetFlashcards 方法的 500 錯誤 - 恢復例句圖片處理功能 (FlashcardExampleImages) - 增強錯誤日誌和調試資訊 - 簡化後重新添加完整圖片處理邏輯 ### 前端測試資料完善 - 轉換 CSV 為完整的 API 響應格式 - 為所有詞彙添加圖片資料結構和URL - 修正 exampleTranslation 為 example 的正確中文翻譯 - 更新 review-design 頁面支援動態卡片切換 - 移除 cardSetId 相關欄位 ### 系統架構簡化 - 移除不使用的 CardSet 功能,專注核心 Flashcard 學習 - 統一資料格式,提升前後端一致性 - 完善測試環境的假資料支援 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../Controllers/FlashcardsController.cs | 15 +- .../Controllers/StatsController.cs | 21 +- .../Controllers/StudyController.cs | 8 +- .../DramaLing.Api/Data/DramaLingDbContext.cs | 21 - ...927144350_RemoveCardSetFeature.Designer.cs | 1496 +++++++++++++++++ .../20250927144350_RemoveCardSetFeature.cs | 83 + .../DramaLingDbContextModelSnapshot.cs | 71 - .../Models/Entities/Flashcard.cs | 3 - backend/DramaLing.Api/Models/Entities/User.cs | 1 - .../Services/StudySessionService.cs | 1 - frontend/app/review-design/example-data.json | 426 +++-- frontend/app/review-design/page.tsx | 76 +- 12 files changed, 1970 insertions(+), 252 deletions(-) create mode 100644 backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.Designer.cs create mode 100644 backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.cs diff --git a/backend/DramaLing.Api/Controllers/FlashcardsController.cs b/backend/DramaLing.Api/Controllers/FlashcardsController.cs index 4f5e5a5..a6393b5 100644 --- a/backend/DramaLing.Api/Controllers/FlashcardsController.cs +++ b/backend/DramaLing.Api/Controllers/FlashcardsController.cs @@ -19,6 +19,7 @@ public class FlashcardsController : ControllerBase private readonly DramaLingDbContext _context; private readonly ILogger _logger; private readonly IImageStorageService _imageStorageService; + private readonly IAuthService _authService; // 🆕 智能複習服務依賴 private readonly ISpacedRepetitionService _spacedRepetitionService; private readonly IReviewTypeSelectorService _reviewTypeSelectorService; @@ -28,6 +29,7 @@ public class FlashcardsController : ControllerBase DramaLingDbContext context, ILogger logger, IImageStorageService imageStorageService, + IAuthService authService, ISpacedRepetitionService spacedRepetitionService, IReviewTypeSelectorService reviewTypeSelectorService, IQuestionGeneratorService questionGeneratorService) @@ -35,6 +37,7 @@ public class FlashcardsController : ControllerBase _context = context; _logger = logger; _imageStorageService = imageStorageService; + _authService = authService; _spacedRepetitionService = spacedRepetitionService; _reviewTypeSelectorService = reviewTypeSelectorService; _questionGeneratorService = questionGeneratorService; @@ -66,6 +69,7 @@ public class FlashcardsController : ControllerBase try { var userId = GetUserId(); + _logger.LogInformation("GetFlashcards called for user: {UserId}", userId); var query = _context.Flashcards .Include(f => f.FlashcardExampleImages) @@ -73,6 +77,8 @@ public class FlashcardsController : ControllerBase .Where(f => f.UserId == userId && !f.IsArchived) .AsQueryable(); + _logger.LogInformation("Base query created successfully"); + // 搜尋篩選 (擴展支援例句內容) if (!string.IsNullOrEmpty(search)) { @@ -119,11 +125,14 @@ public class FlashcardsController : ControllerBase } } + _logger.LogInformation("Executing database query..."); var flashcards = await query .AsNoTracking() // 效能優化:只讀查詢 .OrderByDescending(f => f.CreatedAt) .ToListAsync(); + _logger.LogInformation("Found {Count} flashcards", flashcards.Count); + // 生成圖片資訊 var flashcardDtos = new List(); foreach (var flashcard in flashcards) @@ -177,8 +186,9 @@ public class FlashcardsController : ControllerBase } catch (Exception ex) { - _logger.LogError(ex, "Error getting flashcards for user"); - return StatusCode(500, new { Success = false, Error = "Failed to load flashcards" }); + _logger.LogError(ex, "Error getting flashcards for user: {Message}", ex.Message); + _logger.LogError("Stack trace: {StackTrace}", ex.StackTrace); + return StatusCode(500, new { Success = false, Error = "Failed to load flashcards", Details = ex.Message }); } } @@ -239,7 +249,6 @@ public class FlashcardsController : ControllerBase { Id = Guid.NewGuid(), UserId = userId, - CardSetId = null, // 暫時不使用 CardSet Word = request.Word, Translation = request.Translation, Definition = request.Definition ?? "", diff --git a/backend/DramaLing.Api/Controllers/StatsController.cs b/backend/DramaLing.Api/Controllers/StatsController.cs index 44b5d76..e7e7de7 100644 --- a/backend/DramaLing.Api/Controllers/StatsController.cs +++ b/backend/DramaLing.Api/Controllers/StatsController.cs @@ -39,22 +39,6 @@ public class StatsController : ControllerBase // 並行獲取統計數據 var totalWordsTask = _context.Flashcards.CountAsync(f => f.UserId == userId); - var cardSetsTask = _context.CardSets - .Where(cs => cs.UserId == userId) - .OrderByDescending(cs => cs.CreatedAt) - .Take(5) - .Select(cs => new - { - cs.Id, - cs.Name, - Count = cs.CardCount, - Progress = cs.CardCount > 0 ? - _context.Flashcards - .Where(f => f.CardSetId == cs.Id) - .Average(f => (double?)f.MasteryLevel) ?? 0 : 0, - LastStudied = cs.CreatedAt - }) - .ToListAsync(); var recentCardsTask = _context.Flashcards .Where(f => f.UserId == userId) @@ -73,10 +57,9 @@ public class StatsController : ControllerBase .FirstOrDefaultAsync(ds => ds.UserId == userId && ds.Date == today); // 等待所有查詢完成 - await Task.WhenAll(totalWordsTask, cardSetsTask, recentCardsTask, todayStatsTask); + await Task.WhenAll(totalWordsTask, recentCardsTask, todayStatsTask); var totalWords = await totalWordsTask; - var cardSets = await cardSetsTask; var recentCards = await recentCardsTask; var todayStats = await todayStatsTask; @@ -107,7 +90,7 @@ public class StatsController : ControllerBase new { Word = "perspective", Translation = "觀點", Status = "new" }, new { Word = "substantial", Translation = "大量的", Status = "learned" } }, - CardSets = cardSets + CardSets = new object[0] // 移除 CardSet 功能 } }); } diff --git a/backend/DramaLing.Api/Controllers/StudyController.cs b/backend/DramaLing.Api/Controllers/StudyController.cs index d5b0322..da1ea21 100644 --- a/backend/DramaLing.Api/Controllers/StudyController.cs +++ b/backend/DramaLing.Api/Controllers/StudyController.cs @@ -43,7 +43,6 @@ public class StudyController : ControllerBase var today = DateTime.Today; var query = _context.Flashcards - .Include(f => f.CardSet) .Where(f => f.UserId == userId); // 篩選到期和新詞卡 @@ -88,8 +87,8 @@ public class StudyController : ControllerBase x.Card.DifficultyLevel, CardSet = new { - x.Card.CardSet.Name, - x.Card.CardSet.Color + Name = "Default", + Color = "bg-blue-500" }, x.Priority, x.IsDue, @@ -187,7 +186,6 @@ public class StudyController : ControllerBase // 獲取詞卡詳細資訊 var cards = await _context.Flashcards - .Include(f => f.CardSet) .Where(f => f.UserId == userId && request.CardIds.Contains(f.Id)) .ToListAsync(); @@ -217,7 +215,7 @@ public class StudyController : ControllerBase c.MasteryLevel, c.EasinessFactor, c.Repetitions, - CardSet = new { c.CardSet.Name, c.CardSet.Color } + CardSet = new { Name = "Default", Color = "bg-blue-500" } }), TotalCards = orderedCards.Count, StartedAt = session.StartedAt diff --git a/backend/DramaLing.Api/Data/DramaLingDbContext.cs b/backend/DramaLing.Api/Data/DramaLingDbContext.cs index 1554346..d9068bf 100644 --- a/backend/DramaLing.Api/Data/DramaLingDbContext.cs +++ b/backend/DramaLing.Api/Data/DramaLingDbContext.cs @@ -13,7 +13,6 @@ public class DramaLingDbContext : DbContext // DbSets public DbSet Users { get; set; } public DbSet UserSettings { get; set; } - public DbSet CardSets { get; set; } public DbSet Flashcards { get; set; } public DbSet Tags { get; set; } public DbSet FlashcardTags { get; set; } @@ -39,7 +38,6 @@ public class DramaLingDbContext : DbContext // 設定表名稱 (與 Supabase 一致) modelBuilder.Entity().ToTable("user_profiles"); modelBuilder.Entity().ToTable("user_settings"); - modelBuilder.Entity().ToTable("card_sets"); modelBuilder.Entity().ToTable("flashcards"); modelBuilder.Entity().ToTable("tags"); modelBuilder.Entity().ToTable("flashcard_tags"); @@ -114,7 +112,6 @@ public class DramaLingDbContext : DbContext { var flashcardEntity = modelBuilder.Entity(); flashcardEntity.Property(f => f.UserId).HasColumnName("user_id"); - flashcardEntity.Property(f => f.CardSetId).HasColumnName("card_set_id"); flashcardEntity.Property(f => f.PartOfSpeech).HasColumnName("part_of_speech"); flashcardEntity.Property(f => f.ExampleTranslation).HasColumnName("example_translation"); flashcardEntity.Property(f => f.EasinessFactor).HasColumnName("easiness_factor"); @@ -200,31 +197,13 @@ public class DramaLingDbContext : DbContext private void ConfigureRelationships(ModelBuilder modelBuilder) { - // CardSet 配置 - 手動 GUID 生成 - modelBuilder.Entity() - .Property(cs => cs.Id) - .ValueGeneratedNever(); // 關閉自動生成,允許手動設定 GUID - // User relationships - modelBuilder.Entity() - .HasOne(cs => cs.User) - .WithMany(u => u.CardSets) - .HasForeignKey(cs => cs.UserId) - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() .HasOne(f => f.User) .WithMany(u => u.Flashcards) .HasForeignKey(f => f.UserId) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(f => f.CardSet) - .WithMany(cs => cs.Flashcards) - .HasForeignKey(f => f.CardSetId) - .IsRequired(false) // 允許 CardSetId 為 null - .OnDelete(DeleteBehavior.SetNull); - // Study relationships modelBuilder.Entity() .HasOne(ss => ss.User) diff --git a/backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.Designer.cs b/backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.Designer.cs new file mode 100644 index 0000000..6b892a5 --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.Designer.cs @@ -0,0 +1,1496 @@ +// +using System; +using DramaLing.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + [DbContext(typeof(DramaLingDbContext))] + [Migration("20250927144350_RemoveCardSetFeature")] + partial class RemoveCardSetFeature + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.AudioCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Accent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AudioUrl") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DurationMs") + .HasColumnType("INTEGER") + .HasColumnName("duration_ms"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("LastAccessed") + .HasColumnType("TEXT") + .HasColumnName("last_accessed"); + + b.Property("TextContent") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("text_content"); + + b.Property("TextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("text_hash"); + + b.Property("VoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("voice_id"); + + b.HasKey("Id"); + + b.HasIndex("LastAccessed") + .HasDatabaseName("IX_AudioCache_LastAccessed"); + + b.HasIndex("TextHash") + .IsUnique() + .HasDatabaseName("IX_AudioCache_TextHash"); + + b.ToTable("audio_cache", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AiApiCalls") + .HasColumnType("INTEGER") + .HasColumnName("ai_api_calls"); + + b.Property("CardsGenerated") + .HasColumnType("INTEGER") + .HasColumnName("cards_generated"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("SessionCount") + .HasColumnType("INTEGER") + .HasColumnName("session_count"); + + b.Property("StudyTimeSeconds") + .HasColumnType("INTEGER") + .HasColumnName("study_time_seconds"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("WordsCorrect") + .HasColumnType("INTEGER") + .HasColumnName("words_correct"); + + b.Property("WordsStudied") + .HasColumnType("INTEGER") + .HasColumnName("words_studied"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Date") + .IsUnique(); + + b.ToTable("daily_stats", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AdminNotes") + .HasColumnType("TEXT") + .HasColumnName("admin_notes"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ReportType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("report_type"); + + b.Property("ResolvedAt") + .HasColumnType("TEXT") + .HasColumnName("resolved_at"); + + b.Property("ResolvedBy") + .HasColumnType("TEXT") + .HasColumnName("resolved_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StudyMode") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("study_mode"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("ResolvedBy"); + + b.HasIndex("UserId"); + + b.ToTable("error_reports", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AltText") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("alt_text"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("content_hash"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiDescription") + .HasColumnType("TEXT") + .HasColumnName("gemini_description"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("ImageHeight") + .HasColumnType("INTEGER") + .HasColumnName("image_height"); + + b.Property("ImageWidth") + .HasColumnType("INTEGER") + .HasColumnName("image_width"); + + b.Property("ModerationNotes") + .HasColumnType("TEXT") + .HasColumnName("moderation_notes"); + + b.Property("ModerationStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("moderation_status"); + + b.Property("QualityScore") + .HasColumnType("TEXT") + .HasColumnName("quality_score"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("relative_path"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateModel") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_model"); + + b.Property("ReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("replicate_prompt"); + + b.Property("ReplicateVersion") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_version"); + + b.Property("TotalGenerationCost") + .HasColumnType("TEXT") + .HasColumnName("total_generation_cost"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("AccessCount"); + + b.HasIndex("ContentHash") + .IsUnique(); + + b.ToTable("example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DifficultyLevel") + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("difficulty_level"); + + b.Property("EasinessFactor") + .HasColumnType("REAL") + .HasColumnName("easiness_factor"); + + b.Property("Example") + .HasColumnType("TEXT"); + + b.Property("ExampleTranslation") + .HasColumnType("TEXT") + .HasColumnName("example_translation"); + + b.Property("IntervalDays") + .HasColumnType("INTEGER") + .HasColumnName("interval_days"); + + b.Property("IsArchived") + .HasColumnType("INTEGER") + .HasColumnName("is_archived"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER") + .HasColumnName("is_favorite"); + + b.Property("LastQuestionType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastReviewedAt") + .HasColumnType("TEXT") + .HasColumnName("last_reviewed_at"); + + b.Property("MasteryLevel") + .HasColumnType("INTEGER") + .HasColumnName("mastery_level"); + + b.Property("NextReviewDate") + .HasColumnType("TEXT") + .HasColumnName("next_review_date"); + + b.Property("PartOfSpeech") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("part_of_speech"); + + b.Property("Pronunciation") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Repetitions") + .HasColumnType("INTEGER"); + + b.Property("ReviewHistory") + .HasColumnType("TEXT"); + + b.Property("TimesCorrect") + .HasColumnType("INTEGER") + .HasColumnName("times_correct"); + + b.Property("TimesReviewed") + .HasColumnType("INTEGER") + .HasColumnName("times_reviewed"); + + b.Property("Translation") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("flashcards", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ExampleImageId") + .HasColumnType("TEXT") + .HasColumnName("example_image_id"); + + b.Property("ContextRelevance") + .HasColumnType("TEXT") + .HasColumnName("context_relevance"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER") + .HasColumnName("display_order"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER") + .HasColumnName("is_primary"); + + b.HasKey("FlashcardId", "ExampleImageId"); + + b.HasIndex("ExampleImageId"); + + b.ToTable("flashcard_example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("TagId") + .HasColumnType("TEXT") + .HasColumnName("tag_id"); + + b.HasKey("FlashcardId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("flashcard_tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FinalReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("final_replicate_prompt"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("GeminiCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_completed_at"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("gemini_error_message"); + + b.Property("GeminiProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("gemini_processing_time_ms"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("GeminiStartedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_started_at"); + + b.Property("GeminiStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("gemini_status"); + + b.Property("GeneratedDescription") + .HasColumnType("TEXT") + .HasColumnName("generated_description"); + + b.Property("GeneratedImageId") + .HasColumnType("TEXT") + .HasColumnName("generated_image_id"); + + b.Property("OriginalRequest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("original_request"); + + b.Property("OverallStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("overall_status"); + + b.Property("ReplicateCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_completed_at"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("replicate_error_message"); + + b.Property("ReplicateProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("replicate_processing_time_ms"); + + b.Property("ReplicateStartedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_started_at"); + + b.Property("ReplicateStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("replicate_status"); + + b.Property("TotalCost") + .HasColumnType("TEXT") + .HasColumnName("total_cost"); + + b.Property("TotalProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("total_processing_time_ms"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("GeneratedImageId"); + + b.HasIndex("UserId"); + + b.ToTable("image_generation_requests", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccuracyScore") + .HasColumnType("TEXT") + .HasColumnName("accuracy_score"); + + b.Property("AudioUrl") + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CompletenessScore") + .HasColumnType("TEXT") + .HasColumnName("completeness_score"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("FluencyScore") + .HasColumnType("TEXT") + .HasColumnName("fluency_score"); + + b.Property("OverallScore") + .HasColumnType("INTEGER") + .HasColumnName("overall_score"); + + b.Property("PhonemeScores") + .HasColumnType("TEXT") + .HasColumnName("phoneme_scores"); + + b.Property("PracticeMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("practice_mode"); + + b.Property("ProsodyScore") + .HasColumnType("TEXT") + .HasColumnName("prosody_score"); + + b.Property("StudySessionId") + .HasColumnType("TEXT") + .HasColumnName("study_session_id"); + + b.Property("Suggestions") + .HasColumnType("TEXT") + .HasColumnName("suggestions"); + + b.Property("TargetText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("target_text"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("StudySessionId") + .HasDatabaseName("IX_PronunciationAssessment_Session"); + + b.HasIndex("UserId", "FlashcardId") + .HasDatabaseName("IX_PronunciationAssessment_UserFlashcard"); + + b.ToTable("pronunciation_assessments", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.SentenceAnalysisCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("AnalysisResult") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CorrectedText") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("GrammarCorrections") + .HasColumnType("TEXT"); + + b.Property("HasGrammarErrors") + .HasColumnType("INTEGER"); + + b.Property("HighValueWords") + .HasColumnType("TEXT"); + + b.Property("IdiomsDetected") + .HasColumnType("TEXT"); + + b.Property("InputText") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InputTextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastAccessedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Expires"); + + b.HasIndex("InputTextHash") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash"); + + b.HasIndex("InputTextHash", "ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash_Expires"); + + b.ToTable("SentenceAnalysisCache"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("FlashcardId") + .HasColumnType("TEXT"); + + b.Property("IsCompleted") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("PlannedTests") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlannedTestsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedAt") + .HasColumnType("TEXT"); + + b.Property("StudySessionId") + .HasColumnType("TEXT"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("StudySessionId"); + + b.ToTable("study_cards", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("IsCorrect") + .HasColumnType("INTEGER") + .HasColumnName("is_correct"); + + b.Property("NewEasinessFactor") + .HasColumnType("REAL"); + + b.Property("NewIntervalDays") + .HasColumnType("INTEGER"); + + b.Property("NewRepetitions") + .HasColumnType("INTEGER"); + + b.Property("NextReviewDate") + .HasColumnType("TEXT"); + + b.Property("PreviousEasinessFactor") + .HasColumnType("REAL"); + + b.Property("PreviousIntervalDays") + .HasColumnType("INTEGER"); + + b.Property("PreviousRepetitions") + .HasColumnType("INTEGER"); + + b.Property("QualityRating") + .HasColumnType("INTEGER") + .HasColumnName("quality_rating"); + + b.Property("ResponseTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("response_time_ms"); + + b.Property("SessionId") + .HasColumnType("TEXT") + .HasColumnName("session_id"); + + b.Property("StudiedAt") + .HasColumnType("TEXT") + .HasColumnName("studied_at"); + + b.Property("StudyMode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("study_mode"); + + b.Property("UserAnswer") + .HasColumnType("TEXT") + .HasColumnName("user_answer"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("SessionId"); + + b.HasIndex("UserId", "FlashcardId", "StudyMode") + .IsUnique() + .HasDatabaseName("IX_StudyRecord_UserCard_TestType_Unique"); + + b.ToTable("study_records", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AverageResponseTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("average_response_time_ms"); + + b.Property("CompletedCards") + .HasColumnType("INTEGER"); + + b.Property("CompletedTests") + .HasColumnType("INTEGER"); + + b.Property("CorrectCount") + .HasColumnType("INTEGER") + .HasColumnName("correct_count"); + + b.Property("CurrentCardIndex") + .HasColumnType("INTEGER"); + + b.Property("CurrentTestType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DurationSeconds") + .HasColumnType("INTEGER") + .HasColumnName("duration_seconds"); + + b.Property("EndedAt") + .HasColumnType("TEXT") + .HasColumnName("ended_at"); + + b.Property("SessionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("session_type"); + + b.Property("StartedAt") + .HasColumnType("TEXT") + .HasColumnName("started_at"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TotalCards") + .HasColumnType("INTEGER") + .HasColumnName("total_cards"); + + b.Property("TotalTests") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("study_sessions", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UsageCount") + .HasColumnType("INTEGER") + .HasColumnName("usage_count"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.TestResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("ConfidenceLevel") + .HasColumnType("INTEGER"); + + b.Property("IsCorrect") + .HasColumnType("INTEGER"); + + b.Property("ResponseTimeMs") + .HasColumnType("INTEGER"); + + b.Property("StudyCardId") + .HasColumnType("TEXT"); + + b.Property("TestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UserAnswer") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StudyCardId"); + + b.ToTable("test_results", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarUrl") + .HasColumnType("TEXT") + .HasColumnName("avatar_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayName") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("display_name"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("email"); + + b.Property("EnglishLevel") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("english_level"); + + b.Property("IsLevelVerified") + .HasColumnType("INTEGER") + .HasColumnName("is_level_verified"); + + b.Property("LevelNotes") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("level_notes"); + + b.Property("LevelUpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("level_updated_at"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("password_hash"); + + b.Property("Preferences") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("preferences"); + + b.Property("SubscriptionType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("subscription_type"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("user_profiles", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("AutoPlayEnabled") + .HasColumnType("INTEGER") + .HasColumnName("auto_play_enabled"); + + b.Property("DefaultSpeed") + .HasColumnType("TEXT") + .HasColumnName("default_speed"); + + b.Property("EnableDetailedFeedback") + .HasColumnType("INTEGER") + .HasColumnName("enable_detailed_feedback"); + + b.Property("PreferredAccent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("preferred_accent"); + + b.Property("PreferredVoiceFemale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_female"); + + b.Property("PreferredVoiceMale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_male"); + + b.Property("PronunciationDifficulty") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("pronunciation_difficulty"); + + b.Property("TargetScoreThreshold") + .HasColumnType("INTEGER") + .HasColumnName("target_score_threshold"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("UserId"); + + b.ToTable("user_audio_preferences", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AutoPlayAudio") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DailyGoal") + .HasColumnType("INTEGER"); + + b.Property("DifficultyPreference") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("ReminderTime") + .HasColumnType("TEXT"); + + b.Property("ShowPronunciation") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_settings", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("HighValueWordClicks") + .HasColumnType("INTEGER"); + + b.Property("LowValueWordClicks") + .HasColumnType("INTEGER"); + + b.Property("SentenceAnalysisCount") + .HasColumnType("INTEGER"); + + b.Property("TotalApiCalls") + .HasColumnType("INTEGER"); + + b.Property("UniqueWordsQueried") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_WordQueryUsageStats_CreatedAt"); + + b.HasIndex("UserId", "Date") + .IsUnique() + .HasDatabaseName("IX_WordQueryUsageStats_UserDate"); + + b.ToTable("WordQueryUsageStats"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("DailyStats") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("ErrorReports") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.User", "ResolvedByUser") + .WithMany() + .HasForeignKey("ResolvedBy") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("ErrorReports") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("ResolvedByUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("Flashcards") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "ExampleImage") + .WithMany("FlashcardExampleImages") + .HasForeignKey("ExampleImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardExampleImages") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExampleImage"); + + b.Navigation("Flashcard"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardTags") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Tag", "Tag") + .WithMany("FlashcardTags") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "GeneratedImage") + .WithMany() + .HasForeignKey("GeneratedImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("GeneratedImage"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.StudySession", "StudySession") + .WithMany() + .HasForeignKey("StudySessionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("StudySession"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.StudySession", "StudySession") + .WithMany("StudyCards") + .HasForeignKey("StudySessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("StudySession"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("StudyRecords") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.StudySession", "Session") + .WithMany("StudyRecords") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("Session"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("StudySessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.TestResult", b => + { + b.HasOne("DramaLing.Api.Models.Entities.StudyCard", "StudyCard") + .WithMany("TestResults") + .HasForeignKey("StudyCardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("StudyCard"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne() + .HasForeignKey("DramaLing.Api.Models.Entities.UserAudioPreferences", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne("Settings") + .HasForeignKey("DramaLing.Api.Models.Entities.UserSettings", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Navigation("FlashcardExampleImages"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Navigation("ErrorReports"); + + b.Navigation("FlashcardExampleImages"); + + b.Navigation("FlashcardTags"); + + b.Navigation("StudyRecords"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b => + { + b.Navigation("TestResults"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b => + { + b.Navigation("StudyCards"); + + b.Navigation("StudyRecords"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Navigation("FlashcardTags"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Navigation("DailyStats"); + + b.Navigation("ErrorReports"); + + b.Navigation("Flashcards"); + + b.Navigation("Settings"); + + b.Navigation("StudySessions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.cs b/backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.cs new file mode 100644 index 0000000..5b1bd85 --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.cs @@ -0,0 +1,83 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + /// + public partial class RemoveCardSetFeature : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_flashcards_card_sets_card_set_id", + table: "flashcards"); + + migrationBuilder.DropTable( + name: "card_sets"); + + migrationBuilder.DropIndex( + name: "IX_flashcards_card_set_id", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "card_set_id", + table: "flashcards"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "card_set_id", + table: "flashcards", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateTable( + name: "card_sets", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + CardCount = table.Column(type: "INTEGER", nullable: false), + Color = table.Column(type: "TEXT", maxLength: 50, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + IsDefault = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 255, nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_card_sets", x => x.Id); + table.ForeignKey( + name: "FK_card_sets_user_profiles_UserId", + column: x => x.UserId, + principalTable: "user_profiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_flashcards_card_set_id", + table: "flashcards", + column: "card_set_id"); + + migrationBuilder.CreateIndex( + name: "IX_card_sets_UserId", + table: "card_sets", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_flashcards_card_sets_card_set_id", + table: "flashcards", + column: "card_set_id", + principalTable: "card_sets", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/backend/DramaLing.Api/Migrations/DramaLingDbContextModelSnapshot.cs b/backend/DramaLing.Api/Migrations/DramaLingDbContextModelSnapshot.cs index b4a30ce..4eae750 100644 --- a/backend/DramaLing.Api/Migrations/DramaLingDbContextModelSnapshot.cs +++ b/backend/DramaLing.Api/Migrations/DramaLingDbContextModelSnapshot.cs @@ -82,46 +82,6 @@ namespace DramaLing.Api.Migrations b.ToTable("audio_cache", (string)null); }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.CardSet", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CardCount") - .HasColumnType("INTEGER"); - - b.Property("Color") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("UpdatedAt") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("card_sets", (string)null); - }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => { b.Property("Id") @@ -341,10 +301,6 @@ namespace DramaLing.Api.Migrations .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("CardSetId") - .HasColumnType("TEXT") - .HasColumnName("card_set_id"); - b.Property("CreatedAt") .HasColumnType("TEXT") .HasColumnName("created_at"); @@ -439,8 +395,6 @@ namespace DramaLing.Api.Migrations b.HasKey("Id"); - b.HasIndex("CardSetId"); - b.HasIndex("UserId"); b.ToTable("flashcards", (string)null); @@ -1239,17 +1193,6 @@ namespace DramaLing.Api.Migrations b.ToTable("WordQueryUsageStats"); }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.CardSet", b => - { - b.HasOne("DramaLing.Api.Models.Entities.User", "User") - .WithMany("CardSets") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => { b.HasOne("DramaLing.Api.Models.Entities.User", "User") @@ -1289,19 +1232,12 @@ namespace DramaLing.Api.Migrations modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => { - b.HasOne("DramaLing.Api.Models.Entities.CardSet", "CardSet") - .WithMany("Flashcards") - .HasForeignKey("CardSetId") - .OnDelete(DeleteBehavior.SetNull); - b.HasOne("DramaLing.Api.Models.Entities.User", "User") .WithMany("Flashcards") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("CardSet"); - b.Navigation("User"); }); @@ -1506,11 +1442,6 @@ namespace DramaLing.Api.Migrations b.Navigation("User"); }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.CardSet", b => - { - b.Navigation("Flashcards"); - }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => { b.Navigation("FlashcardExampleImages"); @@ -1546,8 +1477,6 @@ namespace DramaLing.Api.Migrations modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => { - b.Navigation("CardSets"); - b.Navigation("DailyStats"); b.Navigation("ErrorReports"); diff --git a/backend/DramaLing.Api/Models/Entities/Flashcard.cs b/backend/DramaLing.Api/Models/Entities/Flashcard.cs index 6ac6c5c..36c6a3d 100644 --- a/backend/DramaLing.Api/Models/Entities/Flashcard.cs +++ b/backend/DramaLing.Api/Models/Entities/Flashcard.cs @@ -8,8 +8,6 @@ public class Flashcard public Guid UserId { get; set; } - public Guid? CardSetId { get; set; } - // 詞卡內容 [Required] [MaxLength(255)] @@ -71,7 +69,6 @@ public class Flashcard // Navigation Properties public virtual User User { get; set; } = null!; - public virtual CardSet? CardSet { get; set; } public virtual ICollection StudyRecords { get; set; } = new List(); public virtual ICollection FlashcardTags { get; set; } = new List(); public virtual ICollection ErrorReports { get; set; } = new List(); diff --git a/backend/DramaLing.Api/Models/Entities/User.cs b/backend/DramaLing.Api/Models/Entities/User.cs index 2e5353f..35a5cc0 100644 --- a/backend/DramaLing.Api/Models/Entities/User.cs +++ b/backend/DramaLing.Api/Models/Entities/User.cs @@ -43,7 +43,6 @@ public class User public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; // Navigation Properties - public virtual ICollection CardSets { get; set; } = new List(); public virtual ICollection Flashcards { get; set; } = new List(); public virtual UserSettings? Settings { get; set; } public virtual ICollection StudySessions { get; set; } = new List(); diff --git a/backend/DramaLing.Api/Services/StudySessionService.cs b/backend/DramaLing.Api/Services/StudySessionService.cs index a7e07e5..f6eb96b 100644 --- a/backend/DramaLing.Api/Services/StudySessionService.cs +++ b/backend/DramaLing.Api/Services/StudySessionService.cs @@ -127,7 +127,6 @@ public class StudySessionService : IStudySessionService } var flashcard = await _context.Flashcards - .Include(f => f.CardSet) .FirstOrDefaultAsync(f => f.Id == currentCard.FlashcardId); return new CurrentTestDto diff --git a/frontend/app/review-design/example-data.json b/frontend/app/review-design/example-data.json index 13a72bf..6480100 100644 --- a/frontend/app/review-design/example-data.json +++ b/frontend/app/review-design/example-data.json @@ -1,112 +1,314 @@ -[ - { - "id": "1", - "word": "warrants", - "definition": "official papers that allow the police to do something, like search a house or arrest someone.", - "example": "The police obtained warrants to search the building for evidence.", - "exampleTranslation": "we're gonna be signing our own death warrants.", - "difficultyLevel": "B2", - "imageUrl": "https://drive.google.com/uc?export=view&id=1TFuEhblYQ8CxXkKQRvrwOmsJuQKLcHtD", - "pronunciation": "", - "synonyms": ["papers", "documents", "permits", "orders"] - }, - { - "id": "2", - "word": "brought this thing up", - "definition": "To mention or introduce a topic.", - "example": "He brought this thing up during our meeting, and now we have to deal with it.", - "exampleTranslation": "you're the one that brought this thing up.", - "difficultyLevel": "B2", - "imageUrl": "https://drive.google.com/uc?export=view&id=1zWPKI1XWfNbz_n2OvBEp_a2ppQzNz5fT", - "pronunciation": "", - "synonyms": ["talked about it", "said it", "mentioned it"] - }, - { - "id": "3", - "word": "instincts", - "definition": "Instincts are natural tendencies or feelings that guide behavior without conscious thought.", - "example": "Animals use their instincts to find food and stay safe.", - "exampleTranslation": "now you're telling me not to trust my instincts?", - "difficultyLevel": "B2", - "imageUrl": "https://drive.google.com/uc?export=view&id=1vRoTYS--kuXER1AzXKaJddGT4nL8C2v4", - "pronunciation": "", - "synonyms": ["feelings", "sense", "gut feeling"] - }, - { - "id": "4", - "word": "cut some slack", - "definition": "To not be too critical of someone or to allow them some leeway, especially because they are having difficulties.", - "example": "Since he's new to the job, we should cut him some slack.", - "exampleTranslation": "You're the one who said we should cut him as much slack as he needs.", - "difficultyLevel": "B2", - "imageUrl": "https://drive.google.com/uc?export=view&id=1xLOJbkEa5HcYLLKZLJnjmDQ3NZHleeT5", - "pronunciation": "", - "synonyms": ["be nice", "be kind", "forgive", "understand"] - }, - { - "id": "5", - "word": "ashamed", - "definition": "Feeling sorry and embarrassed because you did something wrong.", - "example": "She felt ashamed of her mistake and apologized.", - "exampleTranslation": "He gave me that look because he's ashamed.", - "difficultyLevel": "B1", - "imageUrl": "https://drive.google.com/uc?export=view&id=1_c4HhwYHGAlZyXbdXrwlYm7jOjyquECe", - "pronunciation": "", - "synonyms": ["sorry", "bad", "embarrassed", "guilty"] - }, - { - "id": "6", - "word": "tragedy", - "definition": "A very sad event or situation that causes great suffering.", - "example": "The earthquake was a great tragedy for the small town.", - "exampleTranslation": "The real tragedy is I have to get drinks with Louis.", - "difficultyLevel": "B2", - "imageUrl": "https://drive.google.com/uc?export=view&id=19AfxmUfTuuI2DHURBaXJFFCTPb2wvmRd", - "pronunciation": "", - "synonyms": ["disaster", "sad event", "bad thing", "accident"] - }, - { - "id": "7", - "word": "criticize", - "definition": "To say what you think is bad about someone or something.", - "example": "It's not helpful to criticize someone without offering constructive advice.", - "exampleTranslation": "JUST TO CRITICIZE WHERE I LIVE, OR... THAT'S A SIDE BENEFIT.", - "difficultyLevel": "B2", - "imageUrl": "https://drive.google.com/uc?export=view&id=1xE9i6bc_qtbeb-xowqjwlLTjct1UQxx2", - "pronunciation": "", - "synonyms": ["blame", "say bad things", "complain", "judge"] - }, - { - "id": "8", - "word": "condemned", - "definition": "To say strongly that you do not approve of something or someone.", - "example": "The building was condemned after the earthquake due to structural damage.", - "exampleTranslation": "HOW LONG AGO WAS IT CONDEMNED?", - "difficultyLevel": "B2", - "imageUrl": "https://drive.google.com/uc?export=view&id=1jIBsvySXFTVN73oxDFRBjKlw0KMBWcaW", - "pronunciation": "", - "synonyms": ["said no", "disagreed", "blamed", "refused"] - }, - { - "id": "9", - "word": "blackmail", - "definition": "To get money from someone by saying you will tell a secret about them.", - "example": "The corrupt official tried to blackmail the businessman into paying him money.", - "exampleTranslation": "WHEN YOU CAME FIVE YEARS AGO TO BLACKMAIL ME, I WAS FURIOUS.", - "difficultyLevel": "B2", - "imageUrl": "https://drive.google.com/uc?export=view&id=16x7gyLMYGI4WJAoxPNm2EYG8QkCeSXyb", - "pronunciation": "", - "synonyms": ["threaten", "force", "make someone do"] - }, - { - "id": "10", - "word": "furious", - "definition": "Feeling or showing extreme anger.", - "example": "She was furious when she found out her flight was delayed.", - "exampleTranslation": "WHEN YOU CAME FIVE YEARS AGO TO BLACKMAIL ME, I WAS FURIOUS.", - "difficultyLevel": "B2", - "imageUrl": "https://drive.google.com/uc?export=view&id=1kUn3vDm_YKruuWrh3nq7g3WQ8E7yIkSY", - "pronunciation": "", - "synonyms": ["angry", "mad", "very angry", "upset"] - } -] \ No newline at end of file +{ + "success": true, + "data": [ + { + "id": "580f7a9c-b6cd-4b08-a554-d5f96c2087f9", + "userId": "00000000-0000-0000-0000-000000000001", + "word": "warrants", + "translation": "逮捕令,許可證", + "definition": "official papers that allow the police to do something, like search a house or arrest someone.", + "partOfSpeech": "noun", + "pronunciation": "/ˈwɒrənts/", + "example": "The police obtained warrants to search the building for evidence.", + "exampleTranslation": "警方取得了搜查大樓尋找證據的許可證。", + "easinessFactor": 2.5, + "repetitions": 0, + "intervalDays": 1, + "nextReviewDate": "2025-09-27T00:00:00", + "masteryLevel": 0, + "timesReviewed": 0, + "timesCorrect": 0, + "lastReviewedAt": null, + "isFavorite": false, + "isArchived": false, + "difficultyLevel": "B2", + "reviewHistory": null, + "lastQuestionType": null, + "createdAt": "2025-09-27T13:02:32.13951", + "updatedAt": "2025-09-27T13:02:32.139524", + "user": null, + "studyRecords": [], + "flashcardTags": [], + "errorReports": [], + "flashcardExampleImages": [ + { + "flashcardId": "", + "exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee", + "isPrimary": true, + "exampleImage": { + "id": "bb389dab-582f-405f-8225-5c2749eaceee", + "relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png", + "qualityScore": 95, + "fileSize": 2048576, + "createdAt": "2025-09-27T13:00:00" + } + } + ] + }, + { + "id": "c7d8e9f0-a1b2-3456-7890-abcdef123456", + "userId": "00000000-0000-0000-0000-000000000001", + "word": "ashamed", + "translation": "羞恥的,慚愧的", + "definition": "Feeling sorry and embarrassed because you did something wrong.", + "partOfSpeech": "adjective", + "pronunciation": "/əˈʃeɪmd/", + "example": "She felt ashamed of her mistake and apologized.", + "exampleTranslation": "她為自己的錯誤感到羞愧並道歉。", + "easinessFactor": 2.5, + "repetitions": 0, + "intervalDays": 1, + "nextReviewDate": "2025-09-27T00:00:00", + "masteryLevel": 0, + "timesReviewed": 0, + "timesCorrect": 0, + "lastReviewedAt": null, + "isFavorite": false, + "isArchived": false, + "difficultyLevel": "B1", + "reviewHistory": null, + "lastQuestionType": null, + "createdAt": "2025-09-27T13:06:39.29807", + "updatedAt": "2025-09-27T13:06:39.29807", + "user": null, + "studyRecords": [], + "flashcardTags": [], + "errorReports": [], + "flashcardExampleImages": [ + { + "flashcardId": "", + "exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee", + "isPrimary": true, + "exampleImage": { + "id": "bb389dab-582f-405f-8225-5c2749eaceee", + "relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png", + "qualityScore": 95, + "fileSize": 2048576, + "createdAt": "2025-09-27T13:00:00" + } + } + ] + }, + { + "id": "d9e0f1a2-b3c4-5678-9012-cdef12345678", + "userId": "00000000-0000-0000-0000-000000000001", + "word": "tragedy", + "translation": "悲劇,慘事", + "definition": "A very sad event or situation that causes great suffering.", + "partOfSpeech": "noun", + "pronunciation": "/ˈtrædʒədi/", + "example": "The earthquake was a great tragedy for the small town.", + "exampleTranslation": "地震對這個小鎮來說是一場巨大的悲劇。", + "easinessFactor": 2.5, + "repetitions": 0, + "intervalDays": 1, + "nextReviewDate": "2025-09-27T00:00:00", + "masteryLevel": 0, + "timesReviewed": 0, + "timesCorrect": 0, + "lastReviewedAt": null, + "isFavorite": false, + "isArchived": false, + "difficultyLevel": "B2", + "reviewHistory": null, + "lastQuestionType": null, + "createdAt": "2025-09-27T13:07:39.29807", + "updatedAt": "2025-09-27T13:07:39.29807", + "user": null, + "studyRecords": [], + "flashcardTags": [], + "errorReports": [], + "flashcardExampleImages": [ + { + "flashcardId": "", + "exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee", + "isPrimary": true, + "exampleImage": { + "id": "bb389dab-582f-405f-8225-5c2749eaceee", + "relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png", + "qualityScore": 95, + "fileSize": 2048576, + "createdAt": "2025-09-27T13:00:00" + } + } + ] + }, + { + "id": "e1f2a3b4-c5d6-7890-1234-def123456789", + "userId": "00000000-0000-0000-0000-000000000001", + "word": "criticize", + "translation": "批評,指責", + "definition": "To say what you think is bad about someone or something.", + "partOfSpeech": "verb", + "pronunciation": "/ˈkrɪtɪsaɪz/", + "example": "It's not helpful to criticize someone without offering constructive advice.", + "exampleTranslation": "批評別人而不提供建設性建議是沒有幫助的。", + "easinessFactor": 2.5, + "repetitions": 0, + "intervalDays": 1, + "nextReviewDate": "2025-09-27T00:00:00", + "masteryLevel": 0, + "timesReviewed": 0, + "timesCorrect": 0, + "lastReviewedAt": null, + "isFavorite": false, + "isArchived": false, + "difficultyLevel": "B2", + "reviewHistory": null, + "lastQuestionType": null, + "createdAt": "2025-09-27T13:08:39.29807", + "updatedAt": "2025-09-27T13:08:39.29807", + "user": null, + "studyRecords": [], + "flashcardTags": [], + "errorReports": [], + "flashcardExampleImages": [ + { + "flashcardId": "", + "exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee", + "isPrimary": true, + "exampleImage": { + "id": "bb389dab-582f-405f-8225-5c2749eaceee", + "relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png", + "qualityScore": 95, + "fileSize": 2048576, + "createdAt": "2025-09-27T13:00:00" + } + } + ] + }, + { + "id": "f3a4b5c6-d7e8-9012-3456-f123456789ab", + "userId": "00000000-0000-0000-0000-000000000001", + "word": "condemned", + "translation": "譴責,定罪", + "definition": "To say strongly that you do not approve of something or someone.", + "partOfSpeech": "verb", + "pronunciation": "/kənˈdemd/", + "example": "The building was condemned after the earthquake due to structural damage.", + "exampleTranslation": "地震後由於結構損壞,這棟建築被宣告不適用。", + "easinessFactor": 2.5, + "repetitions": 0, + "intervalDays": 1, + "nextReviewDate": "2025-09-27T00:00:00", + "masteryLevel": 0, + "timesReviewed": 0, + "timesCorrect": 0, + "lastReviewedAt": null, + "isFavorite": false, + "isArchived": false, + "difficultyLevel": "B2", + "reviewHistory": null, + "lastQuestionType": null, + "createdAt": "2025-09-27T13:09:39.29807", + "updatedAt": "2025-09-27T13:09:39.29807", + "user": null, + "studyRecords": [], + "flashcardTags": [], + "errorReports": [], + "flashcardExampleImages": [ + { + "flashcardId": "", + "exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee", + "isPrimary": true, + "exampleImage": { + "id": "bb389dab-582f-405f-8225-5c2749eaceee", + "relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png", + "qualityScore": 95, + "fileSize": 2048576, + "createdAt": "2025-09-27T13:00:00" + } + } + ] + }, + { + "id": "a5b6c7d8-e9f0-1234-5678-123456789abc", + "userId": "00000000-0000-0000-0000-000000000001", + "word": "blackmail", + "translation": "勒索,要脅", + "definition": "To get money from someone by saying you will tell a secret about them.", + "partOfSpeech": "verb", + "pronunciation": "/ˈblækmeɪl/", + "example": "The corrupt official tried to blackmail the businessman into paying him money.", + "exampleTranslation": "腐敗的官員試圖勒索商人要他付錢。", + "easinessFactor": 2.5, + "repetitions": 0, + "intervalDays": 1, + "nextReviewDate": "2025-09-27T00:00:00", + "masteryLevel": 0, + "timesReviewed": 0, + "timesCorrect": 0, + "lastReviewedAt": null, + "isFavorite": false, + "isArchived": false, + "difficultyLevel": "B2", + "reviewHistory": null, + "lastQuestionType": null, + "createdAt": "2025-09-27T13:10:39.29807", + "updatedAt": "2025-09-27T13:10:39.29807", + "user": null, + "studyRecords": [], + "flashcardTags": [], + "errorReports": [], + "flashcardExampleImages": [ + { + "flashcardId": "", + "exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee", + "isPrimary": true, + "exampleImage": { + "id": "bb389dab-582f-405f-8225-5c2749eaceee", + "relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png", + "qualityScore": 95, + "fileSize": 2048576, + "createdAt": "2025-09-27T13:00:00" + } + } + ] + }, + { + "id": "b7c8d9e0-f1a2-3456-7890-23456789abcd", + "userId": "00000000-0000-0000-0000-000000000001", + "word": "furious", + "translation": "憤怒的,狂怒的", + "definition": "Feeling or showing extreme anger.", + "partOfSpeech": "adjective", + "pronunciation": "/ˈfjʊəriəs/", + "example": "She was furious when she found out her flight was delayed.", + "exampleTranslation": "她發現航班延誤時非常憤怒。", + "easinessFactor": 2.5, + "repetitions": 0, + "intervalDays": 1, + "nextReviewDate": "2025-09-27T00:00:00", + "masteryLevel": 0, + "timesReviewed": 0, + "timesCorrect": 0, + "lastReviewedAt": null, + "isFavorite": false, + "isArchived": false, + "difficultyLevel": "B2", + "reviewHistory": null, + "lastQuestionType": null, + "createdAt": "2025-09-27T13:11:39.29807", + "updatedAt": "2025-09-27T13:11:39.29807", + "user": null, + "studyRecords": [], + "flashcardTags": [], + "errorReports": [], + "flashcardExampleImages": [ + { + "flashcardId": "", + "exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee", + "isPrimary": true, + "exampleImage": { + "id": "bb389dab-582f-405f-8225-5c2749eaceee", + "relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png", + "qualityScore": 95, + "fileSize": 2048576, + "createdAt": "2025-09-27T13:00:00" + } + } + ] + } + ], + "count": 10 +} \ No newline at end of file diff --git a/frontend/app/review-design/page.tsx b/frontend/app/review-design/page.tsx index 2275fd4..f5adc1f 100644 --- a/frontend/app/review-design/page.tsx +++ b/frontend/app/review-design/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { Navigation } from '@/components/Navigation' import { FlipMemoryTest, @@ -11,10 +11,12 @@ import { SentenceListeningTest, SentenceSpeakingTest } from '@/components/review/review-tests' +import exampleData from './example-data.json' export default function ReviewTestsPage() { const [logs, setLogs] = useState([]) const [activeTab, setActiveTab] = useState('FlipMemoryTest') + const [currentCardIndex, setCurrentCardIndex] = useState(0) // 測驗組件清單 const testComponents = [ @@ -33,20 +35,41 @@ export default function ReviewTestsPage() { setLogs(prev => [`[${activeTab}] [${timestamp}] ${message}`, ...prev.slice(0, 9)]) } - // 模擬資料 - const mockCardData = { - word: "elaborate", - definition: "To explain something in more detail; to develop or present a theory, policy, or system in further detail", - example: "Could you elaborate on your proposal for the new marketing strategy?", - exampleTranslation: "你能詳細說明一下你對新行銷策略的提案嗎?", - pronunciation: "/ɪˈlæbərət/", - synonyms: ["explain", "detail", "expand", "clarify"], - difficultyLevel: "B2", - exampleImage: "https://via.placeholder.com/400x200?text=Marketing+Strategy" + // 從 API 響應格式獲取當前卡片資料 + const flashcardsData = exampleData.data || [] + const currentCard = flashcardsData[currentCardIndex] || flashcardsData[0] + + // 轉換為組件所需格式 + const mockCardData = currentCard ? { + word: currentCard.word, + definition: currentCard.definition, + example: currentCard.example, + exampleTranslation: currentCard.exampleTranslation, + pronunciation: currentCard.pronunciation, + difficultyLevel: currentCard.difficultyLevel, + translation: currentCard.translation + } : { + word: "loading...", + definition: "Loading...", + example: "Loading...", + exampleTranslation: "載入中...", + pronunciation: "", + difficultyLevel: "A1", + translation: "載入中" } - // 選項題選項 - const vocabChoiceOptions = ["elaborate", "celebrate", "collaborate", "deliberate"] + // 選項題選項 - 從資料中生成 + const generateVocabChoiceOptions = () => { + if (!currentCard) return ["loading"] + const correctAnswer = currentCard.word + const otherWords = flashcardsData + .filter(card => card.word !== correctAnswer) + .slice(0, 3) + .map(card => card.word) + return [correctAnswer, ...otherWords].sort(() => Math.random() - 0.5) + } + + const vocabChoiceOptions = generateVocabChoiceOptions() // 回調函數 const handleConfidenceSubmit = (level: number) => { @@ -74,6 +97,27 @@ export default function ReviewTestsPage() {

Review 組件設計

所有 review-tests 組件的 UI 設計頁面

+ + {/* 卡片切換控制 */} +
+ + + 卡片 {currentCardIndex + 1} / {flashcardsData.length} - {currentCard?.word || 'loading'} + + +
{/* Tab 導航 */} @@ -113,7 +157,7 @@ export default function ReviewTestsPage() { example={mockCardData.example} exampleTranslation={mockCardData.exampleTranslation} pronunciation={mockCardData.pronunciation} - synonyms={mockCardData.synonyms} + synonyms={[]} difficultyLevel={mockCardData.difficultyLevel} onConfidenceSubmit={handleConfidenceSubmit} onReportError={handleReportError} @@ -142,7 +186,7 @@ export default function ReviewTestsPage() { exampleTranslation={mockCardData.exampleTranslation} pronunciation={mockCardData.pronunciation} difficultyLevel={mockCardData.difficultyLevel} - exampleImage={mockCardData.exampleImage} + exampleImage="https://via.placeholder.com/400x200?text=Example+Image" onAnswer={handleAnswer} onReportError={handleReportError} onImageClick={handleImageClick} @@ -191,7 +235,7 @@ export default function ReviewTestsPage() { example={mockCardData.example} exampleTranslation={mockCardData.exampleTranslation} difficultyLevel={mockCardData.difficultyLevel} - exampleImage={mockCardData.exampleImage} + exampleImage="https://via.placeholder.com/400x200?text=Speaking+Test" onAnswer={handleAnswer} onReportError={handleReportError} onImageClick={handleImageClick}