feat: 實現互動式單字查詢系統,包含語法修正和高價值標記功能
## 主要功能 - 智能語法錯誤檢測和修正建議(9種錯誤類型) - 高價值詞彙標記系統(片語/俚語/中高級單字) - 三色視覺區分:🟡高價值片語 🟢高價值單字 🔵普通單字 - 成本優化:高價值詞彙免費查詢,低價值詞彙按需收費 - 字數限制提升:50字→300字 ## 技術實現 - 新增 ClickableTextV2 和 GrammarCorrectionPanel 組件 - 更新 Generate 頁面整合完整功能 - 完整的設計規格文檔(API、UI線框圖、功能規格) - 多個演示頁面展示功能效果 ## 成本效益 - 一次API調用,多次免費查詢 - 預估API成本降低80-95% - 智能收費機制精準控制成本 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
336f235684
commit
76e95dbef2
|
|
@ -63,22 +63,48 @@
|
||||||
- 難度選擇:A1, A2, B1, B2, C1, C2
|
- 難度選擇:A1, A2, B1, B2, C1, C2
|
||||||
|
|
||||||
#### 1.2.2 AI 生成規格
|
#### 1.2.2 AI 生成規格
|
||||||
- **生成方式**
|
- **原始例句輸入**
|
||||||
1. 原始例句類型
|
- 輸入方式
|
||||||
- 影劇截圖(訂閱功能, phase2)
|
1. 影劇截圖(訂閱功能, phase2)
|
||||||
- 手動輸入
|
2. 手動輸入
|
||||||
2. 詞彙萃取:把每個單字拿去查詢字典API,並標記CEFR
|
- 輸入資料
|
||||||
3. 智能萃取(訂閱功能):將原始例句拿去問AI有無常用片語或俚語,並直接生成相關詞彙內容
|
- 可接受多句子
|
||||||
|
- 字數限制規則:
|
||||||
|
- 若為手動輸入,則限定300字以內,在前端畫面做阻擋
|
||||||
|
- 若為影劇截圖,則無300字限制
|
||||||
|
|
||||||
- **生成數量**
|
- **互動式單字查詢(低成本設計)**
|
||||||
- 預設:10個詞卡
|
1. 預分析機制
|
||||||
- 範圍:5-20個(用戶可調)
|
- 用戶輸入句子後,AI 一次性分析整句內容
|
||||||
- 免費用戶:
|
- 獲取原始例句意思
|
||||||
- 無法自行生成例句圖,但若系統中匹配到現成例句圖,可直接使用
|
- 識別具備高學習價值的片語/俚語/單字,並標記為高價值,並於當次直接生成具標記的項目內容詳情(參考「生成內容詳情」)
|
||||||
- 每日學習數量無限制
|
- 分析結果存儲於快取中(避免重複 API 調用)
|
||||||
- 訂閱用戶:每天最多生成50張例句圖
|
- 當次操作扣除使用次數一次
|
||||||
|
|
||||||
|
2. 點擊查詢體驗
|
||||||
|
- 句子顯示為可點擊的單字
|
||||||
|
- 點擊對象
|
||||||
|
- 若為高價值標記,則即時顯示意思(無延遲,讀取預分析資料),不扣除使用次數
|
||||||
|
- 若非高價值標記,則拿當前點擊單字及當前句子,給AI分析並生成內容詳情,扣除使用次數一次
|
||||||
|
- 片語/俚語特殊高亮顯示
|
||||||
|
- 智能提醒:當單字屬於片語/俚語時,優先顯示片語意思並提醒
|
||||||
|
- 若出現多筆片語/俚語需標記時,請使用不同顏色區分
|
||||||
|
|
||||||
|
3. 成本優化策略
|
||||||
|
- **核心原則**:一句一次 API 調用,多次查詢零成本
|
||||||
|
- 相同句子分析結果快取(24小時)
|
||||||
|
- 常用單字基礎資訊本地快取
|
||||||
|
- 預估 API 成本降低 80-95%
|
||||||
|
|
||||||
|
4. 收費策略(phase 2):
|
||||||
|
- 免費用戶:5次/3小時
|
||||||
|
- 付費用戶:無限制
|
||||||
|
|
||||||
- **生成內容詳情**
|
- **生成內容詳情**
|
||||||
|
- **原始例句**
|
||||||
|
- 整體意思:不論原始例句是多句、一句、片段,就是將原始例句整體意思描述出來
|
||||||
|
- 修正語法錯誤:若原始例句有語法錯誤,則進行修正,並說明修正原因,且後續學習內容皆以正確的版本進行
|
||||||
|
|
||||||
- **單字/片語**
|
- **單字/片語**
|
||||||
- 原形展示
|
- 原形展示
|
||||||
- 詞性標註(n./v./adj./adv./phrase/slang)
|
- 詞性標註(n./v./adj./adv./phrase/slang)
|
||||||
|
|
@ -100,6 +126,9 @@
|
||||||
- 例句中文翻譯
|
- 例句中文翻譯
|
||||||
- 重點標示(highlight目標詞)
|
- 重點標示(highlight目標詞)
|
||||||
- 例句圖
|
- 例句圖
|
||||||
|
- 收費策略(phase 2):
|
||||||
|
- 免費用戶:無法自行生成例句圖,但若系統中匹配到現成例句圖,可直接使用
|
||||||
|
- 訂閱用戶:每天最多生成50張例句圖
|
||||||
- 例句發音
|
- 例句發音
|
||||||
|
|
||||||
- **生成後處理**
|
- **生成後處理**
|
||||||
|
|
@ -438,6 +467,11 @@
|
||||||
**目標**:提升用戶體驗
|
**目標**:提升用戶體驗
|
||||||
- ✅ 標籤系統
|
- ✅ 標籤系統
|
||||||
- ✅ 搜尋篩選
|
- ✅ 搜尋篩選
|
||||||
|
- ⬜ **互動式單字查詢系統**
|
||||||
|
- 句子預分析 API 端點
|
||||||
|
- 可點擊文字組件
|
||||||
|
- 片語/俚語智能提醒
|
||||||
|
- 快取機制實現
|
||||||
- ✅ 進階統計圖表
|
- ✅ 進階統計圖表
|
||||||
- ✅ 成就系統
|
- ✅ 成就系統
|
||||||
- ✅ 學習提醒
|
- ✅ 學習提醒
|
||||||
|
|
@ -481,7 +515,9 @@
|
||||||
- 內容版權問題
|
- 內容版權問題
|
||||||
|
|
||||||
### 7.3 緩解措施
|
### 7.3 緩解措施
|
||||||
- 實施 API 快取機制
|
- 實施 API 快取機制(重點:單字查詢預分析快取)
|
||||||
- 準備備用 AI 服務
|
- 準備備用 AI 服務
|
||||||
- 建立用戶反饋循環
|
- 建立用戶反饋循環
|
||||||
- 確保內容合規性
|
- 確保內容合規性
|
||||||
|
- 監控 AI API 使用量並設定預算警告
|
||||||
|
- 實現降級機制:API 配額用盡時使用離線字典
|
||||||
|
|
@ -0,0 +1,801 @@
|
||||||
|
# 互動式單字查詢 API 規格書
|
||||||
|
|
||||||
|
## 1. API 概覽
|
||||||
|
|
||||||
|
### 1.1 基本資訊
|
||||||
|
- **Base URL**: `https://api.dramaling.com/v1`
|
||||||
|
- **認證方式**: Bearer Token (JWT)
|
||||||
|
- **請求格式**: JSON
|
||||||
|
- **響應格式**: JSON
|
||||||
|
- **字元編碼**: UTF-8
|
||||||
|
|
||||||
|
### 1.2 通用響應格式
|
||||||
|
|
||||||
|
#### 成功響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": { /* 實際資料 */ },
|
||||||
|
"message": "操作成功描述",
|
||||||
|
"timestamp": "2025-09-17T09:48:00Z",
|
||||||
|
"requestId": "uuid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 錯誤響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "ERROR_CODE",
|
||||||
|
"message": "錯誤描述",
|
||||||
|
"details": "詳細錯誤資訊",
|
||||||
|
"field": "相關欄位" // 如適用
|
||||||
|
},
|
||||||
|
"timestamp": "2025-09-17T09:48:00Z",
|
||||||
|
"requestId": "uuid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 HTTP 狀態碼
|
||||||
|
- `200 OK`: 請求成功
|
||||||
|
- `201 Created`: 資源創建成功
|
||||||
|
- `400 Bad Request`: 請求參數錯誤
|
||||||
|
- `401 Unauthorized`: 未認證或認證失效
|
||||||
|
- `403 Forbidden`: 無權限訪問
|
||||||
|
- `404 Not Found`: 資源不存在
|
||||||
|
- `429 Too Many Requests`: 超過使用限制
|
||||||
|
- `500 Internal Server Error`: 伺服器內部錯誤
|
||||||
|
|
||||||
|
## 2. 認證 API
|
||||||
|
|
||||||
|
### 2.1 獲取 Access Token
|
||||||
|
```http
|
||||||
|
POST /auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "user_password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**響應**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"expiresIn": 900,
|
||||||
|
"user": {
|
||||||
|
"id": "uuid",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "User Name",
|
||||||
|
"subscription": "free" | "premium",
|
||||||
|
"createdAt": "2025-09-17T09:48:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 句子分析 API
|
||||||
|
|
||||||
|
### 3.1 分析句子
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
POST /ai/analyze-sentence
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"inputText": "He brought this thing up during our meeting and no one agreed.",
|
||||||
|
"options": {
|
||||||
|
"forceRefresh": false,
|
||||||
|
"includeExamples": true,
|
||||||
|
"includeAudio": true,
|
||||||
|
"difficultyLevel": "auto", // auto, A1, A2, B1, B2, C1, C2
|
||||||
|
"analysisMode": "full" // full: 完整分析並標記高價值詞彙
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"analysisId": "uuid",
|
||||||
|
"inputText": "He brought this thing up during our meeting and no one agreed.",
|
||||||
|
"textHash": "sha256_hash",
|
||||||
|
"grammarCorrection": {
|
||||||
|
"hasErrors": false,
|
||||||
|
"originalText": "He brought this thing up during our meeting and no one agreed.",
|
||||||
|
"correctedText": null,
|
||||||
|
"corrections": [],
|
||||||
|
"confidenceScore": 0.98
|
||||||
|
},
|
||||||
|
"sentenceMeaning": {
|
||||||
|
"translation": "他在我們的會議中提出了這件事,但沒有人同意。",
|
||||||
|
"explanation": "這句話表達了在會議中有人提出某個議題或想法,但得不到其他與會者的認同。",
|
||||||
|
"context": "正式會議場合",
|
||||||
|
"tone": "中性陳述"
|
||||||
|
},
|
||||||
|
"finalAnalysisText": "He brought this thing up during our meeting and no one agreed.", // 用於後續分析的文本(修正後)
|
||||||
|
"wordAnalysis": {
|
||||||
|
"he": {
|
||||||
|
"word": "he",
|
||||||
|
"lemma": "he",
|
||||||
|
"translation": "他",
|
||||||
|
"definition": "Used to refer to a male person or animal previously mentioned",
|
||||||
|
"partOfSpeech": "pronoun",
|
||||||
|
"pronunciation": {
|
||||||
|
"ipa": "/hiː/",
|
||||||
|
"us": "/hiː/",
|
||||||
|
"uk": "/hiː/"
|
||||||
|
},
|
||||||
|
"synonyms": ["him", "that man"],
|
||||||
|
"antonyms": [],
|
||||||
|
"isPhrase": false,
|
||||||
|
"phraseInfo": null,
|
||||||
|
"examples": {
|
||||||
|
"original": {
|
||||||
|
"sentence": "He brought this thing up during our meeting",
|
||||||
|
"translation": "他在我們的會議中提出了這件事",
|
||||||
|
"highlightWord": "He"
|
||||||
|
},
|
||||||
|
"generated": {
|
||||||
|
"sentence": "He went to the store yesterday",
|
||||||
|
"translation": "他昨天去了商店",
|
||||||
|
"imageUrl": "/images/examples/he_store.png",
|
||||||
|
"audioUrl": "/audio/examples/he_store.mp3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"difficultyLevel": "A1",
|
||||||
|
"frequency": "very_high",
|
||||||
|
"tags": ["basic", "pronoun"]
|
||||||
|
},
|
||||||
|
"brought": {
|
||||||
|
"word": "brought",
|
||||||
|
"lemma": "bring",
|
||||||
|
"translation": "帶來、提出",
|
||||||
|
"definition": "Past tense of bring; to take or carry something to a place",
|
||||||
|
"partOfSpeech": "verb",
|
||||||
|
"pronunciation": {
|
||||||
|
"ipa": "/brɔːt/",
|
||||||
|
"us": "/brɔːt/",
|
||||||
|
"uk": "/brɔːt/"
|
||||||
|
},
|
||||||
|
"synonyms": ["carried", "took", "delivered"],
|
||||||
|
"antonyms": ["removed", "took away"],
|
||||||
|
"isPhrase": true,
|
||||||
|
"isHighValue": true, // 高學習價值標記
|
||||||
|
"learningPriority": "high", // high, medium, low
|
||||||
|
"phraseInfo": {
|
||||||
|
"phrase": "bring up",
|
||||||
|
"meaning": "提出(話題)、養育",
|
||||||
|
"type": "phrasal_verb",
|
||||||
|
"warning": "在這個句子中,\"brought up\" 是一個片語動詞,意思是\"提出話題\",而不是單純的\"帶來\"",
|
||||||
|
"colorCode": "#F59E0B", // 片語顏色代碼
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"sentence": "She brought up an important point",
|
||||||
|
"translation": "她提出了一個重要觀點"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"original": {
|
||||||
|
"sentence": "He brought this thing up during our meeting",
|
||||||
|
"translation": "他在我們的會議中提出了這件事",
|
||||||
|
"highlightWord": "brought"
|
||||||
|
},
|
||||||
|
"generated": {
|
||||||
|
"sentence": "She brought up the topic in yesterday's discussion",
|
||||||
|
"translation": "她在昨天的討論中提出了這個話題",
|
||||||
|
"imageUrl": "/images/examples/bring_up_meeting.png",
|
||||||
|
"audioUrl": "/audio/examples/bring_up_meeting.mp3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"difficultyLevel": "B1",
|
||||||
|
"frequency": "high",
|
||||||
|
"tags": ["phrasal_verb", "meeting", "communication"]
|
||||||
|
}
|
||||||
|
// ... 其他單字
|
||||||
|
},
|
||||||
|
"highValueWords": ["brought", "up", "meeting"], // 高學習價值詞彙列表
|
||||||
|
"phrases": [
|
||||||
|
{
|
||||||
|
"phrase": "bring up",
|
||||||
|
"words": ["brought", "up"],
|
||||||
|
"meaning": "提出(話題)、養育",
|
||||||
|
"type": "phrasal_verb",
|
||||||
|
"definition": "To mention or introduce a topic in conversation; to raise a child",
|
||||||
|
"colorCode": "#F59E0B",
|
||||||
|
"isHighValue": true,
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"sentence": "Don't bring up that topic again",
|
||||||
|
"translation": "不要再提起那個話題了"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"analysisTime": 2.5,
|
||||||
|
"wordsCount": 12,
|
||||||
|
"phrasesCount": 1,
|
||||||
|
"highValueWordsCount": 3, // 高價值詞彙數量
|
||||||
|
"averageDifficulty": "B1",
|
||||||
|
"detectedLanguage": "en",
|
||||||
|
"confidence": 0.98
|
||||||
|
},
|
||||||
|
"usageStatistics": {
|
||||||
|
"remainingAnalyses": 4,
|
||||||
|
"totalUsedToday": 1,
|
||||||
|
"dailyLimit": 5,
|
||||||
|
"resetTime": "2025-09-17T12:48:00Z",
|
||||||
|
"subscription": "free"
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"cached": false,
|
||||||
|
"cacheKey": "sha256_hash",
|
||||||
|
"expiresAt": "2025-09-18T09:48:00Z",
|
||||||
|
"ttl": 86400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": "句子分析完成"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 單字點擊查詢
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
POST /ai/query-word
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"word": "thing",
|
||||||
|
"sentence": "He brought this thing up during our meeting and no one agreed.",
|
||||||
|
"analysisId": "uuid", // 來自預分析結果
|
||||||
|
"context": {
|
||||||
|
"position": 3, // 單字在句子中的位置
|
||||||
|
"surroundingWords": ["this", "thing", "up"] // 周圍單字上下文
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"word": "thing",
|
||||||
|
"isHighValue": false, // 非高價值詞彙
|
||||||
|
"wasPreAnalyzed": false, // 未在預分析中包含
|
||||||
|
"costIncurred": 1, // 扣除1次使用次數
|
||||||
|
"analysis": {
|
||||||
|
"word": "thing",
|
||||||
|
"translation": "事情、東西",
|
||||||
|
"definition": "An object, fact, or situation",
|
||||||
|
"partOfSpeech": "noun",
|
||||||
|
"pronunciation": {
|
||||||
|
"ipa": "/θɪŋ/",
|
||||||
|
"us": "/θɪŋ/",
|
||||||
|
"uk": "/θɪŋ/"
|
||||||
|
},
|
||||||
|
"synonyms": ["object", "matter", "item"],
|
||||||
|
"antonyms": [],
|
||||||
|
"isPhrase": false,
|
||||||
|
"isHighValue": false,
|
||||||
|
"learningPriority": "low",
|
||||||
|
"examples": {
|
||||||
|
"original": {
|
||||||
|
"sentence": "He brought this thing up during our meeting",
|
||||||
|
"translation": "他在會議中提出了這件事",
|
||||||
|
"highlightWord": "thing"
|
||||||
|
},
|
||||||
|
"generated": {
|
||||||
|
"sentence": "That's an important thing to remember",
|
||||||
|
"translation": "那是需要記住的重要事情",
|
||||||
|
"imageUrl": "/images/examples/important_thing.png",
|
||||||
|
"audioUrl": "/audio/examples/important_thing.mp3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"difficultyLevel": "A1"
|
||||||
|
},
|
||||||
|
"usageStatistics": {
|
||||||
|
"remainingAnalyses": 3,
|
||||||
|
"totalUsedToday": 2,
|
||||||
|
"costType": "word_query" // sentence_analysis, word_query
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": "單字查詢完成"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 獲取快取分析結果
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
GET /ai/analysis-cache/{textHash}
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
// 與分析句子相同的結構
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 語法錯誤修正示例
|
||||||
|
|
||||||
|
#### 有錯誤句子的分析響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"analysisId": "uuid",
|
||||||
|
"inputText": "I go to school yesterday and meet my friends.",
|
||||||
|
"grammarCorrection": {
|
||||||
|
"hasErrors": true,
|
||||||
|
"originalText": "I go to school yesterday and meet my friends.",
|
||||||
|
"correctedText": "I went to school yesterday and met my friends.",
|
||||||
|
"corrections": [
|
||||||
|
{
|
||||||
|
"position": {"start": 2, "end": 4}, // "go" 的位置
|
||||||
|
"errorType": "tense_mismatch",
|
||||||
|
"original": "go",
|
||||||
|
"corrected": "went",
|
||||||
|
"reason": "過去式時態修正:句子中有 'yesterday',應使用過去式",
|
||||||
|
"severity": "high"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": {"start": 29, "end": 33}, // "meet" 的位置
|
||||||
|
"errorType": "tense_mismatch",
|
||||||
|
"original": "meet",
|
||||||
|
"corrected": "met",
|
||||||
|
"reason": "過去式時態修正:與 'went' 保持時態一致",
|
||||||
|
"severity": "high"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"confidenceScore": 0.95
|
||||||
|
},
|
||||||
|
"sentenceMeaning": {
|
||||||
|
"translation": "我昨天去學校遇見了我的朋友們。",
|
||||||
|
"explanation": "這句話描述了過去發生的事情,表達了去學校並遇到朋友的經歷。"
|
||||||
|
},
|
||||||
|
"finalAnalysisText": "I went to school yesterday and met my friends.", // 後續分析使用修正版本
|
||||||
|
"wordAnalysis": {
|
||||||
|
// 基於修正後句子的分析結果
|
||||||
|
"went": {
|
||||||
|
"word": "went",
|
||||||
|
"translation": "去、前往",
|
||||||
|
"definition": "Past tense of go; to move or travel to a place",
|
||||||
|
"isHighValue": true,
|
||||||
|
"learningPriority": "high"
|
||||||
|
// ... 其他詳情
|
||||||
|
}
|
||||||
|
// ... 其他單字
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 清除快取
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
DELETE /ai/analysis-cache/{analysisId}
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "快取已清除"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 使用統計 API
|
||||||
|
|
||||||
|
### 4.1 獲取使用統計
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
GET /users/usage-stats
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"daily": {
|
||||||
|
"date": "2025-09-17",
|
||||||
|
"analysisCount": 1,
|
||||||
|
"wordClickCount": 15,
|
||||||
|
"uniqueWordsQueried": 8,
|
||||||
|
"totalTimeSpent": 320,
|
||||||
|
"remainingAnalyses": 4
|
||||||
|
},
|
||||||
|
"weekly": {
|
||||||
|
"startDate": "2025-09-11",
|
||||||
|
"endDate": "2025-09-17",
|
||||||
|
"analysisCount": 12,
|
||||||
|
"wordClickCount": 180,
|
||||||
|
"uniqueWordsQueried": 95,
|
||||||
|
"totalTimeSpent": 2400
|
||||||
|
},
|
||||||
|
"monthly": {
|
||||||
|
"month": "2025-09",
|
||||||
|
"analysisCount": 45,
|
||||||
|
"wordClickCount": 720,
|
||||||
|
"uniqueWordsQueried": 350,
|
||||||
|
"totalTimeSpent": 9600
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"subscription": "free",
|
||||||
|
"dailyAnalysisLimit": 5,
|
||||||
|
"dailyImageGenerationLimit": 0,
|
||||||
|
"resetTime": "2025-09-17T12:48:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 記錄單字點擊
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
POST /users/word-interactions
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"analysisId": "uuid",
|
||||||
|
"word": "brought",
|
||||||
|
"action": "click", // click, audio_play, image_view
|
||||||
|
"timestamp": "2025-09-17T09:48:00Z",
|
||||||
|
"context": {
|
||||||
|
"sentencePosition": 2,
|
||||||
|
"isPhrase": true,
|
||||||
|
"difficultyLevel": "B1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "互動記錄已保存"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 詞卡生成 API
|
||||||
|
|
||||||
|
### 5.1 從分析結果生成詞卡
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
POST /flashcards/generate-from-analysis
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"analysisId": "uuid",
|
||||||
|
"selectedWords": ["brought", "meeting", "agreed"],
|
||||||
|
"cardSetId": "uuid", // 可選,不提供則創建新卡組
|
||||||
|
"options": {
|
||||||
|
"includePhrasesOnly": false,
|
||||||
|
"difficultyFilter": ["B1", "B2"],
|
||||||
|
"generateImages": true // 付費用戶才能使用
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"cardSetId": "uuid",
|
||||||
|
"cardsGenerated": 3,
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"word": "brought",
|
||||||
|
"translation": "帶來、提出",
|
||||||
|
"definition": "Past tense of bring; to take or carry something to a place",
|
||||||
|
"partOfSpeech": "verb",
|
||||||
|
"pronunciation": "/brɔːt/",
|
||||||
|
"example": "He brought this thing up during our meeting",
|
||||||
|
"exampleTranslation": "他在我們的會議中提出了這件事",
|
||||||
|
"difficultyLevel": "B1",
|
||||||
|
"isPhrase": true,
|
||||||
|
"phraseInfo": {
|
||||||
|
"phrase": "bring up",
|
||||||
|
"meaning": "提出(話題)、養育"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-09-17T09:48:00Z"
|
||||||
|
}
|
||||||
|
// ... 其他詞卡
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "詞卡生成成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 音頻服務 API
|
||||||
|
|
||||||
|
### 6.1 生成單字發音
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
POST /audio/generate-pronunciation
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"text": "brought",
|
||||||
|
"accent": "us", // us, uk
|
||||||
|
"speed": "normal", // slow, normal, fast
|
||||||
|
"format": "mp3" // mp3, wav, ogg
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"audioUrl": "/audio/pronunciation/brought_us_normal.mp3",
|
||||||
|
"duration": 0.8,
|
||||||
|
"fileSize": 12480,
|
||||||
|
"expiresAt": "2025-09-24T09:48:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 生成例句發音
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
POST /audio/generate-sentence
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"text": "He brought this thing up during our meeting",
|
||||||
|
"accent": "us",
|
||||||
|
"speed": "normal",
|
||||||
|
"highlightWord": "brought", // 可選,高亮某個單字
|
||||||
|
"format": "mp3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"audioUrl": "/audio/sentences/sentence_hash_us_normal.mp3",
|
||||||
|
"duration": 3.2,
|
||||||
|
"fileSize": 51200,
|
||||||
|
"wordTimestamps": [
|
||||||
|
{"word": "He", "start": 0.0, "end": 0.2},
|
||||||
|
{"word": "brought", "start": 0.2, "end": 0.6},
|
||||||
|
// ... 其他單字時間戳
|
||||||
|
],
|
||||||
|
"expiresAt": "2025-09-24T09:48:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 圖片服務 API
|
||||||
|
|
||||||
|
### 7.1 生成例句圖片 (付費功能)
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
POST /images/generate-example
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"word": "brought",
|
||||||
|
"phrase": "bring up",
|
||||||
|
"example": "She brought up an important point in the meeting",
|
||||||
|
"context": "business meeting",
|
||||||
|
"style": "illustration", // illustration, photo, cartoon
|
||||||
|
"prompt": "A professional woman raising her hand in a business meeting"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"imageId": "uuid",
|
||||||
|
"imageUrl": "/images/examples/brought_up_meeting_123.png",
|
||||||
|
"thumbnailUrl": "/images/examples/thumbs/brought_up_meeting_123.png",
|
||||||
|
"width": 1024,
|
||||||
|
"height": 768,
|
||||||
|
"fileSize": 245760,
|
||||||
|
"style": "illustration",
|
||||||
|
"prompt": "A professional woman raising her hand in a business meeting",
|
||||||
|
"generatedAt": "2025-09-17T09:48:00Z",
|
||||||
|
"expiresAt": "2025-12-17T09:48:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 獲取現有例句圖片
|
||||||
|
|
||||||
|
#### 請求
|
||||||
|
```http
|
||||||
|
GET /images/examples
|
||||||
|
Authorization: Bearer {accessToken}
|
||||||
|
Query Parameters:
|
||||||
|
- word: string (可選)
|
||||||
|
- phrase: string (可選)
|
||||||
|
- context: string (可選)
|
||||||
|
- limit: number (預設 20)
|
||||||
|
- offset: number (預設 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 響應
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"imageId": "uuid",
|
||||||
|
"word": "brought",
|
||||||
|
"phrase": "bring up",
|
||||||
|
"imageUrl": "/images/examples/brought_up_meeting_123.png",
|
||||||
|
"thumbnailUrl": "/images/examples/thumbs/brought_up_meeting_123.png",
|
||||||
|
"context": "business meeting",
|
||||||
|
"usage": "free", // free, premium
|
||||||
|
"downloads": 1250
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 150,
|
||||||
|
"hasMore": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 錯誤代碼定義
|
||||||
|
|
||||||
|
### 8.1 認證錯誤 (AUTH_*)
|
||||||
|
- `AUTH_INVALID_TOKEN`: 無效的 Token
|
||||||
|
- `AUTH_TOKEN_EXPIRED`: Token 已過期
|
||||||
|
- `AUTH_INSUFFICIENT_PERMISSIONS`: 權限不足
|
||||||
|
|
||||||
|
### 8.2 使用限制錯誤 (LIMIT_*)
|
||||||
|
- `LIMIT_DAILY_ANALYSIS_EXCEEDED`: 超過每日分析次數限制
|
||||||
|
- `LIMIT_TEXT_TOO_LONG`: 文字長度超過限制
|
||||||
|
- `LIMIT_RATE_EXCEEDED`: 請求頻率過高
|
||||||
|
|
||||||
|
### 8.3 AI 服務錯誤 (AI_*)
|
||||||
|
- `AI_SERVICE_UNAVAILABLE`: AI 服務暫時不可用
|
||||||
|
- `AI_ANALYSIS_FAILED`: 句子分析失敗
|
||||||
|
- `AI_INVALID_LANGUAGE`: 不支援的語言
|
||||||
|
|
||||||
|
### 8.4 資源錯誤 (RESOURCE_*)
|
||||||
|
- `RESOURCE_NOT_FOUND`: 資源不存在
|
||||||
|
- `RESOURCE_CACHE_MISS`: 快取未命中
|
||||||
|
- `RESOURCE_GENERATION_FAILED`: 資源生成失敗
|
||||||
|
|
||||||
|
### 8.5 語法修正錯誤 (GRAMMAR_*)
|
||||||
|
- `GRAMMAR_CORRECTION_FAILED`: 語法修正失敗
|
||||||
|
- `GRAMMAR_TOO_MANY_ERRORS`: 錯誤過多無法修正
|
||||||
|
- `GRAMMAR_AMBIGUOUS_MEANING`: 語意模糊無法確定修正方向
|
||||||
|
|
||||||
|
### 8.6 語法錯誤類型定義
|
||||||
|
- `tense_mismatch`: 時態錯誤
|
||||||
|
- `subject_verb_disagreement`: 主謂不一致
|
||||||
|
- `wrong_preposition`: 介詞錯誤
|
||||||
|
- `word_order`: 詞序錯誤
|
||||||
|
- `spelling_error`: 拼寫錯誤
|
||||||
|
- `article_error`: 冠詞錯誤
|
||||||
|
- `plural_singular`: 單複數錯誤
|
||||||
|
- `missing_word`: 缺少詞彙
|
||||||
|
- `redundant_word`: 冗餘詞彙
|
||||||
|
|
||||||
|
## 9. SDK 和範例
|
||||||
|
|
||||||
|
### 9.1 JavaScript SDK 範例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { DramaLingClient } from '@dramaling/sdk';
|
||||||
|
|
||||||
|
const client = new DramaLingClient({
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
baseURL: 'https://api.dramaling.com/v1'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分析句子
|
||||||
|
async function analyzeSentence(text) {
|
||||||
|
try {
|
||||||
|
const result = await client.analyzeSentence({
|
||||||
|
inputText: text,
|
||||||
|
options: {
|
||||||
|
includeExamples: true,
|
||||||
|
includeAudio: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Analysis failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用範例
|
||||||
|
const analysis = await analyzeSentence(
|
||||||
|
"He brought this thing up during our meeting and no one agreed."
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('句子意思:', analysis.sentenceMeaning.translation);
|
||||||
|
console.log('單字分析:', analysis.wordAnalysis);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 cURL 範例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 分析句子
|
||||||
|
curl -X POST https://api.dramaling.com/v1/ai/analyze-sentence \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"inputText": "He brought this thing up during our meeting and no one agreed.",
|
||||||
|
"options": {
|
||||||
|
"includeExamples": true,
|
||||||
|
"includeAudio": true
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
# 獲取使用統計
|
||||||
|
curl -X GET https://api.dramaling.com/v1/users/usage-stats \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. API 版本管理
|
||||||
|
|
||||||
|
### 10.1 版本策略
|
||||||
|
- **當前版本**: v1
|
||||||
|
- **版本格式**: `/v{major}`
|
||||||
|
- **向後相容**: 保持至少 6 個月
|
||||||
|
- **棄用通知**: 提前 3 個月通知
|
||||||
|
|
||||||
|
### 10.2 版本變更日誌
|
||||||
|
|
||||||
|
#### v1.0.0 (2025-09-17)
|
||||||
|
- 初始版本發布
|
||||||
|
- 句子分析 API
|
||||||
|
- 使用統計 API
|
||||||
|
- 音頻服務 API
|
||||||
|
- 基礎圖片服務 API
|
||||||
|
|
||||||
|
#### v1.1.0 (計劃中)
|
||||||
|
- 批量分析 API
|
||||||
|
- 進階圖片生成選項
|
||||||
|
- 詞彙學習追蹤 API
|
||||||
|
- WebSocket 即時通知
|
||||||
|
|
||||||
|
這份 API 規格書提供了完整的介面定義,包括請求/響應格式、錯誤處理、使用範例和 SDK 說明,為前端開發和第三方整合提供了詳細的參考文檔。
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
# Figma 設計稿連結管理
|
|
||||||
|
|
||||||
## 📋 概述
|
|
||||||
|
|
||||||
本文件集中管理所有 Figma 設計稿連結,確保團隊成員能快速找到最新的設計資源。
|
|
||||||
|
|
||||||
> **注意**: Drama Ling 主要使用 HTML/CSS 元件庫作為設計系統,Figma 用於高階概念設計和協作討論。
|
|
||||||
|
|
||||||
## 🎨 設計檔案結構
|
|
||||||
|
|
||||||
### 主設計系統
|
|
||||||
| 檔案名稱 | 連結 | 最後更新 | 負責人 | 狀態 |
|
|
||||||
|---------|------|----------|--------|------|
|
|
||||||
| Drama Ling Design System | [Figma Link](#) | 2025-09-15 | 設計團隊 | 🟢 最新 |
|
|
||||||
| Component Library | [Figma Link](#) | 2025-09-15 | 設計團隊 | 🟢 最新 |
|
|
||||||
| Design Tokens | [Figma Link](#) | 2025-09-15 | 設計團隊 | 🟢 最新 |
|
|
||||||
|
|
||||||
### Web 端設計
|
|
||||||
| 頁面名稱 | 連結 | 狀態 | HTML原型 | 備註 |
|
|
||||||
|---------|------|------|----------|------|
|
|
||||||
| 登入/註冊 | [Figma](#) | ✅ 完成 | [HTML](../component-library/pages/login-page.html) | |
|
|
||||||
| 儀表板 | [Figma](#) | ✅ 完成 | [HTML](../component-library/pages/dashboard.html) | |
|
|
||||||
| 學習頁面 | [Figma](#) | ✅ 完成 | [HTML](../component-library/pages/learning-page.html) | |
|
|
||||||
| 詞彙學習 | [Figma](#) | 🔄 進行中 | - | 預計9/20完成 |
|
|
||||||
| 口說練習 | [Figma](#) | 📋 規劃中 | - | |
|
|
||||||
| 情境對話 | [Figma](#) | 📋 規劃中 | - | |
|
|
||||||
| 成就系統 | [Figma](#) | 📋 規劃中 | - | |
|
|
||||||
| 商店頁面 | [Figma](#) | 📋 規劃中 | - | |
|
|
||||||
|
|
||||||
### 移動端設計
|
|
||||||
| 頁面名稱 | 連結 | 狀態 | 備註 |
|
|
||||||
|---------|------|------|------|
|
|
||||||
| iOS 設計稿 | [Figma](#) | 📋 規劃中 | |
|
|
||||||
| Android 設計稿 | [Figma](#) | 📋 規劃中 | |
|
|
||||||
| 響應式斷點 | [Figma](#) | ✅ 完成 | |
|
|
||||||
|
|
||||||
### 原型和流程
|
|
||||||
| 名稱 | 連結 | 類型 | 備註 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| 用戶流程圖 | [Figma](#) | Flow | |
|
|
||||||
| 互動原型 | [Figma](#) | Prototype | |
|
|
||||||
| 線框圖 | [Figma](#) | Wireframe | |
|
|
||||||
|
|
||||||
## 🔗 快速連結
|
|
||||||
|
|
||||||
### 常用頁面
|
|
||||||
- 🎯 [最新設計系統](#)
|
|
||||||
- 📚 [元件庫](#)
|
|
||||||
- 🎨 [色彩系統](#)
|
|
||||||
- 📝 [字體規範](#)
|
|
||||||
- 📐 [間距系統](#)
|
|
||||||
|
|
||||||
### 開發者資源
|
|
||||||
- 💻 [HTML/CSS 元件庫](../component-library/index.html)
|
|
||||||
- 📖 [設計規範文檔](../design-system/README.md)
|
|
||||||
- 🛠️ [開發者交接文件](#)
|
|
||||||
|
|
||||||
## 📝 使用指南
|
|
||||||
|
|
||||||
### 查看設計稿
|
|
||||||
1. 點擊上方表格中的 Figma 連結
|
|
||||||
2. 使用公司帳號登入 Figma
|
|
||||||
3. 查看最新版本(檢查右上角版本標記)
|
|
||||||
|
|
||||||
### 導出資源
|
|
||||||
1. 在 Figma 中選擇需要的元素
|
|
||||||
2. 右側面板選擇 "Export"
|
|
||||||
3. 選擇格式:
|
|
||||||
- **圖標**: SVG
|
|
||||||
- **圖片**: PNG 2x
|
|
||||||
- **插圖**: SVG 或 PNG
|
|
||||||
|
|
||||||
### 提供反饋
|
|
||||||
1. 在 Figma 中使用評論功能
|
|
||||||
2. 標記 @設計師名稱
|
|
||||||
3. 描述具體問題或建議
|
|
||||||
|
|
||||||
## 🔄 版本管理
|
|
||||||
|
|
||||||
### 命名規範
|
|
||||||
```
|
|
||||||
[項目名稱]_[版本]_[日期]
|
|
||||||
範例: DramaLing_Dashboard_v2.1_20250915
|
|
||||||
```
|
|
||||||
|
|
||||||
### 版本標記
|
|
||||||
- 🟢 **最新**: 生產環境使用
|
|
||||||
- 🟡 **審核中**: 等待確認
|
|
||||||
- 🔴 **過時**: 僅供參考
|
|
||||||
|
|
||||||
## 👥 團隊協作
|
|
||||||
|
|
||||||
### 設計師職責
|
|
||||||
- 維護 Figma 設計稿
|
|
||||||
- 更新此文件連結
|
|
||||||
- 導出設計資源
|
|
||||||
- 與開發團隊溝通
|
|
||||||
|
|
||||||
### 開發者職責
|
|
||||||
- 實現 HTML/CSS 元件
|
|
||||||
- 提供技術反饋
|
|
||||||
- 更新實現狀態
|
|
||||||
- 維護元件庫
|
|
||||||
|
|
||||||
### 產品經理職責
|
|
||||||
- 審核設計方案
|
|
||||||
- 確認用戶流程
|
|
||||||
- 管理設計優先級
|
|
||||||
- 協調資源
|
|
||||||
|
|
||||||
## 📊 設計系統映射
|
|
||||||
|
|
||||||
| Figma 元件 | HTML/CSS 元件 | 狀態 | 備註 |
|
|
||||||
|-----------|--------------|------|------|
|
|
||||||
| Button | [btn-*](../component-library/index.html#buttons) | ✅ | |
|
|
||||||
| Input Field | [input-field](../component-library/index.html#inputs) | ✅ | |
|
|
||||||
| Card | [card-*](../component-library/index.html#cards) | ✅ | |
|
|
||||||
| Modal | [modal-*](../component-library/components/01-interactive/modals.html) | ✅ | |
|
|
||||||
| Navigation | [navbar, sidebar](../component-library/components/05-navigation/navigation.html) | ✅ | |
|
|
||||||
| Form Elements | [forms](../component-library/components/02-input/forms.html) | ✅ | |
|
|
||||||
| Data Display | [table, list](../component-library/components/03-display/data-display.html) | ✅ | |
|
|
||||||
| Gamification | [achievements, levels](../component-library/components/06-gamification/game-elements.html) | ✅ | |
|
|
||||||
|
|
||||||
## 🚀 工作流程
|
|
||||||
|
|
||||||
### 設計到開發流程
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
A[Figma 設計] --> B[設計審核]
|
|
||||||
B --> C[導出資源]
|
|
||||||
C --> D[HTML/CSS 實現]
|
|
||||||
D --> E[元件庫更新]
|
|
||||||
E --> F[開發使用]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 設計更新流程
|
|
||||||
1. **設計師** 更新 Figma 設計稿
|
|
||||||
2. **設計師** 更新此文件連結和狀態
|
|
||||||
3. **開發者** 查看變更並評估影響
|
|
||||||
4. **開發者** 更新 HTML/CSS 元件
|
|
||||||
5. **QA** 驗證實現符合設計
|
|
||||||
|
|
||||||
## 📅 更新記錄
|
|
||||||
|
|
||||||
### 2025-09-15
|
|
||||||
- 建立 Figma 連結管理系統
|
|
||||||
- 整合 HTML/CSS 元件庫映射
|
|
||||||
- 添加團隊協作指南
|
|
||||||
|
|
||||||
### 待更新項目
|
|
||||||
- [ ] 補充實際 Figma 連結
|
|
||||||
- [ ] 添加設計審核流程
|
|
||||||
- [ ] 建立自動同步機制
|
|
||||||
|
|
||||||
## 🔧 工具和插件
|
|
||||||
|
|
||||||
### 推薦 Figma 插件
|
|
||||||
- **Figma Tokens**: 管理設計代幣
|
|
||||||
- **Able**: 無障礙性檢查
|
|
||||||
- **Figma to HTML**: 代碼導出輔助
|
|
||||||
- **Content Reel**: 填充真實數據
|
|
||||||
|
|
||||||
### 開發工具
|
|
||||||
- [設計系統同步工具](../design-system/automation/design-sync.sh)
|
|
||||||
- [元件驗證工具](../design-system/automation/component-validator.js)
|
|
||||||
- [HTML/CSS 元件庫](../component-library/index.html)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**維護者**: Drama Ling 設計團隊
|
|
||||||
**最後更新**: 2025-09-15
|
|
||||||
**聯絡方式**: design@dramaling.com
|
|
||||||
|
|
@ -0,0 +1,935 @@
|
||||||
|
# 互動式單字查詢功能設計規格書
|
||||||
|
|
||||||
|
## 1. 功能概述
|
||||||
|
|
||||||
|
### 1.1 目標
|
||||||
|
實現低成本、高效率的互動式單字查詢系統,讓用戶能夠點擊句子中的任何單字即時查看詳細意思,同時智能識別片語和俚語。
|
||||||
|
|
||||||
|
### 1.2 核心優勢
|
||||||
|
- **成本效益**:一次 API 調用,多次查詢零成本
|
||||||
|
- **即時響應**:點擊查詢無延遲
|
||||||
|
- **智能識別**:片語/俚語優先顯示和警告
|
||||||
|
- **用戶友善**:視覺化高亮和直觀操作
|
||||||
|
|
||||||
|
## 2. 系統架構設計
|
||||||
|
|
||||||
|
### 2.1 整體流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用戶輸入句子 → AI 預分析 → 高價值標記 → 分析結果快取 → 互動式顯示 → 點擊查詢
|
||||||
|
↓ ↓ ↓ ↓ ↓ ↓
|
||||||
|
50字限制 一次API調用 識別學習價值 24小時快取 可點擊文字 智能計費
|
||||||
|
(扣除1次) 重要詞彙 存儲詳情 不同顏色 高價值免費
|
||||||
|
低價值收費
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 API 架構
|
||||||
|
|
||||||
|
#### 2.2.1 句子分析 API
|
||||||
|
```
|
||||||
|
POST /api/ai/analyze-sentence
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"inputText": "He brought this thing up during our meeting and no one agreed.",
|
||||||
|
"userId": "uuid", // 用於使用次數統計
|
||||||
|
"forceRefresh": false, // 是否強制重新分析
|
||||||
|
"analysisMode": "full" // full: 完整分析並標記高價值詞彙
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"analysisId": "uuid",
|
||||||
|
"sentenceMeaning": {
|
||||||
|
"translation": "他在我們的會議中提出了這件事,但沒有人同意。",
|
||||||
|
"explanation": "這句話表達了在會議中有人提出某個議題或想法,但得不到其他與會者的認同。"
|
||||||
|
},
|
||||||
|
"grammarCorrection": {
|
||||||
|
"hasErrors": false, // 是否有語法錯誤
|
||||||
|
"originalText": "He brought this thing up during our meeting and no one agreed.",
|
||||||
|
"correctedText": null, // 如無錯誤則為null
|
||||||
|
"corrections": [] // 錯誤修正列表
|
||||||
|
},
|
||||||
|
"wordAnalysis": {
|
||||||
|
"brought": {
|
||||||
|
"word": "brought",
|
||||||
|
"translation": "帶來、提出",
|
||||||
|
"definition": "Past tense of bring; to take or carry something to a place",
|
||||||
|
"partOfSpeech": "verb",
|
||||||
|
"pronunciation": {
|
||||||
|
"ipa": "/brɔːt/",
|
||||||
|
"us": "/brɔːt/",
|
||||||
|
"uk": "/brɔːt/"
|
||||||
|
},
|
||||||
|
"synonyms": ["carried", "took", "delivered"],
|
||||||
|
"antonyms": ["removed", "took away"],
|
||||||
|
"isPhrase": true,
|
||||||
|
"isHighValue": true, // 高學習價值標記
|
||||||
|
"learningPriority": "high", // high, medium, low
|
||||||
|
"phraseInfo": {
|
||||||
|
"phrase": "bring up",
|
||||||
|
"meaning": "提出(話題)、養育",
|
||||||
|
"warning": "在這個句子中,\"brought up\" 是片語,意思是\"提出話題\",而不是單純的\"帶來\"",
|
||||||
|
"colorCode": "#F59E0B" // 片語顏色代碼
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"original": "He brought this thing up during our meeting",
|
||||||
|
"originalTranslation": "他在會議中提出了這件事",
|
||||||
|
"generated": "She brought up an interesting point",
|
||||||
|
"generatedTranslation": "她提出了一個有趣的觀點",
|
||||||
|
"imageUrl": "/images/examples/bring_up.png",
|
||||||
|
"audioUrl": "/audio/examples/bring_up.mp3"
|
||||||
|
},
|
||||||
|
"difficultyLevel": "B1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finalAnalysisText": "He brought this thing up during our meeting and no one agreed.", // 最終用於學習的文本(修正後)
|
||||||
|
"highValueWords": ["brought", "up", "meeting"], // 高價值詞彙列表
|
||||||
|
"phrasesDetected": [
|
||||||
|
{
|
||||||
|
"phrase": "bring up",
|
||||||
|
"words": ["brought", "up"],
|
||||||
|
"colorCode": "#F59E0B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usageStatistics": {
|
||||||
|
"remainingAnalyses": 4,
|
||||||
|
"resetTime": "2025-09-17T12:48:00Z",
|
||||||
|
"costIncurred": 1 // 本次分析扣除次數
|
||||||
|
},
|
||||||
|
"cachedUntil": "2025-09-18T09:48:00Z"
|
||||||
|
},
|
||||||
|
"message": "Sentence analyzed successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.2 單字點擊查詢 API
|
||||||
|
```
|
||||||
|
POST /api/ai/query-word
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"word": "thing",
|
||||||
|
"sentence": "He brought this thing up during our meeting and no one agreed.",
|
||||||
|
"analysisId": "uuid", // 來自預分析結果
|
||||||
|
"userId": "uuid"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"word": "thing",
|
||||||
|
"isHighValue": false, // 非高價值詞彙
|
||||||
|
"wasPreAnalyzed": false, // 未在預分析中
|
||||||
|
"costIncurred": 1, // 扣除1次使用次數
|
||||||
|
"analysis": {
|
||||||
|
// 完整詞彙分析資料
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.3 快取管理 API
|
||||||
|
```
|
||||||
|
GET /api/ai/analysis-cache/{inputTextHash}
|
||||||
|
DELETE /api/ai/analysis-cache/{analysisId}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 資料庫設計
|
||||||
|
|
||||||
|
#### 2.3.1 句子分析快取表
|
||||||
|
```sql
|
||||||
|
CREATE TABLE SentenceAnalysisCache (
|
||||||
|
Id UNIQUEIDENTIFIER PRIMARY KEY,
|
||||||
|
InputTextHash NVARCHAR(64) NOT NULL, -- SHA256 hash
|
||||||
|
InputText NVARCHAR(1000) NOT NULL,
|
||||||
|
CorrectedText NVARCHAR(1000), -- 修正後的文本
|
||||||
|
HasGrammarErrors BIT DEFAULT 0, -- 是否有語法錯誤
|
||||||
|
GrammarCorrections NVARCHAR(MAX), -- JSON 格式,語法修正詳情
|
||||||
|
AnalysisResult NVARCHAR(MAX) NOT NULL, -- JSON 格式
|
||||||
|
HighValueWords NVARCHAR(MAX) NOT NULL, -- JSON 格式,高價值詞彙列表
|
||||||
|
PhrasesDetected NVARCHAR(MAX), -- JSON 格式,檢測到的片語
|
||||||
|
CreatedAt DATETIME2 NOT NULL,
|
||||||
|
ExpiresAt DATETIME2 NOT NULL,
|
||||||
|
AccessCount INT DEFAULT 0,
|
||||||
|
LastAccessedAt DATETIME2
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_SentenceAnalysisCache_Hash ON SentenceAnalysisCache(InputTextHash);
|
||||||
|
CREATE INDEX IX_SentenceAnalysisCache_Expires ON SentenceAnalysisCache(ExpiresAt);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.2 使用統計表
|
||||||
|
```sql
|
||||||
|
CREATE TABLE WordQueryUsageStats (
|
||||||
|
Id UNIQUEIDENTIFIER PRIMARY KEY,
|
||||||
|
UserId UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
Date DATE NOT NULL,
|
||||||
|
SentenceAnalysisCount INT DEFAULT 0, -- 句子分析次數
|
||||||
|
HighValueWordClicks INT DEFAULT 0, -- 高價值詞彙點擊(免費)
|
||||||
|
LowValueWordClicks INT DEFAULT 0, -- 低價值詞彙點擊(收費)
|
||||||
|
TotalApiCalls INT DEFAULT 0, -- 總 API 調用次數
|
||||||
|
UniqueWordsQueried INT DEFAULT 0,
|
||||||
|
CreatedAt DATETIME2 NOT NULL,
|
||||||
|
UpdatedAt DATETIME2 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IX_WordQueryUsageStats_UserDate ON WordQueryUsageStats(UserId, Date);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 前端組件設計
|
||||||
|
|
||||||
|
### 3.1 主要組件架構
|
||||||
|
|
||||||
|
```
|
||||||
|
WordQueryPage
|
||||||
|
├── SentenceInputForm // 句子輸入表單
|
||||||
|
├── AnalysisLoadingState // 分析中狀態
|
||||||
|
├── GrammarCorrectionPanel // 語法修正面板
|
||||||
|
│ ├── ErrorHighlight // 錯誤標記顯示
|
||||||
|
│ ├── CorrectionSuggestion // 修正建議
|
||||||
|
│ └── UserChoiceButtons // 用戶選擇按鈕
|
||||||
|
├── InteractiveTextDisplay // 互動式文字顯示
|
||||||
|
│ ├── ClickableWord // 可點擊單字
|
||||||
|
│ └── WordInfoPopup // 單字資訊彈窗
|
||||||
|
├── UsageStatistics // 使用統計顯示
|
||||||
|
└── ActionButtons // 操作按鈕組
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 組件詳細設計
|
||||||
|
|
||||||
|
#### 3.2.1 GrammarCorrectionPanel 組件
|
||||||
|
```typescript
|
||||||
|
interface GrammarCorrection {
|
||||||
|
hasErrors: boolean;
|
||||||
|
originalText: string;
|
||||||
|
correctedText: string | null;
|
||||||
|
corrections: Array<{
|
||||||
|
position: { start: number; end: number };
|
||||||
|
errorType: string;
|
||||||
|
original: string;
|
||||||
|
corrected: string;
|
||||||
|
reason: string;
|
||||||
|
severity: 'high' | 'medium' | 'low';
|
||||||
|
}>;
|
||||||
|
confidenceScore: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GrammarCorrectionPanelProps {
|
||||||
|
correction: GrammarCorrection;
|
||||||
|
onAcceptCorrection: () => void;
|
||||||
|
onRejectCorrection: () => void;
|
||||||
|
onManualEdit: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GrammarCorrectionPanel: React.FC<GrammarCorrectionPanelProps> = ({
|
||||||
|
correction,
|
||||||
|
onAcceptCorrection,
|
||||||
|
onRejectCorrection,
|
||||||
|
onManualEdit
|
||||||
|
}) => {
|
||||||
|
// 錯誤高亮顯示
|
||||||
|
// 修正建議卡片
|
||||||
|
// 修正原因說明
|
||||||
|
// 用戶選擇按鈕
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.2 SentenceInputForm 組件
|
||||||
|
```typescript
|
||||||
|
interface SentenceInputFormProps {
|
||||||
|
maxLength: number; // 300 for manual input
|
||||||
|
onSubmit: (text: string) => void;
|
||||||
|
onModeChange: (mode: 'manual' | 'screenshot') => void;
|
||||||
|
disabled: boolean;
|
||||||
|
remainingAnalyses: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SentenceInputForm: React.FC<SentenceInputFormProps> = ({
|
||||||
|
maxLength,
|
||||||
|
onSubmit,
|
||||||
|
onModeChange,
|
||||||
|
disabled,
|
||||||
|
remainingAnalyses
|
||||||
|
}) => {
|
||||||
|
// 即時字數統計
|
||||||
|
// 300字限制阻擋
|
||||||
|
// 模式切換UI
|
||||||
|
// 示例句子填入
|
||||||
|
// 分析按鈕狀態
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.3 InteractiveTextDisplay 組件
|
||||||
|
```typescript
|
||||||
|
interface WordAnalysis {
|
||||||
|
word: string;
|
||||||
|
translation: string;
|
||||||
|
definition: string;
|
||||||
|
partOfSpeech: string;
|
||||||
|
pronunciation: {
|
||||||
|
ipa: string;
|
||||||
|
us: string;
|
||||||
|
uk: string;
|
||||||
|
};
|
||||||
|
synonyms: string[];
|
||||||
|
antonyms: string[];
|
||||||
|
isPhrase: boolean;
|
||||||
|
isHighValue: boolean; // 高學習價值標記
|
||||||
|
learningPriority: 'high' | 'medium' | 'low'; // 學習優先級
|
||||||
|
phraseInfo?: {
|
||||||
|
phrase: string;
|
||||||
|
meaning: string;
|
||||||
|
warning: string;
|
||||||
|
colorCode: string; // 片語顏色代碼
|
||||||
|
};
|
||||||
|
examples: {
|
||||||
|
original: string;
|
||||||
|
originalTranslation: string;
|
||||||
|
generated: string;
|
||||||
|
generatedTranslation: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
audioUrl?: string;
|
||||||
|
};
|
||||||
|
difficultyLevel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InteractiveTextDisplayProps {
|
||||||
|
text: string;
|
||||||
|
analysis: Record<string, WordAnalysis>;
|
||||||
|
onWordClick: (word: string, analysis: WordAnalysis) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.4 WordInfoPopup 組件
|
||||||
|
```typescript
|
||||||
|
interface WordInfoPopupProps {
|
||||||
|
word: string;
|
||||||
|
analysis: WordAnalysis;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
onClose: () => void;
|
||||||
|
onPlayAudio: (audioUrl: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WordInfoPopup: React.FC<WordInfoPopupProps> = ({
|
||||||
|
word,
|
||||||
|
analysis,
|
||||||
|
position,
|
||||||
|
onClose,
|
||||||
|
onPlayAudio
|
||||||
|
}) => {
|
||||||
|
// 片語警告顯示
|
||||||
|
// 發音播放按鈕
|
||||||
|
// 例句圖片顯示
|
||||||
|
// 同義詞/反義詞標籤
|
||||||
|
// 難度等級標示
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 用戶介面設計
|
||||||
|
|
||||||
|
### 4.1 頁面佈局
|
||||||
|
|
||||||
|
#### 4.1.1 輸入階段
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ DramaLing │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ AI 智能生成詞卡 - 互動式單字查詢 │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 原始例句類型 ──────────────────────────────────┐ │
|
||||||
|
│ │ [✍️ 手動輸入] [📷 影劇截圖] (訂閱功能) │ │
|
||||||
|
│ └───────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 輸入英文文本 ──────────────────────────────────┐ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ 輸入英文句子(最多50字)... │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ 最多 50 字元 • 目前:0 字元 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 💡 示例句子: │ │
|
||||||
|
│ │ [點擊使用示例:He brought this thing up...] │ │
|
||||||
|
│ └───────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 🔍 分析句子(點擊查詢單字) │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 免費用戶:已使用 0/5 次 (3小時內) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.1.2 分析結果階段
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ ← 返回 句子分析結果 │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─ 原始句子 ──────────────────────────────────────┐ │
|
||||||
|
│ │ He brought this thing up during our meeting. │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 整句意思: │ │
|
||||||
|
│ │ 他在我們的會議中提出了這件事,但沒有人同意... │ │
|
||||||
|
│ └───────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 點擊查詢單字意思 ──────────────────────────────┐ │
|
||||||
|
│ │ 💡 使用說明:點擊下方句子中的任何單字,可以立即 │ │
|
||||||
|
│ │ 查看詳細意思。黃色背景表示該單字屬於片語或俚語。 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ╔═══════════════════════════════════════════╗ │ │
|
||||||
|
│ │ ║ He [brought] this [thing] [up] during ║ │ │
|
||||||
|
│ │ ║ our [meeting] and no one [agreed]. ║ │ │
|
||||||
|
│ │ ╚═══════════════════════════════════════════╝ │ │
|
||||||
|
│ │ // brought 和 up 有黃色背景 │ │
|
||||||
|
│ │ // 其他單字有藍色下劃線 │ │
|
||||||
|
│ └───────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ [🔄 分析新句子] [📖 生成詞卡] │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.1.3 單字彈窗設計
|
||||||
|
```
|
||||||
|
┌─ brought ─────────────────── × ┐
|
||||||
|
│ │
|
||||||
|
│ ⚠️ 注意:這個單字屬於片語 │
|
||||||
|
│ 片語:bring up │
|
||||||
|
│ 意思:提出(話題)、養育 │
|
||||||
|
│ │
|
||||||
|
│ verb | /brɔːt/ | 🔊 │
|
||||||
|
│ │
|
||||||
|
│ 翻譯:帶來、提出 │
|
||||||
|
│ │
|
||||||
|
│ 定義:Past tense of bring; │
|
||||||
|
│ to take or carry something │
|
||||||
|
│ │
|
||||||
|
│ 同義詞:[carried] [took] │
|
||||||
|
│ 反義詞:[removed] │
|
||||||
|
│ │
|
||||||
|
│ 例句: │
|
||||||
|
│ • 原始:He brought this... │
|
||||||
|
│ 翻譯:他提出了這件事... │
|
||||||
|
│ • 生成:She brought up... │
|
||||||
|
│ 翻譯:她提出了一個... │
|
||||||
|
│ [📷 查看例句圖] [🔊 播放] │
|
||||||
|
│ │
|
||||||
|
│ 難度:B1 │
|
||||||
|
└───────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 視覺設計規範
|
||||||
|
|
||||||
|
#### 4.2.1 顏色系統
|
||||||
|
```css
|
||||||
|
/* 主色彩 */
|
||||||
|
--primary-blue: #3B82F6;
|
||||||
|
--primary-blue-hover: #2563EB;
|
||||||
|
--primary-blue-light: #DBEAFE;
|
||||||
|
|
||||||
|
/* 單字價值顏色系統 */
|
||||||
|
--word-high-phrase: #F59E0B; /* 高價值片語 */
|
||||||
|
--word-high-single: #10B981; /* 高價值單字 */
|
||||||
|
--word-normal: #3B82F6; /* 普通單字 */
|
||||||
|
--word-hover: #1E40AF; /* 懸停狀態 */
|
||||||
|
|
||||||
|
/* 背景顏色 */
|
||||||
|
--word-high-phrase-bg: #FEF3C7; /* 高價值片語背景 */
|
||||||
|
--word-high-single-bg: #ECFDF5; /* 高價值單字背景 */
|
||||||
|
--word-normal-bg: transparent; /* 普通單字背景 */
|
||||||
|
--word-hover-bg: #DBEAFE; /* 懸停背景 */
|
||||||
|
|
||||||
|
/* 邊框顏色 */
|
||||||
|
--border-high-phrase: #F59E0B; /* 高價值片語邊框 */
|
||||||
|
--border-high-single: #10B981; /* 高價值單字邊框 */
|
||||||
|
--border-normal: #3B82F6; /* 普通單字邊框 */
|
||||||
|
|
||||||
|
/* 狀態顏色 */
|
||||||
|
--success: #10B981;
|
||||||
|
--warning: #F59E0B;
|
||||||
|
--error: #EF4444;
|
||||||
|
--info: #3B82F6;
|
||||||
|
--premium: #8B5CF6; /* 付費功能 */
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2.2 互動效果
|
||||||
|
```css
|
||||||
|
/* 可點擊單字基礎樣式 */
|
||||||
|
.clickable-word {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 1px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高價值片語樣式 */
|
||||||
|
.clickable-word.high-value.phrase {
|
||||||
|
background-color: var(--word-high-phrase-bg);
|
||||||
|
border: 2px solid var(--border-high-phrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-word.high-value.phrase::after {
|
||||||
|
content: "⭐";
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高價值單字樣式 */
|
||||||
|
.clickable-word.high-value.single {
|
||||||
|
background-color: var(--word-high-single-bg);
|
||||||
|
border: 2px solid var(--border-high-single);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-word.high-value.single::after {
|
||||||
|
content: "⭐";
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 普通單字樣式 */
|
||||||
|
.clickable-word.normal {
|
||||||
|
border-bottom: 1px solid var(--border-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 懸停效果 */
|
||||||
|
.clickable-word:hover {
|
||||||
|
background-color: var(--word-hover-bg);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2.3 彈窗動畫
|
||||||
|
```css
|
||||||
|
/* 彈窗進入動畫 */
|
||||||
|
@keyframes popup-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -100%) scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -100%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-popup {
|
||||||
|
animation: popup-enter 0.2s ease-out;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 技術實現細節
|
||||||
|
|
||||||
|
### 5.1 前端狀態管理
|
||||||
|
|
||||||
|
#### 5.1.1 Zustand Store 設計
|
||||||
|
```typescript
|
||||||
|
interface WordQueryStore {
|
||||||
|
// 分析狀態
|
||||||
|
isAnalyzing: boolean;
|
||||||
|
analysisResult: SentenceAnalysis | null;
|
||||||
|
analysisError: string | null;
|
||||||
|
|
||||||
|
// 互動狀態
|
||||||
|
selectedWord: string | null;
|
||||||
|
popupPosition: { x: number; y: number } | null;
|
||||||
|
|
||||||
|
// 使用統計
|
||||||
|
usageStats: {
|
||||||
|
remainingAnalyses: number;
|
||||||
|
resetTime: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 快取
|
||||||
|
analysisCache: Map<string, SentenceAnalysis>;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
analyzeSentence: (text: string) => Promise<void>;
|
||||||
|
selectWord: (word: string, position: { x: number; y: number }) => void;
|
||||||
|
closeWordPopup: () => void;
|
||||||
|
clearCache: () => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.1.2 快取策略
|
||||||
|
```typescript
|
||||||
|
// 本地快取實現
|
||||||
|
class AnalysisCache {
|
||||||
|
private cache = new Map<string, CacheItem>();
|
||||||
|
|
||||||
|
get(textHash: string): SentenceAnalysis | null {
|
||||||
|
const item = this.cache.get(textHash);
|
||||||
|
if (!item || this.isExpired(item)) {
|
||||||
|
this.cache.delete(textHash);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return item.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(textHash: string, data: SentenceAnalysis, ttl: number): void {
|
||||||
|
this.cache.set(textHash, {
|
||||||
|
data,
|
||||||
|
expiresAt: Date.now() + ttl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private isExpired(item: CacheItem): boolean {
|
||||||
|
return Date.now() > item.expiresAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 後端實現細節
|
||||||
|
|
||||||
|
#### 5.2.1 句子分析服務
|
||||||
|
```csharp
|
||||||
|
public class SentenceAnalysisService : ISentenceAnalysisService
|
||||||
|
{
|
||||||
|
private readonly IGeminiService _geminiService;
|
||||||
|
private readonly IAnalysisCacheService _cacheService;
|
||||||
|
private readonly IUsageTrackingService _usageService;
|
||||||
|
|
||||||
|
public async Task<SentenceAnalysisResult> AnalyzeSentenceAsync(
|
||||||
|
string inputText,
|
||||||
|
Guid userId,
|
||||||
|
bool forceRefresh = false)
|
||||||
|
{
|
||||||
|
// 1. 檢查使用限制
|
||||||
|
await _usageService.CheckUsageLimitAsync(userId);
|
||||||
|
|
||||||
|
// 2. 檢查快取
|
||||||
|
var textHash = GenerateTextHash(inputText);
|
||||||
|
if (!forceRefresh)
|
||||||
|
{
|
||||||
|
var cached = await _cacheService.GetAsync(textHash);
|
||||||
|
if (cached != null)
|
||||||
|
{
|
||||||
|
await _usageService.RecordCacheHitAsync(userId);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. AI 分析
|
||||||
|
var analysis = await _geminiService.AnalyzeSentenceAsync(inputText);
|
||||||
|
|
||||||
|
// 4. 存入快取
|
||||||
|
await _cacheService.SetAsync(textHash, analysis, TimeSpan.FromHours(24));
|
||||||
|
|
||||||
|
// 5. 記錄使用
|
||||||
|
await _usageService.RecordAnalysisAsync(userId);
|
||||||
|
|
||||||
|
return analysis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2.2 Gemini Prompt 設計
|
||||||
|
```csharp
|
||||||
|
private const string SENTENCE_ANALYSIS_PROMPT = @"
|
||||||
|
請分析以下英文句子,先檢查語法錯誤並修正,然後提供完整的單字和片語解析:
|
||||||
|
|
||||||
|
句子:{inputText}
|
||||||
|
|
||||||
|
請按照以下 JSON 格式回應:
|
||||||
|
|
||||||
|
{
|
||||||
|
""grammarCorrection"": {
|
||||||
|
""hasErrors"": true/false,
|
||||||
|
""originalText"": ""原始輸入句子"",
|
||||||
|
""correctedText"": ""修正後句子"" // 如無錯誤則與原始相同,
|
||||||
|
""corrections"": [
|
||||||
|
{
|
||||||
|
""position"": {""start"": 2, ""end"": 4},
|
||||||
|
""errorType"": ""tense_mismatch"",
|
||||||
|
""original"": ""錯誤詞彙"",
|
||||||
|
""corrected"": ""修正詞彙"",
|
||||||
|
""reason"": ""修正原因說明"",
|
||||||
|
""severity"": ""high/medium/low""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
""confidenceScore"": 0.95
|
||||||
|
},
|
||||||
|
""sentenceMeaning"": {
|
||||||
|
""translation"": ""整句的繁體中文意思"",
|
||||||
|
""explanation"": ""詳細解釋""
|
||||||
|
},
|
||||||
|
""finalAnalysisText"": ""用於後續分析的最終文本(修正後)"",
|
||||||
|
""wordAnalysis"": {
|
||||||
|
""單字原形"": {
|
||||||
|
""word"": ""單字原形"",
|
||||||
|
""translation"": ""繁體中文翻譯"",
|
||||||
|
""definition"": ""英文定義(A1-A2程度)"",
|
||||||
|
""partOfSpeech"": ""詞性(n./v./adj./adv./phrase/slang)"",
|
||||||
|
""pronunciation"": {
|
||||||
|
""ipa"": ""IPA音標"",
|
||||||
|
""us"": ""美式音標"",
|
||||||
|
""uk"": ""英式音標""
|
||||||
|
},
|
||||||
|
""synonyms"": [""同義詞1"", ""同義詞2"", ""同義詞3""],
|
||||||
|
""antonyms"": [""反義詞1"", ""反義詞2""],
|
||||||
|
""isPhrase"": true/false,
|
||||||
|
""phraseInfo"": {
|
||||||
|
""phrase"": ""完整片語"",
|
||||||
|
""meaning"": ""片語意思"",
|
||||||
|
""warning"": ""警告說明""
|
||||||
|
},
|
||||||
|
""examples"": {
|
||||||
|
""original"": ""來自原句的例句"",
|
||||||
|
""originalTranslation"": ""原句例句翻譯"",
|
||||||
|
""generated"": ""AI生成的新例句"",
|
||||||
|
""generatedTranslation"": ""新例句翻譯""
|
||||||
|
},
|
||||||
|
""difficultyLevel"": ""CEFR等級(A1/A2/B1/B2/C1/C2)""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
分析要求:
|
||||||
|
1. **首要任務:語法檢查和修正**
|
||||||
|
- 檢測語法、拼寫、時態、介詞、詞序錯誤
|
||||||
|
- 提供修正建議和詳細說明
|
||||||
|
- 後續分析基於修正後的句子進行
|
||||||
|
- 保持原句意思不變
|
||||||
|
|
||||||
|
2. 識別所有有意義的單字(忽略 a, an, the 等功能詞)
|
||||||
|
|
||||||
|
3. **重點:標記高學習價值詞彙**
|
||||||
|
- 片語和俚語:isHighValue: true, learningPriority: "high"
|
||||||
|
- 中級以上單字(B1+):isHighValue: true, learningPriority: "high"
|
||||||
|
- 專業術語:isHighValue: true, learningPriority: "medium"
|
||||||
|
- 基礎功能詞:isHighValue: false, learningPriority: "low"
|
||||||
|
|
||||||
|
4. 特別注意片語和俚語,設定 isPhrase: true
|
||||||
|
5. 為片語提供警告說明和顏色代碼
|
||||||
|
6. 英文定義保持在 A1-A2 程度
|
||||||
|
7. 提供實用的同義詞和反義詞(如適用)
|
||||||
|
8. 例句要清楚展示單字用法
|
||||||
|
9. 準確標記 CEFR 難度等級
|
||||||
|
10. **優先處理高價值詞彙**:為高價值詞彙生成完整內容詳情
|
||||||
|
";
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 性能優化策略
|
||||||
|
|
||||||
|
### 6.1 前端優化
|
||||||
|
|
||||||
|
#### 6.1.1 組件懶加載
|
||||||
|
```typescript
|
||||||
|
// 懶加載重型組件
|
||||||
|
const WordInfoPopup = lazy(() => import('./WordInfoPopup'));
|
||||||
|
const ExampleImageViewer = lazy(() => import('./ExampleImageViewer'));
|
||||||
|
|
||||||
|
// 使用 Suspense 包裝
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
<WordInfoPopup {...props} />
|
||||||
|
</Suspense>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.1.2 虛擬化長文本
|
||||||
|
```typescript
|
||||||
|
// 對於長句子使用虛擬化渲染
|
||||||
|
const VirtualizedText = ({ words, analysis, onWordClick }) => {
|
||||||
|
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 50 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="virtual-text-container">
|
||||||
|
{words.slice(visibleRange.start, visibleRange.end).map((word, index) => (
|
||||||
|
<ClickableWord
|
||||||
|
key={index}
|
||||||
|
word={word}
|
||||||
|
analysis={analysis[word]}
|
||||||
|
onClick={onWordClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 後端優化
|
||||||
|
|
||||||
|
#### 6.2.1 快取層次設計
|
||||||
|
```
|
||||||
|
L1: 記憶體快取 (Redis) - 1小時 TTL
|
||||||
|
L2: 資料庫快取 (SQLite) - 24小時 TTL
|
||||||
|
L3: 磁碟快取 (File System) - 7天 TTL
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2.2 批量分析優化
|
||||||
|
```csharp
|
||||||
|
// 批量處理多個句子
|
||||||
|
public async Task<List<SentenceAnalysisResult>> AnalyzeMultipleSentencesAsync(
|
||||||
|
List<string> sentences,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// 1. 批量檢查快取
|
||||||
|
var cacheResults = await _cacheService.GetMultipleAsync(
|
||||||
|
sentences.Select(GenerateTextHash)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. 只分析未快取的句子
|
||||||
|
var uncachedSentences = sentences
|
||||||
|
.Where((s, i) => cacheResults[i] == null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 3. 批量調用 AI API
|
||||||
|
var newAnalyses = await _geminiService.AnalyzeMultipleSentencesAsync(
|
||||||
|
uncachedSentences
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. 合併結果
|
||||||
|
return MergeResults(cacheResults, newAnalyses);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 測試策略
|
||||||
|
|
||||||
|
### 7.1 單元測試
|
||||||
|
|
||||||
|
#### 7.1.1 前端組件測試
|
||||||
|
```typescript
|
||||||
|
describe('ClickableText Component', () => {
|
||||||
|
it('should highlight phrase words correctly', () => {
|
||||||
|
const analysis = {
|
||||||
|
'brought': { isPhrase: true, /* ... */ },
|
||||||
|
'up': { isPhrase: true, /* ... */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<ClickableText text="He brought this up" analysis={analysis} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('brought')).toHaveClass('phrase');
|
||||||
|
expect(screen.getByText('up')).toHaveClass('phrase');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show word popup on click', async () => {
|
||||||
|
const mockOnClick = jest.fn();
|
||||||
|
render(<ClickableText onWordClick={mockOnClick} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('brought'));
|
||||||
|
|
||||||
|
expect(mockOnClick).toHaveBeenCalledWith('brought', expect.any(Object));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.1.2 後端服務測試
|
||||||
|
```csharp
|
||||||
|
[Test]
|
||||||
|
public async Task AnalyzeSentence_ShouldReturnCachedResult_WhenCacheExists()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var inputText = "Test sentence";
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
var cachedResult = new SentenceAnalysisResult();
|
||||||
|
|
||||||
|
_cacheService.Setup(c => c.GetAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(cachedResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _analysisService.AnalyzeSentenceAsync(inputText, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(cachedResult, result);
|
||||||
|
_geminiService.Verify(g => g.AnalyzeSentenceAsync(It.IsAny<string>()),
|
||||||
|
Times.Never);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 整合測試
|
||||||
|
|
||||||
|
#### 7.2.1 E2E 測試流程
|
||||||
|
```typescript
|
||||||
|
describe('Word Query Flow', () => {
|
||||||
|
it('should complete full analysis and query flow', async () => {
|
||||||
|
// 1. 輸入句子
|
||||||
|
await page.fill('[data-testid=sentence-input]', 'He brought this up');
|
||||||
|
await page.click('[data-testid=analyze-button]');
|
||||||
|
|
||||||
|
// 2. 等待分析完成
|
||||||
|
await page.waitForSelector('[data-testid=interactive-text]');
|
||||||
|
|
||||||
|
// 3. 點擊單字
|
||||||
|
await page.click('[data-testid=word-brought]');
|
||||||
|
|
||||||
|
// 4. 驗證彈窗顯示
|
||||||
|
await expect(page.locator('[data-testid=word-popup]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid=phrase-warning]')).toBeVisible();
|
||||||
|
|
||||||
|
// 5. 播放發音
|
||||||
|
await page.click('[data-testid=play-pronunciation]');
|
||||||
|
|
||||||
|
// 6. 關閉彈窗
|
||||||
|
await page.click('[data-testid=close-popup]');
|
||||||
|
await expect(page.locator('[data-testid=word-popup]')).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 部署和監控
|
||||||
|
|
||||||
|
### 8.1 部署策略
|
||||||
|
|
||||||
|
#### 8.1.1 前端部署
|
||||||
|
- **平台**:Vercel
|
||||||
|
- **環境變量**:API_BASE_URL, CACHE_TTL
|
||||||
|
- **CDN**:自動優化靜態資源
|
||||||
|
- **快取策略**:分析結果本地存儲 24 小時
|
||||||
|
|
||||||
|
#### 8.1.2 後端部署
|
||||||
|
- **平台**:Azure App Service / AWS Lambda
|
||||||
|
- **資料庫**:Azure SQL Database / AWS RDS
|
||||||
|
- **快取**:Azure Redis Cache / AWS ElastiCache
|
||||||
|
- **檔案存儲**:Azure Blob Storage / AWS S3
|
||||||
|
|
||||||
|
### 8.2 監控指標
|
||||||
|
|
||||||
|
#### 8.2.1 業務指標
|
||||||
|
- 句子分析成功率
|
||||||
|
- 平均分析響應時間
|
||||||
|
- 快取命中率
|
||||||
|
- 用戶使用次數分佈
|
||||||
|
- 單字點擊熱度排行
|
||||||
|
|
||||||
|
#### 8.2.2 技術指標
|
||||||
|
- API 響應時間 (P95 < 200ms)
|
||||||
|
- Gemini API 調用延遲
|
||||||
|
- 快取效能指標
|
||||||
|
- 錯誤率 (< 1%)
|
||||||
|
- 系統可用性 (> 99.9%)
|
||||||
|
|
||||||
|
#### 8.2.3 成本監控
|
||||||
|
- Gemini API 調用次數和費用
|
||||||
|
- 快取存儲成本
|
||||||
|
- CDN 流量費用
|
||||||
|
- 基礎設施總成本
|
||||||
|
|
||||||
|
## 9. 未來擴展計劃
|
||||||
|
|
||||||
|
### 9.1 功能增強
|
||||||
|
- **多語言支持**:支援其他語言的句子分析
|
||||||
|
- **語音輸入**:整合語音識別進行句子輸入
|
||||||
|
- **個人化推薦**:基於用戶查詢歷史推薦相關詞彙
|
||||||
|
- **社交分享**:分享有趣的句子分析結果
|
||||||
|
|
||||||
|
### 9.2 技術升級
|
||||||
|
- **AI 模型本地化**:部署本地 LLM 降低外部依賴
|
||||||
|
- **即時協作**:多用戶同時查詢同一句子
|
||||||
|
- **離線支持**:PWA 實現離線查詢基礎詞彙
|
||||||
|
- **效能優化**:WebAssembly 加速文本處理
|
||||||
|
|
||||||
|
### 9.3 商業化功能
|
||||||
|
- **高級分析**:更深度的語法和語義分析
|
||||||
|
- **專業詞典**:整合專業領域詞典
|
||||||
|
- **學習追蹤**:詳細的學習進度和成效分析
|
||||||
|
- **導師模式**:AI 導師指導詞彙學習
|
||||||
|
|
@ -0,0 +1,600 @@
|
||||||
|
# 互動式單字查詢 UI 線框圖設計
|
||||||
|
|
||||||
|
## 1. 頁面流程概覽
|
||||||
|
|
||||||
|
```
|
||||||
|
首頁 → 登入/註冊 → Dashboard → 詞卡生成頁 → 句子分析模式 → 互動式查詢 → 詞卡生成
|
||||||
|
↓ ↓ ↓ ↓ ↓ ↓ ↓
|
||||||
|
引導頁 認證流程 快速訪問 輸入界面 分析處理 點擊查詢 學習材料
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 主要頁面線框圖
|
||||||
|
|
||||||
|
### 2.1 詞卡生成頁 - 輸入模式
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ [≡] DramaLing [🔔] [👤] jett@email │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ AI 智能生成詞卡 - 互動式單字查詢 │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 原始例句類型 ──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │
|
||||||
|
│ │ │ [✍️ 手動輸入] │ │ [📷 影劇截圖] (訂閱功能) │ │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ │ ● 選中狀態 │ │ ○ 未選中 │ │ │
|
||||||
|
│ │ │ 貼上或輸入英文文本 │ │ 上傳影劇截圖 (Phase 2) │ │ │
|
||||||
|
│ │ └─────────────────────┘ └─────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 輸入英文文本 ──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 輸入英文句子(最多300字)... │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ 最多 300 字元 • 目前:0 字元 [還可輸入 300 字] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 💡 示例句子: │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ [點擊使用] He brought this thing up during our meeting... │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 🔍 分析句子(點擊查詢單字) │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 🆓 免費用戶:已使用 0/5 次 (3小時內) │ │
|
||||||
|
│ │ ⏰ 下次重置時間:2025-09-17 15:48 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 分析中狀態
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ [≡] DramaLing [🔔] [👤] jett@email │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [←] 返回輸入 句子分析中 │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 🔄 正在分析句子... │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ He brought this thing up during our meeting │ │
|
||||||
|
│ │ and no one agreed. │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ⚡ AI 正在解析單字和片語 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ████████████████████░░░░░░ 80% │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 預計完成時間:5 秒 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 💡 小提示:分析完成後,您可以點擊任何單字查看詳細意思! │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 分析結果 - 互動式查詢模式
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ [≡] DramaLing [🔔] [👤] jett@email │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [←] 返回輸入 句子分析結果 │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 原始句子分析 ──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 📝 用戶輸入: │ │
|
||||||
|
│ │ He brought this thing up during our meeting and no one agreed.│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ✅ 語法檢查:無錯誤 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 📖 整句意思: │ │
|
||||||
|
│ │ 他在我們的會議中提出了這件事,但沒有人同意。這句話表達了在會 │ │
|
||||||
|
│ │ 議中有人提出某個議題或想法,但得不到其他與會者的認同。 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 點擊查詢單字意思 ──────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 💡 使用說明:點擊下方句子中的任何單字,可以立即查看詳細意思。 │ │
|
||||||
|
│ │ 🟡 黃色邊框 = 高價值片語 🟢 綠色邊框 = 高價值單字 🔵 藍色 = 其他 │ │
|
||||||
|
│ │ ⭐ 高價值詞彙點擊免費 | 💰 其他詞彙點擊收費 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ╔═══════════════════════════════════════════════════════════╗ │ │
|
||||||
|
│ │ ║ He [brought]⭐ this [thing] [up]⭐ during our [meeting]⭐ ║ │ │
|
||||||
|
│ │ ║ and no [one] [agreed]. ║ │ │
|
||||||
|
│ │ ╚═══════════════════════════════════════════════════════════╝ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 範例: │ │
|
||||||
|
│ │ • brought/up 🟡 黃色邊框 + ⭐(高價值片語,免費點擊) │ │
|
||||||
|
│ │ • meeting 🟢 綠色邊框 + ⭐(高價值單字,免費點擊) │ │
|
||||||
|
│ │ • thing/one 🔵 藍色邊框(低價值單字,點擊扣1次) │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 操作選項 ──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ [🔄 分析新句子] [📖 生成學習詞卡] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 📊 使用統計:今日已分析 1 個句子,剩餘 4 次免費額度 │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 單字資訊彈窗 - 低價值單字 (收費)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ thing ─────────────────────── × ┐
|
||||||
|
│ │
|
||||||
|
│ 💰 低價值詞彙(扣除 1 次使用額度)│
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ ⚠️ 此查詢將消耗 1 次額度 │ │
|
||||||
|
│ │ 剩餘額度:3/5 次 │ │
|
||||||
|
│ │ [✅ 確認查詢] [❌ 取消] │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 📘 noun | /θɪŋ/ | [🔊 US] [🔊 UK] │
|
||||||
|
│ │
|
||||||
|
│ 🇹🇼 翻譯:事情、東西 │
|
||||||
|
│ │
|
||||||
|
│ 📖 定義:An object, fact, or │
|
||||||
|
│ situation │
|
||||||
|
│ │
|
||||||
|
│ 🔗 同義詞: │
|
||||||
|
│ [object] [matter] [item] │
|
||||||
|
│ │
|
||||||
|
│ 📝 例句: │
|
||||||
|
│ • 原始:He brought this thing up │
|
||||||
|
│ 翻譯:他提出了這件事 │
|
||||||
|
│ │
|
||||||
|
│ • 生成:That's an important │
|
||||||
|
│ thing to remember │
|
||||||
|
│ 翻譯:那是需要記住的重要事情 │
|
||||||
|
│ [📷 查看例句圖] [🔊 播放] │
|
||||||
|
│ │
|
||||||
|
│ 🎯 難度:A1 (基礎) │
|
||||||
|
│ │
|
||||||
|
│ [📚 加入學習清單] [📖 生成詞卡] │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 單字資訊彈窗 - 高價值片語 (免費)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ brought ───────────────────────── × ┐
|
||||||
|
│ │
|
||||||
|
│ ⭐ 高價值片語(免費查詢) │
|
||||||
|
│ ┌─────────────────────────────────┐ │
|
||||||
|
│ │ 🟡 片語:bring up │ │
|
||||||
|
│ │ 🇹🇼 意思:提出(話題)、養育 │ │
|
||||||
|
│ │ ⚡ 提醒:在這個句子中,"brought │ │
|
||||||
|
│ │ up" 是一個片語,意思是"提出話題" │ │
|
||||||
|
│ │ ,而不是單純的"帶來" │ │
|
||||||
|
│ │ 🎯 學習價值:⭐⭐⭐⭐⭐ (極高) │ │
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 📘 verb | /brɔːt/ | [🔊 US] [🔊 UK] │
|
||||||
|
│ │
|
||||||
|
│ 🇹🇼 單字翻譯:帶來、提出 │
|
||||||
|
│ │
|
||||||
|
│ 📖 單字定義:Past tense of bring; │
|
||||||
|
│ to take or carry something to a │
|
||||||
|
│ place │
|
||||||
|
│ │
|
||||||
|
│ 🔗 同義詞:[carried] [took] [delivered] │
|
||||||
|
│ 🔄 反義詞:[removed] [took away] │
|
||||||
|
│ │
|
||||||
|
│ 📝 例句: │
|
||||||
|
│ • 原始:He brought this thing up │
|
||||||
|
│ 翻譯:他提出了這件事 │
|
||||||
|
│ │
|
||||||
|
│ • 生成:She brought up an important │
|
||||||
|
│ point about the budget │
|
||||||
|
│ 翻譯:她提出了關於預算的重要觀點 │
|
||||||
|
│ [📷 查看例句圖] [🔊 播放] │
|
||||||
|
│ │
|
||||||
|
│ 🎯 難度:B1 (中級) │
|
||||||
|
│ │
|
||||||
|
│ [📚 加入學習清單] [📖 生成詞卡] │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.6 例句圖片檢視器
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ [× 關閉] │
|
||||||
|
│ │
|
||||||
|
│ 📷 例句情境圖 │
|
||||||
|
│ "bring up" - 提出話題 │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [圖片載入中...] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 會議室場景插圖 │ │
|
||||||
|
│ │ 一個人在會議中舉手發言 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 💬 "Let me bring up another point..." │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 📝 情境說明:會議中提出新話題 │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ [🔊 播放例句] [📱 分享圖片] [💾 儲存到相簿] [❤️ 收藏] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.7 語法錯誤修正顯示
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ [←] 返回輸入 句子分析結果 │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─ 原始句子分析 ──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 📝 用戶輸入: │ │
|
||||||
|
│ │ I go to school yesterday and meet my friends. │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ❌ 語法檢查:發現 2 個錯誤 │ │
|
||||||
|
│ │ ┌─ 建議修正 ───────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ 🔧 修正後: │ │ │
|
||||||
|
│ │ │ I went to school yesterday and met my friends. │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ 📋 修正說明: │ │ │
|
||||||
|
│ │ │ 1. "go" → "went" (過去式時態修正) │ │ │
|
||||||
|
│ │ │ 2. "meet" → "met" (過去式時態修正) │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ [✅ 使用修正版本] [❌ 保持原始版本] │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 📖 整句意思: │ │
|
||||||
|
│ │ 我昨天去學校遇見了我的朋友們。這句話描述了過去發生的事情... │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 點擊查詢單字意思 ──────────────────────────────────────────────┐ │
|
||||||
|
│ │ 💡 以下基於修正後的句子進行分析 │ │
|
||||||
|
│ │ 🟡 黃色邊框 + ⭐ = 高價值片語 🟢 綠色邊框 + ⭐ = 高價值單字 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ╔═══════════════════════════════════════════════════════════╗ │ │
|
||||||
|
│ │ ║ I [went]⭐ to school [yesterday] and [met]⭐ my friends. ║ │ │
|
||||||
|
│ │ ╚═══════════════════════════════════════════════════════════╝ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.8 使用限制提醒
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ⚠️ 使用限制提醒 │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 🆓 免費用戶限制 │
|
||||||
|
│ │
|
||||||
|
│ 您今日的句子分析額度已用完 (5/5 次) │
|
||||||
|
│ │
|
||||||
|
│ ⏰ 下次重置時間:3小時後 │
|
||||||
|
│ (2025-09-17 18:48) │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 升級建議 ──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 🌟 升級到付費方案,享受無限制分析: │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ✅ 無限次句子分析 │ │
|
||||||
|
│ │ ✅ 每日50張例句圖生成 │ │
|
||||||
|
│ │ ✅ 進階AI分析功能 │ │
|
||||||
|
│ │ ✅ 優先客服支援 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [🚀 立即升級 NT$99/月] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 其他選項 ──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ [⏰ 設定提醒] [📚 瀏覽現有詞卡] [🏠 返回首頁] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 行動裝置適配版本
|
||||||
|
|
||||||
|
### 3.1 手機版 - 輸入介面 (375px)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ ≡ DramaLing 🔔 👤 │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ AI 智能詞卡生成 │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 輸入類型 ─────────────────┐ │
|
||||||
|
│ │ [✍️ 手動] [📷 截圖] │ │
|
||||||
|
│ │ ● ○ │ │
|
||||||
|
│ └───────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 英文句子 ─────────────────┐ │
|
||||||
|
│ │ 輸入英文句子(最多300字) │ │
|
||||||
|
│ │ ┌─────────────────────────┐ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ └─────────────────────────┘ │ │
|
||||||
|
│ │ 0/50 字 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 💡 示例: │ │
|
||||||
|
│ │ [He brought this up...] │ │
|
||||||
|
│ └───────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ 🔍 分析句子(點擊查詢) │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 🆓 免費:0/5 次 │
|
||||||
|
│ ⏰ 重置:3小時後 │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 手機版 - 分析結果
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ ← 返回 句子分析結果 │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─ 原始句子 ─────────────────┐ │
|
||||||
|
│ │ He brought this thing up │ │
|
||||||
|
│ │ during our meeting and no │ │
|
||||||
|
│ │ one agreed. │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 📝 整句意思: │ │
|
||||||
|
│ │ 他在會議中提出了這件事... │ │
|
||||||
|
│ └───────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 點擊查詢 ─────────────────┐ │
|
||||||
|
│ │ 💡 點擊單字查看詳細意思 │ │
|
||||||
|
│ │ 🟡 黃色=片語 🔵 藍色=單字 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ╔═════════════════════════╗ │ │
|
||||||
|
│ │ ║ He [brought] this ║ │ │
|
||||||
|
│ │ ║ [thing] [up] during our ║ │ │
|
||||||
|
│ │ ║ [meeting] and no [one] ║ │ │
|
||||||
|
│ │ ║ [agreed]. ║ │ │
|
||||||
|
│ │ ╚═════════════════════════╝ │ │
|
||||||
|
│ └───────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 操作 ─────────────────────┐ │
|
||||||
|
│ │ [🔄 新句子] [📖 生成詞卡] │ │
|
||||||
|
│ └───────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 📊 今日:1/5 次 │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 手機版 - 單字彈窗
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ × │
|
||||||
|
│ brought │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ ⚠️ 這個單字屬於片語! │
|
||||||
|
│ │
|
||||||
|
│ 🔶 片語:bring up │
|
||||||
|
│ 🇹🇼 意思:提出(話題)、養育 │
|
||||||
|
│ ⚡ 提醒:這裡是"提出話題"的意思 │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ 📘 verb | /brɔːt/ │
|
||||||
|
│ [🔊 US] [🔊 UK] │
|
||||||
|
│ │
|
||||||
|
│ 🇹🇼 翻譯:帶來、提出 │
|
||||||
|
│ │
|
||||||
|
│ 📖 定義:Past tense of bring; │
|
||||||
|
│ to take or carry something to │
|
||||||
|
│ a place │
|
||||||
|
│ │
|
||||||
|
│ 🔗 同義詞: │
|
||||||
|
│ [carried] [took] [delivered] │
|
||||||
|
│ │
|
||||||
|
│ 📝 例句: │
|
||||||
|
│ • 原始:He brought this up │
|
||||||
|
│ 翻譯:他提出了這件事 │
|
||||||
|
│ │
|
||||||
|
│ • 生成:She brought up a point │
|
||||||
|
│ 翻譯:她提出了一個觀點 │
|
||||||
|
│ [📷] [🔊] │
|
||||||
|
│ │
|
||||||
|
│ 🎯 難度:B1 │
|
||||||
|
│ │
|
||||||
|
│ [📚 加入清單] [📖 生成詞卡] │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 互動流程圖
|
||||||
|
|
||||||
|
### 4.1 完整用戶旅程
|
||||||
|
|
||||||
|
```
|
||||||
|
開始
|
||||||
|
↓
|
||||||
|
選擇輸入模式 (手動/截圖)
|
||||||
|
↓
|
||||||
|
[手動模式]
|
||||||
|
輸入英文句子 (≤50字)
|
||||||
|
↓
|
||||||
|
點擊「分析句子」
|
||||||
|
↓
|
||||||
|
檢查使用限制
|
||||||
|
↓ ↓
|
||||||
|
通過 超限
|
||||||
|
↓ ↓
|
||||||
|
顯示分析中... 顯示升級提醒
|
||||||
|
↓ ↓
|
||||||
|
AI 分析完成 [升級] or [等待]
|
||||||
|
↓
|
||||||
|
顯示互動式文字
|
||||||
|
↓
|
||||||
|
點擊任意單字
|
||||||
|
↓
|
||||||
|
顯示單字資訊彈窗
|
||||||
|
↓ ↓
|
||||||
|
普通單字 片語/俚語
|
||||||
|
↓ ↓
|
||||||
|
基礎資訊 片語警告 + 基礎資訊
|
||||||
|
↓ ↓
|
||||||
|
[播放發音] [播放發音]
|
||||||
|
[查看例句圖] [查看例句圖]
|
||||||
|
[加入學習清單] [加入學習清單]
|
||||||
|
[生成詞卡] [生成詞卡]
|
||||||
|
↓
|
||||||
|
關閉彈窗
|
||||||
|
↓
|
||||||
|
繼續查詢其他單字 or 分析新句子 or 生成詞卡
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 錯誤處理流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用戶操作
|
||||||
|
↓
|
||||||
|
系統檢查
|
||||||
|
↓ ↓ ↓ ↓
|
||||||
|
正常 網路錯誤 API錯誤 使用超限
|
||||||
|
↓ ↓ ↓ ↓
|
||||||
|
執行 重試提示 錯誤提示 升級提示
|
||||||
|
↓ ↓ ↓
|
||||||
|
[重試] [回報] [升級/等待]
|
||||||
|
[取消] [返回] [返回]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 響應式設計斷點
|
||||||
|
|
||||||
|
### 5.1 斷點定義
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 手機 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container { padding: 1rem; }
|
||||||
|
.word-popup {
|
||||||
|
width: 90vw;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板 */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.container { padding: 1.5rem; }
|
||||||
|
.interactive-text { font-size: 1.1rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面 */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container { padding: 2rem; }
|
||||||
|
.word-popup { min-width: 400px; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 觸控優化
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 手機觸控優化 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.clickable-word {
|
||||||
|
min-height: 44px; /* iOS 建議最小觸控面積 */
|
||||||
|
padding: 8px 4px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 無障礙設計 (A11y)
|
||||||
|
|
||||||
|
### 6.1 鍵盤導航
|
||||||
|
|
||||||
|
```
|
||||||
|
Tab 鍵順序:
|
||||||
|
1. 輸入模式選擇
|
||||||
|
2. 文字輸入框
|
||||||
|
3. 分析按鈕
|
||||||
|
4. 可點擊單字 (依序)
|
||||||
|
5. 彈窗內元素
|
||||||
|
6. 關閉按鈕
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 螢幕閱讀器支援
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 語義化 HTML -->
|
||||||
|
<main role="main" aria-label="互動式單字查詢">
|
||||||
|
<section aria-label="句子輸入區">
|
||||||
|
<input
|
||||||
|
aria-label="輸入英文句子,最多300字元"
|
||||||
|
aria-describedby="char-count"
|
||||||
|
/>
|
||||||
|
<div id="char-count" aria-live="polite">目前:0 字元</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section aria-label="分析結果區">
|
||||||
|
<div role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="單字:brought,點擊查看詳細資訊"
|
||||||
|
data-word="brought">
|
||||||
|
brought
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 彈窗 -->
|
||||||
|
<dialog role="dialog"
|
||||||
|
aria-labelledby="word-title"
|
||||||
|
aria-describedby="word-definition">
|
||||||
|
<h2 id="word-title">brought</h2>
|
||||||
|
<p id="word-definition">Past tense of bring...</p>
|
||||||
|
</dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 色彩對比
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 符合 WCAG AA 標準 */
|
||||||
|
:root {
|
||||||
|
--text-primary: #1a1a1a; /* 對比度 13.26:1 */
|
||||||
|
--text-secondary: #4a4a4a; /* 對比度 8.59:1 */
|
||||||
|
--border-focus: #0066cc; /* 高對比度焦點邊框 */
|
||||||
|
--bg-phrase: #fff4cc; /* 片語背景 */
|
||||||
|
--bg-phrase-text: #8b4513; /* 片語文字 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-word:focus {
|
||||||
|
outline: 2px solid var(--border-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
這份 UI 線框圖設計涵蓋了完整的用戶介面規劃,包括主要頁面、互動流程、響應式適配和無障礙設計,為開發團隊提供了詳細的實現指南。
|
||||||
|
|
@ -0,0 +1,572 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ClickableTextV2 } from '@/components/ClickableTextV2'
|
||||||
|
|
||||||
|
export default function DemoV2Page() {
|
||||||
|
const [mode, setMode] = useState<'manual' | 'screenshot'>('manual')
|
||||||
|
const [textInput, setTextInput] = useState('')
|
||||||
|
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||||
|
const [showAnalysisView, setShowAnalysisView] = useState(false)
|
||||||
|
const [sentenceAnalysis, setSentenceAnalysis] = useState<any>(null)
|
||||||
|
const [sentenceMeaning, setSentenceMeaning] = useState('')
|
||||||
|
const [usageCount, setUsageCount] = useState(0)
|
||||||
|
const [isPremium] = useState(false)
|
||||||
|
|
||||||
|
// 模擬分析後的句子資料(新版本)
|
||||||
|
const mockSentenceAnalysis = {
|
||||||
|
meaning: "他在我們的會議中提出了這件事,但沒有人同意。這句話表達了在會議中有人提出某個議題或想法,但得不到其他與會者的認同。",
|
||||||
|
highValueWords: ["brought", "up", "meeting", "agreed"], // 高價值詞彙
|
||||||
|
phrasesDetected: [
|
||||||
|
{
|
||||||
|
phrase: "bring up",
|
||||||
|
words: ["brought", "up"],
|
||||||
|
colorCode: "#F59E0B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
words: {
|
||||||
|
"he": {
|
||||||
|
word: "he",
|
||||||
|
translation: "他",
|
||||||
|
definition: "Used to refer to a male person or animal",
|
||||||
|
partOfSpeech: "pronoun",
|
||||||
|
pronunciation: "/hiː/",
|
||||||
|
synonyms: ["him", "that man"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false, // 低價值詞彙
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A1",
|
||||||
|
costIncurred: 1
|
||||||
|
},
|
||||||
|
"brought": {
|
||||||
|
word: "brought",
|
||||||
|
translation: "帶來、提出",
|
||||||
|
definition: "Past tense of bring; to take or carry something to a place",
|
||||||
|
partOfSpeech: "verb",
|
||||||
|
pronunciation: "/brɔːt/",
|
||||||
|
synonyms: ["carried", "took", "delivered"],
|
||||||
|
antonyms: ["removed", "took away"],
|
||||||
|
isPhrase: true,
|
||||||
|
isHighValue: true, // 高價值片語
|
||||||
|
learningPriority: "high",
|
||||||
|
phraseInfo: {
|
||||||
|
phrase: "bring up",
|
||||||
|
meaning: "提出(話題)、養育",
|
||||||
|
warning: "在這個句子中,\"brought up\" 是一個片語,意思是\"提出話題\",而不是單純的\"帶來\"",
|
||||||
|
colorCode: "#F59E0B"
|
||||||
|
},
|
||||||
|
difficultyLevel: "B1",
|
||||||
|
costIncurred: 0 // 高價值免費
|
||||||
|
},
|
||||||
|
"this": {
|
||||||
|
word: "this",
|
||||||
|
translation: "這個",
|
||||||
|
definition: "Used to indicate something near or just mentioned",
|
||||||
|
partOfSpeech: "pronoun",
|
||||||
|
pronunciation: "/ðɪs/",
|
||||||
|
synonyms: ["that", "it"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false, // 低價值詞彙
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A1",
|
||||||
|
costIncurred: 1
|
||||||
|
},
|
||||||
|
"thing": {
|
||||||
|
word: "thing",
|
||||||
|
translation: "事情、東西",
|
||||||
|
definition: "An object, fact, or situation",
|
||||||
|
partOfSpeech: "noun",
|
||||||
|
pronunciation: "/θɪŋ/",
|
||||||
|
synonyms: ["object", "matter", "item"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false, // 低價值詞彙
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A1",
|
||||||
|
costIncurred: 1
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
word: "up",
|
||||||
|
translation: "向上",
|
||||||
|
definition: "Toward a higher place or position",
|
||||||
|
partOfSpeech: "adverb",
|
||||||
|
pronunciation: "/ʌp/",
|
||||||
|
synonyms: ["upward", "above"],
|
||||||
|
antonyms: ["down", "below"],
|
||||||
|
isPhrase: true,
|
||||||
|
isHighValue: true, // 高價值片語部分
|
||||||
|
learningPriority: "high",
|
||||||
|
phraseInfo: {
|
||||||
|
phrase: "bring up",
|
||||||
|
meaning: "提出(話題)、養育",
|
||||||
|
warning: "\"up\" 在這裡是片語 \"bring up\" 的一部分,不是單獨的\"向上\"的意思",
|
||||||
|
colorCode: "#F59E0B"
|
||||||
|
},
|
||||||
|
difficultyLevel: "B1",
|
||||||
|
costIncurred: 0 // 高價值免費
|
||||||
|
},
|
||||||
|
"during": {
|
||||||
|
word: "during",
|
||||||
|
translation: "在...期間",
|
||||||
|
definition: "Throughout the course or duration of",
|
||||||
|
partOfSpeech: "preposition",
|
||||||
|
pronunciation: "/ˈdjʊərɪŋ/",
|
||||||
|
synonyms: ["throughout", "while"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false, // 低價值詞彙
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A2",
|
||||||
|
costIncurred: 1
|
||||||
|
},
|
||||||
|
"our": {
|
||||||
|
word: "our",
|
||||||
|
translation: "我們的",
|
||||||
|
definition: "Belonging to us",
|
||||||
|
partOfSpeech: "pronoun",
|
||||||
|
pronunciation: "/aʊər/",
|
||||||
|
synonyms: ["ours"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false, // 低價值詞彙
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A1",
|
||||||
|
costIncurred: 1
|
||||||
|
},
|
||||||
|
"meeting": {
|
||||||
|
word: "meeting",
|
||||||
|
translation: "會議",
|
||||||
|
definition: "An organized gathering of people for discussion",
|
||||||
|
partOfSpeech: "noun",
|
||||||
|
pronunciation: "/ˈmiːtɪŋ/",
|
||||||
|
synonyms: ["conference", "assembly", "gathering"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: true, // 高價值單字(B2級)
|
||||||
|
learningPriority: "high",
|
||||||
|
difficultyLevel: "B2",
|
||||||
|
costIncurred: 0 // 高價值免費
|
||||||
|
},
|
||||||
|
"and": {
|
||||||
|
word: "and",
|
||||||
|
translation: "和、而且",
|
||||||
|
definition: "Used to connect words or clauses",
|
||||||
|
partOfSpeech: "conjunction",
|
||||||
|
pronunciation: "/ænd/",
|
||||||
|
synonyms: ["plus", "also"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false, // 低價值詞彙
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A1",
|
||||||
|
costIncurred: 1
|
||||||
|
},
|
||||||
|
"no": {
|
||||||
|
word: "no",
|
||||||
|
translation: "沒有",
|
||||||
|
definition: "Not any; not one",
|
||||||
|
partOfSpeech: "determiner",
|
||||||
|
pronunciation: "/nəʊ/",
|
||||||
|
synonyms: ["none", "zero"],
|
||||||
|
antonyms: ["some", "any"],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false, // 低價值詞彙
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A1",
|
||||||
|
costIncurred: 1
|
||||||
|
},
|
||||||
|
"one": {
|
||||||
|
word: "one",
|
||||||
|
translation: "一個人、任何人",
|
||||||
|
definition: "A single person or thing",
|
||||||
|
partOfSpeech: "pronoun",
|
||||||
|
pronunciation: "/wʌn/",
|
||||||
|
synonyms: ["someone", "anybody"],
|
||||||
|
antonyms: ["none", "nobody"],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false, // 低價值詞彙
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A1",
|
||||||
|
costIncurred: 1
|
||||||
|
},
|
||||||
|
"agreed": {
|
||||||
|
word: "agreed",
|
||||||
|
translation: "同意",
|
||||||
|
definition: "Past tense of agree; to have the same opinion",
|
||||||
|
partOfSpeech: "verb",
|
||||||
|
pronunciation: "/əˈɡriːd/",
|
||||||
|
synonyms: ["consented", "accepted", "approved"],
|
||||||
|
antonyms: ["disagreed", "refused"],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: true, // 高價值單字(B1級)
|
||||||
|
learningPriority: "medium",
|
||||||
|
difficultyLevel: "B1",
|
||||||
|
costIncurred: 0 // 高價值免費
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 處理句子分析
|
||||||
|
const handleAnalyzeSentence = async () => {
|
||||||
|
if (!textInput.trim()) return
|
||||||
|
|
||||||
|
// 檢查使用次數限制
|
||||||
|
if (!isPremium && usageCount >= 5) {
|
||||||
|
alert('❌ 免費用戶 3 小時內只能分析 5 次句子,請稍後再試或升級到付費版本')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAnalyzing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模擬 API 調用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
setSentenceAnalysis(mockSentenceAnalysis.words)
|
||||||
|
setSentenceMeaning(mockSentenceAnalysis.meaning)
|
||||||
|
setShowAnalysisView(true)
|
||||||
|
setUsageCount(prev => prev + 1) // 句子分析扣除1次
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error analyzing sentence:', error)
|
||||||
|
alert('分析句子時發生錯誤,請稍後再試')
|
||||||
|
} finally {
|
||||||
|
setIsAnalyzing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHighValueCount = () => {
|
||||||
|
if (!sentenceAnalysis) return 0
|
||||||
|
return Object.values(sentenceAnalysis).filter((word: any) => word.isHighValue).length
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLowValueCount = () => {
|
||||||
|
if (!sentenceAnalysis) return 0
|
||||||
|
return Object.values(sentenceAnalysis).filter((word: any) => !word.isHighValue).length
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="bg-white shadow-sm">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<h1 className="text-2xl font-bold text-blue-600">DramaLing</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span className="text-gray-600">高價值標記演示 v2.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{!showAnalysisView ? (
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold mb-8">AI 智能生成詞卡 - 高價值標記系統</h1>
|
||||||
|
|
||||||
|
{/* 功能說明 */}
|
||||||
|
<div className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-xl p-6 mb-6 border border-blue-200">
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-blue-800">🎯 高價值標記系統特色</h2>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-yellow-400 border-2 border-yellow-500 rounded"></div>
|
||||||
|
<span><strong>高價值片語</strong> - 免費無限點擊</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-green-400 border-2 border-green-500 rounded"></div>
|
||||||
|
<span><strong>高價值單字</strong> - 免費無限點擊</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 border-b-2 border-blue-400"></div>
|
||||||
|
<span><strong>普通單字</strong> - 點擊扣除 1 次額度</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">⭐</span>
|
||||||
|
<span><strong>星號標記</strong> - 高學習價值指標</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Mode Selection */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">原始例句類型</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setMode('manual')}
|
||||||
|
className={`p-4 rounded-lg border-2 transition-all ${
|
||||||
|
mode === 'manual'
|
||||||
|
? 'border-blue-600 bg-blue-50'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-2xl mb-2">✍️</div>
|
||||||
|
<div className="font-semibold">手動輸入</div>
|
||||||
|
<div className="text-sm text-gray-600 mt-1">貼上或輸入英文文本</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMode('screenshot')}
|
||||||
|
disabled={!isPremium}
|
||||||
|
className={`p-4 rounded-lg border-2 transition-all relative ${
|
||||||
|
mode === 'screenshot'
|
||||||
|
? 'border-blue-600 bg-blue-50'
|
||||||
|
: isPremium
|
||||||
|
? 'border-gray-200 hover:border-gray-300'
|
||||||
|
: 'border-gray-200 bg-gray-100 cursor-not-allowed opacity-60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-2xl mb-2">📷</div>
|
||||||
|
<div className="font-semibold">影劇截圖</div>
|
||||||
|
<div className="text-sm text-gray-600 mt-1">上傳影劇截圖 (Phase 2)</div>
|
||||||
|
{!isPremium && (
|
||||||
|
<div className="absolute top-2 right-2 px-2 py-1 bg-yellow-100 text-yellow-700 text-xs rounded-full">
|
||||||
|
訂閱功能
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Input */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold mb-4">輸入英文文本</h2>
|
||||||
|
<textarea
|
||||||
|
value={textInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
if (mode === 'manual' && value.length > 50) {
|
||||||
|
return // 阻止輸入超過50字
|
||||||
|
}
|
||||||
|
setTextInput(value)
|
||||||
|
}}
|
||||||
|
placeholder={mode === 'manual'
|
||||||
|
? "輸入英文句子(最多50字)..."
|
||||||
|
: "貼上您想要學習的英文文本,例如影劇對話、文章段落..."
|
||||||
|
}
|
||||||
|
className={`w-full h-40 px-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent outline-none resize-none ${
|
||||||
|
mode === 'manual' && textInput.length >= 45 ? 'border-yellow-400' :
|
||||||
|
mode === 'manual' && textInput.length >= 50 ? 'border-red-400' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="mt-2 flex justify-between text-sm">
|
||||||
|
<span className={`${
|
||||||
|
mode === 'manual' && textInput.length >= 45 ? 'text-yellow-600' :
|
||||||
|
mode === 'manual' && textInput.length >= 50 ? 'text-red-600' : 'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{mode === 'manual' ? `最多 50 字元 • 目前:${textInput.length} 字元` : `最多 5000 字元 • 目前:${textInput.length} 字元`}
|
||||||
|
</span>
|
||||||
|
{mode === 'manual' && textInput.length > 40 && (
|
||||||
|
<span className={textInput.length >= 50 ? 'text-red-600' : 'text-yellow-600'}>
|
||||||
|
{textInput.length >= 50 ? '已達上限!' : `還可輸入 ${50 - textInput.length} 字元`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 預設示例 */}
|
||||||
|
{!textInput && (
|
||||||
|
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-sm text-gray-700 mb-2">
|
||||||
|
<strong>示例句子:</strong>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setTextInput("He brought this thing up during our meeting and no one agreed.")}
|
||||||
|
className="text-sm text-blue-600 hover:text-blue-800 bg-blue-50 px-3 py-1 rounded border border-blue-200"
|
||||||
|
>
|
||||||
|
點擊使用示例:He brought this thing up during our meeting and no one agreed.
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 新的按鈕區域 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 分析句子按鈕 */}
|
||||||
|
<button
|
||||||
|
onClick={handleAnalyzeSentence}
|
||||||
|
disabled={isAnalyzing || (mode === 'manual' && (!textInput || textInput.length > 50)) || (mode === 'screenshot')}
|
||||||
|
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isAnalyzing ? (
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
正在識別高價值詞彙...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'🔍 分析句子並標記高價值詞彙'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 使用次數顯示 */}
|
||||||
|
<div className="text-center text-sm text-gray-600">
|
||||||
|
{isPremium ? (
|
||||||
|
<span className="text-green-600">🌟 付費用戶:無限制使用</span>
|
||||||
|
) : (
|
||||||
|
<span className={usageCount >= 4 ? 'text-red-600' : usageCount >= 3 ? 'text-yellow-600' : 'text-gray-600'}>
|
||||||
|
免費用戶:已使用 {usageCount}/5 次 (3小時內)
|
||||||
|
{usageCount >= 5 && <span className="block text-red-500 mt-1">已達上限,請稍後再試</span>}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 句子分析視圖 */
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-3xl font-bold">句子分析結果 - 高價值標記</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAnalysisView(false)}
|
||||||
|
className="text-gray-600 hover:text-gray-900"
|
||||||
|
>
|
||||||
|
← 返回
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分析統計 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">分析統計</h2>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="text-center p-3 bg-green-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-green-600">{getHighValueCount()}</div>
|
||||||
|
<div className="text-sm text-green-700">高價值詞彙</div>
|
||||||
|
<div className="text-xs text-green-600">⭐ 免費點擊</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-blue-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">{getLowValueCount()}</div>
|
||||||
|
<div className="text-sm text-blue-700">普通詞彙</div>
|
||||||
|
<div className="text-xs text-blue-600">💰 點擊收費</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-gray-600">1</div>
|
||||||
|
<div className="text-sm text-gray-700">本次消耗</div>
|
||||||
|
<div className="text-xs text-gray-600">🔍 句子分析</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 原始句子顯示 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">原始句子</h2>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg mb-4">
|
||||||
|
<div className="text-lg leading-relaxed">
|
||||||
|
{textInput}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-base font-semibold mb-2">整句意思</h3>
|
||||||
|
<div className="text-gray-700 leading-relaxed">
|
||||||
|
{sentenceMeaning}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 互動式文字 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">智能高價值標記 - 點擊查詢單字</h2>
|
||||||
|
|
||||||
|
{/* 圖例說明 */}
|
||||||
|
<div className="p-4 bg-gradient-to-r from-blue-50 to-green-50 rounded-lg border border-blue-200 mb-4">
|
||||||
|
<p className="text-sm text-blue-800 mb-3">
|
||||||
|
<strong>💡 智能標記說明:</strong>AI 已識別高學習價值詞彙並標記為免費查詢
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="px-2 py-1 bg-yellow-100 border-2 border-yellow-400 rounded">brought⭐</div>
|
||||||
|
<span>高價值片語(免費)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="px-2 py-1 bg-green-100 border-2 border-green-400 rounded">meeting⭐</div>
|
||||||
|
<span>高價值單字(免費)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="px-2 py-1 border-b border-blue-300">thing</div>
|
||||||
|
<span>普通單字(扣1次)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
|
||||||
|
<ClickableTextV2
|
||||||
|
text={textInput}
|
||||||
|
analysis={sentenceAnalysis}
|
||||||
|
highValueWords={mockSentenceAnalysis.highValueWords}
|
||||||
|
phrasesDetected={mockSentenceAnalysis.phrasesDetected}
|
||||||
|
remainingUsage={5 - usageCount}
|
||||||
|
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
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 使用統計 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">使用統計</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600">今日句子分析</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-600">{usageCount}</div>
|
||||||
|
<div className="text-xs text-gray-500">每次扣除 1 次額度</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600">剩餘額度</div>
|
||||||
|
<div className={`text-2xl font-bold ${5 - usageCount <= 1 ? 'text-red-600' : 'text-green-600'}`}>
|
||||||
|
{5 - usageCount}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{isPremium ? '無限制' : '3小時內重置'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按鈕 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAnalysisView(false)}
|
||||||
|
className="flex-1 bg-gray-200 text-gray-700 py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
🔄 分析新句子
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => alert('詞卡生成功能開發中...')}
|
||||||
|
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
📖 生成詞卡
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,500 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ClickableTextV2 } from '@/components/ClickableTextV2'
|
||||||
|
import { GrammarCorrectionPanel } from '@/components/GrammarCorrectionPanel'
|
||||||
|
|
||||||
|
export default function DemoV3Page() {
|
||||||
|
const [mode, setMode] = useState<'manual' | 'screenshot'>('manual')
|
||||||
|
const [textInput, setTextInput] = useState('')
|
||||||
|
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||||
|
const [showAnalysisView, setShowAnalysisView] = useState(false)
|
||||||
|
const [sentenceAnalysis, setSentenceAnalysis] = useState<any>(null)
|
||||||
|
const [sentenceMeaning, setSentenceMeaning] = useState('')
|
||||||
|
const [grammarCorrection, setGrammarCorrection] = useState<any>(null)
|
||||||
|
const [finalText, setFinalText] = useState('') // 最終用於分析的文本
|
||||||
|
const [usageCount, setUsageCount] = useState(0)
|
||||||
|
const [isPremium] = useState(false)
|
||||||
|
|
||||||
|
// 模擬正確句子的分析資料
|
||||||
|
const mockCorrectSentenceAnalysis = {
|
||||||
|
meaning: "他在我們的會議中提出了這件事,但沒有人同意。這句話表達了在會議中有人提出某個議題或想法,但得不到其他與會者的認同。",
|
||||||
|
grammarCorrection: {
|
||||||
|
hasErrors: false,
|
||||||
|
originalText: "He brought this thing up during our meeting and no one agreed.",
|
||||||
|
correctedText: null,
|
||||||
|
corrections: [],
|
||||||
|
confidenceScore: 0.98
|
||||||
|
},
|
||||||
|
highValueWords: ["brought", "up", "meeting", "agreed"],
|
||||||
|
words: {
|
||||||
|
"brought": {
|
||||||
|
word: "brought",
|
||||||
|
translation: "帶來、提出",
|
||||||
|
definition: "Past tense of bring; to take or carry something to a place",
|
||||||
|
partOfSpeech: "verb",
|
||||||
|
pronunciation: "/brɔːt/",
|
||||||
|
synonyms: ["carried", "took", "delivered"],
|
||||||
|
antonyms: ["removed", "took away"],
|
||||||
|
isPhrase: true,
|
||||||
|
isHighValue: true,
|
||||||
|
learningPriority: "high",
|
||||||
|
phraseInfo: {
|
||||||
|
phrase: "bring up",
|
||||||
|
meaning: "提出(話題)、養育",
|
||||||
|
warning: "在這個句子中,\"brought up\" 是一個片語,意思是\"提出話題\",而不是單純的\"帶來\"",
|
||||||
|
colorCode: "#F59E0B"
|
||||||
|
},
|
||||||
|
difficultyLevel: "B1"
|
||||||
|
},
|
||||||
|
"meeting": {
|
||||||
|
word: "meeting",
|
||||||
|
translation: "會議",
|
||||||
|
definition: "An organized gathering of people for discussion",
|
||||||
|
partOfSpeech: "noun",
|
||||||
|
pronunciation: "/ˈmiːtɪŋ/",
|
||||||
|
synonyms: ["conference", "assembly", "gathering"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: true,
|
||||||
|
learningPriority: "high",
|
||||||
|
difficultyLevel: "B2"
|
||||||
|
},
|
||||||
|
"thing": {
|
||||||
|
word: "thing",
|
||||||
|
translation: "事情、東西",
|
||||||
|
definition: "An object, fact, or situation",
|
||||||
|
partOfSpeech: "noun",
|
||||||
|
pronunciation: "/θɪŋ/",
|
||||||
|
synonyms: ["object", "matter", "item"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false,
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A1",
|
||||||
|
costIncurred: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模擬有語法錯誤的句子分析資料
|
||||||
|
const mockErrorSentenceAnalysis = {
|
||||||
|
meaning: "我昨天去學校遇見了我的朋友們。這句話描述了過去發生的事情,表達了去學校並遇到朋友的經歷。",
|
||||||
|
grammarCorrection: {
|
||||||
|
hasErrors: true,
|
||||||
|
originalText: "I go to school yesterday and meet my friends.",
|
||||||
|
correctedText: "I went to school yesterday and met my friends.",
|
||||||
|
corrections: [
|
||||||
|
{
|
||||||
|
position: { start: 2, end: 4 },
|
||||||
|
errorType: "tense_mismatch",
|
||||||
|
original: "go",
|
||||||
|
corrected: "went",
|
||||||
|
reason: "過去式時態修正:句子中有 'yesterday',應使用過去式",
|
||||||
|
severity: "high"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: { start: 29, end: 33 },
|
||||||
|
errorType: "tense_mismatch",
|
||||||
|
original: "meet",
|
||||||
|
corrected: "met",
|
||||||
|
reason: "過去式時態修正:與 'went' 保持時態一致",
|
||||||
|
severity: "high"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
confidenceScore: 0.95
|
||||||
|
},
|
||||||
|
highValueWords: ["went", "yesterday", "met", "friends"],
|
||||||
|
words: {
|
||||||
|
"went": {
|
||||||
|
word: "went",
|
||||||
|
translation: "去、前往",
|
||||||
|
definition: "Past tense of go; to move or travel to a place",
|
||||||
|
partOfSpeech: "verb",
|
||||||
|
pronunciation: "/went/",
|
||||||
|
synonyms: ["traveled", "moved", "proceeded"],
|
||||||
|
antonyms: ["stayed", "remained"],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: true,
|
||||||
|
learningPriority: "high",
|
||||||
|
difficultyLevel: "A2"
|
||||||
|
},
|
||||||
|
"yesterday": {
|
||||||
|
word: "yesterday",
|
||||||
|
translation: "昨天",
|
||||||
|
definition: "The day before today",
|
||||||
|
partOfSpeech: "adverb",
|
||||||
|
pronunciation: "/ˈjestədeɪ/",
|
||||||
|
synonyms: ["the day before"],
|
||||||
|
antonyms: ["tomorrow", "today"],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: true,
|
||||||
|
learningPriority: "medium",
|
||||||
|
difficultyLevel: "A1"
|
||||||
|
},
|
||||||
|
"met": {
|
||||||
|
word: "met",
|
||||||
|
translation: "遇見、認識",
|
||||||
|
definition: "Past tense of meet; to encounter or come together with",
|
||||||
|
partOfSpeech: "verb",
|
||||||
|
pronunciation: "/met/",
|
||||||
|
synonyms: ["encountered", "saw", "found"],
|
||||||
|
antonyms: ["avoided", "missed"],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: true,
|
||||||
|
learningPriority: "high",
|
||||||
|
difficultyLevel: "A2"
|
||||||
|
},
|
||||||
|
"friends": {
|
||||||
|
word: "friends",
|
||||||
|
translation: "朋友們",
|
||||||
|
definition: "People you like and know well",
|
||||||
|
partOfSpeech: "noun",
|
||||||
|
pronunciation: "/frends/",
|
||||||
|
synonyms: ["companions", "buddies", "pals"],
|
||||||
|
antonyms: ["enemies", "strangers"],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: true,
|
||||||
|
learningPriority: "medium",
|
||||||
|
difficultyLevel: "A1"
|
||||||
|
},
|
||||||
|
"school": {
|
||||||
|
word: "school",
|
||||||
|
translation: "學校",
|
||||||
|
definition: "A place where children go to learn",
|
||||||
|
partOfSpeech: "noun",
|
||||||
|
pronunciation: "/skuːl/",
|
||||||
|
synonyms: ["educational institution"],
|
||||||
|
antonyms: [],
|
||||||
|
isPhrase: false,
|
||||||
|
isHighValue: false,
|
||||||
|
learningPriority: "low",
|
||||||
|
difficultyLevel: "A1",
|
||||||
|
costIncurred: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 處理句子分析
|
||||||
|
const handleAnalyzeSentence = async () => {
|
||||||
|
if (!textInput.trim()) return
|
||||||
|
|
||||||
|
if (!isPremium && usageCount >= 5) {
|
||||||
|
alert('❌ 免費用戶 3 小時內只能分析 5 次句子,請稍後再試或升級到付費版本')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAnalyzing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2500))
|
||||||
|
|
||||||
|
// 根據輸入文本決定使用哪個模擬資料
|
||||||
|
const hasGrammarErrors = textInput.toLowerCase().includes('go to school yesterday') ||
|
||||||
|
textInput.toLowerCase().includes('meet my friends')
|
||||||
|
|
||||||
|
if (hasGrammarErrors) {
|
||||||
|
setSentenceAnalysis(mockErrorSentenceAnalysis.words)
|
||||||
|
setSentenceMeaning(mockErrorSentenceAnalysis.meaning)
|
||||||
|
setGrammarCorrection(mockErrorSentenceAnalysis.grammarCorrection)
|
||||||
|
setFinalText(mockErrorSentenceAnalysis.grammarCorrection.correctedText || textInput)
|
||||||
|
} else {
|
||||||
|
setSentenceAnalysis(mockCorrectSentenceAnalysis.words)
|
||||||
|
setSentenceMeaning(mockCorrectSentenceAnalysis.meaning)
|
||||||
|
setGrammarCorrection(mockCorrectSentenceAnalysis.grammarCorrection)
|
||||||
|
setFinalText(textInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowAnalysisView(true)
|
||||||
|
setUsageCount(prev => prev + 1)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error analyzing sentence:', error)
|
||||||
|
alert('分析句子時發生錯誤,請稍後再試')
|
||||||
|
} finally {
|
||||||
|
setIsAnalyzing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAcceptCorrection = () => {
|
||||||
|
if (grammarCorrection?.correctedText) {
|
||||||
|
setFinalText(grammarCorrection.correctedText)
|
||||||
|
// 這裡可以重新分析修正後的句子
|
||||||
|
alert('✅ 已採用修正版本,後續學習將基於正確的句子進行!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRejectCorrection = () => {
|
||||||
|
setFinalText(grammarCorrection?.originalText || textInput)
|
||||||
|
alert('📝 已保持原始版本,將基於您的原始輸入進行學習。')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="bg-white shadow-sm">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<h1 className="text-2xl font-bold text-blue-600">DramaLing</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span className="text-gray-600">語法修正演示 v3.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{!showAnalysisView ? (
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold mb-8">AI 智能語法修正 + 高價值標記系統</h1>
|
||||||
|
|
||||||
|
{/* 功能說明 */}
|
||||||
|
<div className="bg-gradient-to-r from-red-50 to-green-50 rounded-xl p-6 mb-6 border border-red-200">
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-red-800">🔧 語法修正 + 高價值標記特色</h2>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">❌</span>
|
||||||
|
<span><strong>智能錯誤檢測</strong> - 9種語法錯誤類型</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">🔧</span>
|
||||||
|
<span><strong>自動修正建議</strong> - 詳細修正說明</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">⭐</span>
|
||||||
|
<span><strong>高價值標記</strong> - 基於修正後句子</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">💰</span>
|
||||||
|
<span><strong>成本優化</strong> - 語法修正不額外收費</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Input */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">輸入英文文本 (更新:300字限制)</h2>
|
||||||
|
<textarea
|
||||||
|
value={textInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
if (mode === 'manual' && value.length > 300) {
|
||||||
|
return // 阻止輸入超過300字
|
||||||
|
}
|
||||||
|
setTextInput(value)
|
||||||
|
}}
|
||||||
|
placeholder={mode === 'manual'
|
||||||
|
? "輸入英文句子(最多300字)..."
|
||||||
|
: "貼上您想要學習的英文文本,例如影劇對話、文章段落..."
|
||||||
|
}
|
||||||
|
className={`w-full h-40 px-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent outline-none resize-none ${
|
||||||
|
mode === 'manual' && textInput.length >= 280 ? 'border-yellow-400' :
|
||||||
|
mode === 'manual' && textInput.length >= 300 ? 'border-red-400' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="mt-2 flex justify-between text-sm">
|
||||||
|
<span className={`${
|
||||||
|
mode === 'manual' && textInput.length >= 280 ? 'text-yellow-600' :
|
||||||
|
mode === 'manual' && textInput.length >= 300 ? 'text-red-600' : 'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{mode === 'manual' ? `最多 300 字元 • 目前:${textInput.length} 字元` : `最多 5000 字元 • 目前:${textInput.length} 字元`}
|
||||||
|
</span>
|
||||||
|
{mode === 'manual' && textInput.length > 250 && (
|
||||||
|
<span className={textInput.length >= 300 ? 'text-red-600' : 'text-yellow-600'}>
|
||||||
|
{textInput.length >= 300 ? '已達上限!' : `還可輸入 ${300 - textInput.length} 字元`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 預設示例 - 包含語法錯誤和正確的句子 */}
|
||||||
|
{!textInput && (
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
<div className="p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-sm text-gray-700 mb-2">
|
||||||
|
<strong>✅ 正確語法示例:</strong>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setTextInput("He brought this thing up during our meeting and no one agreed.")}
|
||||||
|
className="text-sm text-green-600 hover:text-green-800 bg-green-50 px-3 py-1 rounded border border-green-200 w-full text-left"
|
||||||
|
>
|
||||||
|
He brought this thing up during our meeting and no one agreed.
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-red-50 rounded-lg">
|
||||||
|
<div className="text-sm text-red-700 mb-2">
|
||||||
|
<strong>❌ 語法錯誤示例(測試修正功能):</strong>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setTextInput("I go to school yesterday and meet my friends.")}
|
||||||
|
className="text-sm text-red-600 hover:text-red-800 bg-red-50 px-3 py-1 rounded border border-red-200 w-full text-left"
|
||||||
|
>
|
||||||
|
I go to school yesterday and meet my friends.
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分析按鈕 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<button
|
||||||
|
onClick={handleAnalyzeSentence}
|
||||||
|
disabled={isAnalyzing || (mode === 'manual' && (!textInput || textInput.length > 300)) || (mode === 'screenshot')}
|
||||||
|
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isAnalyzing ? (
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
正在檢查語法並標記高價值詞彙...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'🔍 語法檢查 + 高價值詞彙分析'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center text-sm text-gray-600">
|
||||||
|
{isPremium ? (
|
||||||
|
<span className="text-green-600">🌟 付費用戶:無限制使用</span>
|
||||||
|
) : (
|
||||||
|
<span className={usageCount >= 4 ? 'text-red-600' : usageCount >= 3 ? 'text-yellow-600' : 'text-gray-600'}>
|
||||||
|
免費用戶:已使用 {usageCount}/5 次 (3小時內)
|
||||||
|
{usageCount >= 5 && <span className="block text-red-500 mt-1">已達上限,請稍後再試</span>}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 句子分析視圖 */
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-3xl font-bold">語法檢查 + 高價值標記結果</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAnalysisView(false)}
|
||||||
|
className="text-gray-600 hover:text-gray-900"
|
||||||
|
>
|
||||||
|
← 返回
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 語法修正面板 */}
|
||||||
|
{grammarCorrection && (
|
||||||
|
<GrammarCorrectionPanel
|
||||||
|
correction={grammarCorrection}
|
||||||
|
onAcceptCorrection={handleAcceptCorrection}
|
||||||
|
onRejectCorrection={handleRejectCorrection}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 原始句子 vs 分析句子 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">句子對比</h2>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 mb-2">📝 用戶輸入</h3>
|
||||||
|
<div className="p-3 bg-gray-50 rounded-lg border">
|
||||||
|
<div className="text-base">{textInput}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 mb-2">🎯 分析基礎</h3>
|
||||||
|
<div className="p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
|
<div className="text-base font-medium">{finalText}</div>
|
||||||
|
{finalText !== textInput && (
|
||||||
|
<div className="text-xs text-blue-600 mt-1">✨ 已修正語法錯誤</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 mb-2">📖 整句意思</h3>
|
||||||
|
<div className="text-gray-700 leading-relaxed p-3 bg-gray-50 rounded-lg">
|
||||||
|
{sentenceMeaning}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 互動式文字 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">智能高價值標記 - 點擊查詢單字</h2>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gradient-to-r from-blue-50 to-green-50 rounded-lg border border-blue-200 mb-4">
|
||||||
|
<p className="text-sm text-blue-800 mb-3">
|
||||||
|
<strong>💡 基於{finalText !== textInput ? '修正後' : '原始'}句子的智能分析:</strong>
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="px-2 py-1 bg-yellow-100 border-2 border-yellow-400 rounded">片語⭐</div>
|
||||||
|
<span>高價值片語(免費)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="px-2 py-1 bg-green-100 border-2 border-green-400 rounded">單字⭐</div>
|
||||||
|
<span>高價值單字(免費)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="px-2 py-1 border-b border-blue-300">普通</div>
|
||||||
|
<span>普通單字(扣1次)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
|
||||||
|
<ClickableTextV2
|
||||||
|
text={finalText}
|
||||||
|
analysis={sentenceAnalysis}
|
||||||
|
remainingUsage={5 - usageCount}
|
||||||
|
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
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按鈕 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAnalysisView(false)}
|
||||||
|
className="flex-1 bg-gray-200 text-gray-700 py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
🔄 分析新句子
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => alert('詞卡生成功能開發中...')}
|
||||||
|
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
📖 生成詞卡
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,389 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ClickableText } from '@/components/ClickableText'
|
||||||
|
|
||||||
|
export default function GenerateDemoPage() {
|
||||||
|
const [mode, setMode] = useState<'manual' | 'screenshot'>('manual')
|
||||||
|
const [textInput, setTextInput] = useState('')
|
||||||
|
const [extractionType, setExtractionType] = useState<'vocabulary' | 'smart'>('vocabulary')
|
||||||
|
const [cardCount, setCardCount] = useState(10)
|
||||||
|
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||||
|
const [showAnalysisView, setShowAnalysisView] = useState(false)
|
||||||
|
const [sentenceAnalysis, setSentenceAnalysis] = useState<any>(null)
|
||||||
|
const [sentenceMeaning, setSentenceMeaning] = useState('')
|
||||||
|
const [usageCount, setUsageCount] = useState(0)
|
||||||
|
const [isPremium] = useState(false)
|
||||||
|
|
||||||
|
// 模擬分析後的句子資料
|
||||||
|
const mockSentenceAnalysis = {
|
||||||
|
meaning: "他在我們的會議中提出了這件事,但沒有人同意。這句話表達了在會議中有人提出某個議題或想法,但得不到其他與會者的認同。",
|
||||||
|
words: {
|
||||||
|
"he": {
|
||||||
|
word: "he",
|
||||||
|
translation: "他",
|
||||||
|
definition: "Used to refer to a male person or animal",
|
||||||
|
partOfSpeech: "pronoun",
|
||||||
|
pronunciation: "/hiː/",
|
||||||
|
synonyms: ["him", "that man"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"brought": {
|
||||||
|
word: "brought",
|
||||||
|
translation: "帶來、提出",
|
||||||
|
definition: "Past tense of bring; to take or carry something to a place",
|
||||||
|
partOfSpeech: "verb",
|
||||||
|
pronunciation: "/brɔːt/",
|
||||||
|
synonyms: ["carried", "took", "delivered"],
|
||||||
|
isPhrase: true,
|
||||||
|
phraseInfo: {
|
||||||
|
phrase: "bring up",
|
||||||
|
meaning: "提出(話題)、養育",
|
||||||
|
warning: "在這個句子中,\"brought up\" 是一個片語,意思是\"提出話題\",而不是單純的\"帶來\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"this": {
|
||||||
|
word: "this",
|
||||||
|
translation: "這個",
|
||||||
|
definition: "Used to indicate something near or just mentioned",
|
||||||
|
partOfSpeech: "pronoun",
|
||||||
|
pronunciation: "/ðɪs/",
|
||||||
|
synonyms: ["that", "it"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"thing": {
|
||||||
|
word: "thing",
|
||||||
|
translation: "事情、東西",
|
||||||
|
definition: "An object, fact, or situation",
|
||||||
|
partOfSpeech: "noun",
|
||||||
|
pronunciation: "/θɪŋ/",
|
||||||
|
synonyms: ["object", "matter", "item"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
word: "up",
|
||||||
|
translation: "向上",
|
||||||
|
definition: "Toward a higher place or position",
|
||||||
|
partOfSpeech: "adverb",
|
||||||
|
pronunciation: "/ʌp/",
|
||||||
|
synonyms: ["upward", "above"],
|
||||||
|
isPhrase: true,
|
||||||
|
phraseInfo: {
|
||||||
|
phrase: "bring up",
|
||||||
|
meaning: "提出(話題)、養育",
|
||||||
|
warning: "\"up\" 在這裡是片語 \"bring up\" 的一部分,不是單獨的\"向上\"的意思"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"during": {
|
||||||
|
word: "during",
|
||||||
|
translation: "在...期間",
|
||||||
|
definition: "Throughout the course or duration of",
|
||||||
|
partOfSpeech: "preposition",
|
||||||
|
pronunciation: "/ˈdjʊərɪŋ/",
|
||||||
|
synonyms: ["throughout", "while"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"our": {
|
||||||
|
word: "our",
|
||||||
|
translation: "我們的",
|
||||||
|
definition: "Belonging to us",
|
||||||
|
partOfSpeech: "pronoun",
|
||||||
|
pronunciation: "/aʊər/",
|
||||||
|
synonyms: ["ours"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"meeting": {
|
||||||
|
word: "meeting",
|
||||||
|
translation: "會議",
|
||||||
|
definition: "An organized gathering of people for discussion",
|
||||||
|
partOfSpeech: "noun",
|
||||||
|
pronunciation: "/ˈmiːtɪŋ/",
|
||||||
|
synonyms: ["conference", "assembly", "gathering"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"and": {
|
||||||
|
word: "and",
|
||||||
|
translation: "和、而且",
|
||||||
|
definition: "Used to connect words or clauses",
|
||||||
|
partOfSpeech: "conjunction",
|
||||||
|
pronunciation: "/ænd/",
|
||||||
|
synonyms: ["plus", "also"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"no": {
|
||||||
|
word: "no",
|
||||||
|
translation: "沒有",
|
||||||
|
definition: "Not any; not one",
|
||||||
|
partOfSpeech: "determiner",
|
||||||
|
pronunciation: "/nəʊ/",
|
||||||
|
synonyms: ["none", "zero"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"one": {
|
||||||
|
word: "one",
|
||||||
|
translation: "一個人、任何人",
|
||||||
|
definition: "A single person or thing",
|
||||||
|
partOfSpeech: "pronoun",
|
||||||
|
pronunciation: "/wʌn/",
|
||||||
|
synonyms: ["someone", "anybody"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"agreed": {
|
||||||
|
word: "agreed",
|
||||||
|
translation: "同意",
|
||||||
|
definition: "Past tense of agree; to have the same opinion",
|
||||||
|
partOfSpeech: "verb",
|
||||||
|
pronunciation: "/əˈɡriːd/",
|
||||||
|
synonyms: ["consented", "accepted", "approved"],
|
||||||
|
isPhrase: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 處理句子分析
|
||||||
|
const handleAnalyzeSentence = async () => {
|
||||||
|
if (!textInput.trim()) return
|
||||||
|
|
||||||
|
// 檢查使用次數限制
|
||||||
|
if (!isPremium && usageCount >= 5) {
|
||||||
|
alert('❌ 免費用戶 3 小時內只能分析 5 次句子,請稍後再試或升級到付費版本')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAnalyzing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模擬 API 調用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
setSentenceAnalysis(mockSentenceAnalysis.words)
|
||||||
|
setSentenceMeaning(mockSentenceAnalysis.meaning)
|
||||||
|
setShowAnalysisView(true)
|
||||||
|
setUsageCount(prev => prev + 1)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error analyzing sentence:', error)
|
||||||
|
alert('分析句子時發生錯誤,請稍後再試')
|
||||||
|
} finally {
|
||||||
|
setIsAnalyzing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="bg-white shadow-sm">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<h1 className="text-2xl font-bold text-blue-600">DramaLing</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span className="text-gray-600">Demo 版本</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{!showAnalysisView ? (
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold mb-8">AI 智能生成詞卡 - 演示版</h1>
|
||||||
|
|
||||||
|
{/* Input Mode Selection */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">原始例句類型</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setMode('manual')}
|
||||||
|
className={`p-4 rounded-lg border-2 transition-all ${
|
||||||
|
mode === 'manual'
|
||||||
|
? 'border-blue-600 bg-blue-50'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-2xl mb-2">✍️</div>
|
||||||
|
<div className="font-semibold">手動輸入</div>
|
||||||
|
<div className="text-sm text-gray-600 mt-1">貼上或輸入英文文本</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMode('screenshot')}
|
||||||
|
disabled={!isPremium}
|
||||||
|
className={`p-4 rounded-lg border-2 transition-all relative ${
|
||||||
|
mode === 'screenshot'
|
||||||
|
? 'border-blue-600 bg-blue-50'
|
||||||
|
: isPremium
|
||||||
|
? 'border-gray-200 hover:border-gray-300'
|
||||||
|
: 'border-gray-200 bg-gray-100 cursor-not-allowed opacity-60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-2xl mb-2">📷</div>
|
||||||
|
<div className="font-semibold">影劇截圖</div>
|
||||||
|
<div className="text-sm text-gray-600 mt-1">上傳影劇截圖 (Phase 2)</div>
|
||||||
|
{!isPremium && (
|
||||||
|
<div className="absolute top-2 right-2 px-2 py-1 bg-yellow-100 text-yellow-700 text-xs rounded-full">
|
||||||
|
訂閱功能
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Input */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold mb-4">輸入英文文本</h2>
|
||||||
|
<textarea
|
||||||
|
value={textInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
if (mode === 'manual' && value.length > 50) {
|
||||||
|
return // 阻止輸入超過50字
|
||||||
|
}
|
||||||
|
setTextInput(value)
|
||||||
|
}}
|
||||||
|
placeholder={mode === 'manual'
|
||||||
|
? "輸入英文句子(最多50字)..."
|
||||||
|
: "貼上您想要學習的英文文本,例如影劇對話、文章段落..."
|
||||||
|
}
|
||||||
|
className={`w-full h-40 px-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent outline-none resize-none ${
|
||||||
|
mode === 'manual' && textInput.length >= 45 ? 'border-yellow-400' :
|
||||||
|
mode === 'manual' && textInput.length >= 50 ? 'border-red-400' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="mt-2 flex justify-between text-sm">
|
||||||
|
<span className={`${
|
||||||
|
mode === 'manual' && textInput.length >= 45 ? 'text-yellow-600' :
|
||||||
|
mode === 'manual' && textInput.length >= 50 ? 'text-red-600' : 'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{mode === 'manual' ? `最多 50 字元 • 目前:${textInput.length} 字元` : `最多 5000 字元 • 目前:${textInput.length} 字元`}
|
||||||
|
</span>
|
||||||
|
{mode === 'manual' && textInput.length > 40 && (
|
||||||
|
<span className={textInput.length >= 50 ? 'text-red-600' : 'text-yellow-600'}>
|
||||||
|
{textInput.length >= 50 ? '已達上限!' : `還可輸入 ${50 - textInput.length} 字元`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 預設示例 */}
|
||||||
|
{!textInput && (
|
||||||
|
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-sm text-gray-700 mb-2">
|
||||||
|
<strong>示例句子:</strong>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setTextInput("He brought this thing up during our meeting and no one agreed.")}
|
||||||
|
className="text-sm text-blue-600 hover:text-blue-800 bg-blue-50 px-3 py-1 rounded border border-blue-200"
|
||||||
|
>
|
||||||
|
點擊使用示例:He brought this thing up during our meeting and no one agreed.
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 新的按鈕區域 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 分析句子按鈕 */}
|
||||||
|
<button
|
||||||
|
onClick={handleAnalyzeSentence}
|
||||||
|
disabled={isAnalyzing || (mode === 'manual' && (!textInput || textInput.length > 50)) || (mode === 'screenshot')}
|
||||||
|
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isAnalyzing ? (
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
正在分析句子中...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'🔍 分析句子(點擊查詢單字)'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 使用次數顯示 */}
|
||||||
|
<div className="text-center text-sm text-gray-600">
|
||||||
|
{isPremium ? (
|
||||||
|
<span className="text-green-600">🌟 付費用戶:無限制使用</span>
|
||||||
|
) : (
|
||||||
|
<span className={usageCount >= 4 ? 'text-red-600' : usageCount >= 3 ? 'text-yellow-600' : 'text-gray-600'}>
|
||||||
|
免費用戶:已使用 {usageCount}/5 次 (3小時內)
|
||||||
|
{usageCount >= 5 && <span className="block text-red-500 mt-1">已達上限,請稍後再試</span>}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 句子分析視圖 */
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-3xl font-bold">句子分析結果</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAnalysisView(false)}
|
||||||
|
className="text-gray-600 hover:text-gray-900"
|
||||||
|
>
|
||||||
|
← 返回
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 原始句子顯示 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">原始句子</h2>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg mb-4">
|
||||||
|
<div className="text-lg leading-relaxed">
|
||||||
|
{textInput}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-base font-semibold mb-2">整句意思</h3>
|
||||||
|
<div className="text-gray-700 leading-relaxed">
|
||||||
|
{sentenceMeaning}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 互動式文字 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">點擊查詢單字意思</h2>
|
||||||
|
<div className="p-4 bg-blue-50 rounded-lg border-l-4 border-blue-400 mb-4">
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
💡 <strong>使用說明:</strong>點擊下方句子中的任何單字,可以立即查看詳細意思。
|
||||||
|
黃色背景表示該單字屬於片語或俚語,會優先顯示片語意思。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
|
||||||
|
<ClickableText
|
||||||
|
text={textInput}
|
||||||
|
analysis={sentenceAnalysis}
|
||||||
|
onWordClick={(word, analysis) => {
|
||||||
|
console.log('Clicked word:', word, analysis)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按鈕 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAnalysisView(false)}
|
||||||
|
className="flex-1 bg-gray-200 text-gray-700 py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
🔄 分析新句子
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => alert('詞卡生成功能開發中...')}
|
||||||
|
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
📖 生成詞卡
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,82 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ClickableText } from '@/components/ClickableText'
|
||||||
|
|
||||||
|
const mockAnalysis = {
|
||||||
|
"brought": {
|
||||||
|
word: "brought",
|
||||||
|
translation: "帶來、提出",
|
||||||
|
definition: "Past tense of bring; to take or carry something to a place",
|
||||||
|
partOfSpeech: "verb",
|
||||||
|
pronunciation: "/brɔːt/",
|
||||||
|
synonyms: ["carried", "took", "delivered"],
|
||||||
|
isPhrase: true,
|
||||||
|
phraseInfo: {
|
||||||
|
phrase: "bring up",
|
||||||
|
meaning: "提出(話題)、養育",
|
||||||
|
warning: "在這個句子中,\"brought up\" 是一個片語,意思是\"提出話題\",而不是單純的\"帶來\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"this": {
|
||||||
|
word: "this",
|
||||||
|
translation: "這個",
|
||||||
|
definition: "Used to indicate something near or just mentioned",
|
||||||
|
partOfSpeech: "pronoun",
|
||||||
|
pronunciation: "/ðɪs/",
|
||||||
|
synonyms: ["that", "it"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"thing": {
|
||||||
|
word: "thing",
|
||||||
|
translation: "事情、東西",
|
||||||
|
definition: "An object, fact, or situation",
|
||||||
|
partOfSpeech: "noun",
|
||||||
|
pronunciation: "/θɪŋ/",
|
||||||
|
synonyms: ["object", "matter", "item"],
|
||||||
|
isPhrase: false
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
word: "up",
|
||||||
|
translation: "向上",
|
||||||
|
definition: "Toward a higher place or position",
|
||||||
|
partOfSpeech: "adverb",
|
||||||
|
pronunciation: "/ʌp/",
|
||||||
|
synonyms: ["upward", "above"],
|
||||||
|
isPhrase: true,
|
||||||
|
phraseInfo: {
|
||||||
|
phrase: "bring up",
|
||||||
|
meaning: "提出(話題)、養育",
|
||||||
|
warning: "\"up\" 在這裡是片語 \"bring up\" 的一部分,不是單獨的\"向上\"的意思"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TestPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-8">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold mb-8">互動式單字查詢測試</h1>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">點擊查詢單字意思</h2>
|
||||||
|
<div className="p-4 bg-blue-50 rounded-lg border-l-4 border-blue-400 mb-4">
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
💡 <strong>使用說明:</strong>點擊下方句子中的任何單字,可以立即查看詳細意思。
|
||||||
|
黃色背景表示該單字屬於片語或俚語,會優先顯示片語意思。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
|
||||||
|
<ClickableText
|
||||||
|
text="He brought this thing up during our meeting."
|
||||||
|
analysis={mockAnalysis}
|
||||||
|
onWordClick={(word, analysis) => {
|
||||||
|
console.log('Clicked word:', word, analysis)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
// 模擬分析後的詞彙資料
|
||||||
|
interface WordAnalysis {
|
||||||
|
word: string
|
||||||
|
translation: string
|
||||||
|
definition: string
|
||||||
|
partOfSpeech: string
|
||||||
|
pronunciation: string
|
||||||
|
synonyms: string[]
|
||||||
|
isPhrase: boolean
|
||||||
|
phraseInfo?: {
|
||||||
|
phrase: string
|
||||||
|
meaning: string
|
||||||
|
warning: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClickableTextProps {
|
||||||
|
text: string
|
||||||
|
analysis?: Record<string, WordAnalysis>
|
||||||
|
onWordClick?: (word: string, analysis: WordAnalysis) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClickableText({ text, analysis, onWordClick }: ClickableTextProps) {
|
||||||
|
const [selectedWord, setSelectedWord] = useState<string | null>(null)
|
||||||
|
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
// 將文字分割成單字
|
||||||
|
const words = text.split(/(\s+|[.,!?;:])/g).filter(word => word.trim())
|
||||||
|
|
||||||
|
const handleWordClick = (word: string, event: React.MouseEvent) => {
|
||||||
|
const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '')
|
||||||
|
const wordAnalysis = analysis?.[cleanWord]
|
||||||
|
|
||||||
|
if (wordAnalysis) {
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect()
|
||||||
|
setPopupPosition({
|
||||||
|
x: rect.left + rect.width / 2,
|
||||||
|
y: rect.top - 10
|
||||||
|
})
|
||||||
|
setSelectedWord(cleanWord)
|
||||||
|
onWordClick?.(cleanWord, wordAnalysis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePopup = () => {
|
||||||
|
setSelectedWord(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 hover:bg-blue-100 rounded px-1"
|
||||||
|
|
||||||
|
if (wordAnalysis.isPhrase) {
|
||||||
|
return `${baseClass} bg-yellow-100 border-b-2 border-yellow-400 hover:bg-yellow-200`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseClass} hover:bg-blue-200 border-b border-blue-300`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{/* 點擊區域遮罩 */}
|
||||||
|
{selectedWord && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-10"
|
||||||
|
onClick={closePopup}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文字內容 */}
|
||||||
|
<div className="text-lg leading-relaxed">
|
||||||
|
{words.map((word, index) => {
|
||||||
|
if (word.trim() === '') return <span key={index}>{word}</span>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={getWordClass(word)}
|
||||||
|
onClick={(e) => handleWordClick(word, e)}
|
||||||
|
>
|
||||||
|
{word}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 彈出視窗 */}
|
||||||
|
{selectedWord && analysis?.[selectedWord] && (
|
||||||
|
<div
|
||||||
|
className="fixed z-20 bg-white border border-gray-300 rounded-lg shadow-lg p-4 w-80 max-w-sm"
|
||||||
|
style={{
|
||||||
|
left: `${popupPosition.x}px`,
|
||||||
|
top: `${popupPosition.y}px`,
|
||||||
|
transform: 'translate(-50%, -100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 標題 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
|
{analysis[selectedWord].word}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={closePopup}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 片語警告 */}
|
||||||
|
{analysis[selectedWord].isPhrase && analysis[selectedWord].phraseInfo && (
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="text-yellow-600 text-lg">⚠️</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-yellow-800">
|
||||||
|
注意:這個單字屬於片語
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-yellow-700 mt-1">
|
||||||
|
<strong>片語:</strong>{analysis[selectedWord].phraseInfo.phrase}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-yellow-700">
|
||||||
|
<strong>意思:</strong>{analysis[selectedWord].phraseInfo.meaning}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 詞性和發音 */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-sm bg-gray-100 px-2 py-1 rounded">
|
||||||
|
{analysis[selectedWord].partOfSpeech}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{analysis[selectedWord].pronunciation}
|
||||||
|
</span>
|
||||||
|
<button className="text-blue-600 hover:text-blue-800">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 翻譯 */}
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-700">翻譯</div>
|
||||||
|
<div className="text-base text-gray-900">{analysis[selectedWord].translation}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 定義 */}
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-700">定義</div>
|
||||||
|
<div className="text-sm text-gray-600">{analysis[selectedWord].definition}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 同義詞 */}
|
||||||
|
{analysis[selectedWord].synonyms.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-700">同義詞</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{analysis[selectedWord].synonyms.map((synonym, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full"
|
||||||
|
>
|
||||||
|
{synonym}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,375 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
// 更新的詞彙分析介面
|
||||||
|
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
|
||||||
|
costIncurred?: number // 點擊此詞彙的成本
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClickableTextProps {
|
||||||
|
text: string
|
||||||
|
analysis?: Record<string, WordAnalysis>
|
||||||
|
highValueWords?: string[] // 高價值詞彙列表
|
||||||
|
phrasesDetected?: Array<{
|
||||||
|
phrase: string
|
||||||
|
words: string[]
|
||||||
|
colorCode: string
|
||||||
|
}>
|
||||||
|
onWordClick?: (word: string, analysis: WordAnalysis) => void
|
||||||
|
onWordCostConfirm?: (word: string, cost: number) => Promise<boolean> // 收費確認
|
||||||
|
remainingUsage?: number // 剩餘使用次數
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClickableTextV2({
|
||||||
|
text,
|
||||||
|
analysis,
|
||||||
|
highValueWords = [],
|
||||||
|
phrasesDetected = [],
|
||||||
|
onWordClick,
|
||||||
|
onWordCostConfirm,
|
||||||
|
remainingUsage = 5
|
||||||
|
}: ClickableTextProps) {
|
||||||
|
const [selectedWord, setSelectedWord] = useState<string | null>(null)
|
||||||
|
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 })
|
||||||
|
const [showCostConfirm, setShowCostConfirm] = useState<{
|
||||||
|
word: string
|
||||||
|
cost: number
|
||||||
|
position: { x: number, y: number }
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
// 將文字分割成單字
|
||||||
|
const words = text.split(/(\s+|[.,!?;:])/g).filter(word => word.trim())
|
||||||
|
|
||||||
|
const handleWordClick = async (word: string, event: React.MouseEvent) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查是否為高價值詞彙(免費)
|
||||||
|
if (wordAnalysis.isHighValue) {
|
||||||
|
setPopupPosition(position)
|
||||||
|
setSelectedWord(cleanWord)
|
||||||
|
onWordClick?.(cleanWord, wordAnalysis)
|
||||||
|
} else {
|
||||||
|
// 低價值詞彙需要收費確認
|
||||||
|
setShowCostConfirm({
|
||||||
|
word: cleanWord,
|
||||||
|
cost: 1,
|
||||||
|
position
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCostConfirm = async () => {
|
||||||
|
if (!showCostConfirm) return
|
||||||
|
|
||||||
|
const confirmed = await onWordCostConfirm?.(showCostConfirm.word, showCostConfirm.cost)
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
const wordAnalysis = analysis?.[showCostConfirm.word]
|
||||||
|
if (wordAnalysis) {
|
||||||
|
setPopupPosition(showCostConfirm.position)
|
||||||
|
setSelectedWord(showCostConfirm.word)
|
||||||
|
onWordClick?.(showCostConfirm.word, wordAnalysis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowCostConfirm(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePopup = () => {
|
||||||
|
setSelectedWord(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
// 高價值片語(黃色系)
|
||||||
|
if (wordAnalysis.isHighValue && wordAnalysis.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 (wordAnalysis.isHighValue && !wordAnalysis.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`
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderWordWithStar = (word: string, className: string) => {
|
||||||
|
const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '')
|
||||||
|
const wordAnalysis = analysis?.[cleanWord]
|
||||||
|
const isHighValue = wordAnalysis?.isHighValue
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`${className} ${isHighValue ? 'relative' : ''}`}
|
||||||
|
onClick={(e) => handleWordClick(word, e)}
|
||||||
|
>
|
||||||
|
{word}
|
||||||
|
{isHighValue && (
|
||||||
|
<span className="absolute -top-1 -right-1 text-xs">⭐</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{/* 點擊區域遮罩 */}
|
||||||
|
{selectedWord && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-10"
|
||||||
|
onClick={closePopup}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文字內容 */}
|
||||||
|
<div className="text-lg leading-relaxed">
|
||||||
|
{words.map((word, index) => {
|
||||||
|
if (word.trim() === '') return <span key={index}>{word}</span>
|
||||||
|
|
||||||
|
return renderWordWithStar(word, getWordClass(word))
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 單字資訊彈窗 */}
|
||||||
|
{selectedWord && analysis?.[selectedWord] && (
|
||||||
|
<div
|
||||||
|
className="fixed z-20 bg-white border border-gray-300 rounded-lg shadow-lg p-4 w-80 max-w-sm"
|
||||||
|
style={{
|
||||||
|
left: `${popupPosition.x}px`,
|
||||||
|
top: `${popupPosition.y}px`,
|
||||||
|
transform: 'translate(-50%, -100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 標題 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
|
{analysis[selectedWord].word}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={closePopup}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 高價值標記 */}
|
||||||
|
{analysis[selectedWord].isHighValue && (
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-green-600 text-lg">⭐</div>
|
||||||
|
<div className="text-sm font-medium text-green-800">
|
||||||
|
高價值詞彙(免費查詢)
|
||||||
|
</div>
|
||||||
|
<div className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
|
||||||
|
學習價值:{analysis[selectedWord].learningPriority === 'high' ? '⭐⭐⭐⭐⭐' :
|
||||||
|
analysis[selectedWord].learningPriority === 'medium' ? '⭐⭐⭐' : '⭐'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 片語警告 */}
|
||||||
|
{analysis[selectedWord].isPhrase && analysis[selectedWord].phraseInfo && (
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="text-yellow-600 text-lg">⚠️</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-yellow-800">
|
||||||
|
注意:這個單字屬於片語!
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-yellow-700 mt-1">
|
||||||
|
<strong>片語:</strong>{analysis[selectedWord].phraseInfo.phrase}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-yellow-700">
|
||||||
|
<strong>意思:</strong>{analysis[selectedWord].phraseInfo.meaning}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-yellow-600 mt-2 italic">
|
||||||
|
{analysis[selectedWord].phraseInfo.warning}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 詞性和發音 */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-sm bg-gray-100 px-2 py-1 rounded">
|
||||||
|
{analysis[selectedWord].partOfSpeech}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{analysis[selectedWord].pronunciation}
|
||||||
|
</span>
|
||||||
|
<button className="text-blue-600 hover:text-blue-800">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 翻譯 */}
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-700">翻譯</div>
|
||||||
|
<div className="text-base text-gray-900">{analysis[selectedWord].translation}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 定義 */}
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-700">定義</div>
|
||||||
|
<div className="text-sm text-gray-600">{analysis[selectedWord].definition}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 同義詞 */}
|
||||||
|
{analysis[selectedWord].synonyms.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-700">同義詞</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{analysis[selectedWord].synonyms.map((synonym, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full"
|
||||||
|
>
|
||||||
|
{synonym}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 反義詞 */}
|
||||||
|
{analysis[selectedWord].antonyms && analysis[selectedWord].antonyms.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-700">反義詞</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{analysis[selectedWord].antonyms.map((antonym, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="text-xs bg-red-100 text-red-700 px-2 py-1 rounded-full"
|
||||||
|
>
|
||||||
|
{antonym}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 難度等級 */}
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-700">難度等級</div>
|
||||||
|
<div className="inline-flex items-center gap-1 mt-1">
|
||||||
|
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||||
|
analysis[selectedWord].difficultyLevel === 'A1' || analysis[selectedWord].difficultyLevel === 'A2' ? 'bg-green-100 text-green-700' :
|
||||||
|
analysis[selectedWord].difficultyLevel === 'B1' || analysis[selectedWord].difficultyLevel === 'B2' ? 'bg-yellow-100 text-yellow-700' :
|
||||||
|
'bg-red-100 text-red-700'
|
||||||
|
}`}>
|
||||||
|
CEFR {analysis[selectedWord].difficultyLevel}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
({analysis[selectedWord].difficultyLevel === 'A1' || analysis[selectedWord].difficultyLevel === 'A2' ? '基礎' :
|
||||||
|
analysis[selectedWord].difficultyLevel === 'B1' || analysis[selectedWord].difficultyLevel === 'B2' ? '中級' : '高級'})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 收費確認對話框 */}
|
||||||
|
{showCostConfirm && (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 z-10" onClick={() => setShowCostConfirm(null)} />
|
||||||
|
<div
|
||||||
|
className="fixed z-20 bg-white border border-gray-300 rounded-lg shadow-lg p-4 w-72"
|
||||||
|
style={{
|
||||||
|
left: `${showCostConfirm.position.x}px`,
|
||||||
|
top: `${showCostConfirm.position.y}px`,
|
||||||
|
transform: 'translate(-50%, -100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
|
{showCostConfirm.word}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCostConfirm(null)}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="text-orange-600 text-lg">💰</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-orange-800">
|
||||||
|
低價值詞彙(需消耗額度)
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-orange-700 mt-1">
|
||||||
|
此查詢將消耗 <strong>{showCostConfirm.cost} 次</strong> 使用額度
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-orange-600 mt-1">
|
||||||
|
剩餘額度:<strong>{remainingUsage}</strong> 次
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleCostConfirm}
|
||||||
|
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
✅ 確認查詢
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCostConfirm(null)}
|
||||||
|
className="flex-1 bg-gray-200 text-gray-700 py-2 px-4 rounded-lg text-sm font-medium hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
❌ 取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
interface GrammarCorrection {
|
||||||
|
hasErrors: boolean
|
||||||
|
originalText: string
|
||||||
|
correctedText: string | null
|
||||||
|
corrections: Array<{
|
||||||
|
position: { start: number; end: number }
|
||||||
|
errorType: string
|
||||||
|
original: string
|
||||||
|
corrected: string
|
||||||
|
reason: string
|
||||||
|
severity: 'high' | 'medium' | 'low'
|
||||||
|
}>
|
||||||
|
confidenceScore: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GrammarCorrectionPanelProps {
|
||||||
|
correction: GrammarCorrection
|
||||||
|
onAcceptCorrection: () => void
|
||||||
|
onRejectCorrection: () => void
|
||||||
|
onManualEdit?: (text: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GrammarCorrectionPanel({
|
||||||
|
correction,
|
||||||
|
onAcceptCorrection,
|
||||||
|
onRejectCorrection,
|
||||||
|
onManualEdit
|
||||||
|
}: GrammarCorrectionPanelProps) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(true)
|
||||||
|
|
||||||
|
if (!correction.hasErrors) {
|
||||||
|
return (
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-green-600 text-lg">✅</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-green-800">語法檢查:無錯誤</div>
|
||||||
|
<div className="text-sm text-green-700">
|
||||||
|
您的句子語法正確,可以直接進行學習分析!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderHighlightedText = (text: string, corrections: typeof correction.corrections) => {
|
||||||
|
if (corrections.length === 0) return text
|
||||||
|
|
||||||
|
let result: React.ReactNode[] = []
|
||||||
|
let lastIndex = 0
|
||||||
|
|
||||||
|
corrections.forEach((corr, index) => {
|
||||||
|
// 添加錯誤前的正常文字
|
||||||
|
if (corr.position.start > lastIndex) {
|
||||||
|
result.push(
|
||||||
|
<span key={`normal-${index}`}>
|
||||||
|
{text.slice(lastIndex, corr.position.start)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加錯誤文字(紅色標記)
|
||||||
|
result.push(
|
||||||
|
<span
|
||||||
|
key={`error-${index}`}
|
||||||
|
className="relative bg-red-100 border-b-2 border-red-400 px-1 rounded"
|
||||||
|
title={`錯誤:${corr.reason}`}
|
||||||
|
>
|
||||||
|
{corr.original}
|
||||||
|
<span className="absolute -top-1 -right-1 text-xs text-red-600">❌</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
lastIndex = corr.position.end
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加最後剩餘的正常文字
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
result.push(
|
||||||
|
<span key="final">
|
||||||
|
{text.slice(lastIndex)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{result}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderCorrectedText = (text: string, corrections: typeof correction.corrections) => {
|
||||||
|
if (corrections.length === 0 || !text) return text
|
||||||
|
|
||||||
|
let result: React.ReactNode[] = []
|
||||||
|
let lastIndex = 0
|
||||||
|
let offset = 0 // 修正後文字長度變化的偏移量
|
||||||
|
|
||||||
|
corrections.forEach((corr, index) => {
|
||||||
|
const adjustedStart = corr.position.start + offset
|
||||||
|
const originalLength = corr.original.length
|
||||||
|
const correctedLength = corr.corrected.length
|
||||||
|
|
||||||
|
// 添加修正前的正常文字
|
||||||
|
if (adjustedStart > lastIndex) {
|
||||||
|
result.push(
|
||||||
|
<span key={`normal-${index}`}>
|
||||||
|
{text.slice(lastIndex, adjustedStart)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加修正後的文字(綠色標記)
|
||||||
|
result.push(
|
||||||
|
<span
|
||||||
|
key={`corrected-${index}`}
|
||||||
|
className="relative bg-green-100 border-b-2 border-green-400 px-1 rounded font-medium"
|
||||||
|
title={`修正:${corr.reason}`}
|
||||||
|
>
|
||||||
|
{corr.corrected}
|
||||||
|
<span className="absolute -top-1 -right-1 text-xs text-green-600">✅</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
lastIndex = adjustedStart + correctedLength
|
||||||
|
offset += (correctedLength - originalLength)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加最後剩餘的正常文字
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
result.push(
|
||||||
|
<span key="final">
|
||||||
|
{text.slice(lastIndex)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{result}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSeverityColor = (severity: string) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'high':
|
||||||
|
return 'bg-red-100 text-red-700 border-red-300'
|
||||||
|
case 'medium':
|
||||||
|
return 'bg-yellow-100 text-yellow-700 border-yellow-300'
|
||||||
|
case 'low':
|
||||||
|
return 'bg-blue-100 text-blue-700 border-blue-300'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-700 border-gray-300'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white border border-red-200 rounded-lg shadow-sm mb-6">
|
||||||
|
{/* 標題區 */}
|
||||||
|
<div className="bg-red-50 px-6 py-4 border-b border-red-200 rounded-t-lg">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-red-600 text-xl">❌</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-red-800">
|
||||||
|
語法檢查:發現 {correction.corrections.length} 個錯誤
|
||||||
|
</h3>
|
||||||
|
<div className="text-sm text-red-700">
|
||||||
|
建議修正後再進行學習,以確保學習內容的正確性
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="text-red-600 hover:text-red-800"
|
||||||
|
>
|
||||||
|
{isExpanded ? '收起' : '展開'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* 原始句子 */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-800 mb-2">📝 用戶輸入:</h4>
|
||||||
|
<div className="p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<div className="text-lg leading-relaxed">
|
||||||
|
{renderHighlightedText(correction.originalText, correction.corrections)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 修正建議 */}
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<h4 className="font-medium text-green-800 mb-3 flex items-center gap-2">
|
||||||
|
<span className="text-lg">🔧</span>
|
||||||
|
建議修正:
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="p-4 bg-white rounded-lg border border-green-300 mb-4">
|
||||||
|
<div className="text-lg leading-relaxed">
|
||||||
|
{correction.correctedText && renderCorrectedText(correction.correctedText, correction.corrections)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 修正說明列表 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h5 className="font-medium text-green-800">📋 修正說明:</h5>
|
||||||
|
{correction.corrections.map((corr, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`p-3 rounded-lg border ${getSeverityColor(corr.severity)}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-white flex items-center justify-center text-sm font-bold">
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium mb-1">
|
||||||
|
"{corr.original}" → "{corr.corrected}"
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
{corr.reason}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs mt-1 opacity-75">
|
||||||
|
錯誤類型:{corr.errorType} | 嚴重程度:{corr.severity}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 信心度 */}
|
||||||
|
<div className="mt-4 text-sm text-green-700">
|
||||||
|
🎯 修正信心度:{(correction.confidenceScore * 100).toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按鈕 */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={onAcceptCorrection}
|
||||||
|
className="flex-1 bg-green-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-green-700 transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="text-lg">✅</span>
|
||||||
|
使用修正版本
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onRejectCorrection}
|
||||||
|
className="flex-1 bg-gray-200 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-300 transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="text-lg">❌</span>
|
||||||
|
保持原始版本
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 學習提醒 */}
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="text-blue-600 text-lg">💡</div>
|
||||||
|
<div className="text-sm text-blue-800">
|
||||||
|
<strong>學習建議:</strong>
|
||||||
|
建議使用修正版本進行學習,這樣可以確保您學到正確的英語表達方式。
|
||||||
|
所有後續的詞彙分析都將基於修正後的句子進行。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue