560 lines
27 KiB
TypeScript
560 lines
27 KiB
TypeScript
'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: '/ˈwɔː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>
|
||
)
|
||
} |