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
|
||||
|
||||
#### 1.2.2 AI 生成規格
|
||||
- **生成方式**
|
||||
1. 原始例句類型
|
||||
- 影劇截圖(訂閱功能, phase2)
|
||||
- 手動輸入
|
||||
2. 詞彙萃取:把每個單字拿去查詢字典API,並標記CEFR
|
||||
3. 智能萃取(訂閱功能):將原始例句拿去問AI有無常用片語或俚語,並直接生成相關詞彙內容
|
||||
- **原始例句輸入**
|
||||
- 輸入方式
|
||||
1. 影劇截圖(訂閱功能, phase2)
|
||||
2. 手動輸入
|
||||
- 輸入資料
|
||||
- 可接受多句子
|
||||
- 字數限制規則:
|
||||
- 若為手動輸入,則限定300字以內,在前端畫面做阻擋
|
||||
- 若為影劇截圖,則無300字限制
|
||||
|
||||
- **生成數量**
|
||||
- 預設:10個詞卡
|
||||
- 範圍:5-20個(用戶可調)
|
||||
- 免費用戶:
|
||||
- 無法自行生成例句圖,但若系統中匹配到現成例句圖,可直接使用
|
||||
- 每日學習數量無限制
|
||||
- 訂閱用戶:每天最多生成50張例句圖
|
||||
- **互動式單字查詢(低成本設計)**
|
||||
1. 預分析機制
|
||||
- 用戶輸入句子後,AI 一次性分析整句內容
|
||||
- 獲取原始例句意思
|
||||
- 識別具備高學習價值的片語/俚語/單字,並標記為高價值,並於當次直接生成具標記的項目內容詳情(參考「生成內容詳情」)
|
||||
- 分析結果存儲於快取中(避免重複 API 調用)
|
||||
- 當次操作扣除使用次數一次
|
||||
|
||||
2. 點擊查詢體驗
|
||||
- 句子顯示為可點擊的單字
|
||||
- 點擊對象
|
||||
- 若為高價值標記,則即時顯示意思(無延遲,讀取預分析資料),不扣除使用次數
|
||||
- 若非高價值標記,則拿當前點擊單字及當前句子,給AI分析並生成內容詳情,扣除使用次數一次
|
||||
- 片語/俚語特殊高亮顯示
|
||||
- 智能提醒:當單字屬於片語/俚語時,優先顯示片語意思並提醒
|
||||
- 若出現多筆片語/俚語需標記時,請使用不同顏色區分
|
||||
|
||||
3. 成本優化策略
|
||||
- **核心原則**:一句一次 API 調用,多次查詢零成本
|
||||
- 相同句子分析結果快取(24小時)
|
||||
- 常用單字基礎資訊本地快取
|
||||
- 預估 API 成本降低 80-95%
|
||||
|
||||
4. 收費策略(phase 2):
|
||||
- 免費用戶:5次/3小時
|
||||
- 付費用戶:無限制
|
||||
|
||||
- **生成內容詳情**
|
||||
- **原始例句**
|
||||
- 整體意思:不論原始例句是多句、一句、片段,就是將原始例句整體意思描述出來
|
||||
- 修正語法錯誤:若原始例句有語法錯誤,則進行修正,並說明修正原因,且後續學習內容皆以正確的版本進行
|
||||
|
||||
- **單字/片語**
|
||||
- 原形展示
|
||||
- 詞性標註(n./v./adj./adv./phrase/slang)
|
||||
|
|
@ -100,6 +126,9 @@
|
|||
- 例句中文翻譯
|
||||
- 重點標示(highlight目標詞)
|
||||
- 例句圖
|
||||
- 收費策略(phase 2):
|
||||
- 免費用戶:無法自行生成例句圖,但若系統中匹配到現成例句圖,可直接使用
|
||||
- 訂閱用戶:每天最多生成50張例句圖
|
||||
- 例句發音
|
||||
|
||||
- **生成後處理**
|
||||
|
|
@ -438,6 +467,11 @@
|
|||
**目標**:提升用戶體驗
|
||||
- ✅ 標籤系統
|
||||
- ✅ 搜尋篩選
|
||||
- ⬜ **互動式單字查詢系統**
|
||||
- 句子預分析 API 端點
|
||||
- 可點擊文字組件
|
||||
- 片語/俚語智能提醒
|
||||
- 快取機制實現
|
||||
- ✅ 進階統計圖表
|
||||
- ✅ 成就系統
|
||||
- ✅ 學習提醒
|
||||
|
|
@ -481,7 +515,9 @@
|
|||
- 內容版權問題
|
||||
|
||||
### 7.3 緩解措施
|
||||
- 實施 API 快取機制
|
||||
- 實施 API 快取機制(重點:單字查詢預分析快取)
|
||||
- 準備備用 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