diff --git a/AI生成畫面前端程式碼規格.md b/AI生成畫面前端程式碼規格.md new file mode 100644 index 0000000..8115397 --- /dev/null +++ b/AI生成畫面前端程式碼規格.md @@ -0,0 +1,695 @@ +# AI生成畫面前端程式碼規格 + +## 📋 **概述** + +本文件詳細說明DramaLing AI生成功能的前端程式碼架構、API調用、資料流程,以及如何理解和維護相關程式碼。 + +--- + +## 🏗️ **檔案架構圖** + +### **1. 核心檔案結構** + +``` +frontend/ +├── app/generate/ +│ └── page.tsx # 🎯 主分析頁面 +├── components/ +│ ├── ClickableTextV2.tsx # 🔍 可點擊詞彙組件 +│ ├── Navigation.tsx # 🧭 導航組件 +│ └── ProtectedRoute.tsx # 🔒 路由保護組件 +└── lib/services/ + └── flashcards.ts # 💾 詞卡服務層 +``` + +### **2. 依賴關係圖** + +``` +page.tsx + ├── imports Navigation.tsx + ├── imports ProtectedRoute.tsx + ├── imports ClickableTextV2.tsx + └── imports flashcardsService +``` + +--- + +## 🔄 **API調用架構** + +### **1. 主分析頁面 (`/app/generate/page.tsx`)** + +#### **調用的API端點**: +```typescript +POST /api/ai/analyze-sentence +``` + +#### **調用位置**: +```typescript +// 第40行 - handleAnalyzeSentence函數 +const response = await fetch('http://localhost:5000/api/ai/analyze-sentence', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` + }, + body: JSON.stringify({ + inputText: textInput, + userLevel: userLevel, // 個人化重點學習範圍 + analysisMode: 'full' + }) +}) +``` + +#### **API回傳資料格式**: +```json +{ + "success": true, + "data": { + "analysisId": "guid", + "userLevel": "A2", + "highValueCriteria": "B1-B2", + "wordAnalysis": { + "bonus": { + "word": "bonus", + "translation": "獎金", + "definition": "額外給予的金錢", + "partOfSpeech": "noun", + "pronunciation": "/ˈboʊnəs/", + "isHighValue": true, + "difficultyLevel": "B1", + "synonyms": ["reward", "incentive"], + "example": "She received a year-end bonus.", + "exampleTranslation": "她獲得了年終獎金。" + } + }, + "sentenceMeaning": { + "translation": "公司提供了獎金。" + }, + "grammarCorrection": { /*...*/ }, + "highValueWords": ["bonus", "offered"] + } +} +``` + +### **2. 可點擊詞彙組件 (`/components/ClickableTextV2.tsx`)** + +#### **調用的API端點**: +```typescript +POST /api/ai/query-word +``` + +#### **調用位置有兩處**: + +##### **位置1: handleCostConfirm函數 (第245行)** +```typescript +const response = await fetch('http://localhost:5000/api/ai/query-word', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + word: showCostConfirm.word, + sentence: text, + analysisId: null + }) +}) +``` + +##### **位置2: queryWordWithAI函數 (第303行)** +```typescript +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 + }) +}) +``` + +#### **觸發條件**: +- 用戶點擊詞彙時,如果該詞彙不在`analysis`物件中 +- 用戶確認付費查詢詞彙時 + +### **3. 詞卡服務 (`/lib/services/flashcards.ts`)** + +#### **調用的API端點**: +```typescript +POST /api/flashcards // 創建詞卡 +GET /api/flashcards // 查詢詞卡 +GET /api/cardsets // 查詢詞卡組 +``` + +#### **調用方式**: +```typescript +// 透過flashcardsService.createFlashcard()間接調用 +await this.makeRequest>('/flashcards', { + method: 'POST', + body: JSON.stringify(data), +}); +``` + +--- + +## 📊 **資料流程架構** + +### **1. 完整用戶操作流程** + +```mermaid +graph TD + A[用戶輸入句子] --> B[點擊分析按鈕] + B --> C[調用 analyze-sentence API] + C --> D[接收完整詞彙分析資料] + D --> E[顯示可點擊文字] + E --> F[用戶點擊詞彙] + F --> G{詞彙在analysis中?} + G -->|是| H[直接顯示Portal彈窗] + G -->|否| I[調用 query-word API] + I --> J[覆蓋原有資料] + J --> K[顯示Portal彈窗] + H --> L[點擊保存詞卡] + K --> L + L --> M[調用 flashcards API] +``` + +### **2. 狀態管理流程** + +```typescript +// 主頁面狀態 +const [sentenceAnalysis, setSentenceAnalysis] = useState(null) // 完整詞彙分析 +const [sentenceMeaning, setSentenceMeaning] = useState('') // 句子翻譯 +const [grammarCorrection, setGrammarCorrection] = useState(null) // 語法修正 +const [finalText, setFinalText] = useState('') // 最終文本 + +// ClickableTextV2狀態 +const [selectedWord, setSelectedWord] = useState(null) // 選中詞彙 +const [popupPosition, setPopupPosition] = useState({...}) // 彈窗位置 +const [mounted, setMounted] = useState(false) // Portal渲染狀態 +``` + +### **3. 資料傳遞路徑** + +``` +API回應 → setSentenceAnalysis → analysis prop → ClickableTextV2 → Portal彈窗 +``` + +--- + +## 🎯 **組件職責分析** + +### **1. `/app/generate/page.tsx` - 主分析頁面** + +#### **核心職責**: +- 🎯 **句子分析觸發器** - 調用AI分析API +- 📊 **資料狀態管理** - 管理分析結果和UI狀態 +- 🎨 **UI佈局控制** - 控制分析前/後的畫面切換 +- 🔧 **個人化設定** - 取得用戶程度設定 + +#### **關鍵函數**: +```typescript +handleAnalyzeSentence() // 句子分析主函數 +handleSaveWord() // 詞彙儲存函數 +handleAcceptCorrection() // 語法修正處理 +``` + +#### **API依賴**: +- `POST /api/ai/analyze-sentence` - 句子分析 +- `flashcardsService.createFlashcard()` - 詞卡儲存 + +### **2. `/components/ClickableTextV2.tsx` - 可點擊詞彙組件** + +#### **核心職責**: +- 🖱️ **詞彙互動處理** - 處理詞彙點擊事件 +- 🎨 **Portal彈窗管理** - 使用React Portal渲染彈窗 +- 🔍 **詞彙資料查找** - 在analysis中查找或即時查詢 +- 💾 **詞卡儲存整合** - 提供儲存到詞卡功能 + +#### **關鍵函數**: +```typescript +handleWordClick() // 詞彙點擊處理 +queryWordWithAI() // 即時詞彙查詢 +getWordProperty() // 智能屬性讀取 +VocabPopup() // Portal彈窗組件 +``` + +#### **API依賴**: +- `POST /api/ai/query-word` - 即時詞彙查詢 + +#### **⚠️ 已知問題**: +- 使用`query-word` API覆蓋了`analyze-sentence`的完整資料 +- 導致例句和其他資料遺失 + +### **3. `/components/Navigation.tsx` - 導航組件** + +#### **核心職責**: +- 🧭 **頁面導航** - 提供網站主要頁面連結 +- 👤 **用戶狀態顯示** - 顯示登入狀態 +- ⚙️ **設定頁面入口** - 連結到用戶程度設定 + +#### **API依賴**:無直接API調用 + +### **4. `/lib/services/flashcards.ts` - 詞卡服務層** + +#### **核心職責**: +- 💾 **詞卡CRUD操作** - 創建、讀取、更新、刪除詞卡 +- 🗂️ **詞卡組管理** - 管理詞卡分類 +- 🔒 **API認證處理** - 自動添加JWT Token + +#### **API端點封裝**: +```typescript +/api/flashcards // 詞卡CRUD +/api/cardsets // 詞卡組管理 +/api/cardsets/ensure-default // 確保預設詞卡組 +``` + +--- + +## 🔍 **如何分析程式碼中的API調用** + +### **1. 搜索技巧** + +#### **在VS Code或終端中**: +```bash +# 搜索API調用 +grep -r "fetch(" frontend/ +grep -r "api/" frontend/ +grep -r "localhost:5000" frontend/ + +# 搜索特定API端點 +grep -r "analyze-sentence" frontend/ +grep -r "query-word" frontend/ +grep -r "flashcards" frontend/ +``` + +#### **在瀏覽器開發者工具中**: +1. **Network面板** - 查看實際API調用 +2. **Console面板** - 查看調試輸出 +3. **Application面板** - 查看localStorage資料 + +### **2. 程式碼閱讀要點** + +#### **識別API調用的關鍵字**: +```typescript +// 直接API調用 +fetch('http://localhost:5000/api/...') +await fetch(...) + +// 服務層調用 +flashcardsService.createFlashcard() +flashcardsService.getFlashcards() + +// 其他HTTP客戶端 +axios.post(...) +``` + +#### **找到觸發條件**: +```typescript +// 用戶事件觸發 +onClick={handleAnalyzeSentence} +onClick={(e) => handleWordClick(word, e)} + +// 狀態變化觸發 +useEffect(() => { /* API調用 */ }, [dependency]) +``` + +### **3. 資料流追蹤** + +#### **API回應到狀態**: +```typescript +const result = await response.json() +setSentenceAnalysis(result.data.WordAnalysis) // 儲存到狀態 +``` + +#### **狀態到組件**: +```typescript + +``` + +--- + +## 🚨 **當前架構問題分析** + +### **1. API調用衝突問題** + +#### **問題描述**: +- **主頁面** 調用 `analyze-sentence` API → 取得完整詞彙資料(包含例句) +- **詞彙組件** 調用 `query-word` API → 取得簡化資料(無例句) +- **結果** → 好資料被壞資料覆蓋 + +#### **程式碼位置**: +```typescript +// ✅ 正確的API (page.tsx:40) +POST /api/ai/analyze-sentence → 完整資料 + +// ❌ 問題的API (ClickableTextV2.tsx:245, 303) +POST /api/ai/query-word → 簡化資料 +``` + +#### **觸發條件**: +```typescript +// ClickableTextV2.tsx:221 +if (wordAnalysis) { + // 使用預存資料 ✅ +} else { + // 調用 query-word API ❌ + await queryWordWithAI(cleanWord, position) +} +``` + +### **2. 資料不一致問題** + +#### **analyze-sentence 回傳**: +```json +{ + "example": "She received a year-end bonus for her hard work.", + "exampleTranslation": "她因為努力工作獲得了年終獎金。", + "synonyms": ["reward", "incentive", "extra pay"] +} +``` + +#### **query-word 回傳**: +```json +{ + "example": null, + "exampleTranslation": null, + "synonyms": [] +} +``` + +--- + +## 🎨 **UI組件架構** + +### **1. Portal彈窗系統** + +#### **技術實現**: +```typescript +import { createPortal } from 'react-dom' + +const VocabPopup = () => { + if (!selectedWord || !analysis?.[selectedWord] || !mounted) return null + + return createPortal( +
+ {/* 彈窗內容 */} +
, + document.body // 渲染到body,避免CSS繼承 + ) +} +``` + +#### **設計優勢**: +- **完全脫離父級CSS繼承** +- **響應式定位系統** +- **詞卡風格一致性** + +### **2. 個人化標記系統** + +#### **詞彙分類邏輯**: +```typescript +const getWordClass = (word: string) => { + const wordAnalysis = analysis?.[cleanWord] + const isHighValue = getWordProperty(wordAnalysis, 'isHighValue') + + if (isHighValue) { + return "bg-green-100 border-green-400 hover:bg-green-200" // 重點學習 + } else { + return "bg-blue-100 border-blue-300 hover:bg-blue-200" // 普通詞彙 + } +} +``` + +#### **視覺效果**: +- **重點學習詞彙** → 綠色邊框 + ⭐ 標記 +- **普通詞彙** → 藍色邊框 +- **未分析詞彙** → 灰色虛線邊框 + +--- + +## 📊 **狀態管理架構** + +### **1. 主頁面狀態流** + +```typescript +// 分析階段 +[textInput] → handleAnalyzeSentence() → [sentenceAnalysis] + ↓ + [sentenceMeaning] + ↓ + [grammarCorrection] + +// 顯示階段 +[sentenceAnalysis] → ClickableTextV2 → Portal彈窗 +``` + +### **2. 詞彙組件狀態流** + +```typescript +// 點擊階段 +handleWordClick() → [selectedWord] + [popupPosition] + ↓ + VocabPopup() Portal渲染 + +// 儲存階段 +handleSaveWord() → flashcardsService.createFlashcard() +``` + +### **3. 個人化設定流** + +```typescript +localStorage.getItem('userEnglishLevel') → API請求 → 個人化結果 +``` + +--- + +## 🔧 **關鍵技術實現** + +### **1. Portal彈窗技術** + +#### **為什麼使用Portal**: +```typescript +// ❌ 舊方式 - CSS繼承問題 +
+
可點擊文字
+
彈窗
// 會繼承text-lg +
+ +// ✅ Portal方式 - 完全隔離 +
+
可點擊文字
+
+{createPortal( +
彈窗
, // 渲染到body,不繼承 + document.body +)} +``` + +### **2. 智能屬性讀取** + +#### **解決大小寫不一致**: +```typescript +const getWordProperty = (wordData: any, propName: string) => { + const variations = [ + propName, // 原始 + propName.toLowerCase(), // 小寫 + propName.charAt(0).toUpperCase() + propName.slice(1) // 首字母大寫 + ]; + + for (const variation of variations) { + if (wordData[variation] !== undefined) { + return wordData[variation]; + } + } +} +``` + +### **3. 個人化重點學習範圍** + +#### **前端整合**: +```typescript +// 讀取用戶程度 +const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'; + +// 傳遞給API +body: JSON.stringify({ + inputText: textInput, + userLevel: userLevel, // 個人化參數 + analysisMode: 'full' +}) + +// 顯示重點學習範圍 +const getTargetRange = (level: string) => { + const ranges = { + 'A1': 'A2-B1', 'A2': 'B1-B2', 'B1': 'B2-C1', + 'B2': 'C1-C2', 'C1': 'C2', 'C2': 'C2' + }; + return ranges[level] || 'B1-B2'; +}; +``` + +--- + +## 🛠️ **開發維護指南** + +### **1. 如何添加新的API調用** + +#### **步驟**: +1. **選擇調用位置** - 頁面組件或服務層 +2. **定義請求格式** - TypeScript介面 +3. **處理回應資料** - 錯誤處理和狀態更新 +4. **更新UI狀態** - 觸發重新渲染 + +#### **範例**: +```typescript +// 1. 定義介面 +interface NewApiRequest { + input: string; + options: object; +} + +// 2. API調用 +const callNewApi = async (data: NewApiRequest) => { + try { + const response = await fetch('/api/new-endpoint', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + if (response.ok) { + const result = await response.json(); + // 3. 更新狀態 + setNewData(result.data); + } + } catch (error) { + console.error('API調用失敗:', error); + } +}; +``` + +### **2. 如何修改詞彙顯示邏輯** + +#### **修改位置**: +```typescript +// 詞彙分類邏輯 +ClickableTextV2.tsx → getWordClass() 函數 + +// 彈窗內容 +ClickableTextV2.tsx → VocabPopup() 組件 + +// 屬性讀取 +ClickableTextV2.tsx → getWordProperty() 函數 +``` + +### **3. 如何添加新的詞彙屬性** + +#### **步驟**: +1. **後端API** - 確保API回傳新屬性 +2. **前端介面** - 更新TypeScript介面 +3. **屬性讀取** - 在`getWordProperty`中處理 +4. **UI顯示** - 在Portal彈窗中顯示 + +--- + +## 🔍 **問題診斷指南** + +### **1. API調用問題** + +#### **檢查步驟**: +```typescript +// 1. 檢查Network面板 +// 瀏覽器 → F12 → Network → 查看API調用 + +// 2. 檢查Console輸出 +console.log('API回應:', result); + +// 3. 檢查回應格式 +console.log('詞彙資料:', result.data.WordAnalysis?.bonus); +``` + +#### **常見問題**: +- **API端點錯誤** - 檢查URL是否正確 +- **請求格式錯誤** - 檢查Content-Type和body +- **認證問題** - 檢查JWT Token + +### **2. 資料顯示問題** + +#### **檢查步驟**: +```typescript +// 1. 檢查狀態 +console.log('sentenceAnalysis:', sentenceAnalysis); + +// 2. 檢查組件接收 +console.log('analysis prop:', analysis); + +// 3. 檢查屬性讀取 +console.log('getWordProperty結果:', getWordProperty(wordData, 'example')); +``` + +### **3. Portal彈窗問題** + +#### **檢查步驟**: +```typescript +// 1. 檢查Portal渲染條件 +console.log('selectedWord:', selectedWord); +console.log('mounted:', mounted); + +// 2. 檢查彈窗位置 +console.log('popupPosition:', popupPosition); + +// 3. 檢查CSS樣式 +// 瀏覽器 → F12 → Elements → 檢查Portal元素 +``` + +--- + +## 🚀 **最佳實踐建議** + +### **1. API調用** +- ✅ **統一使用服務層** - 避免直接在組件中調用API +- ✅ **錯誤處理** - 每個API調用都要有try-catch +- ✅ **loading狀態** - 提供用戶反饋 +- ✅ **快取策略** - 避免重複調用相同API + +### **2. 狀態管理** +- ✅ **單一資料來源** - 避免狀態重複 +- ✅ **明確的狀態型別** - 使用TypeScript介面 +- ✅ **適當的狀態粒度** - 不要過度細分或合併 + +### **3. 組件設計** +- ✅ **職責單一** - 每個組件專注一個功能 +- ✅ **Props介面** - 明確定義組件輸入 +- ✅ **可重用性** - 組件應該可以在多處使用 + +--- + +## 📝 **未來改進方向** + +### **1. 統一API策略** +- 合併`analyze-sentence`和`query-word`的功能 +- 建立統一的詞彙分析端點 +- 減少API調用複雜度 + +### **2. 效能優化** +- 實現詞彙分析結果快取 +- 減少不必要的API調用 +- 優化Portal渲染效能 + +### **3. 用戶體驗提升** +- 添加載入動畫 +- 優化錯誤處理和用戶提示 +- 增強響應式設計 + +--- + +**文件版本**: v1.0 +**建立日期**: 2025-09-21 +**維護團隊**: DramaLing開發團隊 + +--- + +## 📞 **技術支援** + +如需修改或擴展AI生成功能,請參考本規格文件的相關章節,並遵循最佳實踐建議進行開發。 \ No newline at end of file diff --git a/frontend/app/generate/page.tsx b/frontend/app/generate/page.tsx index 5c5200d..002e37a 100644 --- a/frontend/app/generate/page.tsx +++ b/frontend/app/generate/page.tsx @@ -55,7 +55,10 @@ function GenerateContent() { console.log('✅ API分析完成:', result) if (result.success) { - setSentenceAnalysis(result.data.WordAnalysis || {}) + // 嘗試不同的屬性名稱格式 + const wordAnalysisData = result.data.WordAnalysis || result.data.wordAnalysis || {}; + console.log('🔍 設置sentenceAnalysis:', wordAnalysisData); + setSentenceAnalysis(wordAnalysisData) setSentenceMeaning(result.data.SentenceMeaning?.Translation || '') setGrammarCorrection(result.data.GrammarCorrection || null) setFinalText(result.data.FinalAnalysisText || textInput) @@ -357,13 +360,6 @@ function GenerateContent() { onWordCostConfirm={async () => { return true }} - onNewWordAnalysis={(word, newAnalysis) => { - setSentenceAnalysis((prev: any) => ({ - ...prev, - [word]: newAnalysis - })) - console.log(`✅ 新增詞彙分析: ${word}`, newAnalysis) - }} onSaveWord={handleSaveWord} /> diff --git a/frontend/components/ClickableTextV2.tsx b/frontend/components/ClickableTextV2.tsx index fab0e45..fc0cd46 100644 --- a/frontend/components/ClickableTextV2.tsx +++ b/frontend/components/ClickableTextV2.tsx @@ -62,17 +62,23 @@ interface ClickableTextProps { }> onWordClick?: (word: string, analysis: WordAnalysis) => void onWordCostConfirm?: (word: string, cost: number) => Promise // 收費確認 - onNewWordAnalysis?: (word: string, analysis: WordAnalysis) => void // 新詞彙分析資料回調 onSaveWord?: (word: string, analysis: WordAnalysis) => Promise // 保存詞彙回調 remainingUsage?: number // 剩餘使用次數 } +// Popup 尺寸常數 +const POPUP_CONFIG = { + WIDTH: 320, // w-96 = 384px, 但實際使用320px + HEIGHT: 400, // 估計彈窗高度 + PADDING: 16, // 最小邊距 + MOBILE_BREAKPOINT: 640 // sm斷點 +} as const + export function ClickableTextV2({ text, analysis, onWordClick, onWordCostConfirm, - onNewWordAnalysis, onSaveWord, remainingUsage = 5 }: ClickableTextProps) { @@ -91,6 +97,14 @@ export function ClickableTextV2({ setMounted(true) }, []) + // Debug: 檢查接收到的analysis prop + useEffect(() => { + if (analysis) { + console.log('🔍 ClickableTextV2接收到analysis:', analysis); + console.log('🔍 analysis的keys:', Object.keys(analysis)); + } + }, [analysis]) + // 獲取CEFR等級顏色 - 與詞卡風格完全一致 const getCEFRColor = (level: string) => { switch (level) { @@ -130,19 +144,32 @@ export function ClickableTextV2({ return Array.isArray(synonyms) ? synonyms : []; } - // 特殊處理例句 - 如果AI沒有提供,生成預設例句 + // 特殊處理例句 - 優先使用AI或後端提供的例句 if (propName === 'example') { - return result || `This is an example sentence using ${wordData?.word || 'the word'}.`; + return result; // 不提供預設例句,只使用AI/後端資料 } // 特殊處理例句翻譯 if (propName === 'exampleTranslation') { - return result || `這是使用 ${wordData?.word || '該詞'} 的例句翻譯。`; + return result; // 不提供預設翻譯,只使用AI/後端資料 } return result; } + // 統一的詞彙查找函數 - 處理大小寫不匹配問題 + const findWordAnalysis = (word: string) => { + const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') + + // 嘗試多種格式匹配API回傳的keys + return analysis?.[cleanWord] || // 小寫 + analysis?.[word] || // 原始 + analysis?.[word.toLowerCase()] || // 確保小寫 + analysis?.[word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()] || // 首字母大寫 + analysis?.[word.toUpperCase()] || // 全大寫 + null + } + // 補充同義詞的本地函數 const getSynonymsForWord = (word: string): string[] => { const synonymsMap: Record = { @@ -175,38 +202,34 @@ export function ClickableTextV2({ const handleWordClick = async (word: string, event: React.MouseEvent) => { const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') - const wordAnalysis = analysis?.[cleanWord] + const wordAnalysis = findWordAnalysis(word) const rect = event.currentTarget.getBoundingClientRect() const viewportWidth = window.innerWidth - const viewportHeight = window.innerHeight - const popupWidth = 320 // popup寬度 w-80 = 320px - const popupHeight = 400 // 估計popup高度 // 智能水平定位,適應不同屏幕尺寸 let x = rect.left + rect.width / 2 - const actualPopupWidth = Math.min(popupWidth, viewportWidth - 32) // 實際popup寬度 + const actualPopupWidth = Math.min(POPUP_CONFIG.WIDTH, viewportWidth - 32) // 實際popup寬度 const halfPopupWidth = actualPopupWidth / 2 - const padding = 16 // 最小邊距 // 手機端特殊處理 - if (viewportWidth <= 640) { // sm斷點 + if (viewportWidth <= POPUP_CONFIG.MOBILE_BREAKPOINT) { // sm斷點 // 小屏幕時居中顯示,避免邊緣問題 x = viewportWidth / 2 } else { // 大屏幕時智能調整位置 - if (x + halfPopupWidth + padding > viewportWidth) { - x = viewportWidth - halfPopupWidth - padding + if (x + halfPopupWidth + POPUP_CONFIG.PADDING > viewportWidth) { + x = viewportWidth - halfPopupWidth - POPUP_CONFIG.PADDING } - if (x - halfPopupWidth < padding) { - x = halfPopupWidth + padding + if (x - halfPopupWidth < POPUP_CONFIG.PADDING) { + x = halfPopupWidth + POPUP_CONFIG.PADDING } } // 計算垂直位置 const spaceAbove = rect.top - const showBelow = spaceAbove < popupHeight + const showBelow = spaceAbove < POPUP_CONFIG.HEIGHT const position = { x: x, @@ -229,8 +252,22 @@ export function ClickableTextV2({ onWordClick?.(cleanWord, wordAnalysis) } } else { - // 場景B:無預存資料的詞彙 → 即時調用 AI 查詢 - await queryWordWithAI(cleanWord, position) + // 場景B:詞彙不在analysis中,直接顯示空彈窗或提示 + // 因為analyze-sentence應該已經包含所有詞彙,這種情況很少發生 + setPopupPosition(position) + setSelectedWord(cleanWord) + onWordClick?.(cleanWord, { + word: cleanWord, + translation: '查詢中...', + definition: '正在載入定義...', + partOfSpeech: 'unknown', + pronunciation: `/${cleanWord}/`, + synonyms: [], + isPhrase: false, + isHighValue: false, + learningPriority: 'low', + difficultyLevel: 'A1' + }) } } @@ -240,37 +277,30 @@ export function ClickableTextV2({ const confirmed = await onWordCostConfirm?.(showCostConfirm.word, showCostConfirm.cost) if (confirmed) { - // 調用真實的單字查詢API - try { - const response = await fetch('http://localhost:5000/api/ai/query-word', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - word: showCostConfirm.word, - sentence: text, - analysisId: null // 可以傳入分析ID - }) - }) + // 由於analyze-sentence已提供完整資料,不再需要額外API調用 + // 使用智能查找尋找詞彙資料 + const wordAnalysis = findWordAnalysis(showCostConfirm.word) - if (response.ok) { - const result = await response.json() - if (result.success) { - setPopupPosition({...showCostConfirm.position, showBelow: false}) - setSelectedWord(showCostConfirm.word) - onWordClick?.(showCostConfirm.word, result.data.analysis) - } - } - } catch (error) { - console.error('Query word API error:', error) - // 回退到現有資料 - const wordAnalysis = analysis?.[showCostConfirm.word] - if (wordAnalysis) { - setPopupPosition({...showCostConfirm.position, showBelow: false}) - setSelectedWord(showCostConfirm.word) - onWordClick?.(showCostConfirm.word, wordAnalysis) - } + if (wordAnalysis) { + setPopupPosition({...showCostConfirm.position, showBelow: false}) + setSelectedWord(showCostConfirm.word) + onWordClick?.(showCostConfirm.word, wordAnalysis) + } else { + // 極少數情況:詞彙真的不在analysis中 + setPopupPosition({...showCostConfirm.position, showBelow: false}) + setSelectedWord(showCostConfirm.word) + onWordClick?.(showCostConfirm.word, { + word: showCostConfirm.word, + translation: '此詞彙未在分析中', + definition: '請重新分析句子以獲取完整資訊', + partOfSpeech: 'unknown', + pronunciation: `/${showCostConfirm.word}/`, + synonyms: [], + isPhrase: false, + isHighValue: false, + learningPriority: 'low', + difficultyLevel: 'A1' + }) } } @@ -296,49 +326,9 @@ export function ClickableTextV2({ } } - const queryWordWithAI = async (word: string, position: { x: number, y: number, showBelow: boolean }) => { - 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] + const wordAnalysis = findWordAnalysis(word) const baseClass = "cursor-pointer transition-all duration-200 rounded relative mx-0.5 px-1 py-0.5" @@ -510,9 +500,8 @@ export function ClickableTextV2({ } const className = getWordClass(word) - const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') - const wordAnalysis = analysis?.[cleanWord] - const isHighValue = wordAnalysis?.isHighValue + const wordAnalysis = findWordAnalysis(word) + const isHighValue = getWordProperty(wordAnalysis, 'isHighValue') return (