800 lines
32 KiB
TypeScript
800 lines
32 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 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'
|
||
}
|
||
|
||
interface GrammarCorrection {
|
||
hasErrors: boolean;
|
||
originalText: string;
|
||
correctedText: string;
|
||
corrections: Array<{
|
||
error: string;
|
||
correction: string;
|
||
type: string;
|
||
explanation: string;
|
||
}>;
|
||
}
|
||
|
||
interface PhrasePopup {
|
||
phrase: 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 [finalText, setFinalText] = useState('')
|
||
const [phrasePopup, setPhrasePopup] = useState<PhrasePopup | null>(null)
|
||
|
||
|
||
// 處理句子分析 - 使用假資料測試
|
||
const handleAnalyzeSentence = async () => {
|
||
console.log('🚀 handleAnalyzeSentence 被調用 (假資料模式)')
|
||
|
||
setIsAnalyzing(true)
|
||
|
||
try {
|
||
// 模擬API延遲
|
||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||
|
||
// 使用有語法錯誤的測試句子
|
||
const testSentence = "She just join the team, so let's cut her some slack until she get used to the workflow."
|
||
|
||
// 假資料:完整詞彙分析結果 (包含句子中的所有詞彙)
|
||
const mockAnalysis = {
|
||
"she": {
|
||
word: "she",
|
||
translation: "她",
|
||
definition: "female person pronoun",
|
||
partOfSpeech: "pronoun",
|
||
pronunciation: "/ʃiː/",
|
||
difficultyLevel: "A1",
|
||
isPhrase: false,
|
||
synonyms: ["her"],
|
||
example: "She is a teacher.",
|
||
exampleTranslation: "她是一名老師。"
|
||
},
|
||
"just": {
|
||
word: "just",
|
||
translation: "剛剛;僅僅",
|
||
definition: "recently; only",
|
||
partOfSpeech: "adverb",
|
||
pronunciation: "/dʒʌst/",
|
||
difficultyLevel: "A2",
|
||
isPhrase: false,
|
||
synonyms: ["recently", "only", "merely"],
|
||
example: "I just arrived.",
|
||
exampleTranslation: "我剛到。"
|
||
},
|
||
"join": {
|
||
word: "join",
|
||
translation: "加入",
|
||
definition: "to become a member of",
|
||
partOfSpeech: "verb",
|
||
pronunciation: "/dʒɔɪn/",
|
||
difficultyLevel: "B1",
|
||
isPhrase: false,
|
||
synonyms: ["enter", "become part of"],
|
||
example: "I want to join the team.",
|
||
exampleTranslation: "我想加入團隊。"
|
||
},
|
||
"the": {
|
||
word: "the",
|
||
translation: "定冠詞",
|
||
definition: "definite article",
|
||
partOfSpeech: "article",
|
||
pronunciation: "/ðə/",
|
||
difficultyLevel: "A1",
|
||
isPhrase: false,
|
||
synonyms: [],
|
||
example: "The cat is sleeping.",
|
||
exampleTranslation: "貓在睡覺。"
|
||
},
|
||
"team": {
|
||
word: "team",
|
||
translation: "團隊",
|
||
definition: "a group of people working together",
|
||
partOfSpeech: "noun",
|
||
pronunciation: "/tiːm/",
|
||
difficultyLevel: "A2",
|
||
isPhrase: false,
|
||
synonyms: ["group", "crew"],
|
||
example: "Our team works well together.",
|
||
exampleTranslation: "我們的團隊合作得很好。"
|
||
},
|
||
"so": {
|
||
word: "so",
|
||
translation: "所以;如此",
|
||
definition: "therefore; to such a degree",
|
||
partOfSpeech: "adverb",
|
||
pronunciation: "/soʊ/",
|
||
difficultyLevel: "A1",
|
||
isPhrase: false,
|
||
synonyms: ["therefore", "thus"],
|
||
example: "It was raining, so I stayed home.",
|
||
exampleTranslation: "下雨了,所以我待在家裡。"
|
||
},
|
||
"let's": {
|
||
word: "let's",
|
||
translation: "讓我們",
|
||
definition: "let us (contraction)",
|
||
partOfSpeech: "contraction",
|
||
pronunciation: "/lets/",
|
||
difficultyLevel: "A1",
|
||
isPhrase: false,
|
||
synonyms: ["let us"],
|
||
example: "Let's go to the park.",
|
||
exampleTranslation: "我們去公園吧。"
|
||
},
|
||
"cut": {
|
||
word: "cut",
|
||
translation: "切;削減",
|
||
definition: "to use a knife or other sharp tool to divide something",
|
||
partOfSpeech: "verb",
|
||
pronunciation: "/kʌt/",
|
||
difficultyLevel: "A2",
|
||
isPhrase: false,
|
||
synonyms: ["slice", "chop", "reduce"],
|
||
example: "Please cut the apple.",
|
||
exampleTranslation: "請切蘋果。"
|
||
},
|
||
"her": {
|
||
word: "her",
|
||
translation: "她的;她",
|
||
definition: "belonging to or associated with a female",
|
||
partOfSpeech: "pronoun",
|
||
pronunciation: "/hər/",
|
||
difficultyLevel: "A1",
|
||
isPhrase: false,
|
||
synonyms: ["hers"],
|
||
example: "This is her book.",
|
||
exampleTranslation: "這是她的書。"
|
||
},
|
||
"some": {
|
||
word: "some",
|
||
translation: "一些",
|
||
definition: "an unspecified amount or number of",
|
||
partOfSpeech: "determiner",
|
||
pronunciation: "/sʌm/",
|
||
difficultyLevel: "A1",
|
||
isPhrase: false,
|
||
synonyms: ["several", "a few"],
|
||
example: "I need some help.",
|
||
exampleTranslation: "我需要一些幫助。"
|
||
},
|
||
"slack": {
|
||
word: "slack",
|
||
translation: "寬鬆;懈怠",
|
||
definition: "looseness; lack of tension",
|
||
partOfSpeech: "noun",
|
||
pronunciation: "/slæk/",
|
||
difficultyLevel: "B1",
|
||
isPhrase: false,
|
||
synonyms: ["looseness", "leeway"],
|
||
example: "There's too much slack in this rope.",
|
||
exampleTranslation: "這條繩子太鬆了。"
|
||
},
|
||
"until": {
|
||
word: "until",
|
||
translation: "直到",
|
||
definition: "up to a particular time",
|
||
partOfSpeech: "preposition",
|
||
pronunciation: "/ʌnˈtɪl/",
|
||
difficultyLevel: "A2",
|
||
isPhrase: false,
|
||
synonyms: ["till", "up to"],
|
||
example: "Wait until tomorrow.",
|
||
exampleTranslation: "等到明天。"
|
||
},
|
||
"get": {
|
||
word: "get",
|
||
translation: "變得;獲得",
|
||
definition: "to become or obtain",
|
||
partOfSpeech: "verb",
|
||
pronunciation: "/ɡet/",
|
||
difficultyLevel: "A1",
|
||
isPhrase: false,
|
||
synonyms: ["become", "obtain"],
|
||
example: "I get tired easily.",
|
||
exampleTranslation: "我很容易累。"
|
||
},
|
||
"used": {
|
||
word: "used",
|
||
translation: "習慣的",
|
||
definition: "familiar with something (used to)",
|
||
partOfSpeech: "adjective",
|
||
pronunciation: "/juːzd/",
|
||
difficultyLevel: "A2",
|
||
isPhrase: false,
|
||
synonyms: ["accustomed", "familiar"],
|
||
example: "I'm not used to this weather.",
|
||
exampleTranslation: "我不習慣這種天氣。"
|
||
},
|
||
"to": {
|
||
word: "to",
|
||
translation: "到;向",
|
||
definition: "preposition expressing direction",
|
||
partOfSpeech: "preposition",
|
||
pronunciation: "/tu/",
|
||
difficultyLevel: "A1",
|
||
isPhrase: false,
|
||
synonyms: [],
|
||
example: "I'm going to school.",
|
||
exampleTranslation: "我要去學校。"
|
||
},
|
||
"workflow": {
|
||
word: "workflow",
|
||
translation: "工作流程",
|
||
definition: "the sequence of processes through which work passes",
|
||
partOfSpeech: "noun",
|
||
pronunciation: "/ˈwɜːrkfloʊ/",
|
||
difficultyLevel: "B2",
|
||
isPhrase: false,
|
||
synonyms: ["process", "procedure", "system"],
|
||
example: "We need to improve our workflow.",
|
||
exampleTranslation: "我們需要改善工作流程。"
|
||
},
|
||
"joined": {
|
||
word: "joined",
|
||
translation: "加入",
|
||
definition: "became a member of (past tense of join)",
|
||
partOfSpeech: "verb",
|
||
pronunciation: "/dʒɔɪnd/",
|
||
difficultyLevel: "B1",
|
||
isPhrase: false,
|
||
synonyms: ["entered", "became part of"],
|
||
example: "He joined the company last year.",
|
||
exampleTranslation: "他去年加入了這家公司。"
|
||
},
|
||
"gets": {
|
||
word: "gets",
|
||
translation: "變得;獲得",
|
||
definition: "becomes or obtains (third person singular)",
|
||
partOfSpeech: "verb",
|
||
pronunciation: "/ɡets/",
|
||
difficultyLevel: "A1",
|
||
isPhrase: false,
|
||
synonyms: ["becomes", "obtains"],
|
||
example: "It gets cold at night.",
|
||
exampleTranslation: "晚上會變冷。"
|
||
},
|
||
"cut someone some slack": {
|
||
word: "cut someone some slack",
|
||
translation: "對某人寬容一點",
|
||
definition: "to be more lenient or forgiving with someone",
|
||
partOfSpeech: "idiom",
|
||
pronunciation: "/kʌt ˈsʌmwʌn sʌm slæk/",
|
||
difficultyLevel: "B2",
|
||
isPhrase: true,
|
||
synonyms: ["be lenient", "be forgiving", "give leeway"],
|
||
example: "Cut him some slack, he's new here.",
|
||
exampleTranslation: "對他寬容一點,他是新來的。"
|
||
},
|
||
}
|
||
|
||
// 設定結果 - 包含語法錯誤情境
|
||
setFinalText("She just joined the team, so let's cut her some slack until she gets used to the workflow.") // 修正後的句子
|
||
setSentenceAnalysis(mockAnalysis)
|
||
setSentenceMeaning("她剛加入團隊,所以讓我們對她寬容一點,直到她習慣工作流程。")
|
||
setGrammarCorrection({
|
||
hasErrors: true,
|
||
originalText: testSentence, // 有錯誤的原始句子
|
||
correctedText: "She just joined the team, so let's cut her some slack until she gets used to the workflow.",
|
||
corrections: [
|
||
{
|
||
error: "join",
|
||
correction: "joined",
|
||
type: "時態錯誤",
|
||
explanation: "第三人稱單數過去式應使用 'joined'"
|
||
},
|
||
{
|
||
error: "get",
|
||
correction: "gets",
|
||
type: "時態錯誤",
|
||
explanation: "第三人稱單數現在式應使用 'gets'"
|
||
}
|
||
]
|
||
})
|
||
setShowAnalysisView(true)
|
||
|
||
console.log('✅ 假資料設定完成')
|
||
} catch (error) {
|
||
console.error('Error in sentence analysis:', error)
|
||
setGrammarCorrection({
|
||
hasErrors: true,
|
||
originalText: textInput,
|
||
correctedText: textInput,
|
||
corrections: []
|
||
})
|
||
setSentenceMeaning('分析過程中發生錯誤,請稍後再試。')
|
||
setFinalText(textInput)
|
||
setShowAnalysisView(true)
|
||
} finally {
|
||
setIsAnalyzing(false)
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
const handleAcceptCorrection = useCallback(() => {
|
||
if (grammarCorrection?.correctedText) {
|
||
setFinalText(grammarCorrection.correctedText)
|
||
console.log('✅ 已採用修正版本,後續學習將基於正確的句子進行!')
|
||
}
|
||
}, [grammarCorrection?.correctedText])
|
||
|
||
const handleRejectCorrection = useCallback(() => {
|
||
setFinalText(grammarCorrection?.originalText || textInput)
|
||
console.log('📝 已保持原始版本,將基於您的原始輸入進行學習。')
|
||
}, [grammarCorrection?.originalText, textInput])
|
||
|
||
// 詞彙統計計算 - 移到組件頂層避免Hooks順序問題
|
||
const vocabularyStats = useMemo(() => {
|
||
if (!sentenceAnalysis) return null
|
||
|
||
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
|
||
let simpleCount = 0
|
||
let moderateCount = 0
|
||
let difficultCount = 0
|
||
let phraseCount = 0
|
||
|
||
Object.entries(sentenceAnalysis).forEach(([, wordData]: [string, any]) => {
|
||
const isPhrase = wordData?.isPhrase || wordData?.IsPhrase
|
||
const difficultyLevel = wordData?.difficultyLevel || 'A1'
|
||
|
||
if (isPhrase) {
|
||
phraseCount++
|
||
} else {
|
||
const userIndex = getLevelIndex(userLevel)
|
||
const wordIndex = getLevelIndex(difficultyLevel)
|
||
|
||
if (userIndex > wordIndex) {
|
||
simpleCount++
|
||
} else if (userIndex === wordIndex) {
|
||
moderateCount++
|
||
} else {
|
||
difficultCount++
|
||
}
|
||
}
|
||
})
|
||
|
||
return { simpleCount, moderateCount, difficultCount, phraseCount }
|
||
}, [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 || 'unknown',
|
||
example: `Example sentence with ${word}.` // 提供預設例句
|
||
}
|
||
|
||
const response = await flashcardsService.createFlashcard(cardData)
|
||
|
||
if (response.success) {
|
||
console.log(`✅ 已將「${word}」保存到詞卡!`)
|
||
return { success: true }
|
||
} else {
|
||
throw new Error(response.error || '保存失敗')
|
||
}
|
||
} catch (error) {
|
||
console.error('Save word error:', error)
|
||
return { success: false, error: error instanceof Error ? error.message : '保存失敗' }
|
||
}
|
||
}, [])
|
||
|
||
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 className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-4">
|
||
<h3 className="text-sm font-semibold text-blue-900 mb-2">💡 詞彙標記說明</h3>
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
|
||
<div className="flex items-center gap-2">
|
||
<span className="inline-block w-4 h-4 bg-gray-50 border border-dashed border-gray-300 rounded opacity-80"></span>
|
||
<span className="text-gray-600">太簡單 - 已掌握</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="inline-block w-4 h-4 bg-green-50 border border-green-200 rounded"></span>
|
||
<span className="text-gray-600">重點學習 - 適合程度</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="inline-block w-4 h-4 bg-orange-50 border border-orange-200 rounded"></span>
|
||
<span className="text-gray-600">有挑戰 - 進階詞彙</span>
|
||
</div>
|
||
</div>
|
||
<p className="text-xs text-blue-700 mt-2">
|
||
點擊標記的詞彙可查看詳細解釋,一鍵保存到詞卡複習!
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* 重新設計的句子分析視圖 - 簡潔流暢 */
|
||
<div className="max-w-4xl mx-auto">
|
||
{/* 移除冗餘標題,直接進入內容 */}
|
||
|
||
{/* 語法修正面板 - 如果需要的話 */}
|
||
{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 || finalText}
|
||
</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-xs sm:text-sm 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-xs sm:text-sm 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-xs sm:text-sm 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.phraseCount}</div>
|
||
<div className="text-blue-700 text-xs sm:text-sm 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={finalText}
|
||
analysis={sentenceAnalysis || undefined}
|
||
showPhrasesInline={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) return null
|
||
|
||
// 提取片語
|
||
const phrases: Array<{
|
||
phrase: string
|
||
meaning: string
|
||
difficultyLevel: string
|
||
}> = []
|
||
|
||
Object.entries(sentenceAnalysis).forEach(([word, wordData]: [string, any]) => {
|
||
const isPhrase = wordData?.isPhrase || wordData?.IsPhrase
|
||
if (isPhrase) {
|
||
phrases.push({
|
||
phrase: wordData?.word || word,
|
||
meaning: wordData?.translation || '',
|
||
difficultyLevel: wordData?.difficultyLevel || 'A1'
|
||
})
|
||
}
|
||
})
|
||
|
||
if (phrases.length === 0) return null
|
||
|
||
|
||
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">
|
||
{phrases.map((phrase, index) => (
|
||
<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) => {
|
||
// 找到片語的完整分析資料
|
||
const phraseAnalysis = sentenceAnalysis?.["cut someone some slack"]
|
||
|
||
if (phraseAnalysis) {
|
||
// 設定片語彈窗狀態
|
||
setPhrasePopup({
|
||
phrase: phrase.phrase,
|
||
analysis: phraseAnalysis,
|
||
position: {
|
||
x: e.currentTarget.getBoundingClientRect().left + e.currentTarget.getBoundingClientRect().width / 2,
|
||
y: e.currentTarget.getBoundingClientRect().bottom + 10
|
||
}
|
||
})
|
||
}
|
||
}}
|
||
title={`${phrase.phrase}: ${phrase.meaning}`}
|
||
>
|
||
{phrase.phrase}
|
||
</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>
|
||
)}
|
||
|
||
{/* 片語彈窗 */}
|
||
{phrasePopup && (
|
||
<>
|
||
<div
|
||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||
onClick={() => setPhrasePopup(null)}
|
||
/>
|
||
<div
|
||
className="fixed z-50 bg-white rounded-xl shadow-lg w-96 max-w-md overflow-hidden"
|
||
style={{
|
||
left: `${phrasePopup.position.x}px`,
|
||
top: `${phrasePopup.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={() => setPhrasePopup(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">{phrasePopup.analysis.word}</h3>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-sm bg-gray-100 text-gray-700 px-3 py-1 rounded-full">
|
||
{phrasePopup.analysis.partOfSpeech}
|
||
</span>
|
||
<span className="text-base text-gray-600">{phrasePopup.analysis.pronunciation}</span>
|
||
</div>
|
||
|
||
<span className="px-3 py-1 rounded-full text-sm font-medium border bg-blue-100 text-blue-700 border-blue-200">
|
||
{phrasePopup.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">{phrasePopup.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">{phrasePopup.analysis.definition}</p>
|
||
</div>
|
||
|
||
{phrasePopup.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">
|
||
"{phrasePopup.analysis.example}"
|
||
</p>
|
||
<p className="text-blue-700 text-left text-sm">
|
||
{phrasePopup.analysis.exampleTranslation}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="p-4 pt-2">
|
||
<button
|
||
onClick={async () => {
|
||
const result = await handleSaveWord(phrasePopup.phrase, phrasePopup.analysis)
|
||
if (result.success) {
|
||
setPhrasePopup(null)
|
||
} else {
|
||
console.error('Save phrase 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>
|
||
)
|
||
} |