feat: 重新設計AI生成頁面為統一界面

重大更新:
- 完全重新設計為上下流式統一界面,無需切換頁面
- 移除showAnalysisView狀態,左側輸入右側即時顯示結果
- 添加句子播放按鈕,支援整句語音播放
- 實現localStorage分析結果持久化,跳頁後保留內容
- 統一WordPopup所有區塊顏色為灰色主題,保持視覺一致
- 優化詞彙統計顯示,移除多餘的進度條
- 添加保存提醒警告,避免查詢紀錄消失
- 程度指示器整合到頁面標題區域

用戶體驗大幅提升:
- 更直觀的操作流程
- 更豐富的互動功能
- 更一致的視覺設計
- 更好的數據持久化

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-10-08 03:11:42 +08:00
parent 1b6e62de95
commit b9f89361d9
4 changed files with 1190 additions and 605 deletions

View File

@ -0,0 +1,109 @@
# AI 生成頁面重新設計計劃
## 設計目標
將當前的兩階段界面 (輸入 → 按鈕 → 結果頁面) 重新設計為統一的單頁面界面
## 新的布局設計
### 桌面版布局 (左右分欄)
```
┌─────────────────────────────────────────────────────────────┐
│ AI 智能生成詞卡 [你的程度 A2 ⚙️] │
├─────────────────────┬───────────────────────────────────────┤
│ 左側:輸入區 │ 右側:結果顯示區 │
│ • 文字輸入框 │ • 句子分析結果 (有結果時顯示) │
│ • 分析按鈕 │ • 詞彙統計 │
│ • 歷史記錄 │ • 互動詞彙 │
│ │ • 保存提醒 │
└─────────────────────┴───────────────────────────────────────┘
```
### 手機版布局 (上下分區)
```
┌─────────────────────────────────────┐
│ AI 智能生成詞卡 [你的程度 A2 ⚙️] │
├─────────────────────────────────────┤
│ 輸入區 │
│ • 文字輸入框 │
│ • 分析按鈕 │
├─────────────────────────────────────┤
│ 結果顯示區 (展開/摺疊) │
│ • 句子分析結果 │
│ • 詞彙統計 │
│ • 保存提醒 │
└─────────────────────────────────────┘
```
## 功能增強
### 1. 統一界面設計
- **移除視圖切換**:不再使用 `showAnalysisView` 狀態
- **固定雙欄布局**:輸入區和結果區同時可見
- **即時結果顯示**:分析完成後立即在右側顯示
### 2. 歷史記錄系統
- **localStorage 多記錄**:保存最近 5-10 次分析記錄
- **歷史查詢列表**:在左側輸入區下方顯示
- **快速切換**:點擊歷史記錄可立即載入該分析結果
- **記錄格式**
```javascript
{
id: timestamp,
textInput: "原始輸入文字...",
sentenceAnalysis: {...},
sentenceMeaning: "翻譯",
createdAt: Date,
saved: boolean // 是否已保存詞卡
}
```
### 3. 保存提醒系統
- **警告訊息**:「⚠️ 請及時保存詞卡,避免查詢紀錄消失」
- **未保存計數**:顯示當前分析中有多少詞彙未保存
- **批量保存**:「保存所有重點詞彙」按鈕
- **視覺提醒**:未保存的詞彙有特殊標記
## 技術實施
### 1. 布局重構
- **移除條件渲染**`{!showAnalysisView ? ... : ...}`
- **使用 Grid/Flexbox**:實現響應式左右分欄
- **固定結構**:輸入區和結果區始終存在
### 2. 狀態管理優化
- **移除 showAnalysisView 狀態**
- **新增 analysisHistory 狀態**:管理歷史記錄
- **新增 savedWords 狀態**:追踪已保存的詞彙
### 3. localStorage 擴展
- **升級快取結構**:從單一記錄改為記錄陣列
- **自動清理**:超過最大數量時移除最舊記錄
- **資料完整性**:確保向後兼容性
### 4. 用戶體驗改進
- **空狀態設計**:結果區域在無分析時的友好提示
- **載入狀態**:分析中的視覺反饋
- **成功狀態**:分析完成的視覺確認
## 視覺設計原則
### 1. 一致性
- 保持與詞卡管理頁面的設計語言一致
- 使用相同的顏色系統和組件樣式
### 2. 易用性
- 清楚的操作流程指引
- 重要功能突出顯示
- 減少用戶的操作步驟
### 3. 響應式
- 桌面版左右分欄
- 平板版適當調整比例
- 手機版改為上下堆疊
## 實施優先級
1. **Phase 1**:重構基本布局 (左右分欄)
2. **Phase 2**:實現歷史記錄系統
3. **Phase 3**:添加保存提醒功能
4. **Phase 4**:優化響應式設計和動畫

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,596 @@
'use client'
import { useState, useMemo, useCallback, useEffect } from 'react'
import { ProtectedRoute } from '@/components/shared/ProtectedRoute'
import { Navigation } from '@/components/shared/Navigation'
import { WordPopup } from '@/components/word/WordPopup'
import { useToast } from '@/components/shared/Toast'
import { flashcardsService } from '@/lib/services/flashcards'
import { getLevelIndex } from '@/lib/utils/cefrUtils'
import { useWordAnalysis } from '@/hooks/word/useWordAnalysis'
import { API_CONFIG } from '@/lib/config/api'
import Link from 'next/link'
// 常數定義
const MAX_MANUAL_INPUT_LENGTH = 300
interface GrammarCorrection {
hasErrors: boolean;
originalText: string;
correctedText: string | null;
corrections: Array<{
position: { start: number; end: number };
error: string;
correction: string;
type: string;
explanation: string;
severity: 'high' | 'medium' | 'low';
}>;
confidenceScore: number;
}
// 移除 IdiomPopup - 使用統一的 WordPopup 組件
function GenerateContent() {
const toast = useToast()
const { findWordAnalysis, getWordClass } = useWordAnalysis()
const [textInput, setTextInput] = useState('')
// 獲取用戶等級
const userLevel = typeof window !== 'undefined'
? localStorage.getItem('userEnglishLevel') || 'A2'
: 'A2'
const [isAnalyzing, setIsAnalyzing] = useState(false)
const [showAnalysisView, setShowAnalysisView] = useState(false)
const [sentenceAnalysis, setSentenceAnalysis] = useState<Record<string, any> | null>(null)
const [sentenceMeaning, setSentenceMeaning] = useState('')
const [grammarCorrection, setGrammarCorrection] = useState<GrammarCorrection | null>(null)
const [selectedIdiom, setSelectedIdiom] = useState<string | null>(null)
const [selectedWord, setSelectedWord] = useState<string | null>(null)
// localStorage 快取函數
const saveAnalysisToCache = useCallback((cacheData: any) => {
try {
localStorage.setItem('generate_analysis_cache', JSON.stringify(cacheData))
} catch (error) {
console.warn('無法保存分析快取:', error)
}
}, [])
const loadAnalysisFromCache = useCallback(() => {
try {
const cached = localStorage.getItem('generate_analysis_cache')
return cached ? JSON.parse(cached) : null
} catch (error) {
console.warn('無法載入分析快取:', error)
return null
}
}, [])
const clearAnalysisCache = useCallback(() => {
try {
localStorage.removeItem('generate_analysis_cache')
} catch (error) {
console.warn('無法清除分析快取:', error)
}
}, [])
// 組件載入時恢復快取的分析結果
useEffect(() => {
const cached = loadAnalysisFromCache()
if (cached) {
setTextInput(cached.textInput || '')
setSentenceAnalysis(cached.sentenceAnalysis || null)
setSentenceMeaning(cached.sentenceMeaning || '')
setGrammarCorrection(cached.grammarCorrection || null)
setShowAnalysisView(cached.showAnalysisView || false)
console.log('✅ 已恢復快取的分析結果')
}
}, [loadAnalysisFromCache])
// 處理句子分析 - 使用真實API
const handleAnalyzeSentence = async () => {
// 清除舊的分析快取
clearAnalysisCache()
setIsAnalyzing(true)
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/ai/analyze-sentence`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
inputText: textInput,
analysisMode: 'full',
options: {
includeGrammarCheck: true,
includeVocabularyAnalysis: true,
includeTranslation: true,
includeIdiomDetection: true,
includeExamples: true
}
})
})
if (!response.ok) {
let errorMessage = `API請求失敗: ${response.status}`
try {
const errorData = await response.json()
errorMessage = errorData.error?.message || errorData.message || errorMessage
} catch (e) {
console.warn('無法解析錯誤回應:', e)
}
throw new Error(errorMessage)
}
const result = await response.json()
if (!result.success || !result.data) {
throw new Error('API回應格式錯誤')
}
// 處理API回應 - 適配新的後端格式
const apiData = result.data.data // 需要深入兩層result.data.data
// 設定完整的分析結果包含vocabularyAnalysis和其他數據
const analysisData = {
originalText: apiData.originalText,
sentenceMeaning: apiData.sentenceMeaning,
grammarCorrection: apiData.grammarCorrection,
vocabularyAnalysis: apiData.vocabularyAnalysis,
idioms: apiData.idioms || [],
processingTime: result.processingTime
}
setSentenceAnalysis(analysisData)
setSentenceMeaning(apiData.sentenceMeaning || '')
// 處理語法修正
if (apiData.grammarCorrection) {
setGrammarCorrection({
hasErrors: apiData.grammarCorrection.hasErrors,
originalText: textInput,
correctedText: apiData.grammarCorrection.correctedText || textInput,
corrections: apiData.grammarCorrection.corrections || [],
confidenceScore: apiData.grammarCorrection.confidenceScore || 0.9
})
} else {
setGrammarCorrection({
hasErrors: false,
originalText: textInput,
correctedText: textInput,
corrections: [],
confidenceScore: 1.0
})
}
setShowAnalysisView(true)
// 保存分析結果到快取
const cacheData = {
textInput,
sentenceAnalysis: analysisData,
sentenceMeaning: apiData.sentenceMeaning || '',
grammarCorrection: apiData.grammarCorrection ? {
hasErrors: apiData.grammarCorrection.hasErrors,
originalText: textInput,
correctedText: apiData.grammarCorrection.correctedText || textInput,
corrections: apiData.grammarCorrection.corrections || [],
confidenceScore: apiData.grammarCorrection.confidenceScore || 1.0
} : {
hasErrors: false,
originalText: textInput,
correctedText: textInput,
corrections: [],
confidenceScore: 1.0
},
showAnalysisView: true
}
saveAnalysisToCache(cacheData)
} catch (error) {
console.error('Error in sentence analysis:', error)
setGrammarCorrection({
hasErrors: true,
originalText: textInput,
correctedText: textInput,
corrections: [],
confidenceScore: 0.0
})
setSentenceMeaning('分析過程中發生錯誤,請稍後再試。')
// 錯誤時也不設置finalText使用原始輸入
setShowAnalysisView(true)
} finally {
setIsAnalyzing(false)
}
}
const handleAcceptCorrection = useCallback(() => {
if (grammarCorrection?.correctedText) {
// 更新用戶輸入為修正後的版本
setTextInput(grammarCorrection.correctedText)
console.log('✅ 已採用修正版本,文本已更新為正確版本!')
}
}, [grammarCorrection?.correctedText])
const handleRejectCorrection = useCallback(() => {
// 保持原始輸入不變,只是隱藏語法修正面板
setGrammarCorrection(null)
console.log('📝 已保持原始版本,繼續使用您的原始輸入。')
}, [])
// 詞彙統計計算 - 適配新的後端API格式
const vocabularyStats = useMemo(() => {
if (!sentenceAnalysis?.vocabularyAnalysis) {
return { simpleCount: 0, moderateCount: 0, difficultCount: 0, idiomCount: 0 }
}
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
let simpleCount = 0
let moderateCount = 0
let difficultCount = 0
// 處理vocabularyAnalysis物件
Object.values(sentenceAnalysis.vocabularyAnalysis).forEach((wordData: any) => {
const cefr = wordData?.cefr || 'A1'
const userIndex = getLevelIndex(userLevel)
const wordIndex = getLevelIndex(cefr)
if (userIndex > wordIndex) {
simpleCount++
} else if (userIndex === wordIndex) {
moderateCount++
} else {
difficultCount++
}
})
// 處理慣用語統計
const idiomCount = sentenceAnalysis.idioms?.length || 0
return { simpleCount, moderateCount, difficultCount, idiomCount }
}, [sentenceAnalysis])
// 保存單個詞彙
const handleSaveWord = useCallback(async (word: string, analysis: any) => {
try {
const cefrValue = analysis.cefr || analysis.cefrLevel || analysis.CEFR || 'A0'
const cardData = {
word: word,
translation: analysis.translation || analysis.Translation || '',
definition: analysis.definition || analysis.Definition || '',
pronunciation: analysis.pronunciation || analysis.Pronunciation || `/${word}/`,
partOfSpeech: analysis.partOfSpeech || analysis.PartOfSpeech || 'noun',
example: analysis.example || `Example sentence with ${word}.`, // 使用分析結果的例句
exampleTranslation: analysis.exampleTranslation,
synonyms: analysis.synonyms ? JSON.stringify(analysis.synonyms) : undefined, // 轉換為 JSON 字串
cefr: cefrValue
}
const response = await flashcardsService.createFlashcard(cardData)
if (response.success) {
// 顯示成功提示
const successMessage = `已成功將「${word}」保存到詞卡庫!`
toast.success(successMessage)
console.log('✅', successMessage)
return { success: true }
} else if (response.error && response.error.includes('已存在')) {
// 顯示重複提示
const duplicateMessage = `詞卡「${word}」已經存在於詞卡庫中`
toast.warning(duplicateMessage)
console.log('⚠️', duplicateMessage)
return { success: false, error: 'duplicate', message: duplicateMessage }
} else {
throw new Error(response.error || '保存失敗')
}
} catch (error) {
console.error('Save word error:', error)
const errorMessage = error instanceof Error ? error.message : '保存失敗'
toast.error(`保存詞卡失敗: ${errorMessage}`)
return { success: false, error: errorMessage }
}
}, [])
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<Navigation />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{!showAnalysisView ? (
<>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-8">AI 智能生成詞卡</h1>
{/* Content Input */}
<div className="bg-white rounded-xl shadow-sm p-4 sm:p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">輸入英文文本</h2>
<textarea
value={textInput}
onChange={(e) => {
const value = e.target.value
if (value.length > MAX_MANUAL_INPUT_LENGTH) {
return
}
setTextInput(value)
}}
placeholder={`輸入英文句子(最多${MAX_MANUAL_INPUT_LENGTH}字)...`}
className={`w-full h-32 sm:h-40 px-3 sm:px-4 py-2 sm:py-3 text-sm sm:text-base border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent outline-none resize-none ${
textInput.length >= MAX_MANUAL_INPUT_LENGTH - 20 ? 'border-yellow-400' :
textInput.length >= MAX_MANUAL_INPUT_LENGTH ? 'border-red-400' : 'border-gray-300'
}`}
/>
<div className="mt-2 flex justify-between text-sm">
<span className={`${
textInput.length >= MAX_MANUAL_INPUT_LENGTH - 20 ? 'text-yellow-600' :
textInput.length >= MAX_MANUAL_INPUT_LENGTH ? 'text-red-600' : 'text-gray-600'
}`}>
最多 {MAX_MANUAL_INPUT_LENGTH} 字元 • 目前:{textInput.length} 字元
</span>
{textInput.length > MAX_MANUAL_INPUT_LENGTH - 50 && (
<span className={textInput.length >= MAX_MANUAL_INPUT_LENGTH ? 'text-red-600' : 'text-yellow-600'}>
{textInput.length >= MAX_MANUAL_INPUT_LENGTH ? '已達上限!' : `還可輸入 ${MAX_MANUAL_INPUT_LENGTH - textInput.length} 字元`}
</span>
)}
</div>
</div>
{/* Action Buttons */}
<div className="space-y-4">
{/* 句子分析按鈕 */}
<button
onClick={handleAnalyzeSentence}
disabled={isAnalyzing || !textInput || textInput.length > MAX_MANUAL_INPUT_LENGTH}
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>
正在分析句子... (AI 分析約需 3-5 秒)
</span>
) : (
'🔍 分析句子'
)}
</button>
</div>
</>
) : (
/* 重新設計的句子分析視圖 - 簡潔流暢 */
<>
{/* 用戶程度指示器 */}
<div className="flex justify-end mb-6">
<Link
href="/profile"
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors hover:shadow-md bg-gray-100 text-gray-700 border border-gray-200"
>
<span className="text-sm font-medium">你的程度 {userLevel}</span>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth="2">
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z"/>
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>
</svg>
</Link>
</div>
{/* 語法修正面板 - 如果需要的話 */}
{grammarCorrection && grammarCorrection.hasErrors && (
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-6 mb-6">
<div className="flex items-start gap-3">
<div className="text-yellow-600 text-2xl">⚠️</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-yellow-800 mb-2">發現語法問題</h3>
<p className="text-yellow-700 mb-4">AI建議修正以下內容這將提高學習效果</p>
<div className="space-y-3 mb-4">
<div>
<span className="text-sm font-medium text-yellow-700">原始輸入:</span>
<div className="bg-white p-3 rounded border border-yellow-300 mt-1">
{textInput}
</div>
</div>
<div>
<span className="text-sm font-medium text-yellow-700">建議修正:</span>
<div className="bg-yellow-100 p-3 rounded border border-yellow-300 mt-1 font-medium">
{grammarCorrection.correctedText || textInput}
</div>
</div>
</div>
<div className="flex gap-3">
<button
onClick={handleAcceptCorrection}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
✅ 採用修正
</button>
<button
onClick={handleRejectCorrection}
className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
📝 保持原樣
</button>
</div>
</div>
</div>
</div>
)}
{/* 主句子展示 - 最重要的內容 */}
<div className="bg-white rounded-xl shadow-lg p-8 mb-6">
{/* 詞彙統計卡片區 */}
{vocabularyStats && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-6">
{/* 簡單詞彙卡片 */}
<div className="bg-gray-50 border border-dashed border-gray-300 rounded-lg p-3 sm:p-4 text-center">
<div className="text-xl sm:text-2xl font-bold text-gray-600 mb-1">{vocabularyStats.simpleCount}</div>
<div className="text-gray-600 text-sm sm:text-base font-medium">太簡單啦</div>
</div>
{/* 適中詞彙卡片 */}
<div className="bg-green-50 border border-green-200 rounded-lg p-3 sm:p-4 text-center">
<div className="text-xl sm:text-2xl font-bold text-green-700 mb-1">{vocabularyStats.moderateCount}</div>
<div className="text-green-700 text-sm sm:text-base font-medium">重點學習</div>
</div>
{/* 艱難詞彙卡片 */}
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3 sm:p-4 text-center">
<div className="text-xl sm:text-2xl font-bold text-orange-700 mb-1">{vocabularyStats.difficultCount}</div>
<div className="text-orange-700 text-sm sm:text-base font-medium">有點挑戰</div>
</div>
{/* 片語與俚語卡片 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 sm:p-4 text-center">
<div className="text-xl sm:text-2xl font-bold text-blue-700 mb-1">{vocabularyStats.idiomCount}</div>
<div className="text-blue-700 text-sm sm:text-base font-medium">慣用語</div>
</div>
</div>
)}
{/* 句子主體展示 */}
<div className="text-left mb-8">
<div className="text-lg font-medium text-gray-900 mb-6 select-text leading-relaxed">
{textInput.split(/(\s+)/).map((token, index) => {
const cleanToken = token.replace(/[^\w']/g, '')
if (!cleanToken || /^\s+$/.test(token)) {
return (
<span key={index} className="whitespace-pre">
{token}
</span>
)
}
const analysis = sentenceAnalysis?.vocabularyAnalysis || {}
const wordAnalysis = findWordAnalysis(cleanToken, analysis)
if (!wordAnalysis) {
return (
<span key={index} className="text-gray-900">
{token}
</span>
)
}
return (
<span key={index} className="relative">
<span
className={getWordClass(cleanToken, analysis, userLevel)}
onClick={() => setSelectedWord(cleanToken)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
setSelectedWord(cleanToken)
}
}}
>
{token}
</span>
{/* {shouldShowStar(wordAnalysis) && (
<span className="absolute -top-1 -right-1 text-xs text-yellow-500">
</span>
)} */}
</span>
)
})}
</div>
{/* 翻譯 - 參考翻卡背面設計 */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left">中文翻譯</h3>
<p className="text-gray-700 text-left">{sentenceMeaning}</p>
</div>
{/* 片語和慣用語展示區 */}
{(() => {
if (!sentenceAnalysis?.idioms || sentenceAnalysis.idioms.length === 0) return null
// 使用新的API格式中的idioms陣列
const idioms = sentenceAnalysis.idioms
return (
<div className="bg-gray-50 rounded-lg p-4 mt-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left">慣用語</h3>
<div className="flex flex-wrap gap-2">
{idioms.map((idiom: any, index: number) => (
<span
key={index}
className="cursor-pointer transition-all duration-200 rounded-lg relative mx-0.5 px-1 py-0.5 inline-flex items-center gap-1 bg-blue-50 border border-blue-200 hover:bg-blue-100 hover:shadow-lg transform hover:-translate-y-0.5 text-blue-700 font-medium"
onClick={() => {
// 使用統一的 WordPopup 組件
setSelectedIdiom(idiom.idiom)
}}
title={`${idiom.idiom}: ${idiom.translation}`}
>
{idiom.idiom}
</span>
))}
</div>
</div>
)
})()}
</div>
</div>
{/* 下方操作區 - 簡化 */}
<div className="flex justify-center px-4">
<button
onClick={() => setShowAnalysisView(false)}
className="w-full sm:w-auto px-6 sm:px-8 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary-hover transition-colors flex items-center justify-center gap-2"
>
<span>分析新句子</span>
</button>
</div>
</>
)}
{/* 慣用語彈窗 - 使用統一的 WordPopup */}
<WordPopup
selectedWord={selectedIdiom}
analysis={selectedIdiom ? { [selectedIdiom]: sentenceAnalysis?.idioms?.find((i: any) => i.idiom === selectedIdiom) } : {}}
isOpen={!!selectedIdiom}
onClose={() => setSelectedIdiom(null)}
onSaveWord={async (word, analysis) => {
const result = await handleSaveWord(word, analysis)
return result
}}
/>
{/* 單詞彈窗 - 使用統一的 WordPopup */}
<WordPopup
selectedWord={selectedWord}
analysis={sentenceAnalysis?.vocabularyAnalysis || {}}
isOpen={!!selectedWord}
onClose={() => setSelectedWord(null)}
onSaveWord={async (word, analysis) => {
const result = await handleSaveWord(word, analysis)
return result
}}
/>
{/* Toast 通知系統 */}
<toast.ToastContainer />
</div>
</div>
)
}
export default function GeneratePage() {
return (
<ProtectedRoute>
<GenerateContent />
</ProtectedRoute>
)
}

View File

@ -70,9 +70,9 @@ export const WordPopup: React.FC<WordPopupProps> = ({
{/* Content */}
<div className="p-4 space-y-4 max-h-96 overflow-y-auto">
{/* Translation */}
<div className="bg-green-50 rounded-lg p-3 border border-green-200">
<h4 className="font-semibold text-green-900 mb-2 text-left text-sm"></h4>
<p className="text-green-800 font-medium text-left">
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
<h4 className="font-semibold text-gray-900 mb-2 text-left text-sm"></h4>
<p className="text-gray-700 font-medium text-left">
{getWordProperty(wordAnalysis, 'translation')}
</p>
</div>
@ -87,13 +87,13 @@ export const WordPopup: React.FC<WordPopupProps> = ({
{/* Example */}
{getWordProperty(wordAnalysis, 'example') && (
<div className="bg-blue-50 rounded-lg p-3 border border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2 text-left text-sm"></h4>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
<h4 className="font-semibold text-gray-900 mb-2 text-left text-sm"></h4>
<div className="space-y-2">
<p className="text-blue-800 text-left italic">
<p className="text-gray-700 text-left italic">
"{getWordProperty(wordAnalysis, 'example')}"
</p>
<p className="text-blue-700 text-left">
<p className="text-gray-700 text-left">
{getWordProperty(wordAnalysis, 'exampleTranslation')}
</p>
</div>
@ -102,13 +102,13 @@ export const WordPopup: React.FC<WordPopupProps> = ({
{/* Synonyms */}
{getWordProperty(wordAnalysis, 'synonyms')?.length > 0 && (
<div className="bg-purple-50 rounded-lg p-3 border border-purple-200">
<h4 className="font-semibold text-purple-900 mb-2 text-left text-sm"></h4>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
<h4 className="font-semibold text-gray-900 mb-2 text-left text-sm"></h4>
<div className="flex flex-wrap gap-2">
{getWordProperty(wordAnalysis, 'synonyms')?.map((synonym: string, index: number) => (
<span
key={index}
className="bg-purple-100 text-purple-700 px-2 py-1 rounded-full text-xs font-medium"
className="bg-gray-100 text-gray-700 px-2 py-1 rounded-full text-xs font-medium"
>
{synonym}
</span>