555 lines
23 KiB
TypeScript
555 lines
23 KiB
TypeScript
'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>
|
||
)
|
||
} |