diff --git a/選項詞彙庫功能規格書.md b/選項詞彙庫功能規格書.md index c896ba8..ec81ff7 100644 --- a/選項詞彙庫功能規格書.md +++ b/選項詞彙庫功能規格書.md @@ -104,10 +104,12 @@ public class OptionsVocabulary public string CEFRLevel { get; set; } = string.Empty; /// - /// 詞性 (noun, verb, adjective, adverb, etc.) + /// 詞性 (noun, verb, adjective, adverb, pronoun, preposition, conjunction, interjection, idiom) /// [Required] [MaxLength(20)] + [RegularExpression("^(noun|verb|adjective|adverb|pronoun|preposition|conjunction|interjection|idiom)$", + ErrorMessage = "詞性必須為有效值")] [Index("IX_OptionsVocabulary_PartOfSpeech")] public string PartOfSpeech { get; set; } = string.Empty; @@ -192,62 +194,79 @@ public interface IOptionsVocabularyService } ``` -### DistractorGenerationService 核心邏輯 +### QuestionGeneratorService 整合設計 ```csharp -public class DistractorGenerationService +public class QuestionGeneratorService : IQuestionGeneratorService { private readonly DramaLingDbContext _context; - private readonly IMemoryCache _cache; - private readonly ILogger _logger; + private readonly IOptionsVocabularyService _optionsVocabularyService; + private readonly ILogger _logger; - public async Task> GenerateDistractorsAsync( - string targetWord, - string cefrLevel, - string partOfSpeech) + public QuestionGeneratorService( + DramaLingDbContext context, + IOptionsVocabularyService optionsVocabularyService, + ILogger logger) { - var targetLength = targetWord.Length; - - // 1. 基礎篩選條件 - var baseQuery = _context.OptionsVocabularies - .Where(v => v.IsActive && v.Word != targetWord); - - // 2. CEFR 等級匹配(相同等級 + 相鄰等級) - var allowedCEFRLevels = GetAllowedCEFRLevels(cefrLevel); - baseQuery = baseQuery.Where(v => allowedCEFRLevels.Contains(v.CEFRLevel)); - - // 3. 詞性匹配 - baseQuery = baseQuery.Where(v => v.PartOfSpeech == partOfSpeech); - - // 4. 字數匹配(±2 字元範圍) - var minLength = Math.Max(1, targetLength - 2); - var maxLength = targetLength + 2; - baseQuery = baseQuery.Where(v => v.WordLength >= minLength && v.WordLength <= maxLength); - - // 5. 隨機排序選取候選詞 - var candidates = await baseQuery - .OrderBy(v => Guid.NewGuid()) - .Take(10) // 取更多候選詞再篩選 - .Select(v => v.Word) - .ToListAsync(); - - // 7. 最終篩選和回傳 - return candidates.Take(3).ToList(); + _context = context; + _optionsVocabularyService = optionsVocabularyService; + _logger = logger; } - private List GetAllowedCEFRLevels(string targetLevel) + /// + /// 生成詞彙選擇題選項(整合選項詞彙庫) + /// + private async Task GenerateVocabChoiceAsync(Flashcard flashcard) { - var levels = new[] { "A1", "A2", "B1", "B2", "C1", "C2" }; - var targetIndex = Array.IndexOf(levels, targetLevel); + try + { + // 優先使用選項詞彙庫生成干擾項 + var distractors = await _optionsVocabularyService.GenerateDistractorsAsync( + flashcard.Word, + flashcard.DifficultyLevel ?? "B1", + flashcard.PartOfSpeech ?? "noun"); - if (targetIndex == -1) return new List { targetLevel }; + // 如果詞彙庫沒有足夠的選項,回退到用戶其他詞卡 + if (distractors.Count < 3) + { + var fallbackDistractors = await GetFallbackDistractorsAsync(flashcard); + distractors.AddRange(fallbackDistractors.Take(3 - distractors.Count)); + } - var allowed = new List { targetLevel }; + var options = new List { flashcard.Word }; + options.AddRange(distractors.Take(3)); - // 加入相鄰等級 - if (targetIndex > 0) allowed.Add(levels[targetIndex - 1]); - if (targetIndex < levels.Length - 1) allowed.Add(levels[targetIndex + 1]); + // 隨機打亂選項順序 + var shuffledOptions = options.OrderBy(x => Guid.NewGuid()).ToArray(); - return allowed; + return new QuestionData + { + QuestionType = "vocab-choice", + Options = shuffledOptions, + CorrectAnswer = flashcard.Word + }; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to generate options from vocabulary database, using fallback for {Word}", flashcard.Word); + + // 完全回退到原有邏輯 + return await GenerateVocabChoiceWithFallbackAsync(flashcard); + } + } + + /// + /// 回退選項生成(使用用戶其他詞卡) + /// + private async Task> GetFallbackDistractorsAsync(Flashcard flashcard) + { + return await _context.Flashcards + .Where(f => f.UserId == flashcard.UserId && + f.Id != flashcard.Id && + !f.IsArchived) + .OrderBy(x => Guid.NewGuid()) + .Take(3) + .Select(f => f.Word) + .ToListAsync(); } } ``` @@ -256,82 +275,49 @@ public class DistractorGenerationService ## 🌐 API 設計 -### 新增到 StudyController +### 整合到現有 FlashcardsController +選項詞彙庫功能將整合到現有的 `POST /api/flashcards/{id}/question` API 端點中。 + ```csharp -/// -/// 生成測驗選項(使用詞彙庫) -/// -[HttpGet("question-options/{flashcardId}")] -public async Task> GenerateQuestionOptions( - Guid flashcardId, - [FromQuery] string questionType = "vocab-choice") +// 現有的 FlashcardsController.GenerateQuestion 方法會自動使用改進後的 QuestionGeneratorService +// 不需要新增額外的 API 端點 + +[HttpPost("{id}/question")] +public async Task GenerateQuestion(Guid id, [FromBody] QuestionRequest request) { try { - var flashcard = await _context.Flashcards.FindAsync(flashcardId); - if (flashcard == null) - return NotFound(new { Error = "Flashcard not found" }); + // QuestionGeneratorService 內部會使用 OptionsVocabularyService 生成更好的選項 + var questionData = await _questionGeneratorService.GenerateQuestionAsync(id, request.QuestionType); - var options = await _distractorGenerationService.GenerateDistractorsAsync( - flashcard.Word, - flashcard.DifficultyLevel ?? "B1", - flashcard.PartOfSpeech ?? "noun"); - - // 加入正確答案並隨機打亂 - var allOptions = new List { flashcard.Word }; - allOptions.AddRange(options); - var shuffledOptions = allOptions.OrderBy(x => Guid.NewGuid()).ToArray(); - - return Ok(new QuestionOptionsResponse - { - QuestionType = questionType, - Options = shuffledOptions, - CorrectAnswer = flashcard.Word, - TargetWord = flashcard.Word, - CEFRLevel = flashcard.DifficultyLevel, - PartOfSpeech = flashcard.PartOfSpeech - }); + return Ok(new { success = true, data = questionData }); } catch (Exception ex) { - _logger.LogError(ex, "Error generating question options for flashcard {FlashcardId}", flashcardId); - return StatusCode(500, new { Error = "Internal server error" }); + _logger.LogError(ex, "Error generating question for flashcard {FlashcardId}", id); + return StatusCode(500, new { success = false, error = "Failed to generate question" }); } } ``` -### 詞彙庫管理 API +### 詞彙庫管理 API(選用功能) +> **注意**:以下管理 API 為選用功能,主要供管理員批量管理詞彙庫使用。 +> 核心選項生成功能已整合到現有的測驗 API 中,不依賴這些管理端點。 + ```csharp /// -/// 詞彙庫管理控制器 +/// 詞彙庫管理控制器(選用) +/// 僅在需要管理員批量管理詞彙庫時實作 /// [ApiController] -[Route("api/[controller]")] +[Route("api/admin/[controller]")] [Authorize(Roles = "Admin")] public class OptionsVocabularyController : ControllerBase { private readonly IOptionsVocabularyService _vocabularyService; /// - /// 新增詞彙到選項庫 - /// - [HttpPost] - public async Task AddVocabulary([FromBody] AddVocabularyRequest request) - { - var vocabulary = new OptionsVocabulary - { - Word = request.Word, - CEFRLevel = request.CEFRLevel, - PartOfSpeech = request.PartOfSpeech, - WordLength = request.Word.Length - }; - - var success = await _vocabularyService.AddVocabularyAsync(vocabulary); - return success ? Ok() : BadRequest(); - } - - /// - /// 批量匯入詞彙 + /// 批量匯入詞彙(管理員功能) /// [HttpPost("bulk-import")] public async Task BulkImport([FromBody] List requests) @@ -349,19 +335,13 @@ public class OptionsVocabularyController : ControllerBase } /// - /// 搜尋詞彙庫 + /// 搜尋詞彙庫統計(管理員功能) /// - [HttpGet("search")] - public async Task>> SearchVocabularies( - [FromQuery] string? cefrLevel = null, - [FromQuery] string? partOfSpeech = null, - [FromQuery] int? minLength = null, - [FromQuery] int? maxLength = null, - [FromQuery] int limit = 100) + [HttpGet("stats")] + public async Task GetVocabularyStats() { - var vocabularies = await _vocabularyService.SearchVocabulariesAsync( - cefrLevel, partOfSpeech, minLength, maxLength, limit); - return Ok(vocabularies); + var stats = await _vocabularyService.GetVocabularyStatsAsync(); + return Ok(stats); } } ``` @@ -400,6 +380,8 @@ public class AddVocabularyRequest [Required] [MaxLength(20)] + [RegularExpression("^(noun|verb|adjective|adverb|pronoun|preposition|conjunction|interjection|idiom)$", + ErrorMessage = "詞性必須為有效值")] public string PartOfSpeech { get; set; } = string.Empty; } @@ -462,38 +444,70 @@ public partial class AddOptionsVocabularyTable : Migration ## 🔄 使用案例 -### 案例 1:詞彙選擇題 +### 案例 1:詞彙選擇題 API 流程 ``` -目標詞彙: "beautiful" (B1, adjective, 9字元) +前端請求: +POST /api/flashcards/{id}/question +{ + "questionType": "vocab-choice" +} -篩選條件: -- CEFR: A2, B1, B2 (相鄰等級) -- 詞性: adjective -- 字數: 7-11 字元 +後端處理: +1. 查詢詞卡: "beautiful" (B1, adjective, 9字元) +2. 從選項詞彙庫篩選干擾項: + - CEFR: A2, B1, B2 (相鄰等級) + - 詞性: adjective + - 字數: 7-11 字元 +3. 選出干擾項: ["wonderful", "excellent", "attractive"] -可能的干擾項: -- "wonderful" (B1, adjective, 9字元) -- "excellent" (B2, adjective, 9字元) -- "attractive" (B2, adjective, 10字元) - -最終選項: ["beautiful", "wonderful", "excellent", "attractive"] +API 回應: +{ + "success": true, + "data": { + "questionType": "vocab-choice", + "options": ["beautiful", "wonderful", "excellent", "attractive"], + "correctAnswer": "beautiful" + } +} ``` -### 案例 2:聽力測驗 +### 案例 2:聽力測驗 API 流程 ``` -目標詞彙: "running" (A2, verb, 7字元) +前端請求: +POST /api/flashcards/{id}/question +{ + "questionType": "sentence-listening" +} -篩選條件: -- CEFR: A1, A2, B1 -- 詞性: verb -- 字數: 5-9 字元 +後端處理: +1. 查詢詞卡: "running" (A2, verb, 7字元) +2. 從選項詞彙庫篩選干擾項: + - CEFR: A1, A2, B1 + - 詞性: verb + - 字數: 5-9 字元 +3. 選出干擾項: ["jumping", "walking", "playing"] -可能的干擾項: -- "jumping" (A2, verb, 7字元) -- "walking" (A1, verb, 7字元) -- "playing" (A2, verb, 7字元) +API 回應: +{ + "success": true, + "data": { + "questionType": "sentence-listening", + "options": ["running", "jumping", "walking", "playing"], + "correctAnswer": "running" + } +} +``` -最終選項: ["running", "jumping", "walking", "playing"] +### 案例 3:回退機制 +``` +情境: 詞彙庫中沒有足夠的相符選項 + +處理流程: +1. 嘗試從選項詞彙庫獲取干擾項 → 只找到 1 個 +2. 啟動回退機制:從用戶其他詞卡補足 2 個選項 +3. 確保總是能提供 3 個干擾項 + +優點:確保系統穩定性,即使詞彙庫不完整也能正常運作 ``` --- @@ -575,11 +589,41 @@ public class VocabularySeeder new() { Word = "run", CEFRLevel = "A1", PartOfSpeech = "verb", WordLength = 3 }, new() { Word = "walk", CEFRLevel = "A1", PartOfSpeech = "verb", WordLength = 4 }, + // A1 Level - 代名詞 + new() { Word = "he", CEFRLevel = "A1", PartOfSpeech = "pronoun", WordLength = 2 }, + new() { Word = "she", CEFRLevel = "A1", PartOfSpeech = "pronoun", WordLength = 3 }, + new() { Word = "they", CEFRLevel = "A1", PartOfSpeech = "pronoun", WordLength = 4 }, + + // A2 Level - 介系詞 + new() { Word = "under", CEFRLevel = "A2", PartOfSpeech = "preposition", WordLength = 5 }, + new() { Word = "above", CEFRLevel = "A2", PartOfSpeech = "preposition", WordLength = 5 }, + new() { Word = "behind", CEFRLevel = "A2", PartOfSpeech = "preposition", WordLength = 6 }, + // B1 Level - 形容詞 new() { Word = "beautiful", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 9 }, new() { Word = "wonderful", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 9 }, new() { Word = "excellent", CEFRLevel = "B2", PartOfSpeech = "adjective", WordLength = 9 }, + // B1 Level - 副詞 + new() { Word = "quickly", CEFRLevel = "B1", PartOfSpeech = "adverb", WordLength = 7 }, + new() { Word = "carefully", CEFRLevel = "B1", PartOfSpeech = "adverb", WordLength = 9 }, + new() { Word = "suddenly", CEFRLevel = "B1", PartOfSpeech = "adverb", WordLength = 8 }, + + // B2 Level - 連接詞 + new() { Word = "however", CEFRLevel = "B2", PartOfSpeech = "conjunction", WordLength = 7 }, + new() { Word = "therefore", CEFRLevel = "B2", PartOfSpeech = "conjunction", WordLength = 9 }, + new() { Word = "although", CEFRLevel = "B2", PartOfSpeech = "conjunction", WordLength = 8 }, + + // 感嘆詞 + new() { Word = "wow", CEFRLevel = "A1", PartOfSpeech = "interjection", WordLength = 3 }, + new() { Word = "ouch", CEFRLevel = "A2", PartOfSpeech = "interjection", WordLength = 4 }, + new() { Word = "alas", CEFRLevel = "C1", PartOfSpeech = "interjection", WordLength = 4 }, + + // 慣用語 + new() { Word = "break the ice", CEFRLevel = "B2", PartOfSpeech = "idiom", WordLength = 12 }, + new() { Word = "piece of cake", CEFRLevel = "B1", PartOfSpeech = "idiom", WordLength = 12 }, + new() { Word = "hit the books", CEFRLevel = "B2", PartOfSpeech = "idiom", WordLength = 12 }, + // ... 更多詞彙 }; @@ -644,12 +688,14 @@ public class DistractorQualityMetrics - [ ] 實作品質評分系統 - [ ] 加入快取機制 -### Phase 3: 前端整合 (3-5 天) -- [ ] 修改前端 generateOptions 函數 -- [ ] 整合新的 API 端點 -- [ ] 測試各種測驗類型 +### Phase 3: 前端整合 (1-2 天) +- [ ] 測試現有 API 端點的改進效果 +- [ ] 驗證各種測驗類型的選項品質 - [ ] 效能測試和優化 +> **注意**:由於選項生成功能已整合到現有 API,前端不需要修改任何程式碼。 +> 只需要確保後端改進後的選項生成效果符合預期。 + ### Phase 4: 進階功能 (1-2 週) - [ ] 管理介面開發 - [ ] 批量匯入工具 @@ -666,7 +712,7 @@ public class DistractorQualityMetrics - [ ] 生成的選項無重複 - [ ] 支援各種測驗類型 -### 品質驗收 +### 品質驗收 - [ ] 干擾項難度適中(不會太簡單或太困難) - [ ] 無明顯的同義詞作為干擾項 - [ ] 拼寫差異合理(避免過於相似)