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

416 lines
18 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)
// 處理句子分析 - 使用真實API
const handleAnalyzeSentence = async () => {
console.log('🚀 handleAnalyzeSentence 被調用 (真實API模式)')
console.log('📝 輸入文本:', textInput)
if (!textInput.trim()) {
console.log('❌ 文本為空,退出')
return
}
setIsAnalyzing(true)
try {
// 取得用戶設定的程度
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
console.log('🎯 使用用戶程度:', userLevel)
const response = await fetch('http://localhost:5000/api/ai/analyze-sentence', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
inputText: textInput,
userLevel: userLevel, // 傳遞用戶程度
analysisMode: 'full'
})
})
if (response.ok) {
const result = await response.json()
console.log('✅ API分析完成:', result)
if (result.success) {
// 嘗試不同的屬性名稱格式
const wordAnalysisData = result.data.WordAnalysis || result.data.wordAnalysis || {};
console.log('🔍 設置sentenceAnalysis:', wordAnalysisData);
setSentenceAnalysis(wordAnalysisData)
setSentenceMeaning(result.data.SentenceMeaning?.Translation || '')
setGrammarCorrection(result.data.GrammarCorrection || null)
setFinalText(result.data.FinalAnalysisText || textInput)
setShowAnalysisView(true)
setUsageCount(prev => prev + 1)
} else {
throw new Error(result.error || '分析失敗')
}
} else {
throw new Error(`API錯誤: ${response.status}`)
}
} catch (error) {
console.error('Error in real API analysis:', error)
alert(`分析句子時發生錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`)
} finally {
setIsAnalyzing(false)
}
}
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 = {
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) {
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
}}
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>
)
}