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

560 lines
27 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 Link from 'next/link'
import { ProtectedRoute } from '@/components/ProtectedRoute'
import { Navigation } from '@/components/Navigation'
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 [generatedCards, setGeneratedCards] = useState<any[]>([])
const [showPreview, setShowPreview] = useState(false)
const [isPremium] = useState(false) // Mock premium status
const [showImageForCard, setShowImageForCard] = useState<{ [key: number]: boolean }>({}) // Track which cards show images
const [modalImage, setModalImage] = useState<string | null>(null) // Track modal image
const mockGeneratedCards = [
{
id: 1,
word: 'brought',
partOfSpeech: 'verb',
pronunciation: {
us: '/brɔːt/',
uk: '/brɔːt/'
},
translation: '提出、帶來',
definition: 'Past tense of bring; to mention or introduce a topic in conversation',
synonyms: ['mentioned', 'raised', 'introduced'],
antonyms: ['concealed', 'withheld'],
originalExample: 'He brought this thing up during our meeting and no one agreed.',
originalExampleTranslation: '他在我們的會議中提出了這件事,但沒有人同意。',
generatedExample: {
sentence: 'She brought up an interesting point about the budget.',
translation: '她提出了一個關於預算的有趣觀點。',
imageUrl: '/images/examples/bring_up.png',
audioUrl: '#'
},
difficulty: 'B1'
},
{
id: 2,
word: 'instincts',
partOfSpeech: 'noun',
pronunciation: {
us: '/ˈɪnstɪŋkts/',
uk: '/ˈɪnstɪŋkts/'
},
translation: '本能、直覺',
definition: 'Natural abilities that help living things survive without learning',
synonyms: ['intuition', 'impulse', 'tendency'],
antonyms: ['logic', 'reasoning'],
originalExample: 'Animals use their instincts to find food and stay safe.',
originalExampleTranslation: '動物利用本能來尋找食物並保持安全。',
generatedExample: {
sentence: 'Trust your instincts when making important decisions.',
translation: '在做重要決定時要相信你的直覺。',
imageUrl: '/images/examples/instinct.png',
audioUrl: '#'
},
difficulty: 'B2'
},
{
id: 3,
word: 'warrants',
partOfSpeech: 'noun',
pronunciation: {
us: '/ˈːrənts/',
uk: '/ˈwɒrənts/'
},
translation: '搜查令、授權令',
definition: 'Official documents that give police permission to do something',
synonyms: ['authorization', 'permit', 'license'],
antonyms: ['prohibition', 'ban'],
originalExample: 'The police obtained warrants to search the building.',
originalExampleTranslation: '警方取得了搜查令來搜查這棟建築物。',
generatedExample: {
sentence: 'The judge issued arrest warrants for three suspects.',
translation: '法官對三名嫌疑人發出了逮捕令。',
imageUrl: '/images/examples/warrant.png',
audioUrl: '#'
},
difficulty: 'C1'
}
]
const handleGenerate = () => {
setIsGenerating(true)
// Simulate AI generation
setTimeout(() => {
setGeneratedCards(mockGeneratedCards)
setShowPreview(true)
setIsGenerating(false)
}, 2000)
}
const handleSaveCards = () => {
// Mock save action
alert('詞卡已保存到您的卡組!')
}
const toggleImageForCard = (cardId: number) => {
setShowImageForCard(prev => ({
...prev,
[cardId]: !prev[cardId]
}))
}
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<Navigation />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{!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>
{/* 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>
{/* Content Input */}
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
{mode === 'manual' ? (
<div>
<h2 className="text-lg font-semibold mb-4"></h2>
<textarea
value={textInput}
onChange={(e) => setTextInput(e.target.value)}
placeholder="貼上您想要學習的英文文本,例如影劇對話、文章段落..."
className="w-full h-40 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent outline-none resize-none"
/>
<div className="mt-2 text-sm text-gray-600">
5000 {textInput.length}
</div>
{/* Extraction Type Info */}
{extractionType === 'vocabulary' ? (
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
<div className="text-sm text-blue-800">
<strong>📖 </strong>
<ul className="mt-2 space-y-1 text-xs">
<li> API</li>
<li> CEFR </li>
<li> </li>
</ul>
</div>
</div>
) : (
<div className="mt-4 p-3 bg-purple-50 rounded-lg">
<div className="text-sm text-purple-800">
<strong>🤖 </strong>
<ul className="mt-2 space-y-1 text-xs">
<li> AI </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
)}
</div>
) : (
<div>
<h2 className="text-lg font-semibold mb-4"></h2>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
<div className="text-4xl mb-3">📷</div>
<div className="text-gray-600 mb-2"></div>
<div className="text-sm text-gray-500"> JPG, PNG 10MB</div>
<div className="mt-4">
<span className="inline-block px-4 py-2 bg-gray-100 text-gray-400 rounded-lg">
Phase 2
</span>
</div>
</div>
</div>
)}
</div>
{/* Generation Settings */}
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
<h2 className="text-lg font-semibold mb-4"></h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{cardCount}
</label>
<input
type="range"
min="5"
max="20"
value={cardCount}
onChange={(e) => setCardCount(Number(e.target.value))}
className="w-full"
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>5</span>
<span> 10</span>
<span>20</span>
</div>
</div>
{/* User Limits */}
<div className="p-3 bg-gray-50 rounded-lg">
<div className="text-sm">
{isPremium ? (
<div className="text-green-700">
<strong>🌟 </strong>
<div className="text-xs mt-1">
50
<br />
使0/50
</div>
</div>
) : (
<div className="text-gray-700">
<strong>🆓 </strong>
<div className="text-xs mt-1">
<br />
使
<br />
</div>
</div>
)}
</div>
</div>
</div>
</div>
{/* Generate Button */}
<button
onClick={handleGenerate}
disabled={isGenerating || (mode === 'manual' && !textInput) || (mode === 'screenshot')}
className="w-full bg-primary text-white py-4 rounded-lg font-semibold hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isGenerating ? (
<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>
...
</span>
) : extractionType === 'vocabulary' ? (
'📖 開始詞彙萃取'
) : (
'🤖 開始智能萃取'
)}
</button>
</div>
) : (
/* Preview Generated Cards */
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold"></h1>
<button
onClick={() => setShowPreview(false)}
className="text-gray-600 hover:text-gray-900"
>
</button>
</div>
<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"> {generatedCards.length} </h2>
<div className="space-x-3">
<button className="text-primary hover:text-primary-hover font-medium">
</button>
<button
onClick={handleSaveCards}
className="bg-primary text-white px-6 py-2 rounded-lg font-medium hover:bg-primary-hover transition-colors"
>
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{generatedCards.map((card) => (
<div key={card.id} className="border rounded-lg p-5 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h3 className="text-xl font-bold">{card.word}</h3>
<div className="flex items-center gap-4 mt-1">
<span className="text-sm text-gray-600">{card.partOfSpeech}</span>
<div className="flex items-center gap-2">
<button className="text-xs bg-gray-100 px-2 py-1 rounded hover:bg-gray-200">
🇺🇸 {card.pronunciation.us}
</button>
<button className="text-xs bg-gray-100 px-2 py-1 rounded hover:bg-gray-200">
🇬🇧 {card.pronunciation.uk}
</button>
<button className="text-primary hover:text-primary-hover">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
</button>
</div>
</div>
</div>
<button className="text-red-500 hover:text-red-700 text-2xl">×</button>
</div>
<div className="space-y-3">
<div>
<div className="text-sm font-semibold text-gray-700"></div>
<div className="text-base font-medium">{card.translation}</div>
</div>
<div>
<div className="text-sm font-semibold text-gray-700"></div>
<div className="text-sm text-gray-600">{card.definition}</div>
</div>
{card.synonyms.length > 0 && (
<div>
<div className="text-sm font-semibold text-gray-700"></div>
<div className="flex flex-wrap gap-2 mt-1">
{card.synonyms.map((syn, idx) => (
<span key={idx} className="px-2 py-1 bg-blue-100 text-blue-700 rounded-full text-xs">
{syn}
</span>
))}
</div>
</div>
)}
{card.antonyms.length > 0 && (
<div>
<div className="text-sm font-semibold text-gray-700"></div>
<div className="flex flex-wrap gap-2 mt-1">
{card.antonyms.map((ant, idx) => (
<span key={idx} className="px-2 py-1 bg-red-100 text-red-700 rounded-full text-xs">
{ant}
</span>
))}
</div>
</div>
)}
<div>
<div className="text-sm font-semibold text-gray-700 mb-2"></div>
<div className="space-y-3">
{/* 原始例句 */}
<div className="border-l-2 border-gray-400 pl-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="text-sm text-gray-800">
{card.originalExample.split(card.word).map((part, i) => (
<span key={i}>
{part}
{i < card.originalExample.split(card.word).length - 1 && (
<span className="font-bold text-primary">{card.word}</span>
)}
</span>
))}
</div>
<div className="text-sm text-gray-500 mt-1">{card.originalExampleTranslation}</div>
</div>
<div className="flex items-center gap-1 ml-2">
<button className="text-gray-400 hover:text-primary" title="播放例句">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
</div>
</div>
{/* 生成例句 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="text-xs text-primary mb-1">AI </div>
<div className="text-sm text-gray-800">
{card.generatedExample.sentence.split(card.word).map((part, i) => (
<span key={i}>
{part}
{i < card.generatedExample.sentence.split(card.word).length - 1 && (
<span className="font-bold text-primary">{card.word}</span>
)}
</span>
))}
</div>
<div className="text-sm text-gray-500 mt-1">{card.generatedExample.translation}</div>
</div>
<div className="flex items-center gap-1 ml-2">
<button className="text-gray-400 hover:text-primary" title="播放例句">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<button
onClick={() => toggleImageForCard(card.id)}
className={`${showImageForCard[card.id] ? 'text-primary' : 'text-gray-400'} hover:text-primary`}
title={showImageForCard[card.id] ? "隱藏例句圖" : "查看例句圖"}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</button>
</div>
</div>
{/* Example Image Display */}
{showImageForCard[card.id] && card.generatedExample.imageUrl && (
<div className="mt-3 p-2 bg-gray-50 rounded-lg">
<div className="text-xs text-gray-600 mb-2 flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<img
src={card.generatedExample.imageUrl}
alt="Example context"
className="w-full rounded-md shadow-sm cursor-pointer hover:shadow-lg transition-shadow"
style={{ maxHeight: '250px', objectFit: 'contain', backgroundColor: 'white' }}
onClick={() => setModalImage(card.generatedExample.imageUrl)}
/>
<div className="text-xs text-gray-500 mt-2 text-center"></div>
</div>
)}
</div>
</div>
</div>
</div>
<div className="mt-4 pt-3 border-t flex justify-between items-center">
<span className="text-xs text-gray-500">CEFR {card.difficulty}</span>
<button className="text-primary text-sm hover:text-primary-hover font-medium">
</button>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
{/* Image Modal */}
{modalImage && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 p-4"
onClick={() => setModalImage(null)}
>
<div
className="relative max-w-4xl max-h-[90vh] bg-white rounded-lg overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Close Button */}
<button
onClick={() => setModalImage(null)}
className="absolute top-2 right-2 z-10 p-2 bg-white bg-opacity-90 rounded-full hover:bg-opacity-100 transition-all shadow-lg"
>
<svg className="w-6 h-6 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Image */}
<div className="p-4">
<img
src={modalImage}
alt="Example context enlarged"
className="w-full h-full object-contain"
style={{ maxHeight: 'calc(90vh - 2rem)' }}
/>
</div>
</div>
</div>
)}
</div>
)
}
export default function GeneratePage() {
return (
<ProtectedRoute>
<GenerateContent />
</ProtectedRoute>
)
}