# 智能填空題系統開發計劃 > 基於 `智能填空題系統設計規格.md` 制定的詳細實施計劃 ## 📋 開發階段總覽 ### Phase 1: 資料庫結構調整 (預計 0.5 天) ### Phase 2: 後端服務開發 (預計 2 天) ### Phase 3: 前端組件優化 (預計 1 天) ### Phase 4: 測試與優化 (預計 1 天) --- ## Phase 1: 資料庫結構調整 ### 🎯 目標 為 Flashcard 實體添加 `FilledQuestionText` 欄位,支援儲存挖空後的題目 ### 📝 具體任務 #### 1.1 更新實體模型 **檔案**: `backend/DramaLing.Api/Models/Entities/Flashcard.cs` ```csharp [MaxLength(1000)] public string? FilledQuestionText { get; set; } // 挖空後的題目文字 ``` #### 1.2 資料庫 Migration **命令**: `dotnet ef migrations add AddFilledQuestionText` **檔案**: `backend/DramaLing.Api/Migrations/[timestamp]_AddFilledQuestionText.cs` #### 1.3 更新 DbContext 欄位映射 **檔案**: `backend/DramaLing.Api/Data/DramaLingDbContext.cs` ```csharp private void ConfigureFlashcardEntity(ModelBuilder modelBuilder) { var flashcardEntity = modelBuilder.Entity(); // 現有欄位映射... flashcardEntity.Property(f => f.UserId).HasColumnName("user_id"); flashcardEntity.Property(f => f.PartOfSpeech).HasColumnName("part_of_speech"); flashcardEntity.Property(f => f.ExampleTranslation).HasColumnName("example_translation"); // 新增欄位映射 flashcardEntity.Property(f => f.FilledQuestionText).HasColumnName("filled_question_text"); } ``` #### 1.4 執行 Migration **命令**: `dotnet ef database update` ### ✅ 完成標準 - [ ] Flashcard 實體包含新欄位 - [ ] 資料庫表結構更新完成 - [ ] 現有資料保持完整 - [ ] 後端編譯成功 --- ## Phase 2: 後端服務開發 ### 🎯 目標 實作智能挖空生成服務,支援程式碼挖空和 AI 輔助 ### 📝 具體任務 #### 2.1 建立服務介面 **檔案**: `backend/DramaLing.Api/Services/IBlankGenerationService.cs` ```csharp public interface IBlankGenerationService { Task GenerateBlankQuestionAsync(string word, string example); string? TryProgrammaticBlank(string word, string example); Task GenerateAIBlankAsync(string word, string example); bool HasValidBlank(string blankQuestion); } ``` #### 2.2 實作挖空服務 **檔案**: `backend/DramaLing.Api/Services/BlankGenerationService.cs` ```csharp public class BlankGenerationService : IBlankGenerationService { private readonly IWordVariationService _wordVariationService; private readonly IAIProviderManager _aiProviderManager; private readonly ILogger _logger; public BlankGenerationService( IWordVariationService wordVariationService, IAIProviderManager aiProviderManager, ILogger logger) { _wordVariationService = wordVariationService; _aiProviderManager = aiProviderManager; _logger = logger; } public async Task GenerateBlankQuestionAsync(string word, string example) { if (string.IsNullOrEmpty(word) || string.IsNullOrEmpty(example)) return null; // Step 1: 嘗試程式碼挖空 var programmaticResult = TryProgrammaticBlank(word, example); if (!string.IsNullOrEmpty(programmaticResult)) { _logger.LogInformation("Successfully generated programmatic blank for word: {Word}", word); return programmaticResult; } // Step 2: 程式碼挖空失敗,嘗試 AI 挖空 _logger.LogInformation("Programmatic blank failed for word: {Word}, trying AI blank", word); var aiResult = await GenerateAIBlankAsync(word, example); return aiResult; } public string? TryProgrammaticBlank(string word, string example) { try { // 1. 完全匹配 var exactMatch = Regex.Replace(example, $@"\b{Regex.Escape(word)}\b", "____", RegexOptions.IgnoreCase); if (exactMatch != example) { _logger.LogDebug("Exact match blank successful for word: {Word}", word); return exactMatch; } // 2. 常見變形處理 var variations = _wordVariationService.GetCommonVariations(word); foreach(var variation in variations) { var variantMatch = Regex.Replace(example, $@"\b{Regex.Escape(variation)}\b", "____", RegexOptions.IgnoreCase); if (variantMatch != example) { _logger.LogDebug("Variation match blank successful for word: {Word}, variation: {Variation}", word, variation); return variantMatch; } } _logger.LogDebug("Programmatic blank failed for word: {Word}", word); return null; // 挖空失敗 } catch (Exception ex) { _logger.LogError(ex, "Error in programmatic blank for word: {Word}", word); return null; } } public bool HasValidBlank(string blankQuestion) { return !string.IsNullOrEmpty(blankQuestion) && blankQuestion.Contains("____"); } } ``` ##### AI 挖空邏輯 ```csharp public async Task GenerateAIBlankAsync(string word, string example) { try { var prompt = $@" 請將以下例句中與詞彙「{word}」相關的詞挖空,用____替代: 詞彙: {word} 例句: {example} 規則: 1. 只挖空與目標詞彙相關的詞(包含變形、時態、複數等) 2. 用____替代被挖空的詞 3. 保持句子其他部分不變 4. 直接返回挖空後的句子,不要額外說明 挖空後的句子:"; _logger.LogInformation("Generating AI blank for word: {Word}, example: {Example}", word, example); var result = await _aiProviderManager.GetDefaultProvider() .GenerateTextAsync(prompt); // 驗證 AI 回應格式 if (string.IsNullOrEmpty(result) || !result.Contains("____")) { _logger.LogWarning("AI generated invalid blank question for word: {Word}", word); return null; } _logger.LogInformation("Successfully generated AI blank for word: {Word}", word); return result.Trim(); } catch (Exception ex) { _logger.LogError(ex, "Error generating AI blank for word: {Word}", word); return null; } } ``` #### 2.3 詞彙變形服務 **檔案**: `backend/DramaLing.Api/Services/WordVariationService.cs` ```csharp public interface IWordVariationService { string[] GetCommonVariations(string word); bool IsVariationOf(string baseWord, string variation); } public class WordVariationService : IWordVariationService { private readonly ILogger _logger; private readonly Dictionary CommonVariations = new() { ["eat"] = ["eats", "ate", "eaten", "eating"], ["go"] = ["goes", "went", "gone", "going"], ["have"] = ["has", "had", "having"], ["be"] = ["am", "is", "are", "was", "were", "been", "being"], ["do"] = ["does", "did", "done", "doing"], ["take"] = ["takes", "took", "taken", "taking"], ["make"] = ["makes", "made", "making"], ["come"] = ["comes", "came", "coming"], ["see"] = ["sees", "saw", "seen", "seeing"], ["get"] = ["gets", "got", "gotten", "getting"], // ... 更多常見變形 }; public string[] GetCommonVariations(string word) { return CommonVariations.TryGetValue(word.ToLower(), out var variations) ? variations : Array.Empty(); } public bool IsVariationOf(string baseWord, string variation) { var variations = GetCommonVariations(baseWord); return variations.Contains(variation.ToLower()); } } ``` #### 2.4 修改 FlashcardsController **檔案**: `backend/DramaLing.Api/Controllers/FlashcardsController.cs` ##### GetDueFlashcards 方法強化 ```csharp [HttpGet("due")] public async Task GetDueFlashcards( [FromQuery] string? date = null, [FromQuery] int limit = 50) { try { var userId = GetUserId(); var queryDate = DateTime.TryParse(date, out var parsed) ? parsed : DateTime.Now.Date; var dueCards = await _spacedRepetitionService.GetDueFlashcardsAsync(userId, queryDate, limit); // 檢查並生成缺失的挖空題目 foreach(var flashcard in dueCards) { if(string.IsNullOrEmpty(flashcard.FilledQuestionText)) { var blankQuestion = await _blankGenerationService.GenerateBlankQuestionAsync( flashcard.Word, flashcard.Example); if(!string.IsNullOrEmpty(blankQuestion)) { flashcard.FilledQuestionText = blankQuestion; _context.Entry(flashcard).Property(f => f.FilledQuestionText).IsModified = true; } } } await _context.SaveChangesAsync(); _logger.LogInformation("Retrieved {Count} due flashcards for user {UserId}", dueCards.Count, userId); return Ok(new { success = true, data = dueCards, count = dueCards.Count }); } catch (Exception ex) { _logger.LogError(ex, "Error getting due flashcards"); return StatusCode(500, new { success = false, error = "Failed to get due flashcards" }); } } ``` #### 2.5 新增重新生成端點 ```csharp [HttpPost("{id}/regenerate-blank")] public async Task RegenerateBlankQuestion(Guid id) { try { var userId = GetUserId(); var flashcard = await _context.Flashcards .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId); if (flashcard == null) { return NotFound(new { success = false, error = "Flashcard not found" }); } var blankQuestion = await _blankGenerationService.GenerateBlankQuestionAsync( flashcard.Word, flashcard.Example); if (string.IsNullOrEmpty(blankQuestion)) { return StatusCode(500, new { success = false, error = "Failed to generate blank question" }); } flashcard.FilledQuestionText = blankQuestion; flashcard.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Regenerated blank question for flashcard {Id}, word: {Word}", id, flashcard.Word); return Ok(new { success = true, data = new { filledQuestionText = blankQuestion } }); } catch (Exception ex) { _logger.LogError(ex, "Error regenerating blank question for flashcard {Id}", id); return StatusCode(500, new { success = false, error = "Failed to regenerate blank question" }); } } ``` #### 2.6 服務註冊 **檔案**: `backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs` ```csharp // 在 AddBusinessServices 方法中添加 public static IServiceCollection AddBusinessServices(this IServiceCollection services) { // 現有服務... services.AddScoped(); services.AddScoped(); return services; } ``` **檔案**: `backend/DramaLing.Api/Program.cs` ```csharp // 使用擴展方法 builder.Services.AddBusinessServices(); ``` ### ✅ 完成標準 - [ ] BlankGenerationService 服務實作完成 - [ ] 常見詞彙變形對應表建立 - [ ] AI 挖空整合測試通過 - [ ] API 端點功能驗證 - [ ] 錯誤處理和日誌完善 --- ## Phase 3: 前端組件優化 ### 🎯 目標 簡化 SentenceFillTest 組件,使用後端提供的挖空題目 ### 📝 具體任務 #### 3.1 更新組件 Props 介面 **檔案**: `frontend/components/review/review-tests/SentenceFillTest.tsx` ```typescript interface SentenceFillTestProps { word: string definition: string example: string // 原始例句 filledQuestionText?: string // 挖空後的題目 (新增) exampleTranslation: string pronunciation?: string difficultyLevel: string exampleImage?: string onAnswer: (answer: string) => void onReportError: () => void onImageClick?: (image: string) => void disabled?: boolean } ``` #### 3.2 簡化渲染邏輯 ```typescript // 替換複雜的 renderSentenceWithInput() const renderFilledSentence = () => { if (!filledQuestionText) { // 降級處理:使用當前的程式碼挖空 return renderSentenceWithInput(); } // 使用後端提供的挖空題目 const parts = filledQuestionText.split('____'); return (
{parts.map((part, index) => ( {part} {index < parts.length - 1 && ( setFillAnswer(e.target.value)} // ... 其他輸入框屬性 /> )} ))}
); }; ``` #### 3.3 更新頁面使用 **檔案**: `frontend/app/review-design/page.tsx` ```typescript ``` ### ✅ 完成標準 - [ ] SentenceFillTest 組件支援新欄位 - [ ] 降級處理機制正常運作 - [ ] 前端編譯和類型檢查通過 - [ ] review-design 頁面測試正常 --- ## Phase 4: 測試與優化 ### 🎯 目標 全面測試智能挖空系統,優化效能和準確性 ### 📝 具體任務 #### 4.1 詞彙變形測試 **測試案例**: ```javascript const testCases = [ { word: "eat", example: "She ate an apple", expected: "She ____ an apple" }, { word: "go", example: "He went to school", expected: "He ____ to school" }, { word: "good", example: "This is better", expected: "This is ____" }, { word: "child", example: "The children play", expected: "The ____ play" } ]; ``` #### 4.2 AI 挖空品質驗證 - 測試 AI 挖空準確性 - 驗證回應格式正確性 - 檢查異常情況處理 #### 4.3 效能優化 - 批次處理挖空生成 - 資料庫查詢優化 - 快取機制考量 #### 4.4 錯誤處理完善 - AI 服務異常處理 - 網路超時處理 - 降級策略驗證 ### ✅ 完成標準 - [ ] 所有測試案例通過 - [ ] AI 挖空準確率 > 90% - [ ] API 回應時間 < 2 秒 - [ ] 錯誤處理覆蓋率 100% --- ## 🚀 部署檢查清單 ### 資料庫 - [ ] Migration 執行成功 - [ ] 現有資料完整性確認 - [ ] 新欄位索引建立(如需要) ### 後端服務 - [ ] BlankGenerationService 註冊成功 - [ ] AI 服務整合測試 - [ ] API 端點功能驗證 - [ ] 日誌記錄完善 ### 前端組件 - [ ] SentenceFillTest 組件更新 - [ ] TypeScript 類型檢查通過 - [ ] 降級處理機制測試 - [ ] 用戶介面測試 ### 整合測試 - [ ] 端到端填空功能測試 - [ ] 各種詞彙變形驗證 - [ ] AI 輔助挖空測試 - [ ] 效能和穩定性測試 --- ## 📊 成功指標 ### 功能指標 - ✅ 支援 100% 詞彙變形挖空 - ✅ AI 輔助準確率 > 90% - ✅ 程式碼挖空成功率 > 80% ### 技術指標 - ✅ API 回應時間 < 2 秒 - ✅ 前端組件複雜度降低 50% - ✅ 挖空生成一次處理,多次使用 ### 用戶體驗指標 - ✅ 填空題顯示成功率 100% - ✅ 智能挖空準確性提升 - ✅ 系統回應速度提升 --- ## ⚠️ 風險管控 ### 高風險項目 1. **AI 服務依賴**: Gemini API 可能失敗 - **緩解**: 多層回退機制,程式碼挖空 → AI → 手動標記 2. **資料庫 Migration**: 可能影響現有資料 - **緩解**: 充分備份,漸進式部署 3. **前端相容性**: 新舊版本相容問題 - **緩解**: 降級處理邏輯,漸進式替換 ### 監控機制 - 挖空生成成功率監控 - AI 調用耗時和失敗率追蹤 - 使用者填空題完成率分析 --- ## 📅 時程安排 ### Week 1 - **Day 1-2**: Phase 1 (資料庫結構) - **Day 3-5**: Phase 2 (後端服務開發) ### Week 2 - **Day 1-2**: Phase 3 (前端組件優化) - **Day 3-4**: Phase 4 (測試與優化) - **Day 5**: 部署和監控 --- ## 🔧 開發工具和資源 ### 開發環境 - .NET 8.0 + Entity Framework Core - Next.js + TypeScript - SQLite 資料庫 ### 外部服務 - Google Gemini AI API - 現有的音頻和圖片服務 ### 測試工具 - 單元測試框架 - 整合測試環境 - 效能監控工具 --- ## 📈 後續擴展 ### 可能的增強功能 1. **多語言支援**: 支援其他語言的詞彙變形 2. **自訂挖空規則**: 允許手動調整挖空邏輯 3. **挖空難度分級**: 根據學習者程度調整挖空複雜度 4. **統計分析**: 分析挖空成功率和學習效果 ### 技術改進 1. **機器學習優化**: 基於歷史資料優化挖空準確性 2. **快取策略**: 實作 Redis 快取提升效能 3. **批次處理**: 大量詞彙的批次挖空處理 4. **監控儀表板**: 即時監控系統狀態和效能指標