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

18 KiB
Raw Blame History

智能填空題系統開發計劃

基於 智能填空題系統設計規格.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

[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

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

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

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 挖空邏輯
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

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 方法強化
[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 新增重新生成端點

[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

// 在 AddBusinessServices 方法中添加
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
{
    // 現有服務...
    services.AddScoped<IBlankGenerationService, BlankGenerationService>();
    services.AddScoped<IWordVariationService, WordVariationService>();

    return services;
}

檔案: backend/DramaLing.Api/Program.cs

// 使用擴展方法
builder.Services.AddBusinessServices();

完成標準

  • BlankGenerationService 服務實作完成
  • 常見詞彙變形對應表建立
  • AI 挖空整合測試通過
  • API 端點功能驗證
  • 錯誤處理和日誌完善

Phase 3: 前端組件優化

🎯 目標

簡化 SentenceFillTest 組件,使用後端提供的挖空題目

📝 具體任務

3.1 更新組件 Props 介面

檔案: frontend/components/review/review-tests/SentenceFillTest.tsx

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 簡化渲染邏輯

// 替換複雜的 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

<SentenceFillTest
  word={mockCardData.word}
  definition={mockCardData.definition}
  example={mockCardData.example}
  filledQuestionText={mockCardData.filledQuestionText} // 新增
  exampleTranslation={mockCardData.exampleTranslation}
  // ... 其他屬性
/>

完成標準

  • SentenceFillTest 組件支援新欄位
  • 降級處理機制正常運作
  • 前端編譯和類型檢查通過
  • review-design 頁面測試正常

Phase 4: 測試與優化

🎯 目標

全面測試智能挖空系統,優化效能和準確性

📝 具體任務

4.1 詞彙變形測試

測試案例:

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. 監控儀表板: 即時監控系統狀態和效能指標