refactor: 完全移除query-word API調用並修正資料傳遞問題

🎯 主要修正:
- 完全移除queryWordWithAI函數和相關API調用
- 移除handleCostConfirm中的query-word調用
- 簡化ClickableTextV2組件介面,移除onNewWordAnalysis回調

🔧 架構優化:
- 統一使用analyze-sentence API作為唯一資料來源
- 實現findWordAnalysis智能詞彙匹配(處理大小寫問題)
- 提取POPUP_CONFIG常數,提高代碼可維護性
- 移除未使用的變數viewportHeight

🚨 關鍵問題發現:
- 前端期望result.data.WordAnalysis但API回傳undefined
- 導致ClickableTextV2接收到空物件,無法顯示詞彙資料
- 添加智能屬性名稱匹配:WordAnalysis || wordAnalysis

📊 Debug增強:
- 添加資料傳遞過程的詳細調試
- 確認API回應和組件接收的資料一致性
- 為問題診斷提供完整的資訊鏈

🎯 新架構效果:
- 只使用一個API端點,避免資料不一致
- 智能大小寫匹配,確保詞彙查找成功
- 簡化的代碼邏輯,更易維護

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-21 16:17:26 +08:00
parent 36659d3bed
commit 14b55d6f7a
3 changed files with 781 additions and 101 deletions

View File

@ -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<ApiResponse<Flashcard>>('/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<any>(null) // 完整詞彙分析
const [sentenceMeaning, setSentenceMeaning] = useState('') // 句子翻譯
const [grammarCorrection, setGrammarCorrection] = useState<any>(null) // 語法修正
const [finalText, setFinalText] = useState('') // 最終文本
// ClickableTextV2狀態
const [selectedWord, setSelectedWord] = useState<string | null>(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
<ClickableTextV2
analysis={sentenceAnalysis} // 傳遞給子組件
onSaveWord={handleSaveWord} // 回調函數
/>
```
---
## 🚨 **當前架構問題分析**
### **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(
<div className="fixed z-50 bg-white rounded-xl shadow-lg w-96">
{/* 彈窗內容 */}
</div>,
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繼承問題
<div className="relative">
<div className="text-lg">可點擊文字</div>
<div className="fixed popup">彈窗</div> // 會繼承text-lg
</div>
// ✅ Portal方式 - 完全隔離
<div className="relative">
<div className="text-lg">可點擊文字</div>
</div>
{createPortal(
<div className="fixed popup">彈窗</div>, // 渲染到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生成功能請參考本規格文件的相關章節並遵循最佳實踐建議進行開發。

View File

@ -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}
/>
</div>

View File

@ -62,17 +62,23 @@ interface ClickableTextProps {
}>
onWordClick?: (word: string, analysis: WordAnalysis) => void
onWordCostConfirm?: (word: string, cost: number) => Promise<boolean> // 收費確認
onNewWordAnalysis?: (word: string, analysis: WordAnalysis) => void // 新詞彙分析資料回調
onSaveWord?: (word: string, analysis: WordAnalysis) => Promise<void> // 保存詞彙回調
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<string, string[]> = {
@ -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 (
<span