dramaling-vocab-learning/frontend/app/generate/page.tsx

643 lines
27 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState } from 'react'
import { ProtectedRoute } from '@/components/ProtectedRoute'
import { Navigation } from '@/components/Navigation'
import { ClickableTextV2 } from '@/components/ClickableTextV2'
import { GrammarCorrectionPanel } from '@/components/GrammarCorrectionPanel'
function GenerateContent() {
const [mode, setMode] = useState<'manual' | 'screenshot'>('manual')
const [textInput, setTextInput] = useState('')
const [extractionType, setExtractionType] = useState<'vocabulary' | 'smart'>('vocabulary')
const [cardCount, setCardCount] = useState(10)
const [isGenerating, setIsGenerating] = useState(false)
const [isAnalyzing, setIsAnalyzing] = useState(false)
const [generatedCards, setGeneratedCards] = useState<any[]>([])
const [showPreview, setShowPreview] = 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"
},
"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"
},
"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
},
"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,
learningPriority: "medium",
difficultyLevel: "B1"
}
}
}
// 模擬有語法錯誤的句子分析資料
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('📝 已保持原始版本,將基於您的原始輸入進行學習。')
}
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 */}
<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>
{/* 功能說明 */}
<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>
{/* 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-primary bg-primary-light'
: '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-primary bg-primary-light'
: 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">
<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-primary 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>
{/* Extraction Type 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={() => setExtractionType('vocabulary')}
className={`p-4 rounded-lg border-2 transition-all ${
extractionType === 'vocabulary'
? 'border-primary bg-primary-light'
: '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"> API CEFR</div>
</button>
<button
onClick={() => setExtractionType('smart')}
className={`p-4 rounded-lg border-2 transition-all ${
extractionType === 'smart'
? 'border-primary bg-primary-light'
: '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">AI </div>
</button>
</div>
</div>
{/* 分析按鈕 */}
<div className="space-y-4">
<button
onClick={handleAnalyzeSentence}
disabled={isAnalyzing || (mode === 'manual' && (!textInput || textInput.length > 300)) || (mode === 'screenshot')}
className="w-full bg-primary text-white py-4 rounded-lg font-semibold hover:bg-primary-hover 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}
/>
)}
{/* 分析統計 */}
<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="space-y-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>
{finalText !== textInput && (
<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>
<div className="text-xs text-blue-600 mt-1"> </div>
</div>
</div>
)}
<div>
<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>
{/* 互動式文字 */}
<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={() => {
setShowAnalysisView(false)
setShowPreview(true)
// 這裡可以整合原有的詞卡生成功能
alert('詞卡生成功能整合中...')
}}
className="flex-1 bg-primary text-white py-3 rounded-lg font-medium hover:bg-primary-hover transition-colors"
>
📖
</button>
</div>
</div>
</div>
)}
</div>
</div>
)
}
export default function GeneratePage() {
return (
<ProtectedRoute>
<GenerateContent />
</ProtectedRoute>
)
}