649 lines
27 KiB
TypeScript
649 lines
27 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useMemo, useCallback } from 'react'
|
||
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||
import { Navigation } from '@/components/Navigation'
|
||
import { ClickableTextV2 } from '@/components/ClickableTextV2'
|
||
import { flashcardsService } from '@/lib/services/flashcards'
|
||
import { Play } from 'lucide-react'
|
||
import Link from 'next/link'
|
||
|
||
// 常數定義
|
||
const CEFR_LEVELS = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'] as const
|
||
const MAX_MANUAL_INPUT_LENGTH = 300
|
||
|
||
// 工具函數
|
||
const getLevelIndex = (level: string): number => {
|
||
return CEFR_LEVELS.indexOf(level as typeof CEFR_LEVELS[number])
|
||
}
|
||
|
||
const getTargetLearningRange = (userLevel: string): string => {
|
||
const ranges: Record<string, string> = {
|
||
'A1': 'A2-B1', 'A2': 'B1-B2', 'B1': 'B2-C1',
|
||
'B2': 'C1-C2', 'C1': 'C2', 'C2': 'C2'
|
||
}
|
||
return ranges[userLevel] || 'B1-B2'
|
||
}
|
||
|
||
const compareCEFRLevels = (level1: string, level2: string, operator: '>' | '<' | '==='): boolean => {
|
||
const levels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
|
||
const index1 = levels.indexOf(level1)
|
||
const index2 = levels.indexOf(level2)
|
||
|
||
if (index1 === -1 || index2 === -1) return false
|
||
|
||
switch (operator) {
|
||
case '>': return index1 > index2
|
||
case '<': return index1 < index2
|
||
case '===': return index1 === index2
|
||
default: return false
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
interface IdiomPopup {
|
||
idiom: string;
|
||
analysis: any;
|
||
position: { x: number; y: number };
|
||
}
|
||
|
||
function GenerateContent() {
|
||
const [textInput, setTextInput] = useState('')
|
||
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 [idiomPopup, setIdiomPopup] = useState<IdiomPopup | null>(null)
|
||
|
||
|
||
// 處理句子分析 - 使用真實API
|
||
const handleAnalyzeSentence = async () => {
|
||
console.log('🚀 handleAnalyzeSentence 被調用 (真實API模式)')
|
||
|
||
setIsAnalyzing(true)
|
||
|
||
try {
|
||
const response = await fetch('http://localhost:5008/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
|
||
|
||
// 設定完整的分析結果(包含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)
|
||
console.log('✅ API分析完成', apiData)
|
||
} 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 difficultyLevel = wordData?.difficultyLevel || 'A1'
|
||
const userIndex = getLevelIndex(userLevel)
|
||
const wordIndex = getLevelIndex(difficultyLevel)
|
||
|
||
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 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,
|
||
difficultyLevel: analysis.difficultyLevel || analysis.cefrLevel || 'A2'
|
||
}
|
||
|
||
const response = await flashcardsService.createFlashcard(cardData)
|
||
|
||
if (response.success) {
|
||
// 顯示成功提示
|
||
const successMessage = `✅ 已成功將「${word}」保存到詞卡庫!`
|
||
alert(successMessage)
|
||
console.log(successMessage)
|
||
return { success: true }
|
||
} else if (response.error && response.error.includes('已存在')) {
|
||
// 顯示重複提示
|
||
const duplicateMessage = `⚠️ 詞卡「${word}」已經存在於詞卡庫中`
|
||
alert(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 : '保存失敗'
|
||
alert(`❌ 保存詞卡失敗: ${errorMessage}`)
|
||
return { success: false, error: errorMessage }
|
||
}
|
||
}, [])
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
<Navigation />
|
||
|
||
<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>
|
||
|
||
{/* 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 className="text-center text-sm text-gray-600 mt-2">
|
||
{(() => {
|
||
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
|
||
return (
|
||
<div className="flex items-center justify-center gap-2">
|
||
<span>🎯 您的程度: {userLevel}</span>
|
||
<span className="text-gray-400">|</span>
|
||
<span>📈 重點學習範圍: {getTargetLearningRange(userLevel)}</span>
|
||
<Link
|
||
href="/settings"
|
||
className="text-blue-500 hover:text-blue-700 ml-2"
|
||
>
|
||
調整 ⚙️
|
||
</Link>
|
||
</div>
|
||
)
|
||
})()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* 重新設計的句子分析視圖 - 簡潔流暢 */
|
||
<div className="max-w-4xl mx-auto">
|
||
{/* 星星標記說明 */}
|
||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-6">
|
||
<div className="flex items-center gap-2 text-sm text-yellow-800">
|
||
<span className="text-yellow-500 text-base">⭐</span>
|
||
<span className="font-medium">⭐ 為常用高頻詞彙,建議優先學習!</span>
|
||
</div>
|
||
</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-xl sm:text-2xl lg:text-3xl font-medium text-gray-900 mb-6" >
|
||
<ClickableTextV2
|
||
text={textInput}
|
||
analysis={sentenceAnalysis?.vocabularyAnalysis || undefined}
|
||
showIdiomsInline={false}
|
||
onWordClick={(word, analysis) => {
|
||
console.log('Clicked word:', word, analysis)
|
||
}}
|
||
onSaveWord={handleSaveWord}
|
||
/>
|
||
</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={(e) => {
|
||
// 使用新的API格式,直接使用idiom物件
|
||
setIdiomPopup({
|
||
idiom: idiom.idiom,
|
||
analysis: idiom,
|
||
position: {
|
||
x: e.currentTarget.getBoundingClientRect().left + e.currentTarget.getBoundingClientRect().width / 2,
|
||
y: e.currentTarget.getBoundingClientRect().bottom + 10
|
||
}
|
||
})
|
||
}}
|
||
title={`${idiom.idiom}: ${idiom.translation}`}
|
||
>
|
||
{idiom.idiom}
|
||
{(() => {
|
||
// 只有當慣用語為常用且不是簡單慣用語時才顯示星星
|
||
// 簡單慣用語定義:學習者CEFR > 慣用語CEFR
|
||
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
|
||
const isHighFrequency = idiom?.frequency === 'high'
|
||
const idiomCefr = idiom?.cefrLevel || 'A1'
|
||
const isNotSimpleIdiom = !compareCEFRLevels(userLevel, idiomCefr, '>')
|
||
|
||
return isHighFrequency && isNotSimpleIdiom ? (
|
||
<span
|
||
className="absolute -top-1 -right-1 text-xs pointer-events-none z-10"
|
||
style={{ fontSize: '8px', lineHeight: 1 }}
|
||
>
|
||
⭐
|
||
</span>
|
||
) : null
|
||
})()}
|
||
</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>
|
||
</div>
|
||
)}
|
||
|
||
{/* 片語彈窗 */}
|
||
{idiomPopup && (
|
||
<>
|
||
<div
|
||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||
onClick={() => setIdiomPopup(null)}
|
||
/>
|
||
<div
|
||
className="fixed z-50 bg-white rounded-xl shadow-lg w-96 max-w-md overflow-hidden"
|
||
style={{
|
||
left: `${idiomPopup.position.x}px`,
|
||
top: `${idiomPopup.position.y}px`,
|
||
transform: 'translate(-50%, 8px)',
|
||
maxHeight: '85vh',
|
||
overflowY: 'auto'
|
||
}}
|
||
>
|
||
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-5 border-b border-blue-200">
|
||
<div className="flex justify-end mb-3">
|
||
<button
|
||
onClick={() => setIdiomPopup(null)}
|
||
className="text-gray-400 hover:text-gray-600 w-6 h-6 rounded-full bg-white bg-opacity-80 hover:bg-opacity-100 transition-all flex items-center justify-center"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div className="mb-3">
|
||
<h3 className="text-2xl font-bold text-gray-900">{idiomPopup.analysis.idiom}</h3>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-base text-gray-600">{idiomPopup.analysis.pronunciation}</span>
|
||
<button
|
||
onClick={() => {
|
||
const utterance = new SpeechSynthesisUtterance(idiomPopup.analysis.idiom);
|
||
utterance.lang = 'en-US';
|
||
utterance.rate = 0.8;
|
||
speechSynthesis.speak(utterance);
|
||
}}
|
||
className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-600 hover:bg-blue-700 text-white transition-colors"
|
||
title="播放發音"
|
||
>
|
||
<Play size={16} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<span className="px-3 py-1 rounded-full text-sm font-medium border bg-blue-100 text-blue-700 border-blue-200">
|
||
{idiomPopup.analysis.difficultyLevel}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-4 space-y-4">
|
||
<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">{idiomPopup.analysis.translation}</p>
|
||
</div>
|
||
|
||
<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 text-left text-sm leading-relaxed">{idiomPopup.analysis.definition}</p>
|
||
</div>
|
||
|
||
{idiomPopup.analysis.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="space-y-2">
|
||
<p className="text-blue-800 text-left text-sm italic">
|
||
"{idiomPopup.analysis.example}"
|
||
</p>
|
||
<p className="text-blue-700 text-left text-sm">
|
||
{idiomPopup.analysis.exampleTranslation}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{idiomPopup.analysis.synonyms && Array.isArray(idiomPopup.analysis.synonyms) && idiomPopup.analysis.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="flex flex-wrap gap-2">
|
||
{idiomPopup.analysis.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"
|
||
>
|
||
{synonym}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="p-4 pt-2">
|
||
<button
|
||
onClick={async () => {
|
||
const result = await handleSaveWord(idiomPopup.idiom, idiomPopup.analysis)
|
||
if (result.success) {
|
||
setIdiomPopup(null)
|
||
} else {
|
||
console.error('Save idiom error:', result.error)
|
||
}
|
||
}}
|
||
className="w-full bg-primary text-white py-3 rounded-lg font-medium hover:bg-primary-hover transition-colors flex items-center justify-center gap-2"
|
||
>
|
||
<span className="font-medium">保存到詞卡</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function GeneratePage() {
|
||
return (
|
||
<ProtectedRoute>
|
||
<GenerateContent />
|
||
</ProtectedRoute>
|
||
)
|
||
} |