dramaling-vocab-learning/智能填空題系統開發計劃.md

594 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 智能填空題系統開發計劃
> 基於 `智能填空題系統設計規格.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<Flashcard>();
// 現有欄位映射...
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<string?> GenerateBlankQuestionAsync(string word, string example);
string? TryProgrammaticBlank(string word, string example);
Task<string?> 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<BlankGenerationService> _logger;
public BlankGenerationService(
IWordVariationService wordVariationService,
IAIProviderManager aiProviderManager,
ILogger<BlankGenerationService> logger)
{
_wordVariationService = wordVariationService;
_aiProviderManager = aiProviderManager;
_logger = logger;
}
public async Task<string?> 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<string?> 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<WordVariationService> _logger;
private readonly Dictionary<string, string[]> 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<string>();
}
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<ActionResult> 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<ActionResult> 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<IBlankGenerationService, BlankGenerationService>();
services.AddScoped<IWordVariationService, WordVariationService>();
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 (
<div className="text-lg text-gray-700 leading-relaxed">
{parts.map((part, index) => (
<span key={index}>
{part}
{index < parts.length - 1 && (
<input
type="text"
value={fillAnswer}
onChange={(e) => setFillAnswer(e.target.value)}
// ... 其他輸入框屬性
/>
)}
</span>
))}
</div>
);
};
```
#### 3.3 更新頁面使用
**檔案**: `frontend/app/review-design/page.tsx`
```typescript
<SentenceFillTest
word={mockCardData.word}
definition={mockCardData.definition}
example={mockCardData.example}
filledQuestionText={mockCardData.filledQuestionText} // 新增
exampleTranslation={mockCardData.exampleTranslation}
// ... 其他屬性
/>
```
### ✅ 完成標準
- [ ] 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. **監控儀表板**: 即時監控系統狀態和效能指標