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:
parent
36659d3bed
commit
14b55d6f7a
|
|
@ -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生成功能,請參考本規格文件的相關章節,並遵循最佳實踐建議進行開發。
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue