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

555 lines
23 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, useEffect } 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(true)
const [cacheStatus, setCacheStatus] = useState<{
isCached: boolean
cacheHit: boolean
usingAI: boolean
message: string
} | null>(null)
// 處理句子分析 - 使用真實AI API
const handleAnalyzeSentence = async () => {
console.log('🚀 handleAnalyzeSentence 被調用')
console.log('📝 輸入文本:', textInput)
if (!textInput.trim()) {
console.log('❌ 文本為空,退出')
return
}
if (!isPremium && usageCount >= 5) {
console.log('❌ 使用次數超限')
alert('❌ 免費用戶 3 小時內只能分析 5 次句子,請稍後再試或升級到付費版本')
return
}
console.log('✅ 開始分析,設定 loading 狀態')
setIsAnalyzing(true)
try {
// 調用真實的後端AI API
console.log('🌐 發送API請求到:', 'http://localhost:5000/api/ai/analyze-sentence')
const response = await fetch('http://localhost:5000/api/ai/analyze-sentence', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputText: textInput,
analysisMode: 'full'
})
})
console.log('📡 API響應狀態:', response.status, response.statusText)
if (!response.ok) {
throw new Error(`API 錯誤: ${response.status}`)
}
const result = await response.json()
console.log('📦 完整API響應:', result)
if (result.success) {
// 設定快取狀態
setCacheStatus({
isCached: result.cached || false,
cacheHit: result.cacheHit || false,
usingAI: result.usingAI || false,
message: result.message || '分析完成'
})
// 使用真實AI的回應資料 - 支援兩種key格式 (小寫/大寫)
setSentenceAnalysis(result.data.wordAnalysis || result.data.WordAnalysis || {})
// 安全處理 sentenceMeaning - 支援兩種key格式 (小寫/大寫)
const sentenceMeaning = result.data.sentenceMeaning || result.data.SentenceMeaning || {}
const translation = sentenceMeaning.Translation || sentenceMeaning.translation || '翻譯處理中...'
const explanation = sentenceMeaning.Explanation || sentenceMeaning.explanation || '解釋處理中...'
setSentenceMeaning(translation + ' ' + explanation)
setGrammarCorrection(result.data.grammarCorrection || result.data.GrammarCorrection || { hasErrors: false })
setFinalText(result.data.finalAnalysisText || result.data.FinalAnalysisText || textInput)
setShowAnalysisView(true)
setUsageCount(prev => prev + 1)
} else {
throw new Error(result.error || '分析失敗')
}
} catch (error) {
console.error('Error analyzing sentence:', 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 handleGenerate = async () => {
if (!textInput.trim()) return
setIsGenerating(true)
try {
const response = await fetch('http://localhost:5000/api/ai/test/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputText: textInput,
extractionType: extractionType,
cardCount: cardCount
})
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
if (result.success) {
setGeneratedCards(result.data)
setShowPreview(true)
setShowAnalysisView(false)
} else {
throw new Error(result.error || '生成詞卡失敗')
}
} catch (error) {
console.error('Error generating cards:', error)
alert(`生成詞卡時發生錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`)
} finally {
setIsGenerating(false)
}
}
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 && !showPreview ? (
<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>
</div>
) : showAnalysisView ? (
/* 句子分析視圖 */
<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">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold"></h2>
{cacheStatus && (
<div className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
cacheStatus.isCached
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{cacheStatus.isCached ? (
<>
<span className="mr-1">💾</span>
<span></span>
</>
) : (
<>
<span className="mr-1">🤖</span>
<span>AI </span>
</>
)}
</div>
)}
</div>
<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-blue-50 rounded-lg border-l-4 border-blue-400 mb-4">
<p className="text-sm text-blue-800">
💡 <strong>使</strong><br/>
🟡 <strong> + </strong> = <br/>
🟢 <strong> + </strong> = <br/>
🔵 <strong></strong> = 1
</p>
</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)
// 這裡可以整合從分析結果生成詞卡的功能
}}
className="flex-1 bg-primary text-white py-3 rounded-lg font-medium hover:bg-primary-hover transition-colors"
>
📖
</button>
</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>
<div className="flex gap-2">
<button
onClick={() => {
setShowPreview(false)
setShowAnalysisView(true)
}}
className="text-gray-600 hover:text-gray-900"
>
</button>
<span className="text-gray-300">|</span>
<button
onClick={() => {
setShowPreview(false)
setShowAnalysisView(false)
}}
className="text-gray-600 hover:text-gray-900"
>
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{generatedCards.map((card, index) => (
<div key={index} className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
{/* 詞卡內容 */}
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold text-gray-900">{card.word}</h3>
<span className="text-sm bg-gray-100 text-gray-600 px-2 py-1 rounded">
{card.partOfSpeech}
</span>
</div>
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-500"></span>
<p className="text-gray-700">{card.pronunciation}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500"></span>
<p className="text-gray-900 font-medium">{card.translation}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500"></span>
<p className="text-gray-700">{card.definition}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500"></span>
<p className="text-gray-700 italic">"{card.example}"</p>
<p className="text-gray-600 text-sm mt-1">{card.exampleTranslation}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500"></span>
<div className="flex flex-wrap gap-1 mt-1">
{card.synonyms.map((synonym: string, idx: number) => (
<span key={idx} className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
{synonym}
</span>
))}
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t">
<span className="text-xs text-gray-500">: {card.difficultyLevel}</span>
</div>
</div>
</div>
</div>
))}
</div>
{/* 操作按鈕 */}
<div className="mt-8 flex justify-center gap-4">
<button
onClick={() => setShowPreview(false)}
className="px-6 py-3 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
>
🔄
</button>
<button
onClick={() => alert('保存功能開發中...')}
className="px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors"
>
💾
</button>
</div>
</div>
)}
</div>
</div>
)
}
export default function GeneratePage() {
return (
<ProtectedRoute>
<GenerateContent />
</ProtectedRoute>
)
}