diff --git a/VOCABULARY_CACHE_MECHANISM_SPECIFICATION.md b/VOCABULARY_CACHE_MECHANISM_SPECIFICATION.md new file mode 100644 index 0000000..5d98440 --- /dev/null +++ b/VOCABULARY_CACHE_MECHANISM_SPECIFICATION.md @@ -0,0 +1,478 @@ +# 🗄️ 詞彙快取機制技術規格書 + +**專案**: DramaLing 英語學習平台 +**功能**: 詞彙分析快取系統 +**文檔版本**: v1.0 +**建立日期**: 2025-01-18 +**分析範圍**: 前端快取 + 後端 API 快取 + +--- + +## 📋 **快取系統概述** + +DramaLing 詞彙快取系統包含**三層快取結構**: +1. **前端頁面快取** - 當前頁面的詞彙分析資料 +2. **後端句子快取** - 24小時的句子分析結果快取 +3. **假資料快取** - 開發階段的模擬詞彙資料 + +--- + +## 🎯 **Layer 1: 前端頁面快取** + +### **📁 實現位置** +**檔案**: `/frontend/app/generate/page.tsx` +**狀態管理**: `const [sentenceAnalysis, setSentenceAnalysis] = useState(null)` + +### **🔄 快取行為分析** + +#### **初始化** (第84行) +```typescript +setSentenceAnalysis(result.data.wordAnalysis || result.data.WordAnalysis || {}) +``` + +**行為**: **完全覆蓋式更新** + +#### **動態擴展** (第405-412行) +```typescript +onNewWordAnalysis={(word, newAnalysis) => { + setSentenceAnalysis((prev: any) => ({ + ...prev, // 保留現有資料 + [word]: newAnalysis // 新增單字分析 + })) +}} +``` + +**行為**: **累積式更新** + +### **📊 完整的詞彙資料流程** + +#### **場景測試: "The apple" → "The orange"** + +``` +📍 步驟1: 分析 "The apple" +API 回應: { "apple": {...} } +前端狀態: { "apple": {...} } +結果: "The" = 灰框 (無預存資料) + +📍 步驟2: 點擊 "The" +API 調用: POST /api/ai/query-word {"word": "the", ...} +前端狀態: { "apple": {...}, "the": {...} } +結果: "The" = 藍框 (有預存資料) + +📍 步驟3: 換新句子 "The orange" +API 回應: { "orange": {...} } +前端狀態: { "orange": {...} } ❌ "the" 被清空! +結果: "The" = 灰框 (又變成無預存資料) +``` + +### **🚨 當前問題** + +| 操作 | 預期行為 | 實際行為 | 問題 | +|------|----------|----------|------| +| 查詢過的詞彙 | 保持快取,下次直接顯示 | 換句子後被清空 | ❌ 覆蓋式更新 | +| 跨句子學習 | 累積詞彙庫,提升效率 | 每次重新開始 | ❌ 浪費 AI 資源 | +| 用戶體驗 | 學過的詞彙有記憶 | 需要重複查詢 | ❌ 體驗差 | + +--- + +## 🎯 **Layer 2: 後端句子快取** + +### **📁 實現位置** +**檔案**: `/backend/DramaLing.Api/Controllers/AIController.cs` +**服務**: `IAnalysisCacheService _cacheService` +**資料表**: `SentenceAnalysisCache` + +### **💾 快取機制** + +#### **存入快取** (第589-602行) +```csharp +await _cacheService.SetCachedAnalysisAsync( + request.InputText, // 快取鍵:句子文本 + baseResponseData, // 完整分析結果 + TimeSpan.FromHours(24) // TTL: 24小時 +); +``` + +#### **快取檢索** (第533-561行) +```csharp +var cachedAnalysis = await _cacheService.GetCachedAnalysisAsync(request.InputText); +if (cachedAnalysis != null && !request.ForceRefresh) { + // 返回快取結果,標記為 cached: true +} +``` + +### **📊 快取資料結構** +```sql +CREATE TABLE SentenceAnalysisCache ( + Id UNIQUEIDENTIFIER PRIMARY KEY, + InputText NVARCHAR(1000) NOT NULL, -- 原句 (快取鍵) + InputTextHash NVARCHAR(64) NOT NULL, -- 句子雜湊值 + AnalysisResult NVARCHAR(MAX) NOT NULL, -- JSON 分析結果 + ExpiresAt DATETIME2 NOT NULL, -- 過期時間 + CreatedAt DATETIME2 NOT NULL, -- 建立時間 + LastAccessedAt DATETIME2, -- 最後存取時間 + AccessCount INT NOT NULL DEFAULT 0 -- 存取次數 +); +``` + +### **🔄 快取邏輯流程** +``` +用戶輸入: "Hello world" + ↓ +檢查快取: SELECT * FROM SentenceAnalysisCache WHERE InputTextHash = HASH("Hello world") + ↓ +如果命中: 返回快取結果 (cached: true, cacheHit: true) +如果錯失: 調用 AI → 存入快取 → 返回結果 (cached: false, usingAI: true) +``` + +--- + +## 🎯 **Layer 3: 單字查詢快取 (目前為假資料)** + +### **📁 實現位置** +**檔案**: `/backend/DramaLing.Api/Controllers/AIController.cs:1019-1037` + +### **⚠️ 當前狀態: 混合實現 (需要進一步確認)** + +**根據後端日誌證據,系統確實在調用真實的 Gemini AI**: +``` +info: Calling Gemini AI for text: Learning is fun and exciting +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=AIza... +``` + +**但程式碼顯示模擬實現**: +```csharp +private async Task AnalyzeLowValueWord(string word, string sentence) +{ + // 模擬即時AI分析 + await Task.Delay(200); // 模擬延遲 + + return new { + word = word, + translation = "即時分析的翻譯", // ⚠️ 疑似固定回應 + definition = "即時分析的定義", + // ... + }; +} +``` + +### **📊 API 回應速度分析** + +| API 端點 | 速度 | 實現狀態 | 證據 | +|----------|------|----------|------| +| `/analyze-sentence` | ~3-5秒 | ✅ 確認真實 AI | 日誌顯示 Gemini 調用 | +| `/query-word` | ~200ms-1s | ❓ **需要確認** | 程式碼顯示模擬,但可能有其他路徑 | + +### **🔍 需要進一步調查** +1. `AnalyzeLowValueWord` 是否有其他版本的實現 +2. 是否存在條件分支調用真實 AI +3. 固定回應 "即時分析的翻譯" 是否為測試資料 + +--- + +## 📋 **詳細的預存機制規格** + +### **🔍 您的測試場景分析** + +#### **場景**: "The apple" → 點擊 "The" → "The orange" + +``` +🟦 第1步: 分析 "The apple" +├─ API: POST /analyze-sentence {"inputText": "The apple"} +├─ AI 回應: {"wordAnalysis": {"apple": {...}}} // 不包含 "the" +├─ 前端狀態: sentenceAnalysis = {"apple": {...}} +└─ 視覺: "The"=灰框, "apple"=綠框 + +🟦 第2步: 點擊 "The" +├─ 觸發: queryWordWithAI("the") +├─ API: POST /query-word {"word": "the", "sentence": "The apple"} +├─ 模擬回應: {"word": "the", "translation": "即時分析的翻譯", ...} +├─ 前端狀態: sentenceAnalysis = {"apple": {...}, "the": {...}} +└─ 視覺: "The"=藍框, "apple"=綠框 + +🟦 第3步: 分析 "The orange" +├─ API: POST /analyze-sentence {"inputText": "The orange"} +├─ AI 回應: {"wordAnalysis": {"orange": {...}}} +├─ 前端狀態: sentenceAnalysis = {"orange": {...}} ❌ "the" 被覆蓋清空! +└─ 視覺: "The"=灰框, "orange"=綠框 + +🟦 第4步: 再次點擊 "The" +├─ 發現: sentenceAnalysis["the"] = undefined +├─ 觸發: queryWordWithAI("the") again ❌ 重複查詢! +└─ 結果: 浪費 AI 資源,用戶體驗差 +``` + +### **📊 當前快取機制的優缺點** + +#### ✅ **優點** +1. **句子級快取**: 相同句子 24 小時內不重複分析 +2. **動態擴展**: 點擊的詞彙會加入當前分析 +3. **記憶體效率**: 不會無限累積資料 + +#### ❌ **缺點** +1. **跨句子遺失**: 換句子後之前查詢的詞彙被清空 +2. **重複查詢**: 相同詞彙在不同句子中需要重複查詢 +3. **假資料問題**: query-word 目前不是真實 AI 查詢 + +--- + +## 🛠️ **改善方案規格** + +### **方案1: 全域詞彙快取 (推薦)** + +#### **前端實現** +```typescript +// 新增全域詞彙快取 +const [globalWordCache, setGlobalWordCache] = useState>({}) + +// 修改句子分析更新邏輯 +setSentenceAnalysis(prev => ({ + ...globalWordCache, // 保留全域快取 + ...prev, // 保留當前分析 + ...result.data.wordAnalysis // 新增句子分析 +})) + +// 修改詞彙查詢邏輯 +onNewWordAnalysis={(word, newAnalysis) => { + // 同時更新兩個快取 + setGlobalWordCache(prev => ({ ...prev, [word]: newAnalysis })) + setSentenceAnalysis(prev => ({ ...prev, [word]: newAnalysis })) +}} +``` + +#### **本地存儲持久化** +```typescript +// 保存到 localStorage +useEffect(() => { + const cached = localStorage.getItem('dramalingWordCache') + if (cached) { + setGlobalWordCache(JSON.parse(cached)) + } +}, []) + +useEffect(() => { + localStorage.setItem('dramalingWordCache', JSON.stringify(globalWordCache)) +}, [globalWordCache]) +``` + +### **方案2: 真實 AI 查詢實現** + +#### **後端修改** +```csharp +private async Task AnalyzeLowValueWord(string word, string sentence) +{ + try { + // 🆕 真實調用 Gemini AI + var prompt = $@" +請分析單字 ""{word}"" 在句子 ""{sentence}"" 中的詳細資訊: + +請以JSON格式回應: +{{ + ""word"": ""{word}"", + ""translation"": ""繁體中文翻譯"", + ""definition"": ""英文定義"", + ""partOfSpeech"": ""詞性"", + ""pronunciation"": ""IPA音標"", + ""difficultyLevel"": ""CEFR等級"", + ""contextMeaning"": ""在此句子中的具體含義"", + ""isHighValue"": false +}} +"; + + var response = await _geminiService.CallGeminiApiAsync(prompt); + return _geminiService.ParseWordAnalysisResponse(response); + } + catch { + // 回退到模擬資料 + await Task.Delay(200); + return CreateMockWordAnalysis(word); + } +} +``` + +### **方案3: 後端詞彙快取資料表** + +#### **新資料表設計** +```sql +CREATE TABLE WordAnalysisCache ( + Id UNIQUEIDENTIFIER PRIMARY KEY, + Word NVARCHAR(100) NOT NULL, -- 詞彙 (快取鍵) + WordLowercase NVARCHAR(100) NOT NULL, -- 小寫版本 (查詢用) + Translation NVARCHAR(200) NOT NULL, -- 翻譯 + Definition NVARCHAR(500) NOT NULL, -- 定義 + PartOfSpeech NVARCHAR(50), -- 詞性 + Pronunciation NVARCHAR(100), -- 發音 + DifficultyLevel NVARCHAR(10), -- CEFR 等級 + IsHighValue BIT DEFAULT 0, -- 是否高價值 + Synonyms NVARCHAR(500), -- 同義詞 (JSON) + ExampleSentences NVARCHAR(MAX), -- 例句 (JSON) + CreatedAt DATETIME2 NOT NULL, -- 建立時間 + UpdatedAt DATETIME2 NOT NULL, -- 更新時間 + AccessCount INT DEFAULT 0, -- 存取次數 + + INDEX IX_WordAnalysisCache_WordLowercase (WordLowercase) +); +``` + +#### **後端查詢邏輯** +```csharp +public async Task QueryWordAsync(string word, string sentence) +{ + var wordLower = word.ToLower(); + + // 1. 檢查詞彙快取 + var cached = await _context.WordAnalysisCache + .FirstOrDefaultAsync(w => w.WordLowercase == wordLower); + + if (cached != null) { + // 更新存取統計 + cached.AccessCount++; + cached.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return MapToWordAnalysisResult(cached); + } + + // 2. 快取錯失,調用 AI + var aiResult = await CallGeminiForWordAnalysis(word, sentence); + + // 3. 存入快取 + var cacheEntry = new WordAnalysisCache { + Word = word, + WordLowercase = wordLower, + Translation = aiResult.Translation, + // ... 其他欄位 + }; + + _context.WordAnalysisCache.Add(cacheEntry); + await _context.SaveChangesAsync(); + + return aiResult; +} +``` + +--- + +## 📊 **三種快取策略比較** + +| 策略 | 持久性 | 效能 | 實現複雜度 | AI 成本 | 用戶體驗 | +|------|--------|------|-----------|---------|----------| +| **目前 (頁面級)** | ❌ 換句子清空 | 🟡 中等 | 🟢 簡單 | 🔴 高 (重複查詢) | 🔴 差 | +| **方案1 (前端全域)** | 🟡 瀏覽器重啟清空 | 🟢 高 | 🟡 中等 | 🟢 低 | 🟢 好 | +| **方案2 (後端資料庫)** | ✅ 永久保存 | 🟢 高 | 🔴 複雜 | 🟢 極低 | ✅ 極佳 | + +--- + +## 🔧 **當前 query-word API 的實現細節** + +### **📍 速度快的真相** + +**檔案**: `/backend/DramaLing.Api/Controllers/AIController.cs:1019-1037` + +```csharp +private async Task AnalyzeLowValueWord(string word, string sentence) +{ + // 🚨 這只是模擬實現! + await Task.Delay(200); // 假延遲 + + return new { + word = word, + translation = "即時分析的翻譯", // 🚨 所有詞彙都一樣 + definition = "即時分析的定義", // 🚨 所有詞彙都一樣 + partOfSpeech = "noun", // 🚨 所有詞彙都一樣 + pronunciation = "/example/", // 🚨 所有詞彙都一樣 + // ... + }; +} +``` + +### **🧪 驗證測試** + +#### **測試1: 查詢不同詞彙** +```bash +# 查詢 "hello" +curl -X POST http://localhost:5000/api/ai/query-word \ + -d '{"word": "hello", "sentence": "Hello world"}' +# 結果: translation = "即時分析的翻譯" + +# 查詢 "amazing" +curl -X POST http://localhost:5000/api/ai/query-word \ + -d '{"word": "amazing", "sentence": "Amazing day"}' +# 結果: translation = "即時分析的翻譯" ❌ 完全相同! +``` + +#### **測試2: 檢查是否真的調用 AI** +```bash +# 查看後端日誌 +grep -i "gemini\|ai\|query" backend_logs.txt +# 結果: 沒有真實的 AI API 調用記錄 +``` + +--- + +## 📋 **建議的改善優先級** + +### **🔥 高優先級 (立即修復)** +1. **實現真實的詞彙 AI 查詢** + - 替換假資料為真實 Gemini API 調用 + - 提供準確的詞彙分析 + +2. **前端全域詞彙快取** + - 避免重複查詢相同詞彙 + - 提升用戶體驗 + +### **⚡ 中優先級 (2週內)** +3. **後端詞彙快取資料表** + - 永久保存查詢過的詞彙 + - 跨用戶共享常用詞彙分析 + +4. **智能快取策略** + - 基於詞彙頻率的快取優先級 + - 自動清理低價值快取項目 + +### **💡 低優先級 (未來功能)** +5. **跨設備同步** + - 用戶詞彙學習記錄雲端同步 + - 個人化詞彙掌握程度追蹤 + +--- + +## 🎯 **回答您的問題** + +### **當前實際行為**: + +**場景**: "The apple" → 點擊 "The" → "The orange" + +``` +1. 分析 "The apple" → "The" 無預存資料 (灰框) +2. 點擊 "The" → 假 AI 查詢 → 加入前端快取 → "The" 變藍框 +3. 分析 "The orange" → 前端快取被覆蓋清空 → "The" 又變灰框 ❌ +4. 點擊 "The" → 重新假 AI 查詢 → 重複步驟2 ❌ +``` + +### **問題總結**: +- ❌ **不會在預存裡**: 換句子後快取被清空 +- ❌ **重複假查詢**: 每次都返回相同的假資料 +- ❌ **浪費資源**: 用戶以為是真實 AI 查詢 + +### **建議修復**: +1. **立即**: 修改前端為累積式快取 +2. **短期**: 實現真實的詞彙 AI 查詢 +3. **長期**: 建立後端詞彙快取資料表 + +--- + +## 📞 **技術支援** + +**相關檔案**: +- 前端快取: `/frontend/app/generate/page.tsx:84, 405-412` +- 後端假查詢: `/backend/DramaLing.Api/Controllers/AIController.cs:1019-1037` +- 後端句子快取: `/backend/DramaLing.Api/Services/AnalysisCacheService.cs` + +**建議優先修復**: 前端累積式快取 + 真實 AI 查詢實現 + +--- + +**© 2025 DramaLing Development Team** +**文檔建立**: 2025-01-18 +**分析基於**: 當前系統 commit e940d86 \ No newline at end of file diff --git a/backend/DramaLing.Api/Controllers/AIController.cs b/backend/DramaLing.Api/Controllers/AIController.cs index 5807e0e..fbb9c95 100644 --- a/backend/DramaLing.Api/Controllers/AIController.cs +++ b/backend/DramaLing.Api/Controllers/AIController.cs @@ -577,8 +577,7 @@ public class AIController : ControllerBase GrammarCorrection = aiAnalysis.GrammarCorrection, SentenceMeaning = new { - Translation = aiAnalysis.Translation, - Explanation = aiAnalysis.Explanation + Translation = aiAnalysis.Translation }, FinalAnalysisText = finalText ?? request.InputText, WordAnalysis = aiAnalysis.WordAnalysis, @@ -627,8 +626,7 @@ public class AIController : ControllerBase GrammarCorrection = grammarCorrection, SentenceMeaning = new { - Translation = analysis.Translation, - Explanation = analysis.Explanation + Translation = analysis.Translation }, FinalAnalysisText = finalText, WordAnalysis = analysis.WordAnalysis, diff --git a/backend/DramaLing.Api/Services/GeminiService.cs b/backend/DramaLing.Api/Services/GeminiService.cs index 4265627..222b804 100644 --- a/backend/DramaLing.Api/Services/GeminiService.cs +++ b/backend/DramaLing.Api/Services/GeminiService.cs @@ -61,7 +61,7 @@ public class GeminiService : IGeminiService } var prompt = $@" -請分析以下英文句子,提供完整的分析: +請分析以下英文句子,提供翻譯和詞彙分析: 句子:{inputText} @@ -69,7 +69,6 @@ public class GeminiService : IGeminiService {{ ""translation"": ""自然流暢的繁體中文翻譯"", - ""explanation"": ""詳細解釋句子的語法結構、詞彙特點、使用場景和學習要點"", ""grammarCorrection"": {{ ""hasErrors"": false, ""originalText"": ""{inputText}"", @@ -91,10 +90,9 @@ public class GeminiService : IGeminiService 要求: 1. 翻譯要自然流暢,符合中文語法 -2. 解釋要具體有用,不要空泛 -3. 標記B1以上詞彙為高價值 -4. 如有語法錯誤請指出並修正 -5. 確保JSON格式正確 +2. 標記B1以上詞彙為高價值 +3. 如有語法錯誤請指出並修正 +4. 確保JSON格式正確 "; var response = await CallGeminiApiAsync(prompt); diff --git a/frontend/app/generate/page.tsx b/frontend/app/generate/page.tsx index 3f96514..55d00e5 100644 --- a/frontend/app/generate/page.tsx +++ b/frontend/app/generate/page.tsx @@ -86,9 +86,8 @@ function GenerateContent() { // 安全處理 sentenceMeaning - 支援兩種key格式 (小寫/大寫) const sentenceMeaning = result.data.sentenceMeaning || result.data.SentenceMeaning || {} const translation = sentenceMeaning.Translation || sentenceMeaning.translation || '翻譯處理中...' - const explanation = sentenceMeaning.Explanation || sentenceMeaning.explanation || '解釋處理中...' - setSentenceMeaning(translation + ' ' + explanation) + setSentenceMeaning(translation) setGrammarCorrection(result.data.grammarCorrection || result.data.GrammarCorrection || { hasErrors: false }) setFinalText(result.data.finalAnalysisText || result.data.FinalAnalysisText || textInput) @@ -381,14 +380,14 @@ function GenerateContent() { {/* 互動式文字 */}
-

點擊查詢單字意思

+

詞彙分析

💡 使用說明:點擊下方句子中的任何單字,可以立即查看詳細意思。
- 🟡 黃色邊框 + ⭐ = 高價值片語(免費點擊)
+ {/* 🟡 黃色邊框 + ⭐ = 高價值片語(免費點擊)
🟢 綠色邊框 + ⭐ = 高價值單字(免費點擊)
- 🔵 藍色下劃線 = 普通單字(點擊扣 1 次) + 🔵 藍色下劃線 = 普通單字(點擊扣 1 次) */}

@@ -400,21 +399,16 @@ function GenerateContent() { onWordClick={(word, analysis) => { console.log('Clicked word:', word, analysis) }} - onWordCostConfirm={async (word, cost) => { - if (usageCount >= 5) { - alert('❌ 使用額度不足,無法查詢低價值詞彙') - return false - } - - const confirmed = window.confirm( - `查詢 "${word}" 將消耗 ${cost} 次使用額度,您剩餘 ${5 - usageCount} 次。\n\n是否繼續?` - ) - - if (confirmed) { - setUsageCount(prev => prev + cost) - return true - } - return false + onWordCostConfirm={async () => { + return true // 移除付費限制,直接允許 + }} + onNewWordAnalysis={(word, newAnalysis) => { + // 將新的詞彙分析資料加入到現有分析中 + setSentenceAnalysis((prev: any) => ({ + ...prev, + [word]: newAnalysis + })) + console.log(`✅ 新增詞彙分析: ${word}`, newAnalysis) }} />
diff --git a/frontend/components/ClickableTextV2.tsx b/frontend/components/ClickableTextV2.tsx index 337e2ab..0f5cd9e 100644 --- a/frontend/components/ClickableTextV2.tsx +++ b/frontend/components/ClickableTextV2.tsx @@ -35,6 +35,7 @@ interface ClickableTextProps { }> onWordClick?: (word: string, analysis: WordAnalysis) => void onWordCostConfirm?: (word: string, cost: number) => Promise // 收費確認 + onNewWordAnalysis?: (word: string, analysis: WordAnalysis) => void // 新詞彙分析資料回調 remainingUsage?: number // 剩餘使用次數 } @@ -45,6 +46,7 @@ export function ClickableTextV2({ phrasesDetected = [], onWordClick, onWordCostConfirm, + onNewWordAnalysis, remainingUsage = 5 }: ClickableTextProps) { const [selectedWord, setSelectedWord] = useState(null) @@ -69,27 +71,29 @@ export function ClickableTextV2({ const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') const wordAnalysis = analysis?.[cleanWord] - if (wordAnalysis) { - const rect = event.currentTarget.getBoundingClientRect() - const position = { - x: rect.left + rect.width / 2, - y: rect.top - 10 - } + const rect = event.currentTarget.getBoundingClientRect() + const position = { + x: rect.left + rect.width / 2, + y: rect.top - 10 + } - // 檢查是否為高價值詞彙(免費)- 支援兩種屬性格式 + if (wordAnalysis) { + // 場景A:有預存資料的詞彙 const isHighValue = getWordProperty(wordAnalysis, 'isHighValue') if (isHighValue) { + // 高價值詞彙 → 直接免費顯示 setPopupPosition(position) setSelectedWord(cleanWord) onWordClick?.(cleanWord, wordAnalysis) } else { - // 低價值詞彙需要收費確認 - setShowCostConfirm({ - word: cleanWord, - cost: 1, - position - }) + // 低價值詞彙 → 直接顯示(移除付費限制) + setPopupPosition(position) + setSelectedWord(cleanWord) + onWordClick?.(cleanWord, wordAnalysis) } + } else { + // 場景B:無預存資料的詞彙 → 即時調用 AI 查詢 + await queryWordWithAI(cleanWord, position) } } @@ -146,30 +150,73 @@ export function ClickableTextV2({ setSelectedWord(null) } + const queryWordWithAI = async (word: string, position: { x: number, y: number }) => { + try { + console.log(`🤖 查詢單字: ${word}`) + + const response = await fetch('http://localhost:5000/api/ai/query-word', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + word: word, + sentence: text, + analysisId: null + }) + }) + + if (response.ok) { + const result = await response.json() + console.log('AI 查詢結果:', result) + + if (result.success && result.data?.analysis) { + // 將新的分析資料通知父組件 + onNewWordAnalysis?.(word, result.data.analysis) + + // 顯示分析結果 + setPopupPosition(position) + setSelectedWord(word) + onWordClick?.(word, result.data.analysis) + } else { + alert(`❌ 查詢 "${word}" 失敗,請稍後再試`) + } + } else { + throw new Error(`API 錯誤: ${response.status}`) + } + } catch (error) { + console.error('AI 查詢錯誤:', error) + alert(`❌ 查詢 "${word}" 時發生錯誤,請稍後再試`) + } + } + const getWordClass = (word: string) => { const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') const wordAnalysis = analysis?.[cleanWord] - if (!wordAnalysis) return "cursor-default" - const baseClass = "cursor-pointer transition-all duration-200 rounded relative mx-0.5 px-1 py-0.5" - // 支援兩種屬性名稱格式 - const isHighValue = getWordProperty(wordAnalysis, 'isHighValue') - const isPhrase = getWordProperty(wordAnalysis, 'isPhrase') + if (wordAnalysis) { + // 有預存資料的詞彙 + const isHighValue = getWordProperty(wordAnalysis, 'isHighValue') + const isPhrase = getWordProperty(wordAnalysis, 'isPhrase') - // 高價值片語(黃色系) - if (isHighValue && isPhrase) { - return `${baseClass} bg-yellow-100 border-2 border-yellow-400 hover:bg-yellow-200 hover:shadow-sm transform hover:-translate-y-0.5` + // 高價值片語(黃色系) + if (isHighValue && isPhrase) { + return `${baseClass} bg-yellow-100 border-2 border-yellow-400 hover:bg-yellow-200 hover:shadow-sm transform hover:-translate-y-0.5` + } + + // 高價值單字(綠色系) + if (isHighValue && !isPhrase) { + return `${baseClass} bg-green-100 border-2 border-green-400 hover:bg-green-200 hover:shadow-sm transform hover:-translate-y-0.5` + } + + // 普通單字(藍色系) + return `${baseClass} bg-blue-100 border-2 border-blue-300 hover:bg-blue-200 hover:shadow-sm` + } else { + // 無預存資料的詞彙(灰色虛線,表示需要即時查詢) + return `${baseClass} border-2 border-dashed border-gray-300 hover:border-gray-400 bg-gray-50 hover:bg-gray-100` } - - // 高價值單字(綠色系) - if (isHighValue && !isPhrase) { - return `${baseClass} bg-green-100 border-2 border-green-400 hover:bg-green-200 hover:shadow-sm transform hover:-translate-y-0.5` - } - - // 普通單字(藍色系) - return `${baseClass} border-b border-blue-300 hover:bg-blue-100 hover:border-blue-400` } diff --git a/note/plan/PERSONALIZED_HIGH_VALUE_WORDS_DEVELOPMENT_PLAN.md b/note/plan/PERSONALIZED_HIGH_VALUE_WORDS_DEVELOPMENT_PLAN.md new file mode 100644 index 0000000..c69e523 --- /dev/null +++ b/note/plan/PERSONALIZED_HIGH_VALUE_WORDS_DEVELOPMENT_PLAN.md @@ -0,0 +1,1010 @@ +# 🎯 個人化高價值詞彙判定系統 - 詳細開發計劃 + +**專案**: DramaLing 英語學習平台 +**功能**: 個人化高價值詞彙智能判定 +**計劃版本**: v1.0 +**建立日期**: 2025-01-18 +**預計開發時程**: 2週 + +--- + +## 📋 **問題分析與目標** + +### 🚨 **現有問題** +**當前實現位置**: `/backend/DramaLing.Api/Services/GeminiService.cs:95` +```csharp +// 目前的固定判定邏輯 +"3. 標記B1以上詞彙為高價值" +``` + +**問題分析**: +- **A1 學習者**: 看不到 A2 詞彙的學習價值(應該對他們很重要) +- **C1 學習者**: 被 B1 詞彙干擾(對他們太簡單) +- **缺乏個人化**: 一刀切設計不符合個別學習需求 +- **學習效果差**: 無法提供適當的挑戰程度 + +### 🎯 **設計目標** +``` +新邏輯: 高價值詞彙 = 學習者當前程度 + 1~2 階級 +``` + +**預期效果**: +| 學習者程度 | 當前標記為高價值 | 新標記為高價值 | 改善效果 | +|-----------|----------------|--------------|----------| +| A1 | B1, B2, C1, C2 | **A2, B1** | 更實用的學習目標 | +| A2 | B1, B2, C1, C2 | **B1, B2** | 適當的進階挑戰 | +| B1 | B1, B2, C1, C2 | **B2, C1** | 避免重複簡單詞彙 | +| C1 | B1, B2, C1, C2 | **C2** | 專注高階詞彙精進 | + +--- + +## 🛠️ **詳細開發計劃** + +## **📋 Phase 1: 資料模型擴充 (第1-2天)** + +### **1.1 User 實體擴充** +**檔案**: `/backend/DramaLing.Api/Models/Entities/User.cs` +**當前行數**: 30行 (Preferences 欄位後) + +**新增欄位**: +```csharp +// 在第30行 public Dictionary Preferences { get; set; } = new(); 後新增 + +[MaxLength(10)] +public string EnglishLevel { get; set; } = "A2"; // A1, A2, B1, B2, C1, C2 + +public DateTime LevelUpdatedAt { get; set; } = DateTime.UtcNow; + +public bool IsLevelVerified { get; set; } = false; // 是否通過測試驗證 + +[MaxLength(500)] +public string? LevelNotes { get; set; } // 程度設定備註 +``` + +### **1.2 資料庫遷移** +**執行指令**: +```bash +# 在 /backend/DramaLing.Api/ 目錄下執行 +dotnet ef migrations add AddUserEnglishLevel +dotnet ef database update +``` + +**生成的遷移檔案**: `/backend/DramaLing.Api/Migrations/{timestamp}_AddUserEnglishLevel.cs` + +### **1.3 API 請求模型更新** +**檔案**: `/backend/DramaLing.Api/Controllers/AIController.cs` +**位置**: 第1433行附近的 AnalyzeSentenceRequest 類別 + +**當前結構**: +```csharp +public class AnalyzeSentenceRequest +{ + public string InputText { get; set; } = string.Empty; + public bool ForceRefresh { get; set; } = false; + public string AnalysisMode { get; set; } = "full"; +} +``` + +**修改後**: +```csharp +public class AnalyzeSentenceRequest +{ + public string InputText { get; set; } = string.Empty; + public string UserLevel { get; set; } = "A2"; // 新增:用戶英語程度 + public bool ForceRefresh { get; set; } = false; + public string AnalysisMode { get; set; } = "full"; +} +``` + +--- + +## **📋 Phase 2: 智能判定邏輯建立 (第3-5天)** + +### **2.1 建立 CEFR 等級服務** +**新檔案**: `/backend/DramaLing.Api/Services/CEFRLevelService.cs` + +```csharp +using System; + +namespace DramaLing.Api.Services; + +public static class CEFRLevelService +{ + private static readonly string[] Levels = { "A1", "A2", "B1", "B2", "C1", "C2" }; + + /// + /// 取得 CEFR 等級的數字索引 + /// + public static int GetLevelIndex(string level) + { + if (string.IsNullOrEmpty(level)) return 1; // 預設 A2 + return Array.IndexOf(Levels, level.ToUpper()); + } + + /// + /// 判定詞彙對特定用戶是否為高價值 + /// 規則:比用戶程度高 1-2 級的詞彙為高價值 + /// + public static bool IsHighValueForUser(string wordLevel, string userLevel) + { + var userIndex = GetLevelIndex(userLevel); + var wordIndex = GetLevelIndex(wordLevel); + + // 無效等級處理 + if (userIndex == -1 || wordIndex == -1) return false; + + // 高價值 = 比用戶程度高 1-2 級 + return wordIndex >= userIndex + 1 && wordIndex <= userIndex + 2; + } + + /// + /// 取得用戶的目標學習等級範圍 + /// + public static string GetTargetLevelRange(string userLevel) + { + var userIndex = GetLevelIndex(userLevel); + if (userIndex == -1) return "B1-B2"; + + var targetMin = Levels[Math.Min(userIndex + 1, Levels.Length - 1)]; + var targetMax = Levels[Math.Min(userIndex + 2, Levels.Length - 1)]; + + return targetMin == targetMax ? targetMin : $"{targetMin}-{targetMax}"; + } + + /// + /// 取得下一個等級 + /// + public static string GetNextLevel(string currentLevel, int steps = 1) + { + var currentIndex = GetLevelIndex(currentLevel); + if (currentIndex == -1) return "B1"; + + var nextIndex = Math.Min(currentIndex + steps, Levels.Length - 1); + return Levels[nextIndex]; + } +} +``` + +### **2.2 更新 GeminiService** +**檔案**: `/backend/DramaLing.Api/Services/GeminiService.cs` +**修改位置**: 第54行的 `AnalyzeSentenceAsync` 方法 + +**當前方法簽名**: +```csharp +public async Task AnalyzeSentenceAsync(string inputText) +``` + +**修改後簽名**: +```csharp +public async Task AnalyzeSentenceAsync( + string inputText, + string userLevel = "A2") +``` + +**Prompt 動態化 (第63-98行)**: +```csharp +// 替換現有的固定 prompt +private string BuildSentenceAnalysisPrompt(string inputText, string userLevel) +{ + var targetRange = CEFRLevelService.GetTargetLevelRange(userLevel); + + return $@" +請分析以下英文句子,提供完整的分析: + +句子:{inputText} +學習者程度:{userLevel} + +請按照以下JSON格式回應,不要包含任何其他文字: + +{{ + ""translation"": ""自然流暢的繁體中文翻譯"", + ""explanation"": ""詳細解釋句子的語法結構、詞彙特點、使用場景和學習要點"", + ""grammarCorrection"": {{ + ""hasErrors"": false, + ""originalText"": ""{inputText}"", + ""correctedText"": null, + ""corrections"": [] + }}, + ""highValueWords"": [""重要詞彙1"", ""重要詞彙2""], + ""wordAnalysis"": {{ + ""單字"": {{ + ""translation"": ""中文翻譯"", + ""definition"": ""英文定義"", + ""partOfSpeech"": ""詞性"", + ""pronunciation"": ""音標"", + ""isHighValue"": true, + ""difficultyLevel"": ""CEFR等級"" + }} + }} +}} + +要求: +1. 翻譯要自然流暢,符合中文語法 +2. 解釋要具體有用,不要空泛 +3. **基於學習者程度({userLevel}),標記 {targetRange} 等級的詞彙為高價值** +4. 如有語法錯誤請指出並修正 +5. 確保JSON格式正確 + +高價值判定邏輯: +- 學習者程度: {userLevel} +- 高價值範圍: {targetRange} +- 太簡單的詞彙(≤{userLevel})不要標記為高價值 +- 太難的詞彙(>{targetRange})謹慎標記 +- 重點關注 {targetRange} 範圍內的詞彙 +"; +} +``` + +### **2.3 後處理邏輯新增** +```csharp +// 在 AnalyzeSentenceAsync 方法中新增後處理 +private SentenceAnalysisResponse PostProcessHighValueWords( + SentenceAnalysisResponse result, + string userLevel) +{ + // 二次驗證高價值詞彙判定,確保 AI 判定正確 + foreach (var wordPair in result.WordAnalysis) + { + var word = wordPair.Value; + word.IsHighValue = CEFRLevelService.IsHighValueForUser( + word.DifficultyLevel, + userLevel); + } + + // 更新高價值詞彙列表 + result.HighValueWords = result.WordAnalysis + .Where(w => w.Value.IsHighValue) + .Select(w => w.Key) + .ToList(); + + return result; +} +``` + +--- + +## **📋 Phase 3: Controller 層整合 (第6-7天)** + +### **3.1 更新 AnalyzeSentence API** +**檔案**: `/backend/DramaLing.Api/Controllers/AIController.cs` +**位置**: 第501行的 `AnalyzeSentence` 方法 + +**在第566行前新增用戶程度取得邏輯**: +```csharp +// 第560行後,第566行 aiAnalysis 調用前新增: + +// 取得用戶英語程度 +string userLevel = request.UserLevel; +if (string.IsNullOrEmpty(userLevel)) +{ + // 如果 API 請求未提供程度,嘗試從用戶資料取得 + var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); + if (userId.HasValue) + { + var user = await _context.Users.FindAsync(userId.Value); + userLevel = user?.EnglishLevel ?? "A2"; + _logger.LogInformation("Retrieved user level from database: {UserLevel}", userLevel); + } + else + { + userLevel = "A2"; // 未登入用戶預設程度 + _logger.LogInformation("Using default level for anonymous user: A2"); + } +} + +_logger.LogInformation("Using user level for analysis: {UserLevel}", userLevel); +``` + +**修改第566行的 AI 調用**: +```csharp +// 原本: +// var aiAnalysis = await _geminiService.AnalyzeSentenceAsync(request.InputText); + +// 修改為: +var aiAnalysis = await _geminiService.AnalyzeSentenceAsync(request.InputText, userLevel); +``` + +### **3.2 回應資料增強** +**位置**: 第573行的 baseResponseData 物件 + +**新增用戶程度相關資訊**: +```csharp +var baseResponseData = new +{ + AnalysisId = Guid.NewGuid(), + InputText = request.InputText, + UserLevel = userLevel, // 新增:顯示使用的程度 + HighValueCriteria = CEFRLevelService.GetTargetLevelRange(userLevel), // 新增:顯示高價值判定範圍 + GrammarCorrection = aiAnalysis.GrammarCorrection, + SentenceMeaning = new + { + Translation = aiAnalysis.Translation, + Explanation = aiAnalysis.Explanation + }, + FinalAnalysisText = finalText ?? request.InputText, + WordAnalysis = aiAnalysis.WordAnalysis, + HighValueWords = aiAnalysis.HighValueWords, + PhrasesDetected = new object[0] +}; +``` + +--- + +## **📋 Phase 4: 前端個人化體驗 (第8-11天)** + +### **4.1 建立用戶程度設定頁面** +**新檔案**: `/frontend/app/settings/page.tsx` + +```typescript +'use client' +import { useState, useEffect } from 'react' + +interface LanguageLevel { + value: string; + label: string; + description: string; + examples: string[]; +} + +export default function SettingsPage() { + const [userLevel, setUserLevel] = useState('A2'); + const [isLoading, setIsLoading] = useState(false); + + const levels: LanguageLevel[] = [ + { + value: 'A1', + label: 'A1 - 初學者', + description: '能理解基本詞彙和簡單句子', + examples: ['hello', 'good', 'house', 'eat', 'happy'] + }, + { + value: 'A2', + label: 'A2 - 基礎', + description: '能處理日常對話和常見主題', + examples: ['important', 'difficult', 'interesting', 'beautiful', 'understand'] + }, + { + value: 'B1', + label: 'B1 - 中級', + description: '能理解清楚標準語言的要點', + examples: ['analyze', 'opportunity', 'environment', 'responsibility', 'development'] + }, + { + value: 'B2', + label: 'B2 - 中高級', + description: '能理解複雜文本的主要內容', + examples: ['sophisticated', 'implications', 'comprehensive', 'substantial', 'methodology'] + }, + { + value: 'C1', + label: 'C1 - 高級', + description: '能流利表達,理解含蓄意思', + examples: ['meticulous', 'predominantly', 'intricate', 'corroborate', 'paradigm'] + }, + { + value: 'C2', + label: 'C2 - 精通', + description: '接近母語水平', + examples: ['ubiquitous', 'ephemeral', 'perspicacious', 'multifarious', 'idiosyncratic'] + } + ]; + + const saveUserLevel = async () => { + setIsLoading(true); + try { + // 保存到本地存儲 + localStorage.setItem('userEnglishLevel', userLevel); + + // 如果用戶已登入,也保存到伺服器 + const token = localStorage.getItem('authToken'); + if (token) { + await fetch('/api/user/update-level', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ englishLevel: userLevel }) + }); + } + + alert('✅ 程度設定已保存!系統將為您提供個人化的詞彙標記。'); + } catch (error) { + console.error('Error saving user level:', error); + alert('❌ 保存失敗,請稍後再試'); + } finally { + setIsLoading(false); + } + }; + + const getHighValueRange = (level: string) => { + const ranges = { + 'A1': 'A2-B1', + 'A2': 'B1-B2', + 'B1': 'B2-C1', + 'B2': 'C1-C2', + 'C1': 'C2', + 'C2': '高階詞彙' + }; + return ranges[level as keyof typeof ranges] || 'B1-B2'; + }; + + return ( +
+
+

🎯 英語程度設定

+

+ 設定您的英語程度,系統將為您提供個人化的詞彙學習建議。 + 設定後,我們會重點標記比您目前程度高1-2級的詞彙。 +

+
+ +
+ {levels.map(level => ( + + ))} +
+ + {/* 個人化效果預覽 */} +
+

+ 💡 您的個人化學習效果預覽 +

+
+
+

高價值詞彙範圍

+

+ 系統將重點標記 {getHighValueRange(userLevel)} 等級的詞彙 +

+
+
+

學習建議

+

+ 專注於比您目前程度({userLevel})高1-2級的詞彙,既有挑戰性又不會太困難 +

+
+
+
+ + + +
+

+ 💡 提示: 您隨時可以回到這裡調整程度設定 +

+
+
+ ); +} +``` + +### **4.2 更新導航選單** +**檔案**: `/frontend/components/Navigation.tsx` + +**新增設定頁面連結**: +```typescript +// 在現有導航項目中新增 + + ⚙️ 設定 + +``` + +### **4.3 修改句子分析頁面** +**檔案**: `/frontend/app/generate/page.tsx` +**修改位置**: 第39行的 `handleAnalyzeSentence` 函數 + +**新增用戶程度取得邏輯**: +```typescript +const handleAnalyzeSentence = async () => { + if (!textInput?.trim()) { + alert('🔍 請輸入要分析的句子') + return + } + + // 取得用戶設定的程度 + const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'; + + console.log('🎯 使用用戶程度:', userLevel); + console.log('✅ 開始分析,設定 loading 狀態') + setIsAnalyzing(true) + + try { + console.log('🌐 發送API請求到:', 'http://localhost:5000/api/ai/analyze-sentence') + const response = await fetch('http://localhost:5000/api/ai/analyze-sentence', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + inputText: textInput, + userLevel: userLevel, // 傳遞用戶程度 + analysisMode: 'full' + }) + }) + + // ... 其餘現有邏輯保持不變 + } + // ... catch 和 finally 保持不變 +}; +``` + +### **4.4 個人化詞彙標記顯示** +**檔案**: `/frontend/app/generate/page.tsx` +**位置**: 詞彙顯示區域 (分析結果視圖中) + +**新增個人化詞彙標記組件**: +```typescript +// 新增輔助函數 +const getLevelIndex = (level: string): number => { + const levels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']; + return levels.indexOf(level.toUpperCase()); +}; + +const getWordValueStyle = (wordLevel: string, userLevel: string) => { + const userIndex = getLevelIndex(userLevel); + const wordIndex = getLevelIndex(wordLevel); + + if (wordIndex <= userIndex) { + return { + bg: 'bg-green-100 border-green-300', + text: 'text-green-800', + label: '已掌握', + icon: '✅' + }; + } else if (wordIndex === userIndex + 1) { + return { + bg: 'bg-yellow-100 border-yellow-300', + text: 'text-yellow-800', + label: '適中挑戰', + icon: '🎯' + }; + } else if (wordIndex === userIndex + 2) { + return { + bg: 'bg-orange-100 border-orange-300', + text: 'text-orange-800', + label: '高挑戰', + icon: '🚀' + }; + } else { + return { + bg: 'bg-red-100 border-red-300', + text: 'text-red-800', + label: '太難', + icon: '⚠️' + }; + } +}; + +// 詞彙顯示組件 +function WordAnalysisCard({ word, analysis, userLevel }: Props) { + const style = getWordValueStyle(analysis.difficultyLevel, userLevel); + + return ( +
+
+ {word} +
+ {analysis.difficultyLevel} + + {style.icon} {style.label} + +
+
+ +
+

翻譯: {analysis.translation}

+

發音: {analysis.pronunciation}

+

定義: {analysis.definition}

+
+ + {analysis.isHighValue && ( +
+ 💎 高價值詞彙 - 適合您目前程度的學習重點 +
+ )} +
+ ); +} +``` + +### **4.5 新增程度指示器** +**檔案**: `/frontend/app/generate/page.tsx` +**位置**: 使用次數顯示區域後 (第306行後) + +```typescript +{/* 個人化程度指示器 */} +
+ {(() => { + const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'; + const targetRange = getTargetLevelRange(userLevel); + return ( +
+ 🎯 您的程度: {userLevel} + | + 💎 高價值範圍: {targetRange} + + 調整 ⚙️ + +
+ ); + })()} +
+``` + +--- + +## **📋 Phase 5: 進階功能 (第12-14天)** + +### **5.1 建立用戶程度更新 API** +**檔案**: `/backend/DramaLing.Api/Controllers/UsersController.cs` (可能需要新建) + +```csharp +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class UsersController : ControllerBase +{ + private readonly DramaLingDbContext _context; + private readonly IAuthService _authService; + + [HttpPost("update-level")] + public async Task UpdateUserLevel([FromBody] UpdateLevelRequest request) + { + var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); + if (userId == null) return Unauthorized(); + + var user = await _context.Users.FindAsync(userId.Value); + if (user == null) return NotFound(); + + // 驗證等級有效性 + var validLevels = new[] { "A1", "A2", "B1", "B2", "C1", "C2" }; + if (!validLevels.Contains(request.EnglishLevel.ToUpper())) + { + return BadRequest("Invalid English level"); + } + + user.EnglishLevel = request.EnglishLevel.ToUpper(); + user.LevelUpdatedAt = DateTime.UtcNow; + user.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return Ok(new { + Success = true, + Data = new { + UserId = user.Id, + EnglishLevel = user.EnglishLevel, + UpdatedAt = user.LevelUpdatedAt + }, + Message = "User level updated successfully" + }); + } +} + +public class UpdateLevelRequest +{ + public string EnglishLevel { get; set; } = string.Empty; +} +``` + +### **5.2 程度建議系統** +**檔案**: `/frontend/components/LevelRecommendation.tsx` (新建) + +```typescript +interface LevelRecommendationProps { + userLevel: string; + analysisResult: any; +} + +export function LevelRecommendation({ userLevel, analysisResult }: LevelRecommendationProps) { + const recommendations = { + A1: { + focus: "先掌握基礎詞彙,建立信心", + nextGoal: "目標掌握所有 A2 詞彙", + tips: ["多看簡單對話", "重複練習基礎詞彙", "先不要急於挑戰難詞"] + }, + A2: { + focus: "挑戰 B1 詞彙,擴展詞彙量", + nextGoal: "準備進入中級程度", + tips: ["開始接觸新聞", "學習片語搭配", "練習複雜句型"] + }, + B1: { + focus: "精進 B2 詞彙,提升表達精準度", + nextGoal: "達到中高級水平", + tips: ["閱讀專業文章", "學習學術詞彙", "練習論述表達"] + }, + B2: { + focus: "挑戰 C1 詞彙,培養高階表達", + nextGoal: "邁向高級程度", + tips: ["深度閱讀", "學習抽象概念詞彙", "練習批判性思考表達"] + }, + C1: { + focus: "精進 C2 詞彙,達到接近母語水平", + nextGoal: "達到精通程度", + tips: ["文學作品閱讀", "學習專業術語", "練習修辭技巧"] + }, + C2: { + focus: "保持語言敏感度,精進表達細節", + nextGoal: "維持高水準", + tips: ["關注語言變化", "學習方言俚語", "練習創意表達"] + } + }; + + const rec = recommendations[userLevel as keyof typeof recommendations] || recommendations.A2; + + return ( +
+

+ 🎯 基於您程度({userLevel})的個人化建議 +

+ +
+
+

當前重點

+

{rec.focus}

+
+
+

下階段目標

+

{rec.nextGoal}

+
+
+ +
+

學習技巧

+
    + {rec.tips.map((tip, index) => ( +
  • + + {tip} +
  • + ))} +
+
+
+ ); +} +``` + +--- + +## **📋 Phase 6: 測試與驗證 (第13-14天)** + +### **6.1 單元測試** +**新檔案**: `/backend/DramaLing.Api.Tests/Services/CEFRLevelServiceTests.cs` + +```csharp +using Xunit; +using DramaLing.Api.Services; + +public class CEFRLevelServiceTests +{ + [Theory] + [InlineData("A2", "A1", false)] // 太簡單 + [InlineData("A2", "A2", false)] // 同等級 + [InlineData("A2", "B1", true)] // +1級,高價值 + [InlineData("A2", "B2", true)] // +2級,高價值 + [InlineData("A2", "C1", false)] // 太難 + public void IsHighValueForUser_ShouldReturnExpectedResult( + string userLevel, string wordLevel, bool expected) + { + var result = CEFRLevelService.IsHighValueForUser(wordLevel, userLevel); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("A1", "A2-B1")] + [InlineData("A2", "B1-B2")] + [InlineData("B1", "B2-C1")] + [InlineData("C1", "C2")] + public void GetTargetLevelRange_ShouldReturnCorrectRange( + string userLevel, string expectedRange) + { + var result = CEFRLevelService.GetTargetLevelRange(userLevel); + Assert.Equal(expectedRange, result); + } +} +``` + +### **6.2 整合測試腳本** +**新檔案**: `/test-personalized-analysis.sh` + +```bash +#!/bin/bash + +echo "🧪 測試個人化高價值詞彙判定" + +# 測試句子 +TEST_SENTENCE="The sophisticated analysis reveals important implications for understanding complex linguistic patterns." + +echo "📝 測試句子: $TEST_SENTENCE" +echo "" + +# 測試不同程度用戶 +for level in A1 A2 B1 B2 C1; do + echo "🎯 測試 $level 程度用戶:" + + response=$(curl -s -X POST http://localhost:5000/api/ai/analyze-sentence \ + -H "Content-Type: application/json" \ + -d "{\"inputText\": \"$TEST_SENTENCE\", \"userLevel\": \"$level\"}") + + # 提取高價值詞彙和判定範圍 + highValueWords=$(echo $response | jq -r '.data.highValueWords[]?' 2>/dev/null | tr '\n' ',' | sed 's/,$//') + criteria=$(echo $response | jq -r '.data.highValueCriteria?' 2>/dev/null) + + echo " 高價值範圍: $criteria" + echo " 標記詞彙: $highValueWords" + echo "" +done + +echo "✅ 測試完成!檢查每個程度用戶的高價值詞彙是否不同" +``` + +--- + +## **⏰ 開發時程表** + +| 天數 | 階段 | 主要任務 | 預計工時 | 檔案涉及 | +|------|------|----------|----------|----------| +| Day 1-2 | **資料模型** | User 實體擴充、API 擴充、資料庫遷移 | 12h | `User.cs`, `AIController.cs` | +| Day 3-5 | **判定邏輯** | CEFRLevelService、GeminiService 重構 | 16h | `CEFRLevelService.cs`, `GeminiService.cs` | +| Day 6-7 | **Controller 整合** | API 邏輯整合、錯誤處理 | 10h | `AIController.cs` | +| Day 8-10 | **前端設定頁** | 程度設定介面、導航整合 | 18h | `settings/page.tsx`, `Navigation.tsx` | +| Day 11 | **前端分析整合** | generate 頁面修改、個人化顯示 | 8h | `generate/page.tsx` | +| Day 12-13 | **測試開發** | 單元測試、整合測試、腳本 | 12h | 測試檔案 | +| Day 14 | **優化除錯** | 性能調整、UI 優化、文檔更新 | 6h | 各檔案 | + +**總計**: 82 工時 (約2週全職開發) + +--- + +## **🎯 驗收標準** + +### **功能需求驗收** +- [ ] **個人化判定**: A2 用戶看到 B1-B2 詞彙為高價值,C1 用戶只看到 C2 為高價值 +- [ ] **設定頁面**: 用戶可以選擇並保存 A1-C2 程度 +- [ ] **即時生效**: 程度修改後,下次分析立即使用新程度 +- [ ] **回退機制**: 未設定程度的用戶預設為 A2 +- [ ] **本地存儲**: 程度設定保存在 localStorage,未登入用戶也能使用 + +### **技術需求驗收** +- [ ] **API 相容性**: 現有前端不傳 userLevel 時仍正常運作 +- [ ] **效能基準**: API 回應時間增加 < 200ms +- [ ] **錯誤處理**: 無效程度參數時優雅降級 +- [ ] **資料完整性**: 用戶程度資料正確遷移和驗證 +- [ ] **測試覆蓋**: 單元測試覆蓋率 > 80% + +### **用戶體驗驗收** +- [ ] **視覺清晰**: 不同價值詞彙有明顯的視覺區分 +- [ ] **操作直觀**: 程度設定流程簡單明確 +- [ ] **回饋及時**: 程度變更後有明確的確認訊息 +- [ ] **學習指導**: 提供基於程度的學習建議 + +--- + +## **🚀 部署與上線** + +### **上線前檢查清單** +- [ ] 所有測試通過 +- [ ] 資料庫遷移在生產環境執行 +- [ ] 現有用戶資料正確設定預設程度 (A2) +- [ ] API 文檔更新完成 +- [ ] 監控系統配置完成 + +### **上線後監控指標** +- [ ] **功能使用率**: 多少用戶設定了程度 +- [ ] **學習效果**: 個人化後的詞彙點擊率變化 +- [ ] **錯誤率**: 新功能相關的錯誤追蹤 +- [ ] **效能影響**: API 回應時間變化 + +--- + +## **📞 風險評估與應對** + +### **技術風險** +1. **AI Prompt 複雜化**: 可能影響回應品質 + - **應對**: 充分測試,準備回退機制 + +2. **資料庫效能**: 新增欄位可能影響查詢效能 + - **應對**: 建立適當索引,監控查詢時間 + +3. **前端相容性**: 新功能可能影響現有使用者體驗 + - **應對**: 向下相容設計,預設值保持原有行為 + +### **產品風險** +1. **用戶理解難度**: CEFR 程度對一般用戶可能太專業 + - **應對**: 提供簡單的描述和範例詞彙 + +2. **設定複雜性**: 增加設定可能讓產品變複雜 + - **應對**: 智能預設值,讓設定變為可選功能 + +--- + +## **📋 成功指標** + +### **定量指標** (3個月後評估) +- **程度設定率**: > 60% 的活躍用戶設定了程度 +- **詞彙點擊提升**: 高價值詞彙點擊率提升 > 30% +- **學習時長**: 平均學習時長提升 > 20% +- **用戶滿意度**: 功能滿意度 > 4.0/5.0 + +### **定性指標** +- **學習體驗**: 用戶反應詞彙推薦更精準 +- **挑戰適中**: 減少「太難」或「太簡單」的反饋 +- **進步感知**: 用戶感受到明確的學習進階路徑 + +--- + +**© 2025 DramaLing Development Team** +**文檔建立**: 2025-01-18 +**計劃核准**: 待用戶確認 \ No newline at end of file diff --git a/note/spec/AI_VOCABULARY_API_DOCUMENTATION.md b/note/spec/AI_VOCABULARY_API_DOCUMENTATION.md new file mode 100644 index 0000000..89c519f --- /dev/null +++ b/note/spec/AI_VOCABULARY_API_DOCUMENTATION.md @@ -0,0 +1,1002 @@ +# 🤖 DramaLing AI 功能 - 後端 API 完整文檔 + +**專案**: DramaLing 英語學習平台 +**功能**: AI 智能分析與詞卡生成系統 +**文檔版本**: v1.1 +**最後更新**: 2025-01-18 + +--- + +## 📋 **功能概述** + +DramaLing AI 功能包含兩大核心系統: +1. **句子分析系統**: 提供整句翻譯、語法修正和詞彙分析 +2. **詞卡生成系統**: 透過 AI 分析文本自動生成學習詞卡 + +兩系統都整合 Google Gemini AI,提供高品質的英語學習分析。 + +### 🎯 **主要特點** +- ✅ **智能分析**: 使用 Google Gemini AI 進行文本分析 +- ✅ **雙重模式**: 支援基本詞彙和智能萃取 +- ✅ **完整資訊**: 包含音標、例句、同義詞、CEFR等級 +- ✅ **安全驗證**: JWT認證和輸入驗證 +- ✅ **錯誤處理**: 完善的例外處理和回退機制 +- ✅ **批量操作**: 支援一次生成多張詞卡 + +--- + +## 📁 **核心檔案架構** + +``` +backend/DramaLing.Api/ +├── Controllers/ +│ └── AIController.cs # API 端點控制器 +├── Services/ +│ └── GeminiService.cs # AI 服務整合 +├── Models/ +│ └── Entities/ +│ ├── Flashcard.cs # 詞卡資料模型 +│ └── CardSet.cs # 詞卡集合模型 +└── Data/ + └── DramaLingDbContext.cs # 資料庫上下文 +``` + +--- + +## 🔗 **API 端點列表** + +## 🎯 **A. 句子分析系統** + +### 1. **📝 句子分析 (整句意思生成)** `[AllowAnonymous]` +```http +POST /api/ai/analyze-sentence +Content-Type: application/json + +{ + "inputText": "Hello world", + "forceRefresh": false, + "analysisMode": "full" +} +``` + +**功能**: +- ✅ **整句翻譯**: 自然流暢的繁體中文翻譯 +- ✅ **詳細解釋**: 語法結構、詞彙特點、使用場景分析 +- ✅ **語法修正**: 檢測並修正語法錯誤 +- ✅ **詞彙分析**: 每個單字的詳細資訊 +- ✅ **高價值標記**: 標記學習價值高的詞彙 + +**回應範例**: +```json +{ + "success": true, + "data": { + "analysisId": "a063fb12-b28f-4df8-af4f-eeb2d43fd9c4", + "inputText": "Hello world", + "grammarCorrection": { + "hasErrors": false, + "originalText": "Hello world", + "correctedText": "", + "corrections": [] + }, + "sentenceMeaning": { + "translation": "你好,世界", + "explanation": "這個句子是程式設計中最經典的入門程式碼,也是學習任何程式語言的第一個練習。語法結構非常簡單,主語是隱含的,謂語是 'Hello',賓語是 'world'。" + }, + "finalAnalysisText": "Hello world", + "wordAnalysis": { + "Hello": { + "word": "Hello", + "translation": "你好", + "definition": "used as a greeting or to begin a phone conversation", + "partOfSpeech": "interjection", + "pronunciation": "/həˈloʊ/", + "isHighValue": true, + "difficultyLevel": "A1" + }, + "world": { + "word": "world", + "translation": "世界", + "definition": "the earth, together with all of its countries and peoples", + "partOfSpeech": "noun", + "pronunciation": "/wɜːrld/", + "isHighValue": true, + "difficultyLevel": "A1" + } + }, + "highValueWords": ["Hello", "world"], + "phrasesDetected": [] + }, + "message": "AI句子分析完成", + "cached": false, + "cacheHit": false, + "usingAI": true +} +``` + +**特點**: +- 🔄 **智能快取**: 24小時快取機制,提升回應速度 +- 🛡️ **回退機制**: AI 失敗時使用本地分析 +- 📊 **使用統計**: 記錄分析次數(目前已暫時關閉限制) + +--- + +### 2. **🔍 互動式單字查詢** `[AllowAnonymous]` +```http +POST /api/ai/query-word +Content-Type: application/json + +{ + "word": "beautiful", + "sentence": "The weather is beautiful today" +} +``` + +**功能**: 在句子語境中分析特定單字 +**回應**: 包含詞彙詳細資訊和語境分析 + +--- + +## 🎯 **B. 詞卡生成系統** + +### 3. **🧪 測試生成詞卡** `[AllowAnonymous]` +```http +POST /api/ai/test/generate +Content-Type: application/json + +{ + "inputText": "要分析的英文文本", + "extractionType": "vocabulary", + "cardCount": 10 +} +``` + +**特點**: +- 無需用戶認證 +- 開發測試專用 +- API Key 未配置時返回模擬資料 + +--- + +### 2. **🚀 正式生成詞卡** `[Authorize]` +```http +POST /api/ai/generate +Authorization: Bearer {jwt-token} +Content-Type: application/json + +{ + "inputText": "The weather is beautiful today, and I'm planning to go hiking in the mountains.", + "extractionType": "vocabulary", + "cardCount": 5 +} +``` + +**回應範例**: +```json +{ + "success": true, + "data": { + "taskId": "123e4567-e89b-12d3-a456-426614174000", + "status": "completed", + "generatedCards": [ + { + "word": "beautiful", + "partOfSpeech": "adj.", + "pronunciation": "/ˈbjuːtɪfl/", + "translation": "美麗的", + "definition": "pleasing the senses or mind aesthetically", + "synonyms": ["lovely", "gorgeous"], + "example": "The weather is beautiful today", + "exampleTranslation": "今天天氣很美", + "difficultyLevel": "A2" + }, + { + "word": "hiking", + "partOfSpeech": "n.", + "pronunciation": "/ˈhaɪkɪŋ/", + "translation": "登山健行", + "definition": "the activity of going for long walks in the countryside", + "synonyms": ["trekking", "walking"], + "example": "I'm planning to go hiking in the mountains", + "exampleTranslation": "我計劃去山裡健行", + "difficultyLevel": "B1" + } + ] + }, + "message": "Successfully generated 5 cards" +} +``` + +--- + +### 3. **💾 保存生成的詞卡** `[Authorize]` +```http +POST /api/ai/generate/{taskId}/save +Authorization: Bearer {jwt-token} +Content-Type: application/json + +{ + "cardSetId": "456e7890-e89b-12d3-a456-426614174111", + "selectedCards": [ + { + "word": "beautiful", + "translation": "美麗的", + "definition": "pleasing the senses or mind aesthetically", + "partOfSpeech": "adj.", + "pronunciation": "/ˈbjuːtɪfl/", + "example": "The weather is beautiful today", + "exampleTranslation": "今天天氣很美", + "difficultyLevel": "A2" + } + ] +} +``` + +**回應範例**: +```json +{ + "success": true, + "data": { + "savedCount": 1, + "cardSetId": "456e7890-e89b-12d3-a456-426614174111", + "cardSetName": "我的詞卡集合", + "cards": [ + { + "id": "789e1234-e89b-12d3-a456-426614174222", + "word": "beautiful", + "translation": "美麗的", + "definition": "pleasing the senses or mind aesthetically" + } + ] + }, + "message": "Successfully saved 1 cards to deck '我的詞卡集合'" +} +``` + +--- + +### 4. **💾 快速保存詞卡(測試用)** `[AllowAnonymous]` +```http +POST /api/ai/test/generate/save +Content-Type: application/json + +{ + "selectedCards": [ + { + "word": "example", + "translation": "例子", + "definition": "a thing characteristic of its kind", + "partOfSpeech": "n.", + "pronunciation": "/ɪɡˈzɑːmpl/", + "example": "This is an example sentence", + "exampleTranslation": "這是一個例句", + "difficultyLevel": "B1" + } + ] +} +``` + +**特點**: +- 自動創建預設詞卡集合 +- 測試環境專用 +- 無需指定 CardSetId + +--- + +### 5. **✅ 驗證詞卡內容** `[Authorize]` +```http +POST /api/ai/validate-card +Authorization: Bearer {jwt-token} +Content-Type: application/json + +{ + "flashcardId": "789e1234-e89b-12d3-a456-426614174222" +} +``` + +**用途**: 驗證已保存詞卡的內容準確性 + +--- + +## 📋 **請求參數詳細說明** + +### **GenerateCardsRequest 參數** +| 參數 | 類型 | 必填 | 限制 | 說明 | +|------|------|------|------|------| +| `inputText` | string | ✅ | ≤ 5000 字元 | 要分析的英文文本 | +| `extractionType` | string | ✅ | "vocabulary" 或 "smart" | 萃取模式 | +| `cardCount` | int | ✅ | 1-20 | 要生成的詞卡數量 | + +### **萃取模式說明** +- **`"vocabulary"`**: 基本詞彙萃取 + - 重點:常用單字、動詞、形容詞等 + - 適合:基礎學習者 + +- **`"smart"`**: 智能萃取 + - 重點:片語、俚語、慣用語、搭配 + - 適合:進階學習者 + +### **SaveCardsRequest 參數** +| 參數 | 類型 | 必填 | 說明 | +|------|------|------|------| +| `cardSetId` | Guid | ✅ | 目標詞卡集合 ID | +| `selectedCards` | Array | ✅ | 要保存的詞卡陣列 | + +### **GeneratedCard 結構** +```typescript +interface GeneratedCard { + word: string; // 單字原型 + partOfSpeech: string; // 詞性 (n./v./adj./adv.等) + pronunciation: string; // IPA 音標 + translation: string; // 繁體中文翻譯 + definition: string; // 英文定義 + synonyms?: string[]; // 同義詞 (最多2個) + example: string; // 例句 + exampleTranslation: string; // 例句中文翻譯 + difficultyLevel: string; // CEFR 等級 (A1/A2/B1/B2/C1/C2) +} +``` + +--- + +## 🛡️ **安全與驗證機制** + +### **輸入驗證** +```csharp +// 文本長度驗證 +if (request.InputText.Length > 5000) { + return BadRequest("Input text must be less than 5000 characters"); +} + +// 萃取類型驗證 +if (!new[] { "vocabulary", "smart" }.Contains(request.ExtractionType)) { + return BadRequest("Invalid extraction type"); +} + +// 詞卡數量驗證 +if (request.CardCount < 1 || request.CardCount > 20) { + return BadRequest("Card count must be between 1 and 20"); +} +``` + +### **認證機制** +```csharp +var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); +if (userId == null) { + return Unauthorized(new { Success = false, Error = "Invalid token" }); +} +``` + +### **使用限制** +- 🔄 **目前狀態**: 使用限制已暫時關閉 (`isPremium: true`) +- 📊 **配額檢查**: 支援每日配額限制(可配置) +- 🏷️ **用戶追蹤**: 基於 JWT token 識別用戶身份 + +--- + +## 🔄 **完整工作流程** + +```mermaid +sequenceDiagram + participant F as 前端 + participant A as AIController + participant G as GeminiService + participant D as Database + + F->>A: POST /api/ai/generate + A->>A: 驗證輸入參數 + A->>A: 檢查用戶認證 + A->>A: 檢查使用配額 + A->>G: GenerateCardsAsync() + G->>G: 構建 AI Prompt + G->>G: 呼叫 Gemini API + G->>G: 解析 JSON 回應 + G->>A: 返回生成的詞卡 + A->>F: 返回詞卡結果 + + F->>A: POST /api/ai/generate/{taskId}/save + A->>A: 驗證用戶認證 + A->>D: 檢查詞卡集合存在 + A->>D: 批量保存詞卡 + A->>D: 更新集合統計 + A->>F: 返回保存結果 +``` + +--- + +## 🤖 **AI 服務整合詳解** + +### **句子分析 Prompt 模板** +```csharp +// 位置: GeminiService.cs AnalyzeSentenceAsync() 方法 +var prompt = $@" +請分析以下英文句子,提供完整的分析: + +句子:{inputText} + +請按照以下JSON格式回應,不要包含任何其他文字: + +{ + ""translation"": ""自然流暢的繁體中文翻譯"", + ""explanation"": ""詳細解釋句子的語法結構、詞彙特點、使用場景和學習要點"", + ""grammarCorrection"": { + ""hasErrors"": false, + ""originalText"": ""{inputText}"", + ""correctedText"": null, + ""corrections"": [] + }, + ""highValueWords"": [""重要詞彙1"", ""重要詞彙2""], + ""wordAnalysis"": { + ""單字"": { + ""translation"": ""中文翻譯"", + ""definition"": ""英文定義"", + ""partOfSpeech"": ""詞性"", + ""pronunciation"": ""音標"", + ""isHighValue"": true, + ""difficultyLevel"": ""CEFR等級"" + } + } +} + +要求: +1. 翻譯要自然流暢,符合中文語法 +2. 解釋要具體有用,不要空泛 +3. 標記B1以上詞彙為高價值 +4. 如有語法錯誤請指出並修正 +5. 確保JSON格式正確 +"; +``` + +**關鍵欄位解釋**: +- **`translation`**: 這就是前端顯示的「整句翻譯」 +- **`explanation`**: 這就是前端顯示的「詳細解釋」部分 +- **前端組合**: `translation + ' ' + explanation` = 完整的「整句意思」 + +### **詞卡生成 Prompt 模板** +```csharp +private const string VocabularyExtractionPrompt = @" +從以下英文文本中萃取 {cardCount} 個最重要的詞彙,為每個詞彙生成詞卡資料。 + +輸入文本: +{inputText} + +請按照以下 JSON 格式回應,不要包含任何其他文字或代碼塊標記: + +{ + ""cards"": [ + { + ""word"": ""單字原型"", + ""part_of_speech"": ""詞性(n./v./adj./adv.等)"", + ""pronunciation"": ""IPA音標"", + ""translation"": ""繁體中文翻譯"", + ""definition"": ""英文定義(保持A1-A2程度)"", + ""synonyms"": [""同義詞1"", ""同義詞2""], + ""example"": ""例句(使用原文中的句子或生成新句子)"", + ""example_translation"": ""例句中文翻譯"", + ""difficulty_level"": ""CEFR等級(A1/A2/B1/B2/C1/C2)"" + } + ] +} + +要求: +1. 選擇最有學習價值的詞彙 +2. 定義要簡單易懂,適合英語學習者 +3. 例句要實用且符合語境 +4. 確保 JSON 格式正確 +5. 同義詞最多2個,選擇常用的"; +``` + +### **智能萃取 Prompt** +```csharp +private const string SmartExtractionPrompt = @" +分析以下英文文本,識別片語、俚語和常用表達,生成 {cardCount} 個學習卡片: + +輸入文本: +{inputText} + +重點關注: +1. 片語和俚語 +2. 文化相關表達 +3. 語境特定用法 +4. 慣用語和搭配 + +請按照相同的 JSON 格式回應..."; +``` + +### **錯誤處理與回退機制** +```csharp +try { + var generatedCards = await _geminiService.GenerateCardsAsync( + request.InputText, + request.ExtractionType, + request.CardCount + ); + return Ok(successResponse); +} +catch (InvalidOperationException ex) when (ex.Message.Contains("API key")) { + _logger.LogWarning("Gemini API key not configured, using mock data"); + + // 返回模擬資料 + var mockCards = GenerateMockCards(request.CardCount); + return Ok(mockResponse); +} +catch (Exception ex) { + _logger.LogError(ex, "Error in AI card generation"); + return StatusCode(500, errorResponse); +} +``` + +--- + +## 💾 **資料庫設計** + +### **Flashcard 資料表結構** +```sql +CREATE TABLE Flashcards ( + Id UNIQUEIDENTIFIER PRIMARY KEY, + UserId UNIQUEIDENTIFIER NOT NULL, + CardSetId UNIQUEIDENTIFIER NOT NULL, + Word NVARCHAR(100) NOT NULL, + Translation NVARCHAR(200) NOT NULL, + Definition NVARCHAR(500), + PartOfSpeech NVARCHAR(50), + Pronunciation NVARCHAR(100), + Example NVARCHAR(500), + ExampleTranslation NVARCHAR(500), + DifficultyLevel NVARCHAR(10), + CreatedAt DATETIME2 NOT NULL, + UpdatedAt DATETIME2 NOT NULL, + + FOREIGN KEY (UserId) REFERENCES Users(Id), + FOREIGN KEY (CardSetId) REFERENCES CardSets(Id) +); +``` + +### **CardSet 資料表結構** +```sql +CREATE TABLE CardSets ( + Id UNIQUEIDENTIFIER PRIMARY KEY, + UserId UNIQUEIDENTIFIER NOT NULL, + Name NVARCHAR(100) NOT NULL, + Description NVARCHAR(500), + Color NVARCHAR(20), + CardCount INT DEFAULT 0, + IsDefault BIT DEFAULT 0, + CreatedAt DATETIME2 NOT NULL, + UpdatedAt DATETIME2 NOT NULL, + + FOREIGN KEY (UserId) REFERENCES Users(Id) +); +``` + +### **自動建立預設詞卡集合** +```csharp +var defaultCardSet = await _context.CardSets + .FirstOrDefaultAsync(cs => cs.IsDefault); + +if (defaultCardSet == null) { + defaultCardSet = new CardSet { + Id = Guid.NewGuid(), + UserId = userId.Value, + Name = "AI 生成詞卡", + Description = "通過 AI 智能生成的詞卡集合", + Color = "#3B82F6", + IsDefault = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + _context.CardSets.Add(defaultCardSet); +} +``` + +--- + +## 📊 **錯誤處理與狀態碼** + +### **HTTP 狀態碼說明** +| 狀態碼 | 情境 | 說明 | +|--------|------|------| +| `200 OK` | 成功 | 詞卡生成/保存成功 | +| `400 Bad Request` | 輸入錯誤 | 參數驗證失敗 | +| `401 Unauthorized` | 認證失敗 | JWT Token 無效或過期 | +| `404 Not Found` | 資源不存在 | 詞卡集合或詞卡不存在 | +| `429 Too Many Requests` | 配額超限 | 超過每日生成限制 | +| `500 Internal Server Error` | 服務錯誤 | AI 服務或資料庫錯誤 | + +### **標準錯誤回應格式** +```json +{ + "success": false, + "error": "Input text is required", + "details": "The inputText field cannot be null or empty", + "timestamp": "2025-01-18T15:54:00Z", + "errorCode": "VALIDATION_ERROR" +} +``` + +### **常見錯誤類型** +```csharp +// 輸入驗證錯誤 +return BadRequest(new { + Success = false, + Error = "Input text must be less than 5000 characters", + ErrorCode = "INPUT_TOO_LONG" +}); + +// 認證錯誤 +return Unauthorized(new { + Success = false, + Error = "Invalid token", + ErrorCode = "AUTH_FAILED" +}); + +// 配額超限 +return StatusCode(429, new { + Success = false, + Error = "Daily generation limit exceeded", + ErrorCode = "QUOTA_EXCEEDED", + ResetTime = DateTime.UtcNow.AddDays(1) +}); + +// AI 服務錯誤 +return StatusCode(500, new { + Success = false, + Error = "AI service temporarily unavailable", + ErrorCode = "AI_SERVICE_ERROR" +}); +``` + +--- + +## 🔧 **環境配置** + +### **必要環境變數** +```bash +# AI 服務配置 +export DRAMALING_GEMINI_API_KEY="your-gemini-api-key-here" + +# 資料庫配置 +export DRAMALING_DB_CONNECTION="Host=localhost;Database=dramaling;Username=postgres;Password=your-password" + +# JWT 配置 +export DRAMALING_JWT_SECRET="your-jwt-secret-key" +export DRAMALING_JWT_ISSUER="dramaling-api" +export DRAMALING_JWT_AUDIENCE="dramaling-frontend" +``` + +### **appsettings.json 配置範例** +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=dramaling_test.db" + }, + "JwtSettings": { + "SecretKey": "${DRAMALING_JWT_SECRET}", + "Issuer": "${DRAMALING_JWT_ISSUER}", + "Audience": "${DRAMALING_JWT_AUDIENCE}", + "ExpirationMinutes": 60 + }, + "GeminiSettings": { + "ApiKey": "${DRAMALING_GEMINI_API_KEY}", + "Model": "gemini-pro", + "MaxTokens": 2048, + "Temperature": 0.7 + }, + "UsageLimits": { + "FreeUserDailyLimit": 10, + "PremiumUserDailyLimit": 100, + "MaxTextLength": 5000, + "MaxCardCount": 20 + } +} +``` + +--- + +## 🧪 **測試範例** + +### **使用 cURL 測試** + +#### **A. 句子分析功能測試** +```bash +# 1. 測試句子分析 (整句意思生成) - 無需認證 +curl -X POST http://localhost:5000/api/ai/analyze-sentence \ + -H "Content-Type: application/json" \ + -d '{ + "inputText": "The weather is beautiful today and I am planning to go hiking.", + "forceRefresh": false, + "analysisMode": "full" + }' | jq '.data.sentenceMeaning' + +# 2. 測試互動式單字查詢 +curl -X POST http://localhost:5000/api/ai/query-word \ + -H "Content-Type: application/json" \ + -d '{ + "word": "beautiful", + "sentence": "The weather is beautiful today" + }' +``` + +#### **B. 詞卡生成功能測試** +```bash +# 1. 測試生成詞卡 (無需認證) +curl -X POST http://localhost:5000/api/ai/test/generate \ + -H "Content-Type: application/json" \ + -d '{ + "inputText": "The weather is beautiful today and I am planning to go hiking in the mountains.", + "extractionType": "vocabulary", + "cardCount": 5 + }' + +# 2. 正式生成詞卡 (需要認證) +curl -X POST http://localhost:5000/api/ai/generate \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-jwt-token-here" \ + -d '{ + "inputText": "Learning English through movies and TV shows is an effective method.", + "extractionType": "smart", + "cardCount": 8 + }' + +# 3. 保存詞卡 +curl -X POST http://localhost:5000/api/ai/test/generate/save \ + -H "Content-Type: application/json" \ + -d '{ + "selectedCards": [ + { + "word": "effective", + "translation": "有效的", + "definition": "successful in producing a desired result", + "partOfSpeech": "adj.", + "pronunciation": "/ɪˈfektɪv/", + "example": "This is an effective method", + "exampleTranslation": "這是一個有效的方法", + "difficultyLevel": "B2" + } + ] + }' +``` + +### **前端 JavaScript 整合範例** + +#### **A. 句子分析功能** +```javascript +// 分析句子獲得整句意思 +async function analyzeSentence(text, forceRefresh = false) { + try { + const response = await fetch('/api/ai/analyze-sentence', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + inputText: text, + forceRefresh: forceRefresh, + analysisMode: 'full' + }) + }); + + const result = await response.json(); + + if (result.success) { + // 取得整句意思 + const sentenceMeaning = result.data.sentenceMeaning; + console.log('翻譯:', sentenceMeaning.translation); + console.log('解釋:', sentenceMeaning.explanation); + + // 取得詞彙分析 + console.log('詞彙分析:', result.data.wordAnalysis); + console.log('高價值詞彙:', result.data.highValueWords); + + return result.data; + } else { + throw new Error(result.error); + } + } catch (error) { + console.error('Error analyzing sentence:', error); + throw error; + } +} + +// 查詢特定單字 +async function queryWord(word, sentence) { + try { + const response = await fetch('/api/ai/query-word', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + word: word, + sentence: sentence + }) + }); + + const result = await response.json(); + return result.data; + } catch (error) { + console.error('Error querying word:', error); + throw error; + } +} +``` + +#### **B. 詞卡生成功能** +```javascript +// 生成詞卡 +async function generateCards(text, type = 'vocabulary', count = 10) { + try { + const response = await fetch('/api/ai/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ + inputText: text, + extractionType: type, + cardCount: count + }) + }); + + const result = await response.json(); + + if (result.success) { + console.log('Generated cards:', result.data.generatedCards); + return result.data; + } else { + throw new Error(result.error); + } + } catch (error) { + console.error('Error generating cards:', error); + throw error; + } +} + +// 保存詞卡 +async function saveCards(taskId, cardSetId, selectedCards) { + try { + const response = await fetch(`/api/ai/generate/${taskId}/save`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ + cardSetId: cardSetId, + selectedCards: selectedCards + }) + }); + + const result = await response.json(); + + if (result.success) { + console.log('Cards saved successfully:', result.data); + return result.data; + } else { + throw new Error(result.error); + } + } catch (error) { + console.error('Error saving cards:', error); + throw error; + } +} + +// 使用範例 +(async () => { + try { + // 生成詞卡 + const generateResult = await generateCards( + "Artificial intelligence is transforming the way we learn languages.", + "smart", + 6 + ); + + // 保存選中的詞卡 + const selectedCards = generateResult.generatedCards.slice(0, 3); // 選前3張 + await saveCards( + generateResult.taskId, + 'your-card-set-id', + selectedCards + ); + + console.log('Process completed successfully!'); + } catch (error) { + console.error('Process failed:', error); + } +})(); +``` + +--- + +## 📈 **效能與最佳實踐** + +### **效能考量** +- 🚀 **快取策略**: 相同文本的分析結果可以快取 +- ⚡ **非同步處理**: 長文本分析採用背景處理 +- 🔄 **批量操作**: 一次保存多張詞卡減少資料庫操作 +- 📊 **資源限制**: 限制同時處理的請求數量 + +### **最佳實踐** +```csharp +// 1. 使用交易確保資料一致性 +using var transaction = await _context.Database.BeginTransactionAsync(); +try { + _context.Flashcards.AddRange(flashcardsToSave); + defaultCardSet.CardCount += flashcardsToSave.Count; + await _context.SaveChangesAsync(); + await transaction.CommitAsync(); +} catch { + await transaction.RollbackAsync(); + throw; +} + +// 2. 分頁處理大量結果 +var cards = await _context.Flashcards + .Where(f => f.UserId == userId) + .OrderBy(f => f.CreatedAt) + .Skip(pageIndex * pageSize) + .Take(pageSize) + .ToListAsync(); + +// 3. 使用索引優化查詢 +[Index(nameof(UserId), nameof(CreatedAt))] +public class Flashcard { /* ... */ } +``` + +--- + +## 🚀 **部署注意事項** + +### **生產環境配置** +1. **安全性**: + - 使用 HTTPS 通訊 + - 定期輪換 JWT Secret + - 實施 Rate Limiting + - 加密敏感資料 + +2. **監控**: + - API 回應時間監控 + - 錯誤率追蹤 + - 使用量統計 + - AI 服務可用性監控 + +3. **擴展性**: + - 考慮使用佇列系統處理大量請求 + - 資料庫連接池配置 + - 負載平衡配置 + - CDN 快取靜態資源 + +### **部署檢查清單** +- [ ] 環境變數正確設定 +- [ ] 資料庫遷移完成 +- [ ] AI API 金鑰有效 +- [ ] JWT 配置正確 +- [ ] CORS 設定完成 +- [ ] 監控系統運行 +- [ ] 備份策略實施 +- [ ] 日誌記錄配置 + +--- + +## 📞 **支援與維護** + +### **常見問題** +1. **Q: AI 生成的詞卡質量如何保證?** + A: 使用精心設計的 Prompt 模板,並提供詞卡驗證功能 + +2. **Q: 如何處理 AI 服務不可用的情況?** + A: 實施回退機制,返回預設模擬資料確保服務可用性 + +3. **Q: 支援哪些語言和難度等級?** + A: 目前專注於英語學習,支援 CEFR A1-C2 等級 + +### **技術支援** +- 📧 **開發團隊**: dev@dramaling.com +- 📚 **文檔庫**: `/docs/api/` +- 🐛 **問題回報**: GitHub Issues +- 💬 **即時支援**: Slack #dramaling-dev + +--- + +**© 2025 DramaLing Team. All rights reserved.** \ No newline at end of file diff --git a/note/spec/ENV_SETUP_SECURE.md b/note/spec/ENV_SETUP_SECURE.md new file mode 100644 index 0000000..ddaadae --- /dev/null +++ b/note/spec/ENV_SETUP_SECURE.md @@ -0,0 +1,201 @@ +# 🔐 安全的環境變數設定指南 + +## 🎯 **為什麼使用環境變數?** + +✅ **絕對安全** - 不會被 Git 追蹤 +✅ **不會暴露** - Claude Code 看不到 +✅ **團隊友善** - 每個人設定自己的金鑰 +✅ **生產就緒** - 符合企業級部署標準 + +--- + +## 📋 **Step 1: 取得 Supabase 資訊** + +### 1.1 登入 Supabase Dashboard +```bash +# 在瀏覽器中開啟: +https://app.supabase.com +``` + +### 1.2 取得連接資訊 +**導航**: Settings → Database +``` +Host: db.[your-project-id].supabase.co +Database: postgres +Username: postgres +Password: [您的資料庫密碼] +Port: 5432 +``` + +**完整連接字串格式**: +``` +Host=db.[project-id].supabase.co;Database=postgres;Username=postgres;Password=[password];Port=5432;SSL Mode=Require; +``` + +### 1.3 取得 API 金鑰 +**導航**: Settings → API +``` +Project URL: https://[your-project-id].supabase.co +anon public: eyJ... (前端用) +service_role secret: eyJ... (後端用) +JWT Secret: (在 JWT Settings 部分) +``` + +--- + +## 🔧 **Step 2: 設定環境變數** + +### 2.1 編輯 Shell 配置檔案 +```bash +# macOS 預設使用 zsh +nano ~/.zshrc + +# 如果使用 bash +nano ~/.bashrc +``` + +### 2.2 在檔案末尾加入 (請替換實際值) +```bash +# ================================ +# DramaLing 專案環境變數 +# ================================ + +# 資料庫連接 +export DRAMALING_DB_CONNECTION="Host=db.[your-project-id].supabase.co;Database=postgres;Username=postgres;Password=[your-db-password];Port=5432;SSL Mode=Require;" + +# Supabase 配置 +export DRAMALING_SUPABASE_URL="https://[your-project-id].supabase.co" +export DRAMALING_SUPABASE_SERVICE_KEY="[your-service-role-key]" +export DRAMALING_SUPABASE_JWT_SECRET="[your-jwt-secret]" + +# AI 服務 +export DRAMALING_GEMINI_API_KEY="[your-gemini-api-key]" + +# 前端配置 (如果需要) +export DRAMALING_SUPABASE_ANON_KEY="[your-anon-key]" +``` + +### 2.3 重新載入環境變數 +```bash +source ~/.zshrc +# 或 +source ~/.bashrc +``` + +### 2.4 驗證設定 +```bash +# 檢查環境變數是否設定成功 (不會顯示完整值,保護隱私) +echo "Supabase URL: ${DRAMALING_SUPABASE_URL:0:20}..." +echo "DB Connection: ${DRAMALING_DB_CONNECTION:0:30}..." +``` + +--- + +## 🎨 **Step 3: 配置前端 (安全方式)** + +### 3.1 建立前端環境變數檔案 +```bash +# 建立前端環境變數 (從系統環境變數讀取) +cat > frontend/.env.local << EOF +NEXT_PUBLIC_SUPABASE_URL=\$DRAMALING_SUPABASE_URL +NEXT_PUBLIC_SUPABASE_ANON_KEY=\$DRAMALING_SUPABASE_ANON_KEY +NEXT_PUBLIC_API_URL=http://localhost:5000 +NEXT_PUBLIC_APP_URL=http://localhost:3001 +EOF +``` + +### 3.2 或者使用腳本自動生成 +```bash +# 自動從環境變數生成前端配置 +echo "NEXT_PUBLIC_SUPABASE_URL=$DRAMALING_SUPABASE_URL" > frontend/.env.local +echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=$DRAMALING_SUPABASE_ANON_KEY" >> frontend/.env.local +echo "NEXT_PUBLIC_API_URL=http://localhost:5000" >> frontend/.env.local +echo "NEXT_PUBLIC_APP_URL=http://localhost:3001" >> frontend/.env.local +``` + +--- + +## 🚀 **Step 4: 測試配置** + +### 4.1 重新啟動後端 +```bash +# 重新啟動 .NET API (會讀取新的環境變數) +./start-dotnet-api.sh +``` + +### 4.2 檢查啟動日誌 +```bash +# 應該看到: +# ✅ 建置成功! +# 🌐 啟動 API 服務... +# info: Microsoft.Hosting.Lifetime[0] Application started + +# 如果看到資料庫連接錯誤,表示環境變數有問題 +``` + +### 4.3 測試 API 端點 +```bash +# 測試健康檢查 +curl http://localhost:5000/health + +# 測試 API 認證 (應該返回 401,表示需要認證) +curl http://localhost:5000/api/auth/profile +``` + +--- + +## 🚨 **常見問題解決** + +### ❌ **環境變數沒有生效** +```bash +# 檢查是否正確載入 +echo $DRAMALING_SUPABASE_URL + +# 如果是空的,重新執行: +source ~/.zshrc +``` + +### ❌ **資料庫連接失敗** +```bash +# 檢查連接字串格式 +echo $DRAMALING_DB_CONNECTION + +# 確認 IP 白名單設定 (Supabase Dashboard → Settings → Database → Network) +``` + +### ❌ **JWT 驗證失敗** +```bash +# 檢查 JWT Secret 是否正確 +echo "JWT Secret length: ${#DRAMALING_SUPABASE_JWT_SECRET}" +# 應該是很長的字串 (>100 字元) +``` + +--- + +## 🎯 **完成後的效果** + +### ✅ **安全性** +- 沒有任何機密資訊在專案檔案中 +- Git 只會追蹤安全的配置檔案 +- Claude Code 無法看到敏感資訊 + +### ✅ **功能性** +- 後端可以連接真實的 Supabase 資料庫 +- 前端可以使用 Supabase 認證 +- API 可以驗證用戶身份 + +### ✅ **開發體驗** +- 一次設定,長期使用 +- 團隊成員各自配置 +- 符合業界最佳實踐 + +--- + +## 📞 **需要協助** + +**您現在需要**: +1. 取得 Supabase 專案的實際資訊 +2. 按照 Step 2 設定環境變數 +3. 告訴我設定完成,我會協助測試 + +**您準備好開始設定環境變數了嗎?** 🚀 \ No newline at end of file diff --git a/note/spec/SUPABASE_SETUP_GUIDE.md b/note/spec/SUPABASE_SETUP_GUIDE.md new file mode 100644 index 0000000..31d162c --- /dev/null +++ b/note/spec/SUPABASE_SETUP_GUIDE.md @@ -0,0 +1,262 @@ +# 🗄️ Supabase 環境變數配置指南 + +## 📋 **配置檢查清單** + +### 準備工作 +- [ ] 已有 Supabase 帳號和專案 +- [ ] 已記住資料庫密碼 +- [ ] 瀏覽器已開啟 Supabase Dashboard + +--- + +## 🔑 **Step 1: 從 Supabase 獲取所需資訊** + +### 1.1 登入 Supabase Dashboard +```bash +# 開啟瀏覽器前往: +https://app.supabase.com +``` + +### 1.2 選擇或建立專案 +- 如果沒有專案,點擊 **"New Project"** +- 專案名稱建議: `dramaling-dev` +- 區域選擇: **Singapore (Southeast Asia)** + +### 1.3 獲取 API 設定資訊 +**導航**: 左側選單 → **Settings** → **API** + +**需要複製的資訊**: +``` +1. Project URL: https://[your-project-id].supabase.co +2. anon public: eyJ[...很長的字串...] +3. service_role secret: eyJ[...很長的字串...] +``` + +### 1.4 獲取 JWT Secret +**位置**: 同頁面下方 **JWT Settings** +``` +JWT Secret: [your-super-secret-jwt-token-with-at-least-32-characters-long] +``` + +### 1.5 獲取資料庫連接資訊 +**導航**: 左側選單 → **Settings** → **Database** + +**連接參數**: +``` +Host: db.[your-project-id].supabase.co +Database: postgres +Port: 5432 +User: postgres +Password: [您建立專案時設定的密碼] +``` + +--- + +## 🔧 **Step 2: 配置後端 (.NET Core)** + +### 2.1 編輯後端配置檔案 +**檔案**: `backend/DramaLing.Api/appsettings.Development.json` + +**請將以下範本中的 `[替換這裡]` 替換為實際值**: + +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=db.[your-project-id].supabase.co;Database=postgres;Username=postgres;Password=[your-db-password];Port=5432;SSL Mode=Require;" + }, + "Supabase": { + "Url": "https://[your-project-id].supabase.co", + "ServiceRoleKey": "[your-service-role-key]", + "JwtSecret": "[your-jwt-secret]" + }, + "AI": { + "GeminiApiKey": "[your-gemini-api-key]" + }, + "Frontend": { + "Urls": ["http://localhost:3000", "http://localhost:3001"] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information", + "Microsoft.EntityFrameworkCore": "Information", + "DramaLing.Api": "Debug" + } + } +} +``` + +### 2.2 配置範例 (請替換實際值) +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=db.abcdefghij.supabase.co;Database=postgres;Username=postgres;Password=MySecretPassword123;Port=5432;SSL Mode=Require;" + }, + "Supabase": { + "Url": "https://abcdefghij.supabase.co", + "ServiceRoleKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "JwtSecret": "your-super-secret-jwt-token-with-at-least-32-characters-long" + } +} +``` + +--- + +## 🎨 **Step 3: 配置前端 (Next.js)** + +### 3.1 建立前端環境變數檔案 +**檔案**: `frontend/.env.local` (新建立) + +```bash +# 執行以下指令建立檔案: +cp .env.example frontend/.env.local +``` + +### 3.2 編輯前端環境變數 +**檔案**: `frontend/.env.local` + +**請將以下範本中的 `[替換這裡]` 替換為實際值**: + +```env +# Supabase 前端配置 (認證用) +NEXT_PUBLIC_SUPABASE_URL=https://[your-project-id].supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=[your-anon-public-key] + +# API 服務配置 +NEXT_PUBLIC_API_URL=http://localhost:5000 +NEXT_PUBLIC_APP_URL=http://localhost:3001 +``` + +### 3.3 前端配置範例 (請替換實際值) +```env +NEXT_PUBLIC_SUPABASE_URL=https://abcdefghij.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +NEXT_PUBLIC_API_URL=http://localhost:5000 +NEXT_PUBLIC_APP_URL=http://localhost:3001 +``` + +--- + +## 🧪 **Step 4: 測試配置** + +### 4.1 測試後端資料庫連接 +```bash +# 重新啟動後端 API +./start-dotnet-api.sh + +# 檢查啟動日誌中是否有資料庫連接錯誤 +# 如果成功,應該會看到 "Application started" 訊息 +``` + +### 4.2 測試 API 端點 +```bash +# 測試健康檢查 +curl http://localhost:5000/health + +# 預期回應: +# {"Status":"Healthy","Timestamp":"2025-09-16T..."} +``` + +### 4.3 測試 Swagger API 文檔 +```bash +# 瀏覽器訪問: +http://localhost:5000/swagger + +# 應該看到 4 個控制器: +# - Auth (用戶認證) +# - CardSets (卡組管理) +# - Flashcards (詞卡管理) +# - Stats (統計分析) +``` + +### 4.4 測試前端 Supabase 連接 +```bash +# 重新啟動前端 +./start-frontend.sh + +# 瀏覽器訪問前端並檢查 Console: +http://localhost:3001 + +# 在瀏覽器 Console 中不應該看到 Supabase 連接錯誤 +``` + +--- + +## 🚨 **常見問題和解決方案** + +### ❌ **資料庫連接失敗** +**錯誤**: `Npgsql.NpgsqlException: Connection refused` + +**解決方案**: +1. 檢查 IP 白名單: Settings → Database → **Network Restrictions** +2. 確認密碼正確 +3. 確認 Host 地址正確 + +### ❌ **JWT 驗證失敗** +**錯誤**: `401 Unauthorized` + +**解決方案**: +1. 確認 JWT Secret 正確複製 (完整字串) +2. 檢查 Issuer URL 是否正確 +3. 確認前端傳送的令牌格式 + +### ❌ **CORS 錯誤** +**錯誤**: `Access-Control-Allow-Origin` + +**解決方案**: +1. 檢查 Program.cs 中的 CORS 設定 +2. 確認前端 URL 在允許清單中 + +--- + +## 📝 **實際動手步驟** + +### 👉 **您現在需要做的**: + +**第一步**: 開啟 Supabase Dashboard +```bash +# 1. 在瀏覽器中開啟: +https://app.supabase.com + +# 2. 登入並選擇專案 +# 3. 前往 Settings → API 頁面 +``` + +**第二步**: 複製 API 資訊 +```bash +# 將以下資訊準備好 (可以先貼到記事本): +Project URL: https:// +Anon Key: eyJ +Service Role Key: eyJ +JWT Secret: your-super-secret +Database Password: +``` + +**第三步**: 通知我配置資訊 +```bash +# 告訴我您已經取得了資訊,我會幫您配置檔案 +# (當然不要貼出真實的密鑰,只要說 "我已經取得了" 即可) +``` + +**第四步**: 我會幫您配置檔案並測試 + +--- + +## 🎯 **配置完成後的效果** + +### ✅ **後端功能** +- Entity Framework 可以連接到 Supabase PostgreSQL +- JWT 認證可以驗證前端的 Supabase 令牌 +- API 可以正確識別登入用戶 + +### ✅ **前端功能** +- 可以使用 Supabase Auth 登入 +- 可以呼叫 .NET Core API 並附加認證令牌 +- Dashboard 可以顯示真實的用戶資料 + +### ✅ **整體效果** +- 前後端完全整合 +- 用戶可以登入並看到個人化內容 +- 所有 API 都有適當的認證保護 + +**準備好開始了嗎?請先取得 Supabase 的連接資訊!** 🚀 \ No newline at end of file diff --git a/note/spec/interactive-word-query-system-spec.md b/note/spec/interactive-word-query-system-spec.md new file mode 100644 index 0000000..8832012 --- /dev/null +++ b/note/spec/interactive-word-query-system-spec.md @@ -0,0 +1,795 @@ +# AI 互動式單字查詢系統 - 完整功能規格 + +**項目**: DramaLing 英語學習平台 +**功能模組**: 智能句子分析與互動式單字查詢 +**版本**: v1.0 +**文檔日期**: 2025-01-18 + +## 🎯 功能概述 + +AI 互動式單字查詢系統是一個智能英語學習工具,允許用戶輸入英文句子,獲得 AI 驅動的完整分析,並通過點擊單字的方式進行深度學習。系統具備語法修正、高價值詞彙標記、成本優化和快取機制。 + +## 📋 核心功能特性 + +### 🔍 主要功能 +1. **智能句子分析**: Gemini AI 驅動的句子翻譯和解釋 +2. **語法自動修正**: 檢測並建議語法錯誤修正 +3. **互動式單字查詢**: 點擊任何單字即時查看詳細信息 +4. **高價值詞彙標記**: AI 識別重要學習詞彙(免費查詢) +5. **成本優化設計**: 低價值詞彙收費查詢,防止濫用 +6. **24小時快取機制**: 避免重複 AI 調用,提升響應速度 +7. **使用額度管理**: 免費用戶 3 小時內限制 5 次分析 + +### 🎨 用戶體驗特色 +- **即時回饋**: 新句子 3-5 秒,快取結果 <200ms +- **視覺化快取狀態**: 清楚顯示結果來源(AI/快取) +- **智能語法提示**: 主動發現和修正語法錯誤 +- **分層收費模式**: 高價值詞彙免費,低價值詞彙收費 + +## 🔄 用戶流程圖 (User Flow) + +``` +[1. 用戶登入] + ↓ +[2. 進入分析頁面 (/generate)] + ↓ +[3. 選擇輸入模式] + ├── ✍️ 手動輸入 (最多300字) + └── 📷 影劇截圖 (Phase 2, 付費功能) + ↓ +[4. 輸入英文文字] + ├── 即時字數統計 + ├── 顏色警告 (280字+黃色, 300字+紅色) + └── 輸入驗證 + ↓ +[5. 點擊「🔍 分析句子」] + ├── 檢查使用額度 (免費用戶 5次/3小時) + ├── 顯示載入狀態 "正在分析句子... (AI 分析約需 3-5 秒)" + └── 調用後端 API + ↓ +[6. 後端處理邏輯] + ├── 檢查快取 (24小時TTL) + │ ├── Cache Hit → 立即返回 (💾 快取結果) + │ └── Cache Miss → 調用 Gemini AI (🤖 AI 分析) + ├── 語法檢查和修正 + ├── 高價值詞彙標記 + └── 存入快取 + ↓ +[7. 分析結果顯示] + ├── 快取狀態標籤 (💾 快取結果 / 🤖 AI 分析) + ├── 語法修正面板 (如有錯誤) + │ ├── 顯示原始 vs 修正版本 + │ ├── [✅ 採用修正] [❌ 保持原版] + │ └── 說明修正原因 + ├── 句子翻譯和解釋 + └── 互動式文字區域 + ↓ +[8. 互動式單字查詢] + ├── 點擊單字觸發分析 + ├── 高價值詞彙 (🟢⭐ / 🟡⭐) → 免費彈窗 + ├── 低價值詞彙 (🔵) → 收費確認對話框 + │ ├── 顯示剩餘額度 + │ ├── [✅ 確認查詢] [❌ 取消] + │ └── 扣除使用額度 + └── 顯示詳細詞彙信息彈窗 + ↓ +[9. 詞卡生成 (可選)] + ├── 點擊「📖 生成詞卡」 + ├── AI 自動提取重要詞彙 + ├── 預覽生成的詞卡 + └── 保存到個人詞庫 + ↓ +[10. 導航選項] + ├── 🔄 分析新句子 → 返回步驟 2 + ├── ← 返回分析 → 返回步驟 7 + └── ← 返回輸入 → 返回步驟 4 +``` + +## 📐 詳細功能規格 + +### 🔧 技術規格 + +#### 前端技術棧 +- **框架**: Next.js 15.5.3 + TypeScript +- **UI 組件**: React Hooks + Tailwind CSS +- **狀態管理**: useState (本地狀態) +- **API 調用**: Fetch API +- **路由**: Next.js App Router + +#### 後端技術棧 +- **框架**: ASP.NET Core 8.0 +- **AI 整合**: Google Gemini API +- **資料庫**: SQLite + Entity Framework Core +- **快取**: 資料庫快取 (24小時TTL) +- **認證**: JWT Token + +### 📊 資料模型 + +#### 1. API 請求/回應格式 + +**句子分析請求**: +```typescript +interface AnalyzeSentenceRequest { + inputText: string // 用戶輸入的英文句子 (≤300字) + forceRefresh?: boolean // 強制刷新快取 (預設: false) + analysisMode?: string // 分析模式 (預設: 'full') +} +``` + +**句子分析回應**: +```typescript +interface AnalyzeSentenceResponse { + success: boolean + message: string + cached: boolean // 是否來自快取 + cacheHit: boolean // 快取命中狀態 + usingAI: boolean // 是否使用 AI 分析 + data: { + analysisId: string + inputText: string + grammarCorrection: GrammarCorrectionResult + sentenceMeaning: { + Translation: string // 注意: 首字母大寫 + Explanation: string // 注意: 首字母大寫 + } + finalAnalysisText: string + wordAnalysis: Record + highValueWords: string[] + phrasesDetected: PhraseInfo[] + } +} +``` + +#### 2. 單字分析資料結構 + +```typescript +interface WordAnalysis { + word: string + translation: string + definition: string + partOfSpeech: string + pronunciation: string + synonyms: string[] + antonyms?: string[] + isPhrase: boolean + isHighValue: boolean // 高學習價值標記 + learningPriority: 'high' | 'medium' | 'low' + phraseInfo?: { + phrase: string + meaning: string + warning: string + colorCode: string + } + difficultyLevel: string // CEFR 等級 (A1, A2, B1, B2, C1, C2) + costIncurred?: number // 查詢成本 +} +``` + +#### 3. 語法修正結構 + +```typescript +interface GrammarCorrectionResult { + hasErrors: boolean + originalText: string + correctedText?: string + corrections: GrammarCorrection[] + confidenceScore: number +} + +interface GrammarCorrection { + position: { start: number, end: number } + errorType: string + original: string + corrected: string + reason: string + severity: 'high' | 'medium' | 'low' +} +``` + +### 🎨 UI/UX 規格 + +#### 1. 主介面佈局 (`/generate`) + +``` +┌─────────────────────────────────────────┐ +│ Navigation Bar │ +├─────────────────────────────────────────┤ +│ 📝 AI 智能生成詞卡 │ +│ │ +│ ┌─── 原始例句類型 ──────────────────────┐ │ +│ │ [✍️ 手動輸入] [📷 影劇截圖 🔒] │ │ +│ └───────────────────────────────────────┘ │ +│ │ +│ ┌─── 輸入英文文本 ──────────────────────┐ │ +│ │ ┌─────────────────────────────────┐ │ │ +│ │ │ [Textarea: 最多300字元] │ │ │ +│ │ │ "輸入英文句子(最多300字)..." │ │ │ +│ │ └─────────────────────────────────┘ │ │ +│ │ 最多 300 字元 • 目前:0 字元 │ │ +│ └───────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ [🔍 分析句子] (全寬按鈕) │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ 免費用戶:已使用 0/5 次 (3小時內) │ +└─────────────────────────────────────────┘ +``` + +#### 2. 分析結果介面 + +``` +┌─────────────────────────────────────────┐ +│ 📝 句子分析結果 💾 快取結果 │ +│ [← 返回] │ +├─────────────────────────────────────────┤ +│ ⚠️ 語法修正建議 (如有錯誤) │ +│ ┌─ 原文:I go to school yesterday ──┐ │ +│ │ 修正:I went to school yesterday │ │ +│ │ [✅ 採用修正] [❌ 保持原版] │ │ +│ └───────────────────────────────────┘ │ +├─────────────────────────────────────────┤ +│ 📝 句子分析 │ +│ ┌─ 用戶輸入 ────────────────────────┐ │ +│ │ I go to school yesterday │ │ +│ └───────────────────────────────────┘ │ +│ ┌─ 整句意思 ────────────────────────┐ │ +│ │ 我昨天去學校。這句話表達了過去... │ │ +│ └───────────────────────────────────┘ │ +├─────────────────────────────────────────┤ +│ 💡 點擊查詢單字意思 │ +│ 🟡⭐高價值片語 🟢⭐高價值單字 🔵普通單字 │ +│ ┌─────────────────────────────────────┐ │ +│ │ I [went] to [school] [yesterday] │ │ +│ │ 🟢⭐ 🔵 🟡⭐ │ │ +│ └─────────────────────────────────────┘ │ +├─────────────────────────────────────────┤ +│ [🔄 分析新句子] [📖 生成詞卡] │ +└─────────────────────────────────────────┘ +``` + +#### 3. 單字查詢彈窗 + +``` +┌─── went ─────────────── ✕ ─┐ +│ ⭐ 高價值詞彙(免費查詢) │ +│ ⭐⭐⭐⭐⭐ 學習價值 │ +│ │ +│ [verb] /went/ 🔊 │ +│ │ +│ 翻譯: 去 (go的過去式) │ +│ 定義: Past tense of go │ +│ │ +│ 同義詞: [traveled] [moved] │ +│ 反義詞: [came] [stayed] │ +│ │ +│ 難度等級: [CEFR A1] (基礎) │ +└────────────────────────────┘ +``` + +#### 4. 收費確認對話框 + +``` +┌─── school ─────────── ✕ ─┐ +│ 💰 低價值詞彙(需消耗額度) │ +│ 此查詢將消耗 1 次 使用額度 │ +│ 剩餘額度:4 次 │ +│ │ +│ [✅ 確認查詢] [❌ 取消] │ +└────────────────────────────┘ +``` + +### 🔧 技術實現規格 + +#### 1. 前端組件架構 + +``` +GeneratePage (主頁面) +├── Navigation (導航欄) +├── InputModeSelection (輸入模式選擇) +├── TextInputArea (文字輸入區域) +├── AnalysisButton (分析按鈕) +├── UsageCounter (使用次數顯示) +├── AnalysisView (分析結果檢視) +│ ├── CacheStatusBadge (快取狀態標籤) +│ ├── GrammarCorrectionPanel (語法修正面板) +│ ├── SentenceAnalysisPanel (句子分析面板) +│ └── ClickableTextV2 (互動式文字組件) +│ ├── WordClickHandler (單字點擊處理) +│ ├── CostConfirmDialog (收費確認對話框) +│ └── WordInfoPopup (單字資訊彈窗) +└── CardPreview (詞卡預覽, 可選) +``` + +#### 2. 後端API架構 + +``` +AIController +├── AnalyzeSentence (句子分析主API) +│ ├── 使用限制檢查 +│ ├── 快取查詢邏輯 +│ ├── Gemini AI 調用 +│ ├── 語法修正處理 +│ ├── 高價值詞彙標記 +│ └── 快取寫入 +├── QueryWord (單字查詢API) +│ ├── 高/低價值判斷 +│ ├── 收費邏輯處理 +│ └── 即時詞彙分析 +└── CacheManagement (快取管理API) + ├── GetCacheStats (快取統計) + ├── CleanupCache (清理過期快取) + └── InvalidateCache (手動清除快取) +``` + +#### 3. 快取系統規格 + +``` +SentenceAnalysisCache (資料表) +├── InputTextHash (SHA-256, 索引) +├── AnalysisResult (JSON格式) +├── ExpiresAt (過期時間, 索引) +├── AccessCount (存取次數) +├── CreatedAt / LastAccessedAt +└── 複合索引 (Hash + ExpiresAt) + +快取策略: +├── TTL: 24小時 +├── 清理: 自動背景任務 +├── 命中率: >80% (預期) +└── 儲存格式: JSON序列化 +``` + +## 🎮 詳細互動規格 + +### 📝 輸入階段 + +#### 輸入模式 +- **手動輸入**: 300字元限制,即時字數統計 +- **影劇截圖**: Phase 2 功能,付費用戶限定 + +#### 輸入驗證 +```typescript +// 字數限制邏輯 +if (mode === 'manual' && value.length > 300) { + return // 阻止輸入 +} + +// 視覺回饋 +const borderColor = + textLength >= 300 ? 'border-red-400' : + textLength >= 280 ? 'border-yellow-400' : + 'border-gray-300' +``` + +#### 按鈕狀態 +```typescript +// 分析按鈕啟用條件 +disabled = isAnalyzing || + (mode === 'manual' && (!textInput || textInput.length > 300)) || + (mode === 'screenshot') +``` + +### 🔍 分析階段 + +#### 載入狀態 +- **初始**: "🔍 分析句子" +- **載入中**: "正在分析句子... (AI 分析約需 3-5 秒)" + 旋轉動畫 +- **完成**: 自動跳轉到分析結果頁面 + +#### 快取邏輯 +```csharp +// 後端快取檢查流程 +1. 計算輸入文字的 SHA-256 hash +2. 查詢資料庫是否有未過期的快取 +3. Cache Hit: 立即返回 + 更新統計 +4. Cache Miss: 調用 Gemini AI + 存入快取 +``` + +#### 錯誤處理 +- **API 錯誤**: 顯示錯誤訊息 +- **網路錯誤**: 重試機制 +- **使用額度超限**: 提示升級或等待 + +### 🖱️ 互動查詢階段 + +#### 單字分類和視覺設計 +```css +/* 高價值片語 */ +.high-value-phrase { + background: bg-yellow-100 + border: 2px solid border-yellow-400 + icon: ⭐ + hover: bg-yellow-200 + shadow + transform +} + +/* 高價值單字 */ +.high-value-word { + background: bg-green-100 + border: 2px solid border-green-400 + icon: ⭐ + hover: bg-green-200 + shadow + transform +} + +/* 普通單字 */ +.normal-word { + border-bottom: border-blue-300 + hover: bg-blue-100 + border-blue-400 +} +``` + +#### 點擊行為邏輯 +```typescript +// 單字點擊處理流程 +onClick(word) => { + const wordAnalysis = analysis[cleanWord] + + if (wordAnalysis.isHighValue) { + // 高價值詞彙 - 立即顯示彈窗 + showWordPopup(word, analysis) + // 不扣除使用額度 + } else { + // 低價值詞彙 - 顯示收費確認 + showCostConfirmDialog(word, cost: 1) + // 確認後扣除額度並調用API + } +} +``` + +#### 彈窗內容結構 +``` +WordInfoPopup +├── Header (單字 + 關閉按鈕) +├── ValueBadge (高價值標記 + 學習價值星級) +├── PhraseWarning (片語警告,如適用) +├── BasicInfo (詞性 + 發音 + 發音按鈕) +├── Translation (翻譯) +├── Definition (英文定義) +├── Synonyms (同義詞標籤) +├── Antonyms (反義詞標籤) +└── DifficultyLevel (CEFR難度等級) +``` + +### 💰 收費模式規格 + +#### 免費用戶限制 +```typescript +const FREE_USER_LIMITS = { + sentenceAnalysis: 5, // 3小時內最多5次句子分析 + timeWindow: 3 * 60 * 60, // 3小時窗口 (秒) + wordQueryCost: 1, // 每次低價值詞彙查詢成本 + highValueWordsFree: true // 高價值詞彙永遠免費 +} +``` + +#### 高價值詞彙判定邏輯 +```typescript +// 高價值詞彙標準 +const isHighValue = + cefrLevel >= 'B1' || // B1+ 等級詞彙 + isIdiomOrPhrase || // 慣用語和片語 + isAcademicVocabulary || // 學術詞彙 + learningFrequency === 'high' // 高學習頻率詞彙 +``` + +### 🗄️ 資料持久化規格 + +#### 快取資料表結構 +```sql +CREATE TABLE SentenceAnalysisCache ( + Id UNIQUEIDENTIFIER PRIMARY KEY, + InputTextHash NVARCHAR(64) NOT NULL, -- SHA-256 hash + InputText NVARCHAR(1000) NOT NULL, -- 原始輸入 + AnalysisResult NVARCHAR(MAX) NOT NULL, -- JSON格式分析結果 + HasGrammarErrors BIT NOT NULL, -- 是否有語法錯誤 + CorrectedText NVARCHAR(1000) NULL, -- 修正後文字 + GrammarCorrections NVARCHAR(MAX) NULL, -- 語法修正JSON + HighValueWords NVARCHAR(MAX) NULL, -- 高價值詞彙JSON + PhrasesDetected NVARCHAR(MAX) NULL, -- 檢測到的片語JSON + CreatedAt DATETIME2 NOT NULL, -- 建立時間 + ExpiresAt DATETIME2 NOT NULL, -- 過期時間 + LastAccessedAt DATETIME2 NULL, -- 最後存取時間 + AccessCount INT NOT NULL DEFAULT 0 -- 存取次數 +); + +-- 索引 +CREATE INDEX IX_SentenceAnalysisCache_Hash ON SentenceAnalysisCache(InputTextHash); +CREATE INDEX IX_SentenceAnalysisCache_Expires ON SentenceAnalysisCache(ExpiresAt); +CREATE INDEX IX_SentenceAnalysisCache_Hash_Expires ON SentenceAnalysisCache(InputTextHash, ExpiresAt); +``` + +## 🧪 測試規格 + +### 🔬 功能測試案例 + +#### 1. 句子分析功能測試 + +**測試案例 1.1: 新句子分析** +``` +輸入: "I went to school yesterday" +預期結果: +- 載入時間: 3-5 秒 +- 快取狀態: 🤖 AI 分析 +- 翻譯: "我昨天去學校。" +- 解釋: 包含語法結構說明 +- 高價值詞彙: went, school, yesterday 標記為 ⭐ +``` + +**測試案例 1.2: 快取命中測試** +``` +前置條件: 已分析過 "I went to school yesterday" +輸入: "I went to school yesterday" (相同句子) +預期結果: +- 載入時間: <200ms +- 快取狀態: 💾 快取結果 +- 內容: 與首次分析完全相同 +- 使用額度: 不增加 +``` + +**測試案例 1.3: 語法錯誤修正** +``` +輸入: "I go to school yesterday" +預期結果: +- 語法修正面板出現 +- 原文: "I go to school yesterday" +- 修正: "I went to school yesterday" +- 修正原因: "過去式時態修正:句子中有 'yesterday',應使用過去式" +- 用戶可選擇採用或拒絕修正 +``` + +#### 2. 互動式單字查詢測試 + +**測試案例 2.1: 高價值詞彙查詢** +``` +前置條件: 已完成句子分析 +操作: 點擊標記為 ⭐ 的單字 "went" +預期結果: +- 立即顯示詞彙資訊彈窗 +- 顯示 "⭐ 高價值詞彙(免費查詢)" +- 不扣除使用額度 +- 包含完整詞彙信息 +``` + +**測試案例 2.2: 低價值詞彙查詢** +``` +前置條件: 已完成句子分析,剩餘額度 > 0 +操作: 點擊普通單字 (藍色下劃線) +預期結果: +1. 顯示收費確認對話框 +2. 顯示消耗額度和剩餘額度 +3. 用戶確認後扣除 1 次額度 +4. 調用 query-word API +5. 顯示詞彙資訊彈窗 +``` + +**測試案例 2.3: 額度不足測試** +``` +前置條件: 剩餘額度 = 0 +操作: 點擊低價值詞彙 +預期結果: +- 顯示 "❌ 使用額度不足,無法查詢低價值詞彙" +- 不調用 API +- 不顯示詞彙彈窗 +``` + +#### 3. 使用限制測試 + +**測試案例 3.1: 免費用戶限制** +``` +前置條件: 免費用戶,3小時內已分析 5 次 +操作: 嘗試分析新句子 +預期結果: +- 顯示 "❌ 免費用戶 3 小時內只能分析 5 次句子,請稍後再試或升級到付費版本" +- 不調用分析 API +- 使用計數不增加 +``` + +**測試案例 3.2: 付費用戶無限制** +``` +前置條件: isPremium = true +操作: 多次分析句子 +預期結果: +- 顯示 "🌟 付費用戶:無限制使用" +- 所有分析正常執行 +- 無使用次數限制 +``` + +### 🚀 效能測試規格 + +#### 1. 回應時間測試 + +| 測試情境 | 預期時間 | 測試方法 | +|---------|----------|----------| +| 新句子 AI 分析 | 3-5 秒 | 測量從點擊到結果顯示的時間 | +| 快取命中查詢 | <200ms | 重複查詢相同句子 | +| 高價值詞彙點擊 | <100ms | 點擊已標記的高價值詞彙 | +| 低價值詞彙查詢 | 1-2 秒 | 確認後的API調用時間 | + +#### 2. 快取效能測試 + +| 測試指標 | 目標值 | 測試方法 | +|---------|-------|----------| +| 快取命中率 | >80% | 重複查詢統計 | +| 快取寫入成功率 | >99% | 監控快取失敗日誌 | +| 快取過期清理 | 24小時 | 測試過期資料自動清理 | + +### 🔧 整合測試規格 + +#### 1. 端到端測試流程 + +**完整用戶旅程測試**: +``` +1. 登入系統 +2. 導航到 /generate 頁面 +3. 輸入測試句子 "She felt ashamed of her mistake and apologized" +4. 點擊 "🔍 分析句子" +5. 驗證分析結果正確顯示 +6. 點擊高價值詞彙 "ashamed" (免費) +7. 驗證詞彙彈窗內容 +8. 點擊低價值詞彙 "her" (收費) +9. 確認收費對話框 +10. 驗證額度扣除 +11. 點擊 "🔄 分析新句子" +12. 輸入相同句子 +13. 驗證快取命中 (💾 快取結果) +14. 點擊 "📖 生成詞卡" +15. 驗證詞卡生成功能 +``` + +#### 2. API 整合測試 + +**測試案例: API 連接性** +```bash +# 1. 句子分析 API 測試 +curl -X POST http://localhost:5000/api/ai/analyze-sentence \ + -H "Content-Type: application/json" \ + -d '{"inputText": "Hello world", "analysisMode": "full"}' + +# 2. 單字查詢 API 測試 +curl -X POST http://localhost:5000/api/ai/query-word \ + -H "Content-Type: application/json" \ + -d '{"word": "hello", "sentence": "Hello world"}' + +# 3. 快取統計 API 測試 +curl -X GET http://localhost:5000/api/ai/cache-stats +``` + +#### 3. 邊界條件測試 + +**輸入驗證測試**: +``` +測試案例 3.1: 空白輸入 +輸入: "" +預期: 按鈕禁用,無法提交 + +測試案例 3.2: 超長輸入 +輸入: 301字元的文字 +預期: 無法輸入,紅色邊框警告 + +測試案例 3.3: 特殊字元 +輸入: "Hello @#$%^&*() world!" +預期: 正常分析,特殊字元適當處理 + +測試案例 3.4: 純中文輸入 +輸入: "你好世界" +預期: 系統應適當處理或給出提示 +``` + +### 🐛 錯誤處理測試 + +#### 1. 網路錯誤測試 +``` +測試案例 1: 後端服務停止 +操作: 停止後端服務後嘗試分析 +預期: 顯示連接錯誤訊息,不崩潰 + +測試案例 2: Gemini API 失敗 +模擬: API key 無效或 API 服務不可用 +預期: 回退到本地分析,不中斷用戶體驗 +``` + +#### 2. 資料錯誤測試 +``` +測試案例 1: 損壞的快取資料 +模擬: 資料庫中有格式錯誤的 JSON +預期: 忽略損壞快取,重新調用 AI 分析 + +測試案例 2: 不完整的API回應 +模擬: 後端回傳缺少某些欄位的資料 +預期: 使用預設值,顯示部分資訊而非崩潰 +``` + +## 🎯 驗收標準 + +### ✅ 功能驗收標準 + +1. **基本功能完整性** + - [ ] 用戶可成功輸入並分析英文句子 + - [ ] 所有UI組件正確顯示和互動 + - [ ] API調用成功且資料正確處理 + +2. **AI功能正確性** + - [ ] Gemini AI 整合正常運作 + - [ ] 翻譯和解釋內容品質符合要求 + - [ ] 語法修正建議合理且準確 + +3. **互動查詢功能** + - [ ] 高價值詞彙免費查詢正常 + - [ ] 低價值詞彙收費機制正確 + - [ ] 詞彙彈窗內容完整準確 + +4. **快取系統功能** + - [ ] 新句子使用 AI 分析 + - [ ] 重複句子使用快取結果 + - [ ] 快取狀態正確顯示 + +5. **使用限制功能** + - [ ] 免費用戶額度限制生效 + - [ ] 付費用戶無限制使用 + - [ ] 額度計算準確無誤 + +### 📊 效能驗收標準 + +1. **回應時間要求** + - [ ] 快取命中 < 200ms + - [ ] AI分析 < 10秒 (95%的情況下 < 5秒) + - [ ] 頁面導航 < 100ms + +2. **系統穩定性** + - [ ] 24小時連續運行無崩潰 + - [ ] 記憶體使用穩定 + - [ ] 資料庫連接池正常 + +3. **用戶體驗標準** + - [ ] 用戶操作回饋及時 (<100ms) + - [ ] 載入狀態清晰可見 + - [ ] 錯誤訊息用戶友善 + +## 🔧 開發和部署規格 + +### 📁 檔案結構 +``` +frontend/ +├── app/generate/page.tsx (主要分析頁面) +├── components/ClickableTextV2.tsx (互動式文字組件) +├── components/GrammarCorrectionPanel.tsx (語法修正面板) +└── contexts/AuthContext.tsx (認證上下文) + +backend/ +├── Controllers/AIController.cs (AI API 控制器) +├── Services/GeminiService.cs (Gemini AI 服務) +├── Services/AnalysisCacheService.cs (快取服務) +├── Services/UsageTrackingService.cs (使用追蹤服務) +└── Models/Entities/ (資料模型) +``` + +### 🚀 部署需求 + +#### 環境變數 +```env +# Gemini AI +GEMINI_API_KEY=your_gemini_api_key + +# 資料庫 +CONNECTION_STRING=Data Source=dramaling.db + +# CORS +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3002 + +# 快取設定 +CACHE_TTL_HOURS=24 +CACHE_CLEANUP_INTERVAL_HOURS=6 +``` + +#### 系統需求 +- **前端**: Node.js 18+, Next.js 15+ +- **後端**: .NET 8.0+, ASP.NET Core +- **資料庫**: SQLite 3.x (開發), SQL Server (生產) +- **外部API**: Google Gemini API access + +--- + +**文檔版本**: v1.0 +**最後更新**: 2025-01-18 +**負責人**: Claude Code +**審核狀態**: ✅ 完成 \ No newline at end of file diff --git a/note/spec/usage-limit-implementation-report.md b/note/spec/usage-limit-implementation-report.md new file mode 100644 index 0000000..1628ee4 --- /dev/null +++ b/note/spec/usage-limit-implementation-report.md @@ -0,0 +1,551 @@ +# 免費用戶使用限制功能實現報告 + +**項目**: DramaLing 英語學習平台 +**功能模組**: 免費用戶使用額度管理系統 +**檢查日期**: 2025-01-18 +**檢查者**: Claude Code + +## 📋 功能概述 + +免費用戶使用限制功能是一個雙層限制系統,通過前端本地計數和後端資料庫驗證,確保免費用戶在 3 小時內最多只能進行 5 次句子分析,防止濫用 AI 資源。 + +## 🔧 當前實現架構 + +### 📊 限制參數 +```typescript +// 前端常數 (generate/page.tsx) +const FREE_USER_LIMIT = 5 // 最大分析次數 +const TIME_WINDOW = "3小時" // 時間窗口 +const isPremium = false // 用戶類型 + +// 後端常數 (UsageTrackingService.cs) +const FREE_USER_ANALYSIS_LIMIT = 5 // 最大分析次數 +const FREE_USER_RESET_HOURS = 3 // 3小時重置窗口 +``` + +## 🔄 雙層限制實現機制 + +### 1️⃣ **前端限制層** (第一道防線) + +**檔案位置**: `frontend/app/generate/page.tsx:42-46` + +```typescript +// 前端本地檢查 +if (!isPremium && usageCount >= 5) { + console.log('❌ 使用次數超限') + alert('❌ 免費用戶 3 小時內只能分析 5 次句子,請稍後再試或升級到付費版本') + return +} +``` + +**實現方式**: +- ✅ **本地狀態**: `const [usageCount, setUsageCount] = useState(0)` +- ✅ **即時檢查**: 每次點擊分析按鈕前驗證 +- ✅ **用戶回饋**: 立即顯示限制提示,無需等待API +- ✅ **計數更新**: 成功分析後 `setUsageCount(prev => prev + 1)` + +**特點**: +- 🚀 **即時回應**: 無需API調用,立即提示 +- 🎯 **用戶友善**: 清楚說明限制原因和解決方案 +- 💰 **引導付費**: 提示升級到付費版本 + +### 2️⃣ **後端驗證層** (權威檢查) + +**檔案位置**: `backend/DramaLing.Api/Controllers/AIController.cs:515-531` + +```csharp +// 後端權威驗證 +var mockUserId = Guid.Parse("00000000-0000-0000-0000-000000000001"); +var canUse = await _usageService.CheckUsageLimitAsync(mockUserId, isPremium: false); +if (!canUse) +{ + return StatusCode(429, new + { + Success = false, + Error = "免費用戶使用限制已達上限", + ErrorCode = "USAGE_LIMIT_EXCEEDED", + ResetInfo = new + { + WindowHours = 3, + Limit = 5 + } + }); +} +``` + +**實現方式**: +- ✅ **資料庫查詢**: 基於 `WordQueryUsageStats` 表 +- ✅ **時間窗口**: 查詢過去3小時的使用記錄 +- ✅ **精確計算**: `SentenceAnalysisCount + LowValueWordClicks` +- ✅ **錯誤代碼**: 結構化錯誤回應 + +## 🗄️ 資料庫實現詳情 + +### 使用統計表結構 + +**表名**: `WordQueryUsageStats` + +```sql +-- 使用統計記錄表 +CREATE TABLE WordQueryUsageStats ( + Id UNIQUEIDENTIFIER PRIMARY KEY, + UserId UNIQUEIDENTIFIER NOT NULL, -- 用戶ID + SentenceAnalysisCount INT NOT NULL, -- 句子分析次數 + HighValueWordClicks INT NOT NULL, -- 高價值詞彙點擊次數 + LowValueWordClicks INT NOT NULL, -- 低價值詞彙點擊次數(收費) + CreatedAt DATETIME2 NOT NULL, -- 記錄建立時間 + UpdatedAt DATETIME2 NOT NULL -- 最後更新時間 +); +``` + +### 使用統計查詢邏輯 + +**檔案位置**: `backend/DramaLing.Api/Services/UsageTrackingService.cs:42-47` + +```csharp +// 計算過去3小時的使用量 +var resetTime = DateTime.UtcNow.AddHours(-FREE_USER_RESET_HOURS); +var recentUsage = await _context.WordQueryUsageStats + .Where(stats => stats.UserId == userId && stats.CreatedAt >= resetTime) + .SumAsync(stats => stats.SentenceAnalysisCount + stats.LowValueWordClicks); + +var canUse = recentUsage < FREE_USER_ANALYSIS_LIMIT; +``` + +**特點**: +- 🕐 **滑動窗口**: 過去3小時內的累計使用量 +- 📊 **綜合計算**: 句子分析 + 付費詞彙查詢 +- 🔒 **權威性**: 無法被前端繞過 +- 📈 **可擴展**: 支援不同用戶類型的限制 + +## 🎨 用戶介面實現 + +### UI顯示邏輯 + +**檔案位置**: `frontend/app/generate/page.tsx:295-312` + +```typescript +// 使用次數顯示 +
+ {isPremium ? ( + 🌟 付費用戶:無限制使用 + ) : ( + = 4 ? 'text-red-600' : + usageCount >= 3 ? 'text-yellow-600' : 'text-gray-600'}> + 免費用戶:已使用 {usageCount}/5 次 (3小時內) + {usageCount >= 5 && 已達上限,請稍後再試} + + )} +
+``` + +### 視覺回饋系統 + +| 使用次數 | 顏色 | 狀態 | 用戶體驗 | +|---------|------|------|----------| +| **0-2次** | `text-gray-600` | 正常 | 無特殊提示 | +| **3次** | `text-yellow-600` | 警告 | 黃色提醒剩餘次數 | +| **4次** | `text-red-600` | 危險 | 紅色警告即將達限 | +| **5次** | `text-red-500` | 限制 | 顯示"已達上限"訊息 | + +### 按鈕狀態管理 + +```typescript +// 分析按鈕禁用邏輯 +disabled = isAnalyzing || // 正在分析中 + (mode === 'manual' && (!textInput || textInput.length > 300)) || // 輸入驗證 + (mode === 'screenshot') // 截圖模式未開放 +``` + +**注意**: 前端按鈕禁用主要基於輸入驗證,使用限制檢查在函數內部進行。 + +## 🔍 實現流程分析 + +### 句子分析流程 + +``` +用戶點擊「🔍 分析句子」 + ↓ +┌─── 前端檢查 ─────────────────┐ +│ 1. 檢查 isPremium 狀態 │ +│ 2. 檢查 usageCount >= 5 │ +│ 3. 超限則顯示錯誤並 return │ +└─────────────────────────────┘ + ↓ (通過) +┌─── 發送 API 請求 ────────────┐ +│ POST /api/ai/analyze-sentence │ +└─────────────────────────────┘ + ↓ +┌─── 後端驗證 ─────────────────┐ +│ 1. 檢查資料庫使用記錄 │ +│ 2. 計算過去3小時累計使用量 │ +│ 3. 超限則返回 429 錯誤 │ +└─────────────────────────────┘ + ↓ (通過) +┌─── 執行分析 ─────────────────┐ +│ 1. 調用 Gemini AI │ +│ 2. 處理分析結果 │ +│ 3. 存入快取 │ +│ 4. 更新使用統計 │ +└─────────────────────────────┘ + ↓ +┌─── 前端更新 ─────────────────┐ +│ 1. 顯示分析結果 │ +│ 2. usageCount + 1 │ +│ 3. 更新UI狀態顯示 │ +└─────────────────────────────┘ +``` + +## 🚨 問題分析:前後端不同步 + +### ⚠️ **發現的設計問題** + +#### 1. **計數邏輯不一致** + +**前端計數**: +```typescript +// 簡單累加,不考慮時間窗口 +setUsageCount(prev => prev + 1) +``` + +**後端計數**: +```csharp +// 基於3小時滑動窗口的資料庫查詢 +var recentUsage = await _context.WordQueryUsageStats + .Where(stats => stats.UserId == userId && stats.CreatedAt >= resetTime) + .SumAsync(stats => stats.SentenceAnalysisCount + stats.LowValueWordClicks); +``` + +#### 2. **時間重置機制** + +| 層級 | 重置機制 | 問題 | +|------|----------|------| +| **前端** | ❌ 無重置 | 頁面刷新會重置為0,但實際限制未重置 | +| **後端** | ✅ 3小時滑動窗口 | 正確實現,但前端不知道何時重置 | + +#### 3. **快取命中對計數的影響** + +**當前行為**: +- ✅ **新句子**: 消耗1次額度 (正確) +- ❌ **快取命中**: 也消耗1次額度 (可能不合理) + +**問題**: 快取命中不應該消耗額度,因為沒有調用AI API。 + +### 🔧 **當前實現的優缺點** + +#### ✅ **優點** +1. **雙重保護**: 前端 + 後端雙重驗證 +2. **即時回饋**: 前端檢查提供即時用戶體驗 +3. **安全性**: 後端驗證防止繞過 +4. **視覺提示**: 分級顏色警告系統 +5. **付費引導**: 清楚的升級提示 + +#### ❌ **問題** +1. **不同步**: 前後端計數邏輯不一致 +2. **無時間重置**: 前端不知道何時重置額度 +3. **快取誤計**: 快取命中也消耗額度 +4. **頁面重置**: 刷新頁面會重置前端計數器 +5. **無持久化**: 前端計數器無法跨頁面保持 + +## 💡 建議改善方案 + +### 🎯 **短期修復** + +#### 1. **修復快取計數問題** +```typescript +// 修改前端:快取命中不增加計數 +if (result.success) { + // ... 其他處理 + + // 只有非快取結果才增加計數 + if (!result.cached) { + setUsageCount(prev => prev + 1) + } +} +``` + +#### 2. **前端計數同步** +```typescript +// 添加從後端獲取實際使用量的功能 +const fetchUsageStats = async () => { + const response = await fetch('/api/ai/usage-stats') + const stats = await response.json() + setUsageCount(stats.data.recentUsage) +} + +// 在組件初始化時同步 +useEffect(() => { + fetchUsageStats() +}, []) +``` + +#### 3. **時間重置提示** +```typescript +// 添加重置時間顯示 +const nextResetTime = new Date(Date.now() + (3 * 60 * 60 * 1000)) + + 額度將於 {nextResetTime.toLocaleTimeString()} 重置 + +``` + +### 🚀 **中期改善** + +#### 1. **統一計數系統** +- 移除前端計數器 +- 完全依賴後端API提供的使用統計 +- 每次操作後同步最新狀態 + +#### 2. **智能快取策略** +- 快取命中不消耗額度 +- 高價值詞彙查詢永遠免費 +- 只有實際AI調用才計費 + +#### 3. **增強的用戶體驗** +- 實時剩餘額度顯示 +- 重置時間倒計時 +- 使用歷史記錄 + +## 📊 當前實現狀態評估 + +### ✅ **運作正常的部分** + +1. **基本限制機制**: 確實能防止超量使用 +2. **視覺回饋系統**: 用戶能清楚看到使用狀態 +3. **後端安全驗證**: 無法繞過的伺服器端檢查 +4. **付費用戶支援**: 正確識別並給予無限制使用 + +### ⚠️ **存在問題的部分** + +1. **前後端不同步**: + - 前端: 簡單累加計數器 + - 後端: 3小時滑動窗口計算 + +2. **快取計數邏輯**: + - 快取命中仍消耗前端計數器 + - 實際上沒有消耗AI資源 + +3. **頁面狀態持久性**: + - 頁面刷新會重置前端計數器 + - 用戶可能誤以為額度重置 + +### 🔍 **技術債務** + +1. **資料一致性**: 需要統一前後端計數邏輯 +2. **狀態管理**: 需要持久化前端狀態 +3. **用戶體驗**: 需要更準確的額度資訊 + +## 🧪 測試用例分析 + +### **目前的實現問題測試** + +#### 測試案例 1: 快取計數問題 +``` +步驟: +1. 分析句子A (usageCount = 1) +2. 返回並重新分析句子A (快取命中) +預期: usageCount 應該保持 1 +實際: usageCount 變成 2 ❌ + +問題: 快取命中不應該消耗額度 +``` + +#### 測試案例 2: 頁面刷新問題 +``` +步驟: +1. 分析5次句子 (usageCount = 5) +2. 刷新頁面 +預期: 仍然顯示限制狀態 +實際: usageCount 重置為 0,可以繼續使用 ❌ + +問題: 前端狀態沒有持久化 +``` + +#### 測試案例 3: 後端驗證 +``` +步驟: +1. 繞過前端檢查,直接調用API +2. 在3小時內調用超過5次 +預期: 後端應該返回 429 錯誤 +實際: 需要驗證 ⚠️ + +狀態: 邏輯存在,但需要實際測試 +``` + +## 📈 效能影響分析 + +### 前端效能 +- **狀態管理**: 輕量級 (`useState`) +- **檢查成本**: O(1) 常數時間 +- **記憶體使用**: 微量 (單一整數值) + +### 後端效能 +- **資料庫查詢**: 每次分析需要查詢使用統計 +- **索引需求**: `UserId + CreatedAt` 複合索引 +- **查詢複雜度**: 簡單時間範圍查詢 + +### 網路效能 +- **額外API調用**: 可能需要獨立的使用統計API +- **回應大小**: 增加使用統計資訊 + +## 🎯 功能完整性評估 + +### ✅ **已實現功能** + +1. **基本限制**: 5次/3小時限制正確執行 +2. **付費區分**: 付費用戶無限制使用 +3. **視覺提示**: 清楚的使用狀態顯示 +4. **錯誤處理**: 適當的錯誤訊息和引導 + +### ❌ **缺失功能** + +1. **狀態同步**: 前後端計數器不一致 +2. **時間重置**: 用戶不知道何時重置 +3. **快取優化**: 快取命中仍計費 +4. **歷史記錄**: 無使用歷史追蹤 +5. **統計面板**: 無詳細使用統計展示 + +### ⚠️ **部分功能** + +1. **後端驗證**: 邏輯存在但需要實際測試 +2. **錯誤處理**: 基本實現,可更完善 +3. **用戶體驗**: 功能性足夠,體驗可優化 + +## 🚀 改善優先級建議 + +### **高優先級** (立即修復) +1. ✅ **修復快取計數**: 快取命中不消耗額度 +2. ✅ **前端狀態同步**: 從後端獲取實際使用量 +3. ✅ **頁面刷新處理**: 持久化或重新獲取狀態 + +### **中優先級** (1-2週內) +1. **時間重置提示**: 顯示下次重置時間 +2. **使用統計API**: 獨立的使用統計端點 +3. **增強錯誤處理**: 更友善的錯誤訊息 + +### **低優先級** (未來功能) +1. **使用歷史記錄**: 詳細的使用歷史 +2. **彈性限制**: 基於用戶行為的動態限制 +3. **統計儀表板**: 管理員使用統計面板 + +## 🏁 結論 + +### **當前狀態**: ⚠️ **基本可用,存在改善空間** + +**功能性**: ✅ 基本限制機制運作正常 +**用戶體驗**: ⚠️ 可用但有混亂點 (快取計數、頁面重置) +**技術實現**: ⚠️ 雙層保護好,但同步性有問題 +**商業價值**: ✅ 有效防止濫用,引導付費 + +### **關鍵改善點** + +1. **統一計數邏輯**: 前後端使用相同的計算方式 +2. **快取計數修復**: 快取命中不應消耗額度 +3. **狀態持久化**: 解決頁面刷新重置問題 +4. **時間透明度**: 讓用戶知道重置時間 + +### **建議實施** + +**第一階段**: 修復快取計數和狀態同步問題 +**第二階段**: 增加時間重置提示和統計API +**第三階段**: 完整的使用歷史和管理功能 + +--- + +## 🔄 系統暫時關閉記錄 + +**執行時間**: 2025-01-18 15:54 +**執行者**: Claude Code +**狀態**: ✅ 使用限制系統已暫時關閉 + +### 📝 修改記錄 + +#### 1. **前端修改** +**檔案**: `frontend/app/generate/page.tsx` +**位置**: 第24行 +**修改內容**: +```typescript +// 修改前 +const [isPremium] = useState(false) + +// 修改後 +const [isPremium] = useState(true) +``` + +#### 2. **後端修改** +**檔案**: `backend/DramaLing.Api/Controllers/AIController.cs` +**位置**: 第517行 +**修改內容**: +```csharp +// 修改前 +var canUse = await _usageService.CheckUsageLimitAsync(mockUserId, isPremium: false); + +// 修改後 +var canUse = await _usageService.CheckUsageLimitAsync(mockUserId, isPremium: true); +``` + +#### 3. **編譯問題修復** +**檔案**: `backend/DramaLing.Api/Controllers/AIController.cs` +**位置**: 第7行 (using statements) +**修改內容**: +```csharp +// 新增 +using System.Text.Json; +``` + +### 🧪 **測試結果** + +✅ **多次調用測試通過**: +- 第1次調用: 成功 +- 第6次調用: 成功 (原本第6次會被限制) +- API無限制調用確認成功 + +✅ **UI顯示效果**: +``` +🌟 付費用戶:無限制使用 +``` + +### 🔄 **復原使用限制的步驟** + +當需要重新啟用使用限制時,請執行以下步驟: + +#### **步驟1: 復原前端設定** +```bash +# 編輯檔案: frontend/app/generate/page.tsx +# 第24行改回: +const [isPremium] = useState(false) +``` + +#### **步驟2: 復原後端設定** +```bash +# 編輯檔案: backend/DramaLing.Api/Controllers/AIController.cs +# 第517行改回: +var canUse = await _usageService.CheckUsageLimitAsync(mockUserId, isPremium: false); +``` + +#### **步驟3: 重新啟動服務** +```bash +# 重新啟動後端 +cd backend/DramaLing.Api +dotnet run --urls http://localhost:5000 + +# 前端會自動重新編譯 +``` + +#### **步驟4: 驗證復原** +復原後應該看到: +``` +免費用戶:已使用 0/5 次 (3小時內) +``` + +### ⚠️ **重要提醒** + +1. **資料庫統計**: 後端的使用統計仍在記錄,復原限制後會基於實際使用記錄計算 +2. **快取清理**: 如需要完全重置統計,可考慮清理 `WordQueryUsageStats` 表 +3. **前端狀態**: 前端計數器會在頁面刷新後重置,但後端限制基於資料庫記錄 + +--- + +**報告生成時間**: 2025-01-18 +**最後更新時間**: 2025-01-18 15:54 +**分析範圍**: 前端 + 後端 + 資料庫 +**功能狀態**: 🔄 **暫時關閉,隨時可復原** \ No newline at end of file