feat: 實現完整測驗模式與 AI 生成功能更新
主要更新: 1. 測驗模式重構 - 選擇題:改為英文定義選中文翻譯 - 新增填空題模式(含例句圖片顯示) - 新增聽力測試模式 - 新增口說測試模式 2. AI 生成功能更新 - 改為詞彙萃取和智能萃取兩種模式 - 實現用戶權限差異(免費/訂閱) - 例句圖改為模態視窗顯示 3. 其他改進 - 整合真實例句圖片資源 - 難度等級改用 CEFR 標準 (A1-C2) - 優化圖片顯示比例與體驗 - 移除測驗模式正確率顯示 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1d0acf5111
commit
96e205fd7f
|
|
@ -11,7 +11,8 @@
|
|||
"Bash(xargs:*)",
|
||||
"Bash(npm init:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(npm uninstall:*)"
|
||||
"Bash(npm uninstall:*)",
|
||||
"Bash(git push:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -4,57 +4,83 @@ import { useState } from 'react'
|
|||
import Link from 'next/link'
|
||||
|
||||
export default function GeneratePage() {
|
||||
const [mode, setMode] = useState<'text' | 'theme'>('text')
|
||||
const [mode, setMode] = useState<'manual' | 'screenshot'>('manual')
|
||||
const [textInput, setTextInput] = useState('')
|
||||
const [selectedTheme, setSelectedTheme] = useState('')
|
||||
const [difficulty, setDifficulty] = useState('intermediate')
|
||||
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 themes = [
|
||||
{ id: 'daily', name: '日常對話', icon: '🗣️' },
|
||||
{ id: 'business', name: '商務英語', icon: '💼' },
|
||||
{ id: 'tv', name: '美劇經典', icon: '📺' },
|
||||
{ id: 'movie', name: '電影台詞', icon: '🎬' },
|
||||
{ id: 'academic', name: '學術英語', icon: '🎓' },
|
||||
{ id: 'travel', name: '旅遊英語', icon: '✈️' },
|
||||
]
|
||||
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: 'negotiate',
|
||||
word: 'brought',
|
||||
partOfSpeech: 'verb',
|
||||
pronunciation: '/nɪˈɡoʊʃieɪt/',
|
||||
translation: '協商、談判',
|
||||
definition: 'To discuss something with someone in order to reach an agreement',
|
||||
example: 'We need to negotiate a better deal with our suppliers.',
|
||||
exampleTranslation: '我們需要與供應商協商更好的交易。',
|
||||
difficulty: 'intermediate'
|
||||
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: 'perspective',
|
||||
word: 'instincts',
|
||||
partOfSpeech: 'noun',
|
||||
pronunciation: '/pərˈspektɪv/',
|
||||
translation: '觀點、看法',
|
||||
definition: 'A particular way of considering something',
|
||||
example: 'From my perspective, this is the best solution.',
|
||||
exampleTranslation: '從我的角度來看,這是最好的解決方案。',
|
||||
difficulty: 'intermediate'
|
||||
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: 'accomplish',
|
||||
partOfSpeech: 'verb',
|
||||
pronunciation: '/əˈkɒmplɪʃ/',
|
||||
translation: '完成、達成',
|
||||
definition: 'To finish something successfully or to achieve something',
|
||||
example: 'She accomplished her goal of running a marathon.',
|
||||
exampleTranslation: '她完成了跑馬拉松的目標。',
|
||||
difficulty: 'intermediate'
|
||||
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'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -73,6 +99,13 @@ export default function GeneratePage() {
|
|||
alert('詞卡已保存到您的卡組!')
|
||||
}
|
||||
|
||||
const toggleImageForCard = (cardId: number) => {
|
||||
setShowImageForCard(prev => ({
|
||||
...prev,
|
||||
[cardId]: !prev[cardId]
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Navigation */}
|
||||
|
|
@ -97,42 +130,81 @@ export default function GeneratePage() {
|
|||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-8">AI 智能生成詞卡</h1>
|
||||
|
||||
{/* Mode Selection */}
|
||||
{/* Input Mode Selection */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4">選擇生成模式</h2>
|
||||
<h2 className="text-lg font-semibold mb-4">原始例句類型</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => setMode('text')}
|
||||
onClick={() => setMode('manual')}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
mode === 'text'
|
||||
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>
|
||||
<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('theme')}
|
||||
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 ${
|
||||
mode === 'theme'
|
||||
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">選擇預設學習主題</div>
|
||||
<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 === 'text' ? (
|
||||
{mode === 'manual' ? (
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4">輸入文本內容</h2>
|
||||
<h2 className="text-lg font-semibold mb-4">輸入英文文本</h2>
|
||||
<textarea
|
||||
value={textInput}
|
||||
onChange={(e) => setTextInput(e.target.value)}
|
||||
|
|
@ -142,25 +214,44 @@ export default function GeneratePage() {
|
|||
<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="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{themes.map((theme) => (
|
||||
<button
|
||||
key={theme.id}
|
||||
onClick={() => setSelectedTheme(theme.id)}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
selectedTheme === theme.id
|
||||
? 'border-primary bg-primary-light'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl mb-1">{theme.icon}</div>
|
||||
<div className="font-medium text-sm">{theme.name}</div>
|
||||
</button>
|
||||
))}
|
||||
<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>
|
||||
)}
|
||||
|
|
@ -172,21 +263,7 @@ export default function GeneratePage() {
|
|||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
難度等級
|
||||
</label>
|
||||
<select
|
||||
value={difficulty}
|
||||
onChange={(e) => setDifficulty(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent outline-none"
|
||||
>
|
||||
<option value="beginner">初級</option>
|
||||
<option value="intermediate">中級</option>
|
||||
<option value="advanced">高級</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
生成數量:{cardCount} 個
|
||||
生成數量:{cardCount} 個詞卡
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
|
|
@ -198,16 +275,44 @@ export default function GeneratePage() {
|
|||
/>
|
||||
<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 === 'text' && !textInput) || (mode === 'theme' && !selectedTheme)}
|
||||
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 ? (
|
||||
|
|
@ -216,10 +321,12 @@ export default function GeneratePage() {
|
|||
<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 正在生成中...
|
||||
正在萃取詞彙中...
|
||||
</span>
|
||||
) : extractionType === 'vocabulary' ? (
|
||||
'📖 開始詞彙萃取'
|
||||
) : (
|
||||
'🤖 開始生成詞卡'
|
||||
'🤖 開始智能萃取'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -252,37 +359,163 @@ export default function GeneratePage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{generatedCards.map((card) => (
|
||||
<div key={card.id} className="border rounded-lg p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{card.word}</h3>
|
||||
<div className="text-sm text-gray-600">
|
||||
{card.partOfSpeech} • {card.pronunciation}
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-red-500 hover:text-red-700">
|
||||
×
|
||||
<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 className="space-y-2">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700">翻譯</div>
|
||||
<div className="text-sm">{card.translation}</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-medium text-gray-700">定義</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-medium text-gray-700">例句</div>
|
||||
<div className="text-sm text-gray-600">{card.example}</div>
|
||||
<div className="text-sm text-gray-500 mt-1">{card.exampleTranslation}</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>
|
||||
<div className="mt-3 pt-3 border-t">
|
||||
<button className="text-primary text-sm hover:text-primary-hover">
|
||||
)}
|
||||
|
||||
{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>
|
||||
|
|
@ -293,6 +526,39 @@ export default function GeneratePage() {
|
|||
</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>
|
||||
)
|
||||
}
|
||||
|
|
@ -6,60 +6,70 @@ import Link from 'next/link'
|
|||
export default function LearnPage() {
|
||||
const [currentCardIndex, setCurrentCardIndex] = useState(0)
|
||||
const [isFlipped, setIsFlipped] = useState(false)
|
||||
const [mode, setMode] = useState<'flip' | 'quiz'>('flip')
|
||||
const [mode, setMode] = useState<'flip' | 'quiz' | 'fill' | 'listening' | 'speaking'>('flip')
|
||||
const [score, setScore] = useState({ correct: 0, total: 0 })
|
||||
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null)
|
||||
const [showResult, setShowResult] = useState(false)
|
||||
const [fillAnswer, setFillAnswer] = useState('')
|
||||
const [showHint, setShowHint] = useState(false)
|
||||
const [isRecording, setIsRecording] = useState(false)
|
||||
const [audioPlaying, setAudioPlaying] = useState(false)
|
||||
const [modalImage, setModalImage] = useState<string | null>(null)
|
||||
|
||||
// Mock data
|
||||
// Mock data with real example images
|
||||
const cards = [
|
||||
{
|
||||
id: 1,
|
||||
word: 'negotiate',
|
||||
word: 'brought',
|
||||
partOfSpeech: 'verb',
|
||||
pronunciation: '/nɪˈɡoʊʃieɪt/',
|
||||
translation: '協商、談判',
|
||||
definition: 'To discuss something with someone in order to reach an agreement',
|
||||
example: 'We need to negotiate a better deal with our suppliers.',
|
||||
exampleTranslation: '我們需要與供應商協商更好的交易。',
|
||||
synonyms: ['bargain', 'discuss', 'mediate'],
|
||||
difficulty: 'intermediate'
|
||||
pronunciation: '/brɔːt/',
|
||||
translation: '提出、帶來',
|
||||
definition: 'Past tense of bring; to mention or introduce a topic in conversation',
|
||||
example: 'He brought this thing up during our meeting and no one agreed.',
|
||||
exampleTranslation: '他在我們的會議中提出了這件事,但沒有人同意。',
|
||||
exampleImage: '/images/examples/bring_up.png',
|
||||
synonyms: ['mentioned', 'raised', 'introduced'],
|
||||
difficulty: 'B1'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
word: 'perspective',
|
||||
word: 'instincts',
|
||||
partOfSpeech: 'noun',
|
||||
pronunciation: '/pərˈspektɪv/',
|
||||
translation: '觀點、看法',
|
||||
definition: 'A particular way of considering something',
|
||||
example: 'From my perspective, this is the best solution.',
|
||||
exampleTranslation: '從我的角度來看,這是最好的解決方案。',
|
||||
synonyms: ['viewpoint', 'outlook', 'stance'],
|
||||
difficulty: 'intermediate'
|
||||
pronunciation: '/ˈɪnstɪŋkts/',
|
||||
translation: '本能、直覺',
|
||||
definition: 'Natural abilities that help living things survive without learning',
|
||||
example: 'Animals use their instincts to find food and stay safe.',
|
||||
exampleTranslation: '動物利用本能來尋找食物並保持安全。',
|
||||
exampleImage: '/images/examples/instinct.png',
|
||||
synonyms: ['intuition', 'impulse', 'tendency'],
|
||||
difficulty: 'B2'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
word: 'accomplish',
|
||||
partOfSpeech: 'verb',
|
||||
pronunciation: '/əˈkɒmplɪʃ/',
|
||||
translation: '完成、達成',
|
||||
definition: 'To finish something successfully or to achieve something',
|
||||
example: 'She accomplished her goal of running a marathon.',
|
||||
exampleTranslation: '她完成了跑馬拉松的目標。',
|
||||
synonyms: ['achieve', 'complete', 'fulfill'],
|
||||
difficulty: 'intermediate'
|
||||
word: 'warrants',
|
||||
partOfSpeech: 'noun',
|
||||
pronunciation: '/ˈwɔːrənts/',
|
||||
translation: '搜查令、授權令',
|
||||
definition: 'Official documents that give police permission to do something',
|
||||
example: 'The police obtained warrants to search the building.',
|
||||
exampleTranslation: '警方取得了搜查令來搜查這棟建築物。',
|
||||
exampleImage: '/images/examples/warrant.png',
|
||||
synonyms: ['authorization', 'permit', 'license'],
|
||||
difficulty: 'C1'
|
||||
}
|
||||
]
|
||||
|
||||
const currentCard = cards[currentCardIndex]
|
||||
|
||||
// Quiz mode options
|
||||
// Quiz mode options - dynamically generate from current cards
|
||||
const quizOptions = [
|
||||
'協商、談判',
|
||||
'觀點、看法',
|
||||
'完成、達成',
|
||||
'建議、提議'
|
||||
]
|
||||
cards[currentCardIndex].translation,
|
||||
...cards
|
||||
.filter((_, idx) => idx !== currentCardIndex)
|
||||
.map(card => card.translation)
|
||||
.slice(0, 2),
|
||||
'建議、提議' // additional wrong option
|
||||
].sort(() => Math.random() - 0.5) // shuffle options
|
||||
|
||||
const handleFlip = () => {
|
||||
setIsFlipped(!isFlipped)
|
||||
|
|
@ -71,6 +81,8 @@ export default function LearnPage() {
|
|||
setIsFlipped(false)
|
||||
setSelectedAnswer(null)
|
||||
setShowResult(false)
|
||||
setFillAnswer('')
|
||||
setShowHint(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +92,8 @@ export default function LearnPage() {
|
|||
setIsFlipped(false)
|
||||
setSelectedAnswer(null)
|
||||
setShowResult(false)
|
||||
setFillAnswer('')
|
||||
setShowHint(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,10 +157,10 @@ export default function LearnPage() {
|
|||
|
||||
{/* Mode Toggle */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="bg-white rounded-lg shadow-sm p-1 inline-flex">
|
||||
<div className="bg-white rounded-lg shadow-sm p-1 inline-flex flex-wrap">
|
||||
<button
|
||||
onClick={() => setMode('flip')}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
className={`px-3 py-2 rounded-md transition-colors ${
|
||||
mode === 'flip'
|
||||
? 'bg-primary text-white'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
|
|
@ -156,13 +170,43 @@ export default function LearnPage() {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setMode('quiz')}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
className={`px-3 py-2 rounded-md transition-colors ${
|
||||
mode === 'quiz'
|
||||
? 'bg-primary text-white'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
測驗模式
|
||||
選擇題
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('fill')}
|
||||
className={`px-3 py-2 rounded-md transition-colors ${
|
||||
mode === 'fill'
|
||||
? 'bg-primary text-white'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
填空題
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('listening')}
|
||||
className={`px-3 py-2 rounded-md transition-colors ${
|
||||
mode === 'listening'
|
||||
? 'bg-primary text-white'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
聽力測試
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('speaking')}
|
||||
className={`px-3 py-2 rounded-md transition-colors ${
|
||||
mode === 'speaking'
|
||||
? 'bg-primary text-white'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
口說測試
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -269,13 +313,17 @@ export default function LearnPage() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* Quiz Mode */
|
||||
) : mode === 'quiz' ? (
|
||||
/* Quiz Mode - 選擇題:英文定義選中文翻譯 */
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-2">選擇正確的翻譯</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{currentCard.word}</div>
|
||||
<div className="text-lg text-gray-500 mt-1">{currentCard.pronunciation}</div>
|
||||
<div className="text-sm text-gray-600 mb-2">根據定義選擇正確的中文翻譯</div>
|
||||
<div className="text-xl text-gray-800 leading-relaxed">
|
||||
{currentCard.definition}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
({currentCard.partOfSpeech})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
|
|
@ -318,15 +366,263 @@ export default function LearnPage() {
|
|||
<div className="text-gray-500 text-sm mt-1">{currentCard.exampleTranslation}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<div className="text-sm text-gray-600">
|
||||
正確率:{score.total > 0 ? Math.round((score.correct / score.total) * 100) : 0}%
|
||||
({score.correct}/{score.total})
|
||||
</div>
|
||||
) : mode === 'fill' ? (
|
||||
/* Fill in the Blank Mode - 填空題 */
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-4">根據例句圖片和句子填入正確的詞彙</div>
|
||||
|
||||
{/* Example Image */}
|
||||
{currentCard.exampleImage && (
|
||||
<div className="mb-4">
|
||||
<img
|
||||
src={currentCard.exampleImage}
|
||||
alt="Example context"
|
||||
className="w-full rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-shadow"
|
||||
style={{ maxHeight: '400px', objectFit: 'contain' }}
|
||||
onClick={() => setModalImage(currentCard.exampleImage)}
|
||||
/>
|
||||
<div className="text-xs text-gray-500 mt-2 text-center">點擊圖片可放大查看</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Example Sentence with Blank */}
|
||||
<div className="text-lg text-gray-800 mb-4">
|
||||
{currentCard.example.split(currentCard.word).map((part, i) => (
|
||||
<span key={i}>
|
||||
{part}
|
||||
{i < currentCard.example.split(currentCard.word).length - 1 && (
|
||||
<span className="inline-block w-32 border-b-2 border-gray-400 mx-1"></span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Hint Button */}
|
||||
{!showHint && (
|
||||
<button
|
||||
onClick={() => setShowHint(true)}
|
||||
className="text-sm text-primary hover:text-primary-hover flex items-center gap-1"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
點擊查看提示
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Definition Hint */}
|
||||
{showHint && (
|
||||
<div className="mt-3 p-3 bg-blue-50 rounded-lg">
|
||||
<div className="text-sm text-blue-800">
|
||||
<strong>定義提示:</strong> {currentCard.definition}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Answer Input */}
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={fillAnswer}
|
||||
onChange={(e) => setFillAnswer(e.target.value)}
|
||||
placeholder="輸入答案..."
|
||||
className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:border-primary focus:outline-none text-lg"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter' && fillAnswer) {
|
||||
setShowResult(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
{!showResult && (
|
||||
<button
|
||||
onClick={() => fillAnswer && setShowResult(true)}
|
||||
disabled={!fillAnswer}
|
||||
className="w-full py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
提交答案
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Result Display */}
|
||||
{showResult && (
|
||||
<div className={`p-4 rounded-lg ${
|
||||
fillAnswer.toLowerCase() === currentCard.word.toLowerCase()
|
||||
? 'bg-green-50 border-2 border-green-500'
|
||||
: 'bg-red-50 border-2 border-red-500'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className={`font-semibold ${
|
||||
fillAnswer.toLowerCase() === currentCard.word.toLowerCase()
|
||||
? 'text-green-700'
|
||||
: 'text-red-700'
|
||||
}`}>
|
||||
{fillAnswer.toLowerCase() === currentCard.word.toLowerCase() ? '✓ 正確!' : '✗ 錯誤'}
|
||||
</span>
|
||||
</div>
|
||||
{fillAnswer.toLowerCase() !== currentCard.word.toLowerCase() && (
|
||||
<div className="text-sm text-gray-700">
|
||||
正確答案:<span className="font-bold">{currentCard.word}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
<div className="font-semibold mb-1">完整例句:</div>
|
||||
<div>{currentCard.example}</div>
|
||||
<div className="text-gray-500 mt-1">{currentCard.exampleTranslation}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : mode === 'listening' ? (
|
||||
/* Listening Test Mode - 聽力測試 */
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6 text-center">
|
||||
<div className="text-sm text-gray-600 mb-4">聽音頻,選擇正確的單字</div>
|
||||
|
||||
{/* Audio Play Button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setAudioPlaying(true)
|
||||
// Simulate audio playing
|
||||
setTimeout(() => setAudioPlaying(false), 2000)
|
||||
}}
|
||||
className="mx-auto mb-6 p-8 bg-gray-100 rounded-full hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
{audioPlaying ? (
|
||||
<svg className="w-16 h-16 text-primary animate-pulse" 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>
|
||||
) : (
|
||||
<svg className="w-16 h-16 text-gray-600" 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 className="text-sm text-gray-500">點擊播放按鈕聽發音</div>
|
||||
</div>
|
||||
|
||||
{/* Word Options */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[currentCard.word, 'determine', 'achieve', 'consider'].map((word) => (
|
||||
<button
|
||||
key={word}
|
||||
onClick={() => !showResult && handleQuizAnswer(word)}
|
||||
disabled={showResult}
|
||||
className={`p-4 text-lg font-medium rounded-lg border-2 transition-all ${
|
||||
showResult && word === currentCard.word
|
||||
? 'border-green-500 bg-green-50'
|
||||
: showResult && word === selectedAnswer && word !== currentCard.word
|
||||
? 'border-red-500 bg-red-50'
|
||||
: selectedAnswer === word
|
||||
? 'border-primary bg-primary-light'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{word}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Result Display */}
|
||||
{showResult && (
|
||||
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-semibold text-gray-700 mb-2">單字詳情</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="font-semibold">{currentCard.word}</span> - {currentCard.translation}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">{currentCard.definition}</div>
|
||||
<div className="text-sm text-gray-500 italic">"{currentCard.example}"</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : mode === 'speaking' ? (
|
||||
/* Speaking Test Mode - 口說測試 */
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-4">念出以下例句</div>
|
||||
|
||||
{/* Target Sentence */}
|
||||
<div className="p-6 bg-gray-50 rounded-lg mb-6">
|
||||
<div className="text-xl text-gray-800 leading-relaxed mb-3">
|
||||
{currentCard.example}
|
||||
</div>
|
||||
<div className="text-gray-600">
|
||||
{currentCard.exampleTranslation}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pronunciation Guide */}
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-2">重點單字發音:</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="font-semibold text-lg">{currentCard.word}</span>
|
||||
<span className="text-gray-500">{currentCard.pronunciation}</span>
|
||||
<button className="text-primary hover:text-primary-hover">
|
||||
<svg className="w-5 h-5" 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>
|
||||
|
||||
{/* Recording Button */}
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRecording(!isRecording)
|
||||
if (!isRecording) {
|
||||
// Start recording
|
||||
setTimeout(() => {
|
||||
setIsRecording(false)
|
||||
setShowResult(true)
|
||||
}, 3000)
|
||||
}
|
||||
}}
|
||||
className={`p-6 rounded-full transition-all ${
|
||||
isRecording
|
||||
? 'bg-red-500 hover:bg-red-600 animate-pulse'
|
||||
: 'bg-primary hover:bg-primary-hover'
|
||||
}`}
|
||||
>
|
||||
{isRecording ? (
|
||||
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
{isRecording ? '錄音中... 點擊停止' : '點擊開始錄音'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Result Display */}
|
||||
{showResult && (
|
||||
<div className="mt-6 p-4 bg-green-50 border-2 border-green-500 rounded-lg">
|
||||
<div className="text-green-700 font-semibold mb-2">
|
||||
✓ 完成口說練習!
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
提示:持續練習可以提高發音準確度和流暢度
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex justify-between mt-8">
|
||||
|
|
@ -353,6 +649,39 @@ export default function LearnPage() {
|
|||
</button>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
@ -60,26 +60,34 @@
|
|||
- 電影台詞(Movie Quotes)
|
||||
- 學術英語(Academic English)
|
||||
- 自定義主題輸入
|
||||
- 難度選擇:初級/中級/高級
|
||||
- 難度選擇:A1, A2, B1, B2, C1, C2
|
||||
|
||||
#### 1.2.2 AI 生成規格
|
||||
- **生成方式**
|
||||
1. 原始例句類型
|
||||
- 影劇截圖(訂閱功能, phase2)
|
||||
- 手動輸入
|
||||
2. 詞彙萃取:把每個單字拿去查詢字典API,並標記CEFR
|
||||
3. 智能萃取(訂閱功能):將原始例句拿去問AI有無常用片語或俚語,並直接生成相關詞彙內容
|
||||
|
||||
- **生成數量**
|
||||
- 預設:10個詞卡
|
||||
- 範圍:5-20個(用戶可調)
|
||||
- 免費用戶:每日50個限制
|
||||
- 付費用戶:無限制
|
||||
- 免費用戶:
|
||||
- 無法自行生成例句圖,但若系統中匹配到現成例句圖,可直接使用
|
||||
- 每日學習數量無限制
|
||||
- 訂閱用戶:每天最多生成50張例句圖
|
||||
|
||||
- **生成內容詳情**
|
||||
- **單字/片語**
|
||||
- 原形展示
|
||||
- 詞性標註(n./v./adj./adv./phrase)
|
||||
- 同義詞(最多3個)
|
||||
- 詞性標註(n./v./adj./adv./phrase/slang)
|
||||
- 英文定義 (程度應維持在A1-A2)
|
||||
- 同義詞(最多3個且程度應維持在A1-A2)
|
||||
- 反義詞(如適用)
|
||||
|
||||
- **翻譯**
|
||||
- 繁體中文翻譯
|
||||
- 多義詞說明
|
||||
- 慣用語解釋
|
||||
|
||||
- **發音**
|
||||
- IPA 國際音標
|
||||
|
|
@ -88,19 +96,11 @@
|
|||
|
||||
- **例句**
|
||||
- 原始例句(來自輸入文本)
|
||||
- 生成例句(2-3個)
|
||||
- 生成例句(1個)
|
||||
- 例句中文翻譯
|
||||
- 重點標示(highlight目標詞)
|
||||
|
||||
- **使用情境**
|
||||
- 正式/非正式場合
|
||||
- 使用頻率(常用/進階/罕見)
|
||||
- 文化背景說明(如有)
|
||||
|
||||
- **記憶提示**
|
||||
- 詞根詞綴分析
|
||||
- 聯想記憶法
|
||||
- 圖像記憶(未來功能)
|
||||
- 例句圖
|
||||
- 例句發音
|
||||
|
||||
- **生成後處理**
|
||||
- 預覽所有生成詞卡
|
||||
|
|
@ -170,12 +170,12 @@
|
|||
|
||||
#### 1.4.1 間隔重複算法(SM-2)
|
||||
- **算法參數**
|
||||
- 初始間隔:1天、3天、7天、14天、30天
|
||||
- 初始間隔:2^0天、2^1天...依此類推
|
||||
- 難度係數:0.8-2.5
|
||||
- 最小間隔:1天
|
||||
- 最大間隔:365天
|
||||
|
||||
- **評分系統**
|
||||
- **評分及間隔時間**
|
||||
- 1分:完全不記得(重置進度)
|
||||
- 2分:有印象但錯誤(間隔×0.6)
|
||||
- 3分:困難但正確(間隔×0.8)
|
||||
|
|
@ -189,17 +189,20 @@
|
|||
|
||||
#### 1.4.2 學習模式
|
||||
- **翻卡模式**
|
||||
- 正面:英文單字
|
||||
- 背面:翻譯、例句、發音
|
||||
- 正面:英文詞彙
|
||||
- 背面:英文定義、例句、發音、例句圖
|
||||
- 手勢操作:左滑(不記得)、右滑(記得)、上滑(收藏)
|
||||
- 鍵盤快捷鍵支援
|
||||
|
||||
- **測驗模式**
|
||||
- 選擇題(4選1)
|
||||
- 填空題(例句挖空)
|
||||
- 拼寫測試
|
||||
- 選擇題(題目是英文定義,答案中文翻譯4選1)
|
||||
- 填空題
|
||||
- 題目是顯示例句圖和挖空的例句
|
||||
- 點擊提示,會出現詞彙的英文定義
|
||||
- 答案就是詞彙,但是不是原型,而是例句挖空的部分
|
||||
- 拼寫測試 (phase 2)
|
||||
- 聽力測試(聽音選詞)
|
||||
- 即時反饋和解釋
|
||||
- 口說測試 (念例句)
|
||||
|
||||
- **沉浸模式**
|
||||
- 全螢幕學習
|
||||
|
|
|
|||
|
|
@ -0,0 +1,742 @@
|
|||
# DramaLing API 端點詳細規格
|
||||
|
||||
## 🌐 API 基礎設定
|
||||
|
||||
### Base URL
|
||||
```
|
||||
Development: http://localhost:3000/api
|
||||
Production: https://api.dramaling.com
|
||||
```
|
||||
|
||||
### 認證方式
|
||||
```http
|
||||
Authorization: Bearer <JWT_TOKEN>
|
||||
```
|
||||
|
||||
### 響應格式
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: {
|
||||
code: string
|
||||
message: string
|
||||
details?: any
|
||||
}
|
||||
meta?: {
|
||||
pagination?: {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
totalPages: number
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔐 認證 API
|
||||
|
||||
### POST /api/auth/register
|
||||
**註冊新用戶**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface RegisterRequest {
|
||||
email: string
|
||||
username: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface RegisterResponse {
|
||||
user: {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
}
|
||||
message: string
|
||||
}
|
||||
|
||||
// Validation
|
||||
- email: valid email format
|
||||
- username: 3-20 characters, alphanumeric and underscore
|
||||
- password: min 8 chars, must contain uppercase, lowercase, number
|
||||
- confirmPassword: must match password
|
||||
```
|
||||
|
||||
### POST /api/auth/login
|
||||
**用戶登入**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface LoginRequest {
|
||||
email: string
|
||||
password: string
|
||||
rememberMe?: boolean
|
||||
}
|
||||
|
||||
// Response
|
||||
interface LoginResponse {
|
||||
user: User
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiresIn: number
|
||||
}
|
||||
|
||||
// Error codes
|
||||
- INVALID_CREDENTIALS: 帳號或密碼錯誤
|
||||
- ACCOUNT_LOCKED: 帳號被鎖定
|
||||
- EMAIL_NOT_VERIFIED: Email 未驗證
|
||||
```
|
||||
|
||||
### POST /api/auth/logout
|
||||
**登出**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface LogoutRequest {
|
||||
refreshToken?: string
|
||||
allDevices?: boolean
|
||||
}
|
||||
|
||||
// Response
|
||||
interface LogoutResponse {
|
||||
message: string
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/auth/refresh
|
||||
**更新 Token**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface RefreshRequest {
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface RefreshResponse {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiresIn: number
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/auth/verify-email
|
||||
**驗證 Email**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface VerifyEmailRequest {
|
||||
token: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface VerifyEmailResponse {
|
||||
message: string
|
||||
user: User
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/auth/forgot-password
|
||||
**忘記密碼**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface ForgotPasswordRequest {
|
||||
email: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface ForgotPasswordResponse {
|
||||
message: string
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/auth/reset-password
|
||||
**重設密碼**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface ResetPasswordRequest {
|
||||
token: string
|
||||
newPassword: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface ResetPasswordResponse {
|
||||
message: string
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/auth/google
|
||||
**Google OAuth**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface GoogleAuthRequest {
|
||||
idToken: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface GoogleAuthResponse {
|
||||
user: User
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
isNewUser: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## 🎃 用戶 API
|
||||
|
||||
### GET /api/users/profile
|
||||
**獲取用戶資料**
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
interface ProfileResponse {
|
||||
user: User
|
||||
profile: UserProfile
|
||||
stats: UserStats
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /api/users/profile
|
||||
**更新用戶資料**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface UpdateProfileRequest {
|
||||
username?: string
|
||||
displayName?: string
|
||||
bio?: string
|
||||
avatar?: File
|
||||
preferredLanguage?: string
|
||||
timezone?: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface UpdateProfileResponse {
|
||||
user: User
|
||||
message: string
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /api/users/settings
|
||||
**更新用戶設定**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface UpdateSettingsRequest {
|
||||
dailyGoal?: number
|
||||
reminderEnabled?: boolean
|
||||
reminderTime?: string
|
||||
emailNotifications?: boolean
|
||||
pushNotifications?: boolean
|
||||
theme?: 'light' | 'dark' | 'auto'
|
||||
studyMode?: 'flip' | 'quiz' | 'typing'
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE /api/users/account
|
||||
**刪除帳號**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface DeleteAccountRequest {
|
||||
password: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface DeleteAccountResponse {
|
||||
message: string
|
||||
}
|
||||
```
|
||||
|
||||
## 🎓 詞卡 API
|
||||
|
||||
### GET /api/flashcard-sets
|
||||
**獲取卡組列表**
|
||||
|
||||
```typescript
|
||||
// Query parameters
|
||||
interface GetSetsQuery {
|
||||
page?: number
|
||||
limit?: number
|
||||
search?: string
|
||||
tags?: string[]
|
||||
orderBy?: 'created' | 'updated' | 'name' | 'progress'
|
||||
order?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
// Response
|
||||
interface GetSetsResponse {
|
||||
sets: FlashcardSet[]
|
||||
pagination: Pagination
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/flashcard-sets
|
||||
**創建卡組**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface CreateSetRequest {
|
||||
name: string
|
||||
description?: string
|
||||
color?: string
|
||||
tags?: string[]
|
||||
isPublic?: boolean
|
||||
}
|
||||
|
||||
// Response
|
||||
interface CreateSetResponse {
|
||||
set: FlashcardSet
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/flashcard-sets/:setId
|
||||
**獲取卡組詳情**
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
interface GetSetDetailResponse {
|
||||
set: FlashcardSet
|
||||
cards: Flashcard[]
|
||||
stats: SetStats
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /api/flashcard-sets/:setId
|
||||
**更新卡組**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface UpdateSetRequest {
|
||||
name?: string
|
||||
description?: string
|
||||
color?: string
|
||||
tags?: string[]
|
||||
isPublic?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE /api/flashcard-sets/:setId
|
||||
**刪除卡組**
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
interface DeleteSetResponse {
|
||||
message: string
|
||||
deletedCount: number
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/flashcards
|
||||
**獲取詞卡列表**
|
||||
|
||||
```typescript
|
||||
// Query parameters
|
||||
interface GetCardsQuery {
|
||||
setId?: string
|
||||
search?: string
|
||||
tags?: string[]
|
||||
difficulty?: 'beginner' | 'intermediate' | 'advanced'
|
||||
mastered?: boolean
|
||||
favorite?: boolean
|
||||
dueForReview?: boolean
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
// Response
|
||||
interface GetCardsResponse {
|
||||
cards: Flashcard[]
|
||||
pagination: Pagination
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/flashcards
|
||||
**創建詞卡**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface CreateCardRequest {
|
||||
setId: string
|
||||
word: string
|
||||
translation: string
|
||||
partOfSpeech?: string
|
||||
pronunciation?: string
|
||||
definition?: string
|
||||
exampleSentence?: string
|
||||
exampleTranslation?: string
|
||||
synonyms?: string[]
|
||||
tags?: string[]
|
||||
difficulty?: 'beginner' | 'intermediate' | 'advanced'
|
||||
}
|
||||
|
||||
// Response
|
||||
interface CreateCardResponse {
|
||||
card: Flashcard
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /api/flashcards/:cardId
|
||||
**更新詞卡**
|
||||
|
||||
```typescript
|
||||
// Request - same as CreateCardRequest but all fields optional
|
||||
```
|
||||
|
||||
### DELETE /api/flashcards/:cardId
|
||||
**刪除詞卡**
|
||||
|
||||
### POST /api/flashcards/batch
|
||||
**批量操作**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface BatchOperationRequest {
|
||||
operation: 'create' | 'update' | 'delete' | 'move'
|
||||
cardIds?: string[]
|
||||
data?: any
|
||||
targetSetId?: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface BatchOperationResponse {
|
||||
success: number
|
||||
failed: number
|
||||
errors?: any[]
|
||||
}
|
||||
```
|
||||
|
||||
## 🤖 AI 生成 API
|
||||
|
||||
### POST /api/ai/generate
|
||||
**AI 生成詞卡**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface GenerateRequest {
|
||||
mode: 'text' | 'theme'
|
||||
input: string // text content or theme name
|
||||
difficulty: 'beginner' | 'intermediate' | 'advanced'
|
||||
count: number // 5-20
|
||||
includeExamples?: boolean
|
||||
includeSynonyms?: boolean
|
||||
}
|
||||
|
||||
// Response
|
||||
interface GenerateResponse {
|
||||
cards: GeneratedCard[]
|
||||
usage: {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
}
|
||||
}
|
||||
|
||||
interface GeneratedCard {
|
||||
word: string
|
||||
translation: string
|
||||
partOfSpeech: string
|
||||
pronunciation: string
|
||||
definition: string
|
||||
example: string
|
||||
exampleTranslation: string
|
||||
synonyms: string[]
|
||||
difficulty: string
|
||||
confidence: number // 0-1
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/ai/analyze-text
|
||||
**分析文本難度**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface AnalyzeTextRequest {
|
||||
text: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface AnalyzeTextResponse {
|
||||
difficulty: 'beginner' | 'intermediate' | 'advanced'
|
||||
wordCount: number
|
||||
uniqueWords: number
|
||||
averageSentenceLength: number
|
||||
readabilityScore: number
|
||||
suggestedWords: string[]
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/ai/quota
|
||||
**查詢 AI 使用額度**
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
interface QuotaResponse {
|
||||
used: number
|
||||
limit: number
|
||||
resetAt: string
|
||||
isPremium: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 學習 API
|
||||
|
||||
### GET /api/learning/due
|
||||
**獲取待複習詞卡**
|
||||
|
||||
```typescript
|
||||
// Query parameters
|
||||
interface GetDueCardsQuery {
|
||||
setId?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
// Response
|
||||
interface GetDueCardsResponse {
|
||||
cards: StudyCard[]
|
||||
totalDue: number
|
||||
}
|
||||
|
||||
interface StudyCard extends Flashcard {
|
||||
learningRecord: LearningRecord
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/learning/review
|
||||
**提交評分**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface ReviewRequest {
|
||||
cardId: string
|
||||
rating: 1 | 2 | 3 | 4 | 5
|
||||
responseTime: number // milliseconds
|
||||
mode: 'flip' | 'quiz' | 'typing'
|
||||
}
|
||||
|
||||
// Response
|
||||
interface ReviewResponse {
|
||||
nextReviewDate: string
|
||||
interval: number
|
||||
easeFactor: number
|
||||
masteryLevel: number
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/learning/stats
|
||||
**獲取學習統計**
|
||||
|
||||
```typescript
|
||||
// Query parameters
|
||||
interface GetStatsQuery {
|
||||
period: 'today' | 'week' | 'month' | 'year' | 'all'
|
||||
setId?: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface StatsResponse {
|
||||
cardsReviewed: number
|
||||
cardsLearned: number
|
||||
cardsMastered: number
|
||||
studyTime: number // minutes
|
||||
accuracy: number // percentage
|
||||
streak: number
|
||||
dailyGoalProgress: number
|
||||
chart: {
|
||||
dates: string[]
|
||||
values: number[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/learning/session/start
|
||||
**開始學習會話**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface StartSessionRequest {
|
||||
mode: 'review' | 'learn' | 'cram'
|
||||
setId?: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface StartSessionResponse {
|
||||
sessionId: string
|
||||
cards: StudyCard[]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/learning/session/end
|
||||
**結束學習會話**
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface EndSessionRequest {
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
// Response
|
||||
interface EndSessionResponse {
|
||||
duration: number
|
||||
cardsStudied: number
|
||||
accuracy: number
|
||||
xpEarned: number
|
||||
achievements: Achievement[]
|
||||
}
|
||||
```
|
||||
|
||||
## 🏆 成就 API
|
||||
|
||||
### GET /api/achievements
|
||||
**獲取成就列表**
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
interface GetAchievementsResponse {
|
||||
unlocked: Achievement[]
|
||||
locked: Achievement[]
|
||||
recent: Achievement[]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/achievements/:achievementId/claim
|
||||
**領取成就獎勵**
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
interface ClaimAchievementResponse {
|
||||
xpEarned: number
|
||||
totalXp: number
|
||||
newLevel?: number
|
||||
}
|
||||
```
|
||||
|
||||
## 🔔 通知 API
|
||||
|
||||
### GET /api/notifications
|
||||
**獲取通知**
|
||||
|
||||
```typescript
|
||||
// Query parameters
|
||||
interface GetNotificationsQuery {
|
||||
unreadOnly?: boolean
|
||||
type?: string
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
// Response
|
||||
interface GetNotificationsResponse {
|
||||
notifications: Notification[]
|
||||
unreadCount: number
|
||||
pagination: Pagination
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /api/notifications/:notificationId/read
|
||||
**標記已讀**
|
||||
|
||||
### PUT /api/notifications/read-all
|
||||
**全部標記已讀**
|
||||
|
||||
### DELETE /api/notifications/:notificationId
|
||||
**刪除通知**
|
||||
|
||||
## 📁 匯出/匯入 API
|
||||
|
||||
### GET /api/export/cards
|
||||
**匯出詞卡**
|
||||
|
||||
```typescript
|
||||
// Query parameters
|
||||
interface ExportQuery {
|
||||
format: 'csv' | 'json' | 'anki'
|
||||
setId?: string
|
||||
cardIds?: string[]
|
||||
}
|
||||
|
||||
// Response: File download
|
||||
```
|
||||
|
||||
### POST /api/import/cards
|
||||
**匯入詞卡**
|
||||
|
||||
```typescript
|
||||
// Request: FormData with file
|
||||
interface ImportRequest {
|
||||
file: File
|
||||
setId: string
|
||||
format: 'csv' | 'json' | 'anki'
|
||||
duplicateStrategy: 'skip' | 'replace' | 'create'
|
||||
}
|
||||
|
||||
// Response
|
||||
interface ImportResponse {
|
||||
imported: number
|
||||
skipped: number
|
||||
errors: any[]
|
||||
}
|
||||
```
|
||||
|
||||
## 🔒 錯誤碼規範
|
||||
|
||||
```typescript
|
||||
enum ErrorCode {
|
||||
// Authentication
|
||||
AUTH_INVALID_TOKEN = 'AUTH001',
|
||||
AUTH_TOKEN_EXPIRED = 'AUTH002',
|
||||
AUTH_UNAUTHORIZED = 'AUTH003',
|
||||
|
||||
// Validation
|
||||
VALIDATION_ERROR = 'VAL001',
|
||||
INVALID_INPUT = 'VAL002',
|
||||
|
||||
// Resource
|
||||
NOT_FOUND = 'RES001',
|
||||
ALREADY_EXISTS = 'RES002',
|
||||
|
||||
// Rate Limit
|
||||
RATE_LIMIT_EXCEEDED = 'RATE001',
|
||||
QUOTA_EXCEEDED = 'RATE002',
|
||||
|
||||
// Server
|
||||
INTERNAL_ERROR = 'SRV001',
|
||||
SERVICE_UNAVAILABLE = 'SRV002',
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Rate Limiting
|
||||
|
||||
| Endpoint | Free User | Premium User |
|
||||
|----------|-----------|-------------|
|
||||
| AI Generate | 50/day | Unlimited |
|
||||
| API Calls | 1000/hour | 10000/hour |
|
||||
| File Upload | 10MB | 50MB |
|
||||
| Export | 5/day | Unlimited |
|
||||
|
||||
## 🔧 Webhooks
|
||||
|
||||
### 可用 Webhooks
|
||||
1. `user.created` - 新用戶註冊
|
||||
2. `user.deleted` - 用戶刪除
|
||||
3. `subscription.created` - 訂閱創建
|
||||
4. `subscription.updated` - 訂閱更新
|
||||
5. `achievement.unlocked` - 成就解鎖
|
||||
|
||||
### Webhook Payload
|
||||
```typescript
|
||||
interface WebhookPayload {
|
||||
event: string
|
||||
timestamp: string
|
||||
data: any
|
||||
signature: string
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
# DramaLing 後端開發計劃
|
||||
|
||||
## 📋 現況評估
|
||||
|
||||
### ✅ 已完成項目
|
||||
1. **前端 Prototype** - 完整的 UI/UX 流程
|
||||
2. **功能需求文檔** - 詳細的功能規格
|
||||
3. **資料模型設計** - 基本的數據結構
|
||||
4. **API 規格** - 初步的 API 端點設計
|
||||
5. **用戶流程** - 完整的業務邏輯流程
|
||||
|
||||
### ❌ 缺失項目
|
||||
1. **詳細的資料庫 Schema**
|
||||
2. **API 認證與授權機制細節**
|
||||
3. **第三方服務整合規格**
|
||||
4. **錯誤處理與日誌規範**
|
||||
5. **效能與擴展性要求**
|
||||
6. **部署與 DevOps 流程**
|
||||
|
||||
## 🎯 後端開發計劃
|
||||
|
||||
### Phase 1: 基礎架構設置 (第1週)
|
||||
|
||||
#### 1.1 專案初始化
|
||||
```bash
|
||||
# 技術棧
|
||||
- Framework: Next.js 14 API Routes
|
||||
- Database: Supabase (PostgreSQL)
|
||||
- Authentication: NextAuth.js + Supabase Auth
|
||||
- ORM: Prisma
|
||||
- Validation: Zod
|
||||
- API Documentation: Swagger/OpenAPI
|
||||
```
|
||||
|
||||
#### 1.2 需要完成的規格文檔
|
||||
- [ ] **資料庫 Schema 詳細設計**
|
||||
- 所有表格結構
|
||||
- 索引設計
|
||||
- 關聯關係
|
||||
- 資料遷移策略
|
||||
|
||||
- [ ] **環境配置規格**
|
||||
- 開發/測試/生產環境變數
|
||||
- 資料庫連接配置
|
||||
- 第三方服務 API Keys
|
||||
|
||||
#### 1.3 實作任務
|
||||
- [ ] 設置 Supabase 專案
|
||||
- [ ] 配置 Prisma ORM
|
||||
- [ ] 建立資料庫 Schema
|
||||
- [ ] 設置環境變數管理
|
||||
- [ ] 配置 TypeScript 類型定義
|
||||
|
||||
### Phase 2: 認證系統 (第1-2週)
|
||||
|
||||
#### 2.1 需要的規格
|
||||
- [ ] **認證流程詳細設計**
|
||||
```typescript
|
||||
interface AuthSpec {
|
||||
// Email/Password 註冊流程
|
||||
emailRegistration: {
|
||||
validation: ValidationRules
|
||||
emailVerification: VerificationProcess
|
||||
welcomeEmail: EmailTemplate
|
||||
}
|
||||
|
||||
// OAuth 整合
|
||||
googleOAuth: {
|
||||
scopes: string[]
|
||||
dataMapping: UserDataMapping
|
||||
errorHandling: ErrorScenarios
|
||||
}
|
||||
|
||||
// Session 管理
|
||||
sessionManagement: {
|
||||
tokenExpiry: TokenExpiryRules
|
||||
refreshStrategy: RefreshTokenStrategy
|
||||
deviceManagement: MultiDevicePolicy
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **安全性規格**
|
||||
- 密碼加密策略
|
||||
- Rate limiting 規則
|
||||
- CORS 設定
|
||||
- CSRF 保護
|
||||
|
||||
#### 2.2 實作任務
|
||||
- [ ] 實作註冊 API
|
||||
- [ ] 實作登入/登出 API
|
||||
- [ ] 整合 Google OAuth
|
||||
- [ ] 實作 Email 驗證
|
||||
- [ ] 實作忘記密碼功能
|
||||
- [ ] Session/Token 管理
|
||||
|
||||
### Phase 3: 核心業務邏輯 (第2-3週)
|
||||
|
||||
#### 3.1 AI 詞卡生成系統
|
||||
|
||||
**需要的規格:**
|
||||
- [ ] **Gemini API 整合規格**
|
||||
```typescript
|
||||
interface GeminiIntegration {
|
||||
apiKey: string
|
||||
model: 'gemini-pro' | 'gemini-pro-vision'
|
||||
|
||||
prompts: {
|
||||
textAnalysis: PromptTemplate
|
||||
vocabularyExtraction: PromptTemplate
|
||||
exampleGeneration: PromptTemplate
|
||||
}
|
||||
|
||||
rateLimit: {
|
||||
requestsPerMinute: number
|
||||
tokensPerRequest: number
|
||||
retryStrategy: RetryPolicy
|
||||
}
|
||||
|
||||
errorHandling: {
|
||||
apiErrors: ErrorMapping
|
||||
fallbackStrategy: FallbackOptions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **詞卡生成邏輯**
|
||||
- 文本分析算法
|
||||
- 難度評估規則
|
||||
- 重複詞彙處理
|
||||
- 批量生成優化
|
||||
|
||||
#### 3.2 學習系統
|
||||
|
||||
**需要的規格:**
|
||||
- [ ] **SM-2 算法實現細節**
|
||||
```typescript
|
||||
interface SM2Algorithm {
|
||||
// 初始參數
|
||||
initialInterval: number
|
||||
easinessFactor: number
|
||||
|
||||
// 計算規則
|
||||
calculateNextReview: (rating: 1-5, currentInterval: number) => Date
|
||||
updateEasinessFactor: (rating: 1-5, currentEF: number) => number
|
||||
|
||||
// 特殊情況處理
|
||||
handleOverdue: (daysOverdue: number) => Adjustment
|
||||
handleStreaks: (consecutiveCorrect: number) => Bonus
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **學習進度追蹤**
|
||||
- 學習記錄存儲結構
|
||||
- 統計數據計算方法
|
||||
- 成就系統規則
|
||||
|
||||
#### 3.3 實作任務
|
||||
- [ ] Gemini API 整合
|
||||
- [ ] 詞卡生成 API
|
||||
- [ ] 卡組管理 CRUD API
|
||||
- [ ] SM-2 算法實現
|
||||
- [ ] 學習記錄 API
|
||||
- [ ] 統計數據 API
|
||||
|
||||
### Phase 4: 資料管理 (第3-4週)
|
||||
|
||||
#### 4.1 需要的規格
|
||||
|
||||
- [ ] **資料驗證規則**
|
||||
```typescript
|
||||
// 每個 API 端點的輸入驗證
|
||||
const schemas = {
|
||||
createFlashcard: z.object({
|
||||
word: z.string().min(1).max(100),
|
||||
translation: z.string().min(1).max(200),
|
||||
example: z.string().max(500),
|
||||
// ...
|
||||
}),
|
||||
// 其他 schemas...
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **資料存取層設計**
|
||||
- Repository Pattern 實現
|
||||
- 查詢優化策略
|
||||
- 快取策略
|
||||
|
||||
- [ ] **批量操作規格**
|
||||
- 批量導入限制
|
||||
- 批量更新策略
|
||||
- 事務處理規則
|
||||
|
||||
#### 4.2 實作任務
|
||||
- [ ] 實作資料驗證中間件
|
||||
- [ ] 建立 Repository 層
|
||||
- [ ] 實作快取機制
|
||||
- [ ] 批量操作 API
|
||||
- [ ] 資料導出功能
|
||||
|
||||
### Phase 5: 第三方整合 (第4週)
|
||||
|
||||
#### 5.1 需要的規格
|
||||
|
||||
- [ ] **Email 服務整合**
|
||||
```typescript
|
||||
interface EmailService {
|
||||
provider: 'Resend' | 'SendGrid'
|
||||
templates: {
|
||||
welcome: EmailTemplate
|
||||
verification: EmailTemplate
|
||||
passwordReset: EmailTemplate
|
||||
learningReminder: EmailTemplate
|
||||
}
|
||||
sendingRules: {
|
||||
retryPolicy: RetryPolicy
|
||||
bounceHandling: BounceStrategy
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **支付系統整合** (未來功能)
|
||||
- Stripe/綠界設置
|
||||
- 訂閱方案邏輯
|
||||
- 支付 Webhook 處理
|
||||
|
||||
- [ ] **分析服務整合**
|
||||
- Google Analytics 事件
|
||||
- 自定義指標追蹤
|
||||
- 用戶行為分析
|
||||
|
||||
#### 5.2 實作任務
|
||||
- [ ] Email 服務設置
|
||||
- [ ] Email 模板實作
|
||||
- [ ] 分析事件追蹤
|
||||
- [ ] 錯誤追蹤 (Sentry)
|
||||
|
||||
### Phase 6: 效能與安全 (第5週)
|
||||
|
||||
#### 6.1 需要的規格
|
||||
|
||||
- [ ] **API Rate Limiting**
|
||||
```typescript
|
||||
interface RateLimiting {
|
||||
rules: {
|
||||
global: { requests: number, window: string }
|
||||
perUser: { requests: number, window: string }
|
||||
perEndpoint: Map<string, LimitRule>
|
||||
}
|
||||
storage: 'memory' | 'redis'
|
||||
response: {
|
||||
headers: RateLimitHeaders
|
||||
errorMessage: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **安全性措施**
|
||||
- SQL Injection 防護
|
||||
- XSS 防護
|
||||
- Input Sanitization
|
||||
- API Key 管理
|
||||
|
||||
- [ ] **效能優化**
|
||||
- 資料庫查詢優化
|
||||
- N+1 問題解決
|
||||
- API Response 壓縮
|
||||
- CDN 設置
|
||||
|
||||
#### 6.2 實作任務
|
||||
- [ ] 實作 Rate Limiting
|
||||
- [ ] 安全中間件設置
|
||||
- [ ] 查詢優化
|
||||
- [ ] 快取策略實作
|
||||
- [ ] 監控設置
|
||||
|
||||
### Phase 7: 測試與部署 (第5-6週)
|
||||
|
||||
#### 7.1 需要的規格
|
||||
|
||||
- [ ] **測試策略**
|
||||
```typescript
|
||||
interface TestStrategy {
|
||||
unitTests: {
|
||||
coverage: number // 目標: 80%
|
||||
framework: 'Jest'
|
||||
}
|
||||
integrationTests: {
|
||||
apiEndpoints: string[]
|
||||
scenarios: TestScenario[]
|
||||
}
|
||||
e2eTests: {
|
||||
userFlows: string[]
|
||||
framework: 'Cypress' | 'Playwright'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **部署流程**
|
||||
- CI/CD Pipeline
|
||||
- 環境變數管理
|
||||
- 資料庫遷移策略
|
||||
- 零停機部署
|
||||
|
||||
- [ ] **監控與日誌**
|
||||
- 錯誤監控
|
||||
- 效能監控
|
||||
- 日誌聚合
|
||||
- 告警設置
|
||||
|
||||
#### 7.2 實作任務
|
||||
- [ ] 撰寫單元測試
|
||||
- [ ] 撰寫整合測試
|
||||
- [ ] 設置 CI/CD
|
||||
- [ ] 部署到 Vercel
|
||||
- [ ] 監控系統設置
|
||||
|
||||
## 📊 資源需求
|
||||
|
||||
### 技術資源
|
||||
- **Supabase**: Free tier → Pro ($25/月)
|
||||
- **Gemini API**: Free tier (60 requests/minute)
|
||||
- **Vercel**: Free tier → Pro ($20/月)
|
||||
- **Email Service**: Resend free tier (100/day)
|
||||
- **Monitoring**: Sentry free tier
|
||||
|
||||
### 時間預估
|
||||
- **總開發時間**: 5-6 週
|
||||
- **每週工時**: 40 小時
|
||||
- **總工時**: 200-240 小時
|
||||
|
||||
### 風險評估
|
||||
1. **Gemini API 限制**: 需要實作 queue 系統
|
||||
2. **資料庫效能**: 需要適當的索引設計
|
||||
3. **成本控制**: 監控 API 使用量
|
||||
4. **安全風險**: 定期安全審計
|
||||
|
||||
## 🚀 下一步行動
|
||||
|
||||
### 立即需要完成的規格文檔
|
||||
1. **資料庫 Schema 完整設計** (2天)
|
||||
2. **API 端點詳細規格** (2天)
|
||||
3. **Gemini API 整合規格** (1天)
|
||||
4. **認證流程詳細設計** (1天)
|
||||
5. **錯誤處理規範** (1天)
|
||||
|
||||
### 開發優先順序
|
||||
1. 🔴 **必要**: 認證系統、基本 CRUD、AI 生成
|
||||
2. 🟡 **重要**: 學習系統、統計功能
|
||||
3. 🟢 **可選**: 進階功能、優化
|
||||
|
||||
## 📝 檢查清單
|
||||
|
||||
### Week 1
|
||||
- [ ] 完成所有規格文檔
|
||||
- [ ] 設置開發環境
|
||||
- [ ] 建立資料庫
|
||||
- [ ] 實作認證 API
|
||||
|
||||
### Week 2-3
|
||||
- [ ] 完成核心 API
|
||||
- [ ] 整合 Gemini
|
||||
- [ ] 實作學習邏輯
|
||||
|
||||
### Week 4-5
|
||||
- [ ] 第三方服務整合
|
||||
- [ ] 效能優化
|
||||
- [ ] 安全加固
|
||||
|
||||
### Week 6
|
||||
- [ ] 完整測試
|
||||
- [ ] 部署上線
|
||||
- [ ] 監控設置
|
||||
|
||||
## 🎯 成功指標
|
||||
|
||||
1. **功能完整性**: 100% 核心功能實作
|
||||
2. **效能指標**: API 響應 < 200ms
|
||||
3. **可靠性**: 99.9% uptime
|
||||
4. **安全性**: 通過 OWASP 基本檢查
|
||||
5. **測試覆蓋**: > 80% 代碼覆蓋率
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
# Claude Code 開發 SOP 流程
|
||||
|
||||
## 🎯 目標
|
||||
確保使用 Claude Code 進行開發時,能夠穩定、有序地修正和增強現有功能。
|
||||
|
||||
## 📋 標準作業流程
|
||||
|
||||
### 1️⃣ 需求分析階段
|
||||
|
||||
#### 1.1 明確定義需求
|
||||
```markdown
|
||||
## 修改需求
|
||||
- **功能名稱**: [例如: 登入流程]
|
||||
- **現況問題**: [描述現有問題]
|
||||
- **期望結果**: [描述預期行為]
|
||||
- **影響範圍**: [列出可能受影響的檔案/功能]
|
||||
- **優先級**: [高/中/低]
|
||||
```
|
||||
|
||||
#### 1.2 需求模板
|
||||
```markdown
|
||||
# 需求:[功能名稱]
|
||||
|
||||
## 現況
|
||||
- 現有行為:
|
||||
- 問題描述:
|
||||
- 相關檔案:
|
||||
|
||||
## 期望
|
||||
- 新行為:
|
||||
- 成功標準:
|
||||
- 測試案例:
|
||||
|
||||
## 限制
|
||||
- 必須保留:
|
||||
- 不可更動:
|
||||
- 效能要求:
|
||||
```
|
||||
|
||||
### 2️⃣ 開發前準備
|
||||
|
||||
#### 2.1 環境檢查
|
||||
```bash
|
||||
# 1. 確認開發伺服器狀態
|
||||
npm run dev
|
||||
|
||||
# 2. 確認 Git 狀態
|
||||
git status
|
||||
|
||||
# 3. 創建新分支
|
||||
git checkout -b feature/[功能名稱]
|
||||
```
|
||||
|
||||
#### 2.2 備份關鍵檔案
|
||||
```bash
|
||||
# 備份將要修改的檔案
|
||||
cp app/[檔案名].tsx app/[檔案名].tsx.backup
|
||||
```
|
||||
|
||||
### 3️⃣ 與 Claude Code 協作流程
|
||||
|
||||
#### 3.1 初始對話模板
|
||||
```markdown
|
||||
我需要修改 [功能名稱],請幫我:
|
||||
|
||||
1. 先讀取相關檔案:
|
||||
- [檔案路徑1]
|
||||
- [檔案路徑2]
|
||||
|
||||
2. 分析現有實作
|
||||
|
||||
3. 確認修改方案
|
||||
|
||||
4. 實施修改
|
||||
|
||||
要求:
|
||||
- 保持現有功能不受影響
|
||||
- 遵循現有程式碼風格
|
||||
- 加入適當的錯誤處理
|
||||
```
|
||||
|
||||
#### 3.2 階段性確認
|
||||
```markdown
|
||||
## 檢查點
|
||||
- [ ] 需求理解正確
|
||||
- [ ] 影響範圍評估完成
|
||||
- [ ] 修改方案確認
|
||||
- [ ] 程式碼修改完成
|
||||
- [ ] 功能測試通過
|
||||
- [ ] 無破壞現有功能
|
||||
```
|
||||
|
||||
### 4️⃣ 實作階段
|
||||
|
||||
#### 4.1 修改優先順序
|
||||
1. **資料模型** (types, interfaces)
|
||||
2. **API 邏輯** (API routes, services)
|
||||
3. **狀態管理** (stores, contexts)
|
||||
4. **UI 組件** (components)
|
||||
5. **樣式調整** (CSS, Tailwind)
|
||||
|
||||
#### 4.2 程式碼修改原則
|
||||
```typescript
|
||||
// ❌ 避免:直接覆蓋整個檔案
|
||||
// ✅ 建議:精準修改特定函數或區塊
|
||||
|
||||
// ❌ 避免:刪除未使用的程式碼
|
||||
// ✅ 建議:先註解,確認無誤後再刪除
|
||||
|
||||
// ❌ 避免:一次修改多個功能
|
||||
// ✅ 建議:單一功能逐步修改
|
||||
```
|
||||
|
||||
#### 4.3 安全修改策略
|
||||
```markdown
|
||||
## 修改策略
|
||||
1. **增量修改**:先新增,後替換,最後刪除舊代碼
|
||||
2. **功能開關**:使用 feature flag 控制新舊功能
|
||||
3. **漸進式重構**:保持功能可用的前提下逐步改進
|
||||
```
|
||||
|
||||
### 5️⃣ 測試驗證
|
||||
|
||||
#### 5.1 功能測試清單
|
||||
```markdown
|
||||
## 測試項目
|
||||
- [ ] 正常流程測試
|
||||
- [ ] 邊界條件測試
|
||||
- [ ] 錯誤處理測試
|
||||
- [ ] 響應式設計測試
|
||||
- [ ] 效能測試
|
||||
```
|
||||
|
||||
#### 5.2 測試指令
|
||||
```bash
|
||||
# 1. 本地測試
|
||||
npm run dev
|
||||
# 手動測試各項功能
|
||||
|
||||
# 2. 建置測試
|
||||
npm run build
|
||||
# 確認無編譯錯誤
|
||||
|
||||
# 3. TypeScript 檢查
|
||||
npx tsc --noEmit
|
||||
|
||||
# 4. Lint 檢查
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### 6️⃣ 版本控制
|
||||
|
||||
#### 6.1 提交規範
|
||||
```bash
|
||||
# 提交格式
|
||||
git add [修改的檔案]
|
||||
git commit -m "[type]: [description]"
|
||||
|
||||
# Type 類型:
|
||||
# feat: 新功能
|
||||
# fix: 修復錯誤
|
||||
# refactor: 重構
|
||||
# style: 樣式修改
|
||||
# docs: 文檔更新
|
||||
# test: 測試相關
|
||||
# chore: 其他修改
|
||||
```
|
||||
|
||||
#### 6.2 提交前檢查
|
||||
```markdown
|
||||
## 提交前確認
|
||||
- [ ] 功能正常運作
|
||||
- [ ] 無 console.log 遺留
|
||||
- [ ] 無敏感資訊外洩
|
||||
- [ ] 程式碼已格式化
|
||||
- [ ] 註解已更新
|
||||
```
|
||||
|
||||
### 7️⃣ 問題處理
|
||||
|
||||
#### 7.1 常見問題與解決
|
||||
```markdown
|
||||
## 編譯錯誤
|
||||
1. 清除快取:rm -rf .next
|
||||
2. 重新安裝:rm -rf node_modules && npm install
|
||||
3. 重啟開發伺服器
|
||||
|
||||
## 樣式不生效
|
||||
1. 檢查 Tailwind 類名
|
||||
2. 清除瀏覽器快取
|
||||
3. 檢查 CSS 載入順序
|
||||
|
||||
## 狀態不同步
|
||||
1. 檢查 useState/useEffect
|
||||
2. 確認資料流向
|
||||
3. 使用 React DevTools 調試
|
||||
```
|
||||
|
||||
#### 7.2 回滾策略
|
||||
```bash
|
||||
# 方法 1:使用備份
|
||||
cp app/[檔案名].tsx.backup app/[檔案名].tsx
|
||||
|
||||
# 方法 2:Git 回滾
|
||||
git checkout -- [檔案路徑]
|
||||
|
||||
# 方法 3:回到上一個提交
|
||||
git reset --hard HEAD^
|
||||
```
|
||||
|
||||
### 8️⃣ 最佳實踐
|
||||
|
||||
#### 8.1 與 Claude Code 溝通技巧
|
||||
```markdown
|
||||
## 有效溝通
|
||||
1. **具體明確**:提供檔案路徑、行號、函數名
|
||||
2. **分步進行**:複雜修改分解為多個小步驟
|
||||
3. **即時反饋**:發現問題立即告知
|
||||
4. **保存對話**:重要決策和方案記錄下來
|
||||
```
|
||||
|
||||
#### 8.2 開發習慣
|
||||
```markdown
|
||||
## 良好習慣
|
||||
- ✅ 每完成一個小功能就測試
|
||||
- ✅ 定期提交到 Git
|
||||
- ✅ 保持開發伺服器運行
|
||||
- ✅ 使用瀏覽器開發工具監控
|
||||
- ✅ 記錄修改日誌
|
||||
```
|
||||
|
||||
### 9️⃣ 文檔維護
|
||||
|
||||
#### 9.1 修改記錄模板
|
||||
```markdown
|
||||
# 修改記錄
|
||||
|
||||
## [日期] - [功能名稱]
|
||||
### 修改內容
|
||||
- 檔案:[路徑]
|
||||
- 改動:[描述]
|
||||
- 原因:[為什麼修改]
|
||||
|
||||
### 測試結果
|
||||
- [x] 功能測試通過
|
||||
- [x] 無副作用
|
||||
- [x] 效能正常
|
||||
|
||||
### 備註
|
||||
[任何特殊說明]
|
||||
```
|
||||
|
||||
#### 9.2 知識累積
|
||||
```markdown
|
||||
## 經驗總結
|
||||
- 問題:[遇到的問題]
|
||||
- 解決:[解決方案]
|
||||
- 學習:[獲得的經驗]
|
||||
- 建議:[未來改進建議]
|
||||
```
|
||||
|
||||
## 🔄 持續改進
|
||||
|
||||
### 定期檢視
|
||||
- 每週回顧開發流程
|
||||
- 記錄痛點和改進點
|
||||
- 更新 SOP 文檔
|
||||
|
||||
### 工具優化
|
||||
- 建立程式碼片段庫
|
||||
- 自動化重複任務
|
||||
- 優化開發環境配置
|
||||
|
||||
## 📊 檢查清單總覽
|
||||
|
||||
```markdown
|
||||
# 快速檢查清單
|
||||
|
||||
## 開始前
|
||||
- [ ] 需求明確
|
||||
- [ ] 環境就緒
|
||||
- [ ] 分支創建
|
||||
|
||||
## 開發中
|
||||
- [ ] 讀取相關檔案
|
||||
- [ ] 分析影響範圍
|
||||
- [ ] 逐步實施修改
|
||||
- [ ] 即時測試驗證
|
||||
|
||||
## 完成後
|
||||
- [ ] 全面功能測試
|
||||
- [ ] 程式碼檢查
|
||||
- [ ] Git 提交
|
||||
- [ ] 文檔更新
|
||||
```
|
||||
|
||||
## 🚀 快速開始指令
|
||||
|
||||
```bash
|
||||
# 1. 開始新功能
|
||||
git checkout -b feature/new-feature
|
||||
npm run dev
|
||||
|
||||
# 2. 測試修改
|
||||
npm run build
|
||||
npx tsc --noEmit
|
||||
|
||||
# 3. 提交更改
|
||||
git add .
|
||||
git commit -m "feat: add new feature"
|
||||
git push origin feature/new-feature
|
||||
```
|
||||
|
|
@ -0,0 +1,465 @@
|
|||
# DramaLing 資料庫 Schema 詳細設計
|
||||
|
||||
## 🗄️ 資料庫架構
|
||||
|
||||
### 技術棧
|
||||
- **Database**: PostgreSQL 15+ (via Supabase)
|
||||
- **ORM**: Prisma
|
||||
- **Migration Tool**: Prisma Migrate
|
||||
- **Backup**: Daily automated backups via Supabase
|
||||
|
||||
## 📦 資料表設計
|
||||
|
||||
### 1. 用戶相關表
|
||||
|
||||
#### users
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255), -- NULL for OAuth users
|
||||
provider VARCHAR(20) DEFAULT 'email', -- email, google, facebook
|
||||
provider_id VARCHAR(255),
|
||||
email_verified BOOLEAN DEFAULT FALSE,
|
||||
avatar_url TEXT,
|
||||
display_name VARCHAR(100),
|
||||
bio TEXT,
|
||||
preferred_language VARCHAR(10) DEFAULT 'zh-TW',
|
||||
timezone VARCHAR(50) DEFAULT 'Asia/Taipei',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at TIMESTAMP WITH TIME ZONE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_premium BOOLEAN DEFAULT FALSE,
|
||||
premium_expires_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Indexes
|
||||
INDEX idx_users_email (email),
|
||||
INDEX idx_users_username (username),
|
||||
INDEX idx_users_provider (provider, provider_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### user_profiles
|
||||
```sql
|
||||
CREATE TABLE user_profiles (
|
||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
daily_goal INTEGER DEFAULT 10,
|
||||
reminder_enabled BOOLEAN DEFAULT TRUE,
|
||||
reminder_time TIME DEFAULT '20:00:00',
|
||||
study_streak INTEGER DEFAULT 0,
|
||||
longest_streak INTEGER DEFAULT 0,
|
||||
total_study_time INTEGER DEFAULT 0, -- in minutes
|
||||
total_words_learned INTEGER DEFAULT 0,
|
||||
experience_points INTEGER DEFAULT 0,
|
||||
level INTEGER DEFAULT 1,
|
||||
achievements JSONB DEFAULT '[]',
|
||||
preferences JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
#### sessions
|
||||
```sql
|
||||
CREATE TABLE sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token VARCHAR(255) UNIQUE NOT NULL,
|
||||
refresh_token VARCHAR(255) UNIQUE,
|
||||
device_info JSONB,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_activity TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_sessions_token (token),
|
||||
INDEX idx_sessions_user (user_id),
|
||||
INDEX idx_sessions_expires (expires_at)
|
||||
);
|
||||
```
|
||||
|
||||
### 2. 詞卡相關表
|
||||
|
||||
#### flashcard_sets
|
||||
```sql
|
||||
CREATE TABLE flashcard_sets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
cover_image_url TEXT,
|
||||
color VARCHAR(20) DEFAULT '#3B82F6',
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
is_system BOOLEAN DEFAULT FALSE,
|
||||
tags TEXT[] DEFAULT '{}',
|
||||
source_type VARCHAR(50), -- 'manual', 'ai_generated', 'imported'
|
||||
source_content TEXT, -- Original text for AI generated sets
|
||||
card_count INTEGER DEFAULT 0,
|
||||
completed_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_studied_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
INDEX idx_sets_user (user_id),
|
||||
INDEX idx_sets_public (is_public),
|
||||
INDEX idx_sets_tags (tags) USING GIN
|
||||
);
|
||||
```
|
||||
|
||||
#### flashcards
|
||||
```sql
|
||||
CREATE TABLE flashcards (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
set_id UUID NOT NULL REFERENCES flashcard_sets(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Core content
|
||||
word VARCHAR(200) NOT NULL,
|
||||
part_of_speech VARCHAR(50),
|
||||
pronunciation VARCHAR(200),
|
||||
audio_url TEXT,
|
||||
|
||||
-- Translations
|
||||
translation VARCHAR(500) NOT NULL,
|
||||
definition TEXT,
|
||||
definition_cn TEXT,
|
||||
|
||||
-- Examples
|
||||
example_sentence TEXT,
|
||||
example_translation TEXT,
|
||||
additional_examples JSONB DEFAULT '[]',
|
||||
|
||||
-- Learning aids
|
||||
synonyms TEXT[] DEFAULT '{}',
|
||||
antonyms TEXT[] DEFAULT '{}',
|
||||
related_words TEXT[] DEFAULT '{}',
|
||||
memory_tips TEXT,
|
||||
notes TEXT,
|
||||
image_url TEXT,
|
||||
|
||||
-- Metadata
|
||||
difficulty_level VARCHAR(20) DEFAULT 'intermediate',
|
||||
frequency_rank INTEGER,
|
||||
tags TEXT[] DEFAULT '{}',
|
||||
context_tags TEXT[] DEFAULT '{}', -- 'business', 'casual', 'academic', etc.
|
||||
|
||||
-- Learning data
|
||||
is_favorite BOOLEAN DEFAULT FALSE,
|
||||
is_mastered BOOLEAN DEFAULT FALSE,
|
||||
mastery_level INTEGER DEFAULT 0, -- 0-100
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_cards_set (set_id),
|
||||
INDEX idx_cards_user (user_id),
|
||||
INDEX idx_cards_word (word),
|
||||
INDEX idx_cards_mastery (mastery_level),
|
||||
INDEX idx_cards_tags (tags) USING GIN,
|
||||
UNIQUE(set_id, word)
|
||||
);
|
||||
```
|
||||
|
||||
### 3. 學習系統表
|
||||
|
||||
#### learning_records
|
||||
```sql
|
||||
CREATE TABLE learning_records (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
flashcard_id UUID NOT NULL REFERENCES flashcards(id) ON DELETE CASCADE,
|
||||
|
||||
-- SM-2 Algorithm fields
|
||||
ease_factor DECIMAL(3,2) DEFAULT 2.5,
|
||||
interval INTEGER DEFAULT 1, -- days
|
||||
repetitions INTEGER DEFAULT 0,
|
||||
|
||||
-- Review data
|
||||
last_reviewed_at TIMESTAMP WITH TIME ZONE,
|
||||
next_review_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Performance tracking
|
||||
total_reviews INTEGER DEFAULT 0,
|
||||
correct_reviews INTEGER DEFAULT 0,
|
||||
incorrect_reviews INTEGER DEFAULT 0,
|
||||
average_rating DECIMAL(2,1),
|
||||
|
||||
-- Time tracking
|
||||
total_study_time INTEGER DEFAULT 0, -- seconds
|
||||
last_study_duration INTEGER DEFAULT 0,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_learning_user (user_id),
|
||||
INDEX idx_learning_card (flashcard_id),
|
||||
INDEX idx_learning_next_review (next_review_at),
|
||||
UNIQUE(user_id, flashcard_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### review_logs
|
||||
```sql
|
||||
CREATE TABLE review_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
flashcard_id UUID NOT NULL REFERENCES flashcards(id) ON DELETE CASCADE,
|
||||
learning_record_id UUID REFERENCES learning_records(id) ON DELETE CASCADE,
|
||||
|
||||
-- Review details
|
||||
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
||||
response_time INTEGER, -- milliseconds
|
||||
review_type VARCHAR(20), -- 'scheduled', 'practice', 'cram'
|
||||
mode VARCHAR(20), -- 'flip', 'quiz', 'typing'
|
||||
|
||||
-- Context
|
||||
session_id UUID,
|
||||
device_type VARCHAR(20),
|
||||
|
||||
reviewed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_review_user (user_id),
|
||||
INDEX idx_review_card (flashcard_id),
|
||||
INDEX idx_review_date (reviewed_at)
|
||||
);
|
||||
```
|
||||
|
||||
#### study_sessions
|
||||
```sql
|
||||
CREATE TABLE study_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Session info
|
||||
start_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
end_time TIMESTAMP WITH TIME ZONE,
|
||||
duration INTEGER, -- seconds
|
||||
|
||||
-- Performance
|
||||
cards_studied INTEGER DEFAULT 0,
|
||||
cards_mastered INTEGER DEFAULT 0,
|
||||
correct_count INTEGER DEFAULT 0,
|
||||
incorrect_count INTEGER DEFAULT 0,
|
||||
accuracy_rate DECIMAL(5,2),
|
||||
|
||||
-- Context
|
||||
study_mode VARCHAR(20), -- 'review', 'learn', 'cram'
|
||||
set_id UUID REFERENCES flashcard_sets(id) ON DELETE SET NULL,
|
||||
device_type VARCHAR(20),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_sessions_user_date (user_id, start_time)
|
||||
);
|
||||
```
|
||||
|
||||
### 4. AI 生成相關表
|
||||
|
||||
#### ai_generation_logs
|
||||
```sql
|
||||
CREATE TABLE ai_generation_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Request info
|
||||
input_text TEXT,
|
||||
input_type VARCHAR(50), -- 'text', 'theme'
|
||||
theme VARCHAR(100),
|
||||
difficulty_level VARCHAR(20),
|
||||
card_count INTEGER,
|
||||
|
||||
-- Generation details
|
||||
model_used VARCHAR(50),
|
||||
prompt_tokens INTEGER,
|
||||
completion_tokens INTEGER,
|
||||
total_tokens INTEGER,
|
||||
generation_time INTEGER, -- milliseconds
|
||||
|
||||
-- Results
|
||||
generated_cards JSONB,
|
||||
cards_saved INTEGER,
|
||||
set_id UUID REFERENCES flashcard_sets(id) ON DELETE SET NULL,
|
||||
|
||||
-- Status
|
||||
status VARCHAR(20), -- 'pending', 'completed', 'failed'
|
||||
error_message TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_ai_gen_user (user_id),
|
||||
INDEX idx_ai_gen_date (created_at)
|
||||
);
|
||||
```
|
||||
|
||||
### 5. 統計與分析表
|
||||
|
||||
#### daily_stats
|
||||
```sql
|
||||
CREATE TABLE daily_stats (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
date DATE NOT NULL,
|
||||
|
||||
-- Learning metrics
|
||||
cards_reviewed INTEGER DEFAULT 0,
|
||||
cards_learned INTEGER DEFAULT 0,
|
||||
cards_mastered INTEGER DEFAULT 0,
|
||||
study_time INTEGER DEFAULT 0, -- minutes
|
||||
sessions_count INTEGER DEFAULT 0,
|
||||
|
||||
-- Performance
|
||||
average_accuracy DECIMAL(5,2),
|
||||
average_ease_factor DECIMAL(3,2),
|
||||
|
||||
-- Streaks
|
||||
streak_maintained BOOLEAN DEFAULT FALSE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(user_id, date),
|
||||
INDEX idx_daily_stats_user_date (user_id, date)
|
||||
);
|
||||
```
|
||||
|
||||
#### achievements
|
||||
```sql
|
||||
CREATE TABLE achievements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
icon_url TEXT,
|
||||
category VARCHAR(50),
|
||||
points INTEGER DEFAULT 0,
|
||||
requirement_type VARCHAR(50), -- 'streak', 'total_words', 'accuracy', etc.
|
||||
requirement_value INTEGER,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
#### user_achievements
|
||||
```sql
|
||||
CREATE TABLE user_achievements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
achievement_id UUID NOT NULL REFERENCES achievements(id) ON DELETE CASCADE,
|
||||
unlocked_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
progress INTEGER DEFAULT 0,
|
||||
|
||||
UNIQUE(user_id, achievement_id),
|
||||
INDEX idx_user_achievements (user_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 6. 通知與設定表
|
||||
|
||||
#### notifications
|
||||
```sql
|
||||
CREATE TABLE notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
type VARCHAR(50) NOT NULL, -- 'reminder', 'achievement', 'streak', etc.
|
||||
title VARCHAR(200) NOT NULL,
|
||||
message TEXT,
|
||||
data JSONB,
|
||||
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
read_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
INDEX idx_notifications_user (user_id),
|
||||
INDEX idx_notifications_unread (user_id, is_read)
|
||||
);
|
||||
```
|
||||
|
||||
#### user_settings
|
||||
```sql
|
||||
CREATE TABLE user_settings (
|
||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Notification settings
|
||||
email_notifications BOOLEAN DEFAULT TRUE,
|
||||
push_notifications BOOLEAN DEFAULT TRUE,
|
||||
reminder_notifications BOOLEAN DEFAULT TRUE,
|
||||
achievement_notifications BOOLEAN DEFAULT TRUE,
|
||||
|
||||
-- Learning preferences
|
||||
default_study_mode VARCHAR(20) DEFAULT 'flip',
|
||||
auto_play_audio BOOLEAN DEFAULT TRUE,
|
||||
show_pronunciation BOOLEAN DEFAULT TRUE,
|
||||
review_order VARCHAR(20) DEFAULT 'due_date', -- 'due_date', 'random', 'difficulty'
|
||||
|
||||
-- Display preferences
|
||||
theme VARCHAR(20) DEFAULT 'light',
|
||||
font_size VARCHAR(20) DEFAULT 'medium',
|
||||
card_layout VARCHAR(20) DEFAULT 'standard',
|
||||
|
||||
-- Privacy
|
||||
profile_visibility VARCHAR(20) DEFAULT 'private',
|
||||
share_progress BOOLEAN DEFAULT FALSE,
|
||||
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## 🔗 關聯關係
|
||||
|
||||
### 主要關係
|
||||
1. **users → flashcard_sets**: 一對多 (用戶擁有多個卡組)
|
||||
2. **flashcard_sets → flashcards**: 一對多 (卡組包含多個詞卡)
|
||||
3. **users → learning_records**: 一對多 (用戶的學習記錄)
|
||||
4. **flashcards → learning_records**: 一對多 (每個詞卡的學習記錄)
|
||||
5. **learning_records → review_logs**: 一對多 (學習記錄的詳細日誌)
|
||||
|
||||
## 🔑 索引策略
|
||||
|
||||
### 主要索引
|
||||
1. **用戶查詢**: email, username
|
||||
2. **詞卡查詢**: word, set_id, user_id
|
||||
3. **學習查詢**: next_review_at, user_id
|
||||
4. **統計查詢**: user_id + date
|
||||
5. **全文搜索**: 使用 PostgreSQL 的 GIN 索引於 tags 和 JSONB 欄位
|
||||
|
||||
## 🔄 資料遷移策略
|
||||
|
||||
### 版本控制
|
||||
```bash
|
||||
# Prisma migration commands
|
||||
npx prisma migrate dev --name init
|
||||
npx prisma migrate deploy
|
||||
npx prisma migrate status
|
||||
```
|
||||
|
||||
### 備份策略
|
||||
1. **自動備份**: 每日 02:00 UTC
|
||||
2. **保留期限**: 30 天
|
||||
3. **地理位置**: 跨區域複製
|
||||
4. **恢復測試**: 每季度一次
|
||||
|
||||
## 📊 效能優化
|
||||
|
||||
### 查詢優化
|
||||
1. **分頁**: 使用 cursor-based pagination
|
||||
2. **快取**: Redis 快取熱門查詢
|
||||
3. **預載**: Eager loading 避免 N+1 問題
|
||||
4. **分區**: 考慮將 review_logs 按月份分區
|
||||
|
||||
### 容量規劃
|
||||
- **預期用戶數**: 10,000
|
||||
- **平均詞卡/用戶**: 1,000
|
||||
- **平均評論/詞卡**: 50
|
||||
- **總資料量**: ~10GB
|
||||
|
||||
## 🔒 安全考慮
|
||||
|
||||
1. **Row Level Security (RLS)**: Supabase 提供
|
||||
2. **加密**: 敏感資料加密存儲
|
||||
3. **稽核**: 所有資料變更記錄
|
||||
4. **資料遮罩**: 個人資訊不外洩
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
# 🚀 簡化開發流程 (開發階段專用)
|
||||
|
||||
## 核心原則:快速迭代、立即看結果
|
||||
|
||||
---
|
||||
|
||||
## 📝 超簡單三步驟流程
|
||||
|
||||
### 1️⃣ 告訴我要改什麼
|
||||
```markdown
|
||||
「我要修改 [功能名稱]」
|
||||
「現在是 [A],我要改成 [B]」
|
||||
```
|
||||
|
||||
### 2️⃣ 我幫你改
|
||||
- 我會先看檔案
|
||||
- 直接修改
|
||||
- 告訴你改了什麼
|
||||
|
||||
### 3️⃣ 你測試確認
|
||||
- 瀏覽器看結果
|
||||
- OK → 繼續下一個
|
||||
- 不OK → 告訴我,馬上修
|
||||
|
||||
---
|
||||
|
||||
## 💬 對話範例
|
||||
|
||||
### 範例 1:改文字
|
||||
```
|
||||
你:「Dashboard 的標題要改成『學習中心』」
|
||||
我:改好了
|
||||
你:看瀏覽器確認
|
||||
```
|
||||
|
||||
### 範例 2:改功能
|
||||
```
|
||||
你:「登入成功後要跳到 /dashboard 不是 /home」
|
||||
我:改好了,在 login/page.tsx 第 XX 行
|
||||
你:測試 OK
|
||||
```
|
||||
|
||||
### 範例 3:改樣式
|
||||
```
|
||||
你:「按鈕太小,改大一點,顏色改成綠色」
|
||||
我:改好了
|
||||
你:「再大一點」
|
||||
我:調整了
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 常用需求快速指令
|
||||
|
||||
### UI 調整
|
||||
- 「把 X 改成 Y」
|
||||
- 「這個太小/太大」
|
||||
- 「顏色改成 #XXXXXX」
|
||||
- 「加個按鈕做 XXX」
|
||||
|
||||
### 流程修改
|
||||
- 「點擊後要跳到 XXX 頁」
|
||||
- 「這個欄位要必填」
|
||||
- 「加個 loading 狀態」
|
||||
- 「錯誤訊息改成 XXX」
|
||||
|
||||
### 資料調整
|
||||
- 「這個假資料改成 XXX」
|
||||
- 「多加幾筆測試資料」
|
||||
- 「預設值改成 XXX」
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 快速修復
|
||||
|
||||
### 頁面壞了
|
||||
```
|
||||
你:「/flashcards 打不開」
|
||||
我:馬上修復
|
||||
```
|
||||
|
||||
### 樣式跑版
|
||||
```
|
||||
你:「手機版跑版了」
|
||||
我:調整 responsive
|
||||
```
|
||||
|
||||
### 功能失效
|
||||
```
|
||||
你:「按鈕點了沒反應」
|
||||
我:檢查並修復
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 檔案位置速查
|
||||
|
||||
```
|
||||
頁面 → /app/[頁面名]/page.tsx
|
||||
樣式 → /app/globals.css 或 Tailwind classes
|
||||
設定 → /next.config.mjs, /tailwind.config.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 一句話原則
|
||||
|
||||
**「先做出來,能動就好,之後再優化」**
|
||||
|
||||
---
|
||||
|
||||
## 💡 小技巧
|
||||
|
||||
1. **不用管 Git**:開發階段改了再說,最後再整理 commit
|
||||
2. **不用寫文檔**:先把功能做出來
|
||||
3. **不用完美**:70% 完成度就可以繼續下一個
|
||||
4. **隨時問**:卡住就問,不要糾結
|
||||
|
||||
---
|
||||
|
||||
## 🔄 每日收尾(可選)
|
||||
|
||||
結束前跟我說:
|
||||
```
|
||||
「今天改完了,幫我 git add + commit」
|
||||
```
|
||||
|
||||
我會幫你:
|
||||
- 整理今天的修改
|
||||
- 寫 commit message
|
||||
- 推上 Git
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 唯一要注意的
|
||||
|
||||
**開發伺服器要開著**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
如果壞了:
|
||||
1. Ctrl+C 停止
|
||||
2. `npm run dev` 重開
|
||||
3. 還是壞了就叫我
|
||||
|
||||
---
|
||||
|
||||
## 🚫 不用管的事
|
||||
|
||||
- ❌ 測試
|
||||
- ❌ 文檔
|
||||
- ❌ Code Review
|
||||
- ❌ 效能優化
|
||||
- ❌ TypeScript 錯誤(如果能動)
|
||||
- ❌ Console 警告(如果能動)
|
||||
|
||||
**這些等要上線前再處理**
|
||||
|
||||
---
|
||||
|
||||
## 📢 記住
|
||||
|
||||
> 現在是開發階段,目標是:
|
||||
> 1. 快速看到結果
|
||||
> 2. 快速調整
|
||||
> 3. 快速迭代
|
||||
|
||||
**完美是上線前的事!**
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 561 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 603 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 628 KiB |
Loading…
Reference in New Issue