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

548 lines
22 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 { flashcardsService } from '@/lib/services/flashcards'
import Link from 'next/link'
function GenerateContent() {
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(true)
// 處理句子分析 - 使用假數據進行快速測試
const handleAnalyzeSentence = async () => {
console.log('🚀 handleAnalyzeSentence 被調用 (假數據模式)')
console.log('📝 輸入文本:', textInput)
if (!textInput.trim()) {
console.log('❌ 文本為空,退出')
return
}
setIsAnalyzing(true)
try {
// 模擬API延遲
await new Promise(resolve => setTimeout(resolve, 1500))
// 生成假的分析數據
const mockAnalysis = generateMockAnalysis(textInput)
setSentenceAnalysis(mockAnalysis.wordAnalysis)
setSentenceMeaning(mockAnalysis.sentenceTranslation)
setGrammarCorrection(mockAnalysis.grammarCorrection)
setFinalText(mockAnalysis.finalText)
setShowAnalysisView(true)
setUsageCount(prev => prev + 1)
console.log('✅ 假數據分析完成:', mockAnalysis)
} catch (error) {
console.error('Error in mock analysis:', error)
alert(`分析句子時發生錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`)
} finally {
setIsAnalyzing(false)
}
}
// 生成假的分析數據
const generateMockAnalysis = (inputText: string) => {
const words = inputText.toLowerCase().split(/\s+/).filter(word =>
word.length > 2 && /^[a-z]+$/.test(word.replace(/[.,!?;:]/g, ''))
)
const wordAnalysis: any = {}
words.forEach((word, index) => {
const cleanWord = word.replace(/[.,!?;:]/g, '')
const isHighValue = index % 3 === 0 // 每3個詞中有1個高價值
const isPhrase = cleanWord.length > 6 // 長詞視為片語
wordAnalysis[cleanWord] = {
word: cleanWord,
translation: getRandomTranslation(cleanWord),
definition: `Definition of ${cleanWord} - a common English word`,
partOfSpeech: getRandomPartOfSpeech(),
pronunciation: `/${cleanWord}/`,
isHighValue: isHighValue,
isPhrase: isPhrase,
difficultyLevel: getRandomDifficulty(),
synonyms: [getRandomSynonym(cleanWord), getRandomSynonym(cleanWord)],
learningPriority: isHighValue ? 'high' : 'medium'
}
})
return {
wordAnalysis,
sentenceTranslation: `這是「${inputText}」的中文翻譯。`,
grammarCorrection: {
hasErrors: Math.random() > 0.7, // 30%機率有語法錯誤
correctedText: inputText,
originalText: inputText
},
finalText: inputText
}
}
// 輔助函數 - 改進翻譯生成
const getRandomTranslation = (word: string) => {
// 常見英文單字的實際翻譯
const commonTranslations: {[key: string]: string} = {
'hello': '你好',
'how': '如何',
'are': '是',
'you': '你',
'today': '今天',
'good': '好的',
'morning': '早晨',
'evening': '晚上',
'thank': '謝謝',
'please': '請',
'sorry': '抱歉',
'love': '愛',
'like': '喜歡',
'want': '想要',
'need': '需要',
'think': '思考',
'know': '知道',
'see': '看見',
'go': '去',
'come': '來',
'get': '得到',
'make': '製作',
'take': '拿取',
'give': '給予',
'find': '找到',
'work': '工作',
'feel': '感覺',
'become': '成為',
'leave': '離開',
'put': '放置',
'mean': '意思',
'keep': '保持',
'let': '讓',
'begin': '開始',
'seem': '似乎',
'help': '幫助',
'talk': '談話',
'turn': '轉向',
'start': '開始',
'show': '顯示',
'hear': '聽見',
'play': '玩耍',
'run': '跑步',
'move': '移動',
'live': '生活',
'believe': '相信',
'bring': '帶來',
'happen': '發生',
'write': '寫作',
'provide': '提供',
'sit': '坐下',
'stand': '站立',
'lose': '失去',
'pay': '付費',
'meet': '遇見',
'include': '包含',
'continue': '繼續',
'set': '設置',
'learn': '學習',
'change': '改變',
'lead': '領導',
'understand': '理解',
'watch': '觀看',
'follow': '跟隨',
'stop': '停止',
'create': '創造',
'speak': '說話',
'read': '閱讀',
'allow': '允許',
'add': '添加',
'spend': '花費',
'grow': '成長',
'open': '打開',
'walk': '走路',
'win': '獲勝',
'offer': '提供',
'remember': '記住',
'consider': '考慮',
'appear': '出現',
'buy': '購買',
'wait': '等待',
'serve': '服務',
'die': '死亡',
'send': '發送',
'expect': '期待',
'build': '建造',
'stay': '停留',
'fall': '跌倒',
'cut': '切割',
'reach': '到達',
'kill': '殺死',
'remain': '保持'
}
return commonTranslations[word.toLowerCase()] || `${word}的翻譯`
}
const getRandomPartOfSpeech = () => {
const parts = ['noun', 'verb', 'adjective', 'adverb', 'preposition']
return parts[Math.floor(Math.random() * parts.length)]
}
const getRandomDifficulty = () => {
const levels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
return levels[Math.floor(Math.random() * levels.length)]
}
const getRandomSynonym = (word: string) => {
return `synonym_${word}_${Math.floor(Math.random() * 10)}`
}
const handleAcceptCorrection = () => {
if (grammarCorrection?.correctedText) {
setFinalText(grammarCorrection.correctedText)
alert('✅ 已採用修正版本,後續學習將基於正確的句子進行!')
}
}
const handleRejectCorrection = () => {
setFinalText(grammarCorrection?.originalText || textInput)
alert('📝 已保持原始版本,將基於您的原始輸入進行學習。')
}
// 保存單個詞彙
const handleSaveWord = async (word: string, analysis: any) => {
try {
const cardData = {
english: word, // 修正API欄位名稱
chinese: analysis.translation || analysis.Translation || '',
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) {
alert(`✅ 已將「${word}」保存到詞卡!`)
} else {
throw new Error(response.error || '保存失敗')
}
} catch (error) {
console.error('Save word error:', error)
throw error // 重新拋出錯誤讓組件處理
}
}
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>
{/* 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"></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>
</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> */}
{/* Action Buttons */}
<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>
... (AI 3-5 )
</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 className="text-center text-sm text-gray-600 mt-2">
{(() => {
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2';
const getTargetRange = (level: string) => {
const ranges = {
'A1': 'A2-B1', 'A2': 'B1-B2', 'B1': 'B2-C1',
'B2': 'C1-C2', 'C1': 'C2', 'C2': 'C2'
};
return ranges[level as keyof typeof ranges] || 'B1-B2';
};
return (
<div className="flex items-center justify-center gap-2">
<span>🎯 : {userLevel}</span>
<span className="text-gray-400">|</span>
<span>📈 : {getTargetRange(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">
{/* 移除冗餘標題,直接進入內容 */}
{/* 語法修正面板 - 如果需要的話 */}
{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">
{/* 句子主體展示 */}
<div className="text-center mb-8">
<div className="text-3xl leading-relaxed font-medium text-gray-900 mb-6">
<ClickableTextV2
text={finalText}
analysis={sentenceAnalysis}
remainingUsage={5 - usageCount}
onWordClick={(word, analysis) => {
console.log('Clicked word:', word, analysis)
}}
onWordCostConfirm={async () => {
return true
}}
onNewWordAnalysis={(word, newAnalysis) => {
setSentenceAnalysis((prev: any) => ({
...prev,
[word]: newAnalysis
}))
console.log(`✅ 新增詞彙分析: ${word}`, newAnalysis)
}}
onSaveWord={handleSaveWord}
/>
</div>
{/* 翻譯 - 次要但重要 */}
<div className="text-xl text-gray-600 leading-relaxed bg-gray-50 p-4 rounded-lg">
{sentenceMeaning}
</div>
</div>
{/* 學習提示 - 精簡版 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center justify-center gap-6 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-400 border border-green-500 rounded"></div>
<span className="text-green-700"> </span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-yellow-400 border border-yellow-500 rounded"></div>
<span className="text-yellow-700"> </span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-400 border border-blue-500 rounded"></div>
<span className="text-blue-700"></span>
</div>
<span className="text-gray-600"> </span>
</div>
</div>
</div>
{/* 下方操作區 - 簡化 */}
<div className="flex justify-center">
<button
onClick={() => setShowAnalysisView(false)}
className="px-8 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary-hover transition-colors flex items-center gap-2"
>
<span>🔄</span>
<span></span>
</button>
</div>
</div>
)}
</div>
</div>
)
}
export default function GeneratePage() {
return (
<ProtectedRoute>
<GenerateContent />
</ProtectedRoute>
)
}