548 lines
22 KiB
TypeScript
548 lines
22 KiB
TypeScript
'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>
|
||
)
|
||
} |