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

1941 lines
76 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Navigation } from '@/components/Navigation'
import AudioPlayer from '@/components/AudioPlayer'
import VoiceRecorder from '@/components/VoiceRecorder'
import LearningComplete from '@/components/LearningComplete'
import ReviewTypeIndicator from '@/components/review/ReviewTypeIndicator'
import MasteryIndicator from '@/components/review/MasteryIndicator'
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards'
import { calculateCurrentMastery, getReviewTypesByDifficulty } from '@/lib/utils/masteryCalculator'
// 擴展的Flashcard接口包含智能複習需要的欄位
interface ExtendedFlashcard extends Omit<Flashcard, 'nextReviewDate'> {
userLevel?: number; // 學習者程度 (1-100) - 向後兼容
wordLevel?: number; // 詞彙難度 (1-100) - 向後兼容
nextReviewDate?: string; // 下次復習日期 (可選)
currentInterval?: number; // 當前間隔天數
isOverdue?: boolean; // 是否逾期
overdueDays?: number; // 逾期天數
baseMasteryLevel?: number; // 基礎熟悉度
lastReviewDate?: string; // 最後復習日期
synonyms?: string[]; // 同義詞 (向後兼容)
exampleImage?: string; // 例句圖片 (向後兼容)
// 注意difficultyLevel已在基礎Flashcard接口中定義
}
export default function LearnPage() {
const router = useRouter()
const [mounted, setMounted] = useState(false)
// 智能複習狀態
const [currentCard, setCurrentCard] = useState<ExtendedFlashcard | null>(null)
const [dueCards, setDueCards] = useState<ExtendedFlashcard[]>([])
const [currentCardIndex, setCurrentCardIndex] = useState(0)
const [isLoadingCard, setIsLoadingCard] = useState(false)
// 複習模式狀態 (系統自動選擇)
const [mode, setMode] = useState<'flip-memory' | 'vocab-choice' | 'vocab-listening' | 'sentence-listening' | 'sentence-fill' | 'sentence-reorder' | 'sentence-speaking'>('flip-memory')
const [isAutoSelecting, setIsAutoSelecting] = useState(true)
// 答題狀態
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 [isFlipped, setIsFlipped] = useState(false)
// UI狀態
const [modalImage, setModalImage] = useState<string | null>(null)
const [showReportModal, setShowReportModal] = useState(false)
const [reportReason, setReportReason] = useState('')
const [reportingCard, setReportingCard] = useState<any>(null)
const [showComplete, setShowComplete] = useState(false)
const [showNoDueCards, setShowNoDueCards] = useState(false)
const [cardHeight, setCardHeight] = useState<number>(400)
// 題型特定狀態
const [quizOptions, setQuizOptions] = useState<string[]>([])
const [sentenceOptions, setSentenceOptions] = useState<string[]>([])
// 例句重組狀態
const [shuffledWords, setShuffledWords] = useState<string[]>([])
const [arrangedWords, setArrangedWords] = useState<string[]>([])
const [reorderResult, setReorderResult] = useState<boolean | null>(null)
// Refs for measuring card content heights
const cardFrontRef = useRef<HTMLDivElement>(null)
const cardBackRef = useRef<HTMLDivElement>(null)
const cardContainerRef = useRef<HTMLDivElement>(null)
// Calculate optimal card height based on content (only when card changes)
const calculateCardHeight = () => {
if (!cardFrontRef.current || !cardBackRef.current) return 400;
// Get the scroll heights to measure actual content
const frontHeight = cardFrontRef.current.scrollHeight;
const backHeight = cardBackRef.current.scrollHeight;
console.log('Heights calculated:', { frontHeight, backHeight }); // Debug log
// Use the maximum height with padding
const maxHeight = Math.max(frontHeight, backHeight);
const paddedHeight = maxHeight + 40; // Add padding for visual spacing
// Ensure minimum height for visual consistency
return Math.max(paddedHeight, 450);
};
// Update card height only when card content changes (not on flip)
useLayoutEffect(() => {
if (mounted && cardFrontRef.current && cardBackRef.current) {
// Wait for DOM to be fully rendered
const timer = setTimeout(() => {
const newHeight = calculateCardHeight();
setCardHeight(newHeight);
}, 50);
return () => clearTimeout(timer);
}
}, [currentCardIndex, mounted]);
// Client-side mounting
useEffect(() => {
setMounted(true)
loadDueCards() // 載入到期詞卡
}, [])
// 載入到期詞卡列表
const loadDueCards = async () => {
try {
setIsLoadingCard(true)
// 完全使用後端API數據
const apiResult = await flashcardsService.getDueFlashcards(50);
if (apiResult.success && apiResult.data && apiResult.data.length > 0) {
const cardsToUse = apiResult.data;
console.log('載入後端API數據:', cardsToUse.length, '張詞卡');
setDueCards(cardsToUse);
// 設置第一張卡片
const firstCard = cardsToUse[0];
setCurrentCard(firstCard);
setCurrentCardIndex(0);
// 系統自動選擇模式
const selectedMode = await selectOptimalReviewMode(firstCard);
setMode(selectedMode);
setIsAutoSelecting(false);
console.log(`初始載入: ${firstCard.word}, 選擇模式: ${selectedMode}`);
} else {
// 沒有到期詞卡
console.log('沒有到期的詞卡');
setDueCards([]);
setCurrentCard(null);
setShowNoDueCards(true);
}
} catch (error) {
console.error('載入到期詞卡失敗:', error);
setDueCards([]);
setCurrentCard(null);
} finally {
setIsLoadingCard(false);
}
}
// 智能載入下一張卡片並自動選擇模式
const loadNextCardWithAutoMode = async (cardIndex: number) => {
try {
setIsAutoSelecting(true);
// 等待dueCards載入完成
if (dueCards.length === 0) {
console.log('等待詞卡載入...');
return;
}
const card = dueCards[cardIndex];
if (!card) {
setShowComplete(true);
setIsAutoSelecting(false);
return;
}
setCurrentCard(card);
setCurrentCardIndex(cardIndex);
// 系統自動選擇最適合的複習模式
const selectedMode = await selectOptimalReviewMode(card);
setMode(selectedMode);
// 重置所有答題狀態
resetAllStates();
console.log(`載入卡片: ${card.word}, 選擇模式: ${selectedMode}`);
} catch (error) {
console.error('載入卡片失敗:', error);
} finally {
setIsAutoSelecting(false);
}
}
// 系統自動選擇最適合的複習模式
const selectOptimalReviewMode = async (card: ExtendedFlashcard): Promise<typeof mode> => {
try {
// 使用CEFR字符串進行智能選擇
const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFRLevel = card.difficultyLevel || 'A2';
console.log(`CEFR智能選擇: 用戶${userCEFRLevel} vs 詞彙${wordCEFRLevel}`);
const apiResult = await flashcardsService.getOptimalReviewMode(card.id, userCEFRLevel, wordCEFRLevel);
if (apiResult.success && apiResult.data?.selectedMode) {
const selectedMode = apiResult.data.selectedMode;
console.log(`後端智能選擇: ${selectedMode}`);
// 映射到前端模式名稱
const modeMapping: { [key: string]: typeof mode } = {
'flip-memory': 'flip-memory',
'vocab-choice': 'vocab-choice',
'vocab-listening': 'vocab-listening',
'sentence-fill': 'sentence-fill',
'sentence-reorder': 'sentence-reorder',
'sentence-speaking': 'sentence-speaking',
'sentence-listening': 'sentence-listening'
};
return modeMapping[selectedMode] || 'flip-memory';
} else {
console.log('後端API失敗使用前端邏輯');
}
} catch (error) {
console.error('智能選擇API錯誤:', error);
}
// 備用: 使用前端CEFR邏輯
const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFRLevel = card.difficultyLevel || 'A2';
const availableModes = getReviewTypesByCEFR(userCEFRLevel, wordCEFRLevel);
const modeMapping: { [key: string]: typeof mode } = {
'flip-memory': 'flip-memory',
'vocab-choice': 'vocab-choice',
'vocab-listening': 'vocab-listening',
'sentence-fill': 'sentence-fill',
'sentence-reorder': 'sentence-reorder',
'sentence-speaking': 'sentence-speaking',
'sentence-listening': 'sentence-listening'
};
const selectedType = availableModes[0] || 'flip-memory';
console.log(`前端CEFR邏輯選擇: ${selectedType}`);
return modeMapping[selectedType] || 'flip-memory';
}
// 前端CEFR備用選擇邏輯
const getReviewTypesByCEFR = (userCEFR: string, wordCEFR: string): string[] => {
const userLevel = getCEFRToLevel(userCEFR);
const wordLevel = getCEFRToLevel(wordCEFR);
const difficulty = wordLevel - userLevel;
if (userCEFR === 'A1') {
return ['flip-memory', 'vocab-choice', 'vocab-listening'];
} else if (difficulty < -10) {
return ['sentence-reorder', 'sentence-fill'];
} else if (difficulty >= -10 && difficulty <= 10) {
return ['sentence-fill', 'sentence-reorder', 'sentence-speaking'];
} else {
return ['flip-memory', 'vocab-choice'];
}
}
// CEFR轉換為數值 (前端計算用)
const getCEFRToLevel = (cefr: string): number => {
const mapping: { [key: string]: number } = {
'A1': 20, 'A2': 35, 'B1': 50, 'B2': 65, 'C1': 80, 'C2': 95
};
return mapping[cefr] || 50;
}
// 取得當前學習情境
const getCurrentContext = (userCEFR: string, wordCEFR: string): string => {
const userLevel = getCEFRToLevel(userCEFR);
const wordLevel = getCEFRToLevel(wordCEFR);
const difficulty = wordLevel - userLevel;
if (userCEFR === 'A1') return 'A1學習者';
if (difficulty < -10) return '簡單詞彙';
if (difficulty >= -10 && difficulty <= 10) return '適中詞彙';
return '困難詞彙';
}
// 生成完整四情境對照表數據
const generateContextTable = (currentUserCEFR: string, currentWordCEFR: string) => {
const currentContext = getCurrentContext(currentUserCEFR, currentWordCEFR);
const contexts = [
{
type: 'A1學習者',
icon: '🛡️',
reviewTypes: ['🔄翻卡記憶', '✅詞彙選擇', '🎧詞彙聽力'],
purpose: '建立基礎信心',
condition: '用戶等級 = A1',
description: '初學者保護機制使用最基礎的3種題型'
},
{
type: '簡單詞彙',
icon: '🎯',
reviewTypes: ['✏️例句填空', '🔀例句重組'],
purpose: '應用練習',
condition: '用戶等級 > 詞彙等級',
description: '詞彙對您較簡單,重點練習拼寫和語法應用'
},
{
type: '適中詞彙',
icon: '⚖️',
reviewTypes: ['✏️例句填空', '🔀例句重組', '🗣️例句口說'],
purpose: '全方位練習',
condition: '用戶等級 ≈ 詞彙等級',
description: '詞彙難度適中,進行聽說讀寫全方位練習'
},
{
type: '困難詞彙',
icon: '📚',
reviewTypes: ['🔄翻卡記憶', '✅詞彙選擇'],
purpose: '基礎重建',
condition: '用戶等級 < 詞彙等級',
description: '詞彙對您較困難,回歸基礎重建記憶'
}
];
return contexts.map(context => ({
...context,
isCurrent: context.type === currentContext
}));
}
// 取得題型圖標
const getModeIcon = (mode: string): string => {
const icons: { [key: string]: string } = {
'flip-memory': '🔄',
'vocab-choice': '✅',
'vocab-listening': '🎧',
'sentence-listening': '👂',
'sentence-fill': '✏️',
'sentence-reorder': '🔀',
'sentence-speaking': '🗣️'
};
return icons[mode] || '📝';
}
// 取得題型中文名稱
const getModeLabel = (mode: string): string => {
const labels: { [key: string]: string } = {
'flip-memory': '翻卡記憶',
'vocab-choice': '詞彙選擇',
'vocab-listening': '詞彙聽力',
'sentence-listening': '例句聽力',
'sentence-fill': '例句填空',
'sentence-reorder': '例句重組',
'sentence-speaking': '例句口說'
};
return labels[mode] || mode;
}
// 重置所有答題狀態
const resetAllStates = () => {
setIsFlipped(false);
setSelectedAnswer(null);
setShowResult(false);
setFillAnswer('');
setShowHint(false);
setShuffledWords([]);
setArrangedWords([]);
setReorderResult(null);
setQuizOptions([]);
}
// Quiz options generation
useEffect(() => {
if (!currentCard) return;
const currentWord = currentCard.word;
// Generate quiz options with current word and other words
const otherWords = dueCards
.filter(card => card.id !== currentCard.id)
.map(card => card.word);
// If we don't have enough words in the deck, add some default options
const additionalOptions = ['determine', 'achieve', 'consider', 'negotiate', 'establish', 'maintain'];
const allOtherWords = [...otherWords, ...additionalOptions];
// Take 3 other words (avoiding duplicates)
const selectedOtherWords: string[] = [];
for (const word of allOtherWords) {
if (selectedOtherWords.length >= 3) break;
if (word !== currentWord && !selectedOtherWords.includes(word)) {
selectedOtherWords.push(word);
}
}
// Ensure we have exactly 4 options: current word + 3 others
const options = [currentWord, ...selectedOtherWords].sort(() => Math.random() - 0.5);
setQuizOptions(options);
// Reset quiz state when card changes
setSelectedAnswer(null);
setShowResult(false);
}, [currentCard, dueCards])
// Sentence options generation for sentence listening
useEffect(() => {
if (!currentCard || mode !== 'sentence-listening') return;
const currentSentence = currentCard.example;
// Generate sentence options with current sentence and other sentences
const otherSentences = dueCards
.filter(card => card.id !== currentCard.id)
.map(card => card.example);
// Add some default sentence options if not enough
const additionalSentences = [
'I think this is a good opportunity for us.',
'She decided to take a different approach.',
'They managed to solve the problem quickly.',
'We need to consider all possible solutions.'
];
const allOtherSentences = [...otherSentences, ...additionalSentences];
// Take 3 other sentences (avoiding duplicates)
const selectedOtherSentences: string[] = [];
for (const sentence of allOtherSentences) {
if (selectedOtherSentences.length >= 3) break;
if (sentence !== currentSentence && !selectedOtherSentences.includes(sentence)) {
selectedOtherSentences.push(sentence);
}
}
// Ensure we have exactly 4 options: current sentence + 3 others
const options = [currentSentence, ...selectedOtherSentences].sort(() => Math.random() - 0.5);
setSentenceOptions(options);
}, [currentCard, dueCards, mode])
// Initialize sentence reorder when card changes or mode switches to sentence-reorder
useEffect(() => {
if (mode === 'sentence-reorder' && currentCard) {
const words = currentCard.example.split(/\s+/).filter(word => word.length > 0)
const shuffled = [...words].sort(() => Math.random() - 0.5)
setShuffledWords(shuffled)
setArrangedWords([])
// 只在卡片或模式切換時重置結果,不在其他狀態變化時重置
if (reorderResult !== null) {
setReorderResult(null)
}
}
}, [currentCard, mode]) // 移除reorderResult依賴避免循環重置
// Sentence reorder handlers
const handleWordClick = (word: string) => {
// Move word from shuffled to arranged
setShuffledWords(prev => prev.filter(w => w !== word))
setArrangedWords(prev => [...prev, word])
setReorderResult(null)
}
const handleRemoveFromArranged = (word: string) => {
setArrangedWords(prev => prev.filter(w => w !== word))
setShuffledWords(prev => [...prev, word])
setReorderResult(null)
}
const handleCheckReorderAnswer = async () => {
if (!currentCard) return;
const userSentence = arrangedWords.join(' ')
const correctSentence = currentCard.example
const isCorrect = userSentence.toLowerCase().trim() === correctSentence.toLowerCase().trim()
setReorderResult(isCorrect)
// 更新分數
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 提交復習結果
await submitReviewResult(isCorrect, userSentence);
}
const handleResetReorder = () => {
if (!currentCard) return;
const words = currentCard.example.split(/\s+/).filter(word => word.length > 0)
const shuffled = [...words].sort(() => Math.random() - 0.5)
setShuffledWords(shuffled)
setArrangedWords([])
setReorderResult(null)
}
const handleFlip = () => {
setIsFlipped(!isFlipped)
}
const handleNext = async () => {
if (currentCardIndex < dueCards.length - 1) {
await loadNextCardWithAutoMode(currentCardIndex + 1);
} else {
setShowComplete(true);
}
}
const handlePrevious = async () => {
if (currentCardIndex > 0) {
await loadNextCardWithAutoMode(currentCardIndex - 1);
}
}
const handleQuizAnswer = async (answer: string) => {
if (showResult || !currentCard) return
setSelectedAnswer(answer)
setShowResult(true)
const isCorrect = answer === currentCard.word
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 提交復習結果到後端
await submitReviewResult(isCorrect, answer);
}
// 提交復習結果
const submitReviewResult = async (isCorrect: boolean, userAnswer?: string, confidenceLevel?: number) => {
if (!currentCard) return;
try {
const result = await flashcardsService.submitReview(currentCard.id, {
isCorrect,
confidenceLevel,
questionType: mode,
userAnswer,
timeTaken: Date.now() - Date.now() // 簡化時間計算
});
if (result.success && result.data) {
console.log('復習結果提交成功:', result.data);
// 更新卡片的熟悉度等資訊,但不觸發卡片重新載入
setCurrentCard(prev => prev ? {
...prev,
masteryLevel: result.data!.masteryLevel,
nextReviewDate: result.data!.nextReviewDate
} : null);
} else {
console.log('復習結果提交失敗,繼續運行');
}
} catch (error) {
console.error('提交復習結果失敗:', error);
// 不中斷流程,允許用戶繼續學習
}
}
const handleFillAnswer = async () => {
if (showResult || !currentCard) return
setShowResult(true)
const isCorrect = fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 提交復習結果
await submitReviewResult(isCorrect, fillAnswer);
}
const handleListeningAnswer = async (answer: string) => {
if (showResult || !currentCard) return
setSelectedAnswer(answer)
setShowResult(true)
const isCorrect = answer === currentCard.word
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 提交復習結果
await submitReviewResult(isCorrect, answer);
}
const handleSpeakingAnswer = async (transcript: string) => {
if (!currentCard) return
setShowResult(true)
const isCorrect = transcript.toLowerCase().includes(currentCard.word.toLowerCase())
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 提交復習結果
await submitReviewResult(isCorrect, transcript);
}
const handleSentenceListeningAnswer = async (answer: string) => {
if (showResult || !currentCard) return
setSelectedAnswer(answer)
setShowResult(true)
const isCorrect = answer === currentCard.example
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 提交復習結果
await submitReviewResult(isCorrect, answer);
}
const handleReportSubmit = () => {
console.log('Report submitted:', {
card: reportingCard,
reason: reportReason
})
setShowReportModal(false)
setReportReason('')
setReportingCard(null)
}
const handleRestart = async () => {
setScore({ correct: 0, total: 0 })
setShowComplete(false)
setShowNoDueCards(false)
await loadDueCards(); // 重新載入到期詞卡
}
// Show loading screen until mounted or while loading cards
if (!mounted || isLoadingCard) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-gray-500 text-lg">
{isAutoSelecting ? '系統正在選擇最適合的複習方式...' : '載入中...'}
</div>
</div>
)
}
// Show no due cards screen
if (showNoDueCards) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<Navigation />
<div className="max-w-4xl mx-auto px-4 py-8 flex items-center justify-center min-h-[calc(100vh-80px)]">
<div className="bg-white rounded-xl p-8 max-w-md w-full text-center shadow-lg">
<div className="text-6xl mb-4">📚</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
</h2>
<p className="text-gray-600 mb-6">
</p>
<div className="space-y-3 mb-6">
<div className="bg-blue-50 rounded-lg p-3 text-left">
<div className="font-medium text-blue-900 mb-1">💡 </div>
<ul className="text-blue-800 text-sm space-y-1">
<li> </li>
<li> </li>
<li> 調</li>
</ul>
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => router.push('/flashcards')}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
</button>
<button
onClick={() => router.push('/dashboard')}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors font-medium"
>
</button>
</div>
<button
onClick={handleRestart}
className="w-full mt-3 py-2 text-blue-600 hover:text-blue-800 transition-colors text-sm"
>
🔄
</button>
</div>
</div>
</div>
)
}
// Show current card interface
if (!currentCard) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-gray-500 text-lg">...</div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* Navigation */}
<Navigation />
<div className="max-w-4xl mx-auto px-4 py-8">
{/* Progress Bar */}
<div className="mb-8">
<div className="flex justify-between items-center mb-2">
<span className="text-sm text-gray-600"></span>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">
{currentCardIndex + 1} / {dueCards.length}
</span>
<div className="text-sm">
<span className="text-green-600 font-semibold">{score.correct}</span>
<span className="text-gray-500">/</span>
<span className="text-gray-600">{score.total}</span>
{score.total > 0 && (
<span className="text-blue-600 ml-2">
({Math.round((score.correct / score.total) * 100)}%)
</span>
)}
</div>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${((currentCardIndex + 1) / dueCards.length) * 100}%` }}
></div>
</div>
</div>
{/* Demo Information Panel */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold text-blue-900 mb-3">🎯 </h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 text-sm mb-4">
<div className="bg-white rounded p-3">
<div className="text-blue-700 font-medium"></div>
<div className="text-gray-600">
{currentCard && (() => {
const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFRLevel = currentCard.difficultyLevel || 'A2';
const userLevel = getCEFRToLevel(userCEFRLevel);
const wordLevel = getCEFRToLevel(wordCEFRLevel);
const difficulty = wordLevel - userLevel;
if (userCEFRLevel === 'A1') return 'A1學習者';
if (difficulty < -10) return '簡單詞彙';
if (difficulty >= -10 && difficulty <= 10) return '適中詞彙';
return '困難詞彙';
})()}
</div>
</div>
<div className="bg-white rounded p-3">
<div className="text-blue-700 font-medium"></div>
<div className="text-gray-600">{localStorage.getItem('userEnglishLevel') || 'A2'}</div>
</div>
<div className="bg-white rounded p-3">
<div className="text-blue-700 font-medium"></div>
<div className="text-gray-600">{currentCard?.difficultyLevel || 'A2'}</div>
</div>
<div className="bg-white rounded p-3">
<div className="text-blue-700 font-medium"></div>
<div className="text-gray-600">
{currentCard ? (() => {
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFR = currentCard.difficultyLevel || 'A2';
const diff = getCEFRToLevel(wordCEFR) - getCEFRToLevel(userCEFR);
return diff > 0 ? `+${diff}` : diff.toString();
})() : '--'}
</div>
</div>
</div>
{/* 當前選擇突出顯示 */}
<div className="bg-gradient-to-r from-blue-50 to-blue-100 rounded-lg p-3 mb-3">
<div className="flex items-center justify-between">
<div>
{currentCard && (() => {
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFR = currentCard.difficultyLevel || 'A2';
const context = getCurrentContext(userCEFR, wordCEFR);
const contextData = generateContextTable(userCEFR, wordCEFR).find(c => c.isCurrent);
return (
<>
<div>
<span className="text-blue-800 font-medium">
: {contextData?.icon} {context}
</span>
<div className="text-blue-600 text-xs mt-1">
: {contextData?.reviewTypes.join(' | ')}
</div>
</div>
</>
);
})()}
</div>
<div className="text-blue-800 text-right">
<div className="text-xs"></div>
<div className="font-medium flex items-center gap-1">
<span>{getModeIcon(mode)}</span>
<span>{getModeLabel(mode)}</span>
</div>
</div>
</div>
</div>
{/* 完整四情境對照表 */}
<div className="bg-white rounded-lg p-4">
<div className="text-blue-700 font-medium mb-3">📚 </div>
<div className="overflow-x-auto">
<table className="w-full text-xs border-collapse">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left p-2 font-medium text-gray-700"></th>
<th className="text-left p-2 font-medium text-gray-700"></th>
<th className="text-left p-2 font-medium text-gray-700"></th>
<th className="text-left p-2 font-medium text-gray-700"></th>
<th className="text-center p-2 font-medium text-gray-700"></th>
</tr>
</thead>
<tbody>
{currentCard && (() => {
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFR = currentCard.difficultyLevel || 'A2';
const tableData = generateContextTable(userCEFR, wordCEFR);
return tableData.map((row, index) => (
<tr key={index} className={`border-b border-gray-100 ${row.isCurrent ? 'bg-blue-50 ring-1 ring-blue-200' : ''}`}>
<td className="p-2">
<span className="flex items-center gap-1 font-medium">
{row.icon} {row.type}
</span>
</td>
<td className="p-2">
<div className="flex flex-wrap gap-1">
{row.reviewTypes.map((type, idx) => (
<span key={idx} className="text-xs whitespace-nowrap">{type}</span>
))}
</div>
</td>
<td className="p-2 text-gray-600">{row.purpose}</td>
<td className="p-2 text-gray-500 text-xs">{row.condition}</td>
<td className="p-2 text-center">
{row.isCurrent && <span className="text-blue-600 font-medium"> </span>}
</td>
</tr>
));
})()}
</tbody>
</table>
</div>
<div className="mt-3 p-2 bg-gray-50 rounded text-xs text-gray-600">
<div className="font-medium mb-1">🧠 </div>
<div>CEFR等級和詞彙CEFR等級自動判斷學習情境</div>
</div>
</div>
</div>
{/* Current Card Mastery Level */}
{currentCard?.baseMasteryLevel && currentCard?.lastReviewDate && (
<div className="mb-4">
<MasteryIndicator
level={calculateCurrentMastery(currentCard.baseMasteryLevel, currentCard.lastReviewDate)}
baseMasteryLevel={currentCard.baseMasteryLevel}
size="medium"
showPercentage={true}
/>
</div>
)}
{/* System Auto-Selected Review Type Indicator */}
<ReviewTypeIndicator
currentMode={mode}
userCEFRLevel={localStorage.getItem('userEnglishLevel') || 'A2'}
wordCEFRLevel={currentCard?.difficultyLevel}
/>
{mode === 'flip-memory' ? (
/* Flip Card Mode */
<div className="relative">
{/* Error Report Button for Flip Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div
ref={cardContainerRef}
className="card-container"
onClick={handleFlip}
style={{ height: `${cardHeight}px` }}
>
<div className={`card ${isFlipped ? 'flipped' : ''}`}>
{/* Front */}
<div className="card-front">
<div
ref={cardFrontRef}
className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow p-8"
>
{/* Title and Instructions */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Word Display */}
<div className="flex-1 flex items-center justify-center mt-6">
<div className="bg-gray-50 rounded-lg p-8 w-full text-center">
<h3 className="text-4xl font-bold text-gray-900 mb-6">
{currentCard.word}
</h3>
<div className="flex items-center justify-center gap-3">
<span className="text-lg text-gray-500">
{currentCard.pronunciation}
</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
</div>
</div>
</div>
{/* Back */}
<div className="card-back">
<div
ref={cardBackRef}
className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow"
>
{/* Content Sections */}
<div className="space-y-4">
{/* Definition */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<p className="text-gray-700 text-left">{currentCard.definition}</p>
</div>
{/* Example */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="relative">
<p className="text-gray-700 italic mb-2 text-left pr-12">"{currentCard.example}"</p>
<div className="absolute bottom-0 right-0">
<AudioPlayer text={currentCard.example} />
</div>
</div>
<p className="text-gray-600 text-sm text-left">"{currentCard.exampleTranslation}"</p>
</div>
{/* Synonyms */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="flex flex-wrap gap-2">
{(currentCard.synonyms || []).map((synonym, index) => (
<span
key={index}
className="bg-white text-gray-700 px-3 py-1 rounded-full text-sm border border-gray-200"
>
{synonym}
</span>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'vocab-choice' ? (
/* Vocab Choice Mode - 詞彙選擇 */
<div className="relative">
{/* Error Report Button for Quiz Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
<div className="text-center mb-8">
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<p className="text-gray-700 text-left">{currentCard.definition}</p>
</div>
</div>
<div className="space-y-3 mb-6">
{quizOptions.map((option, idx) => (
<button
key={idx}
onClick={() => !showResult && handleQuizAnswer(option)}
disabled={showResult}
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
showResult
? option === currentCard.word
? 'border-green-500 bg-green-50 text-green-700'
: option === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
{option}
</button>
))}
</div>
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === currentCard.word
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === currentCard.word
? 'text-green-700'
: 'text-red-700'
}`}>
{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}
</p>
{selectedAnswer !== currentCard.word && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{currentCard.word}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<div className="flex items-center text-gray-600">
<strong></strong>
<span className="mx-2">{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
</div>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-fill' ? (
/* Fill in the Blank Mode - 填空題 */
<div className="relative">
{/* Error Report Button for Fill Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Example Image */}
{currentCard.exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<img
src={currentCard.exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => setModalImage(currentCard.exampleImage || null)}
/>
</div>
</div>
)}
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Example Sentence with Blanks */}
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-6">
<div className="text-lg text-gray-700 leading-relaxed">
{currentCard.example.split(new RegExp(`(${currentCard.word})`, 'gi')).map((part, index) => {
const isTargetWord = part.toLowerCase() === currentCard.word.toLowerCase();
return isTargetWord ? (
<span key={index} className="relative inline-block mx-1">
<input
type="text"
value={fillAnswer}
onChange={(e) => setFillAnswer(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !showResult && fillAnswer.trim()) {
handleFillAnswer()
}
}}
placeholder=""
disabled={showResult}
className={`inline-block px-2 py-1 text-center bg-transparent focus:outline-none disabled:bg-gray-100 ${
fillAnswer
? 'border-b-2 border-blue-500'
: 'border-b-2 border-dashed border-gray-400 focus:border-blue-400 focus:border-solid'
}`}
style={{ width: `${Math.max(100, Math.max(currentCard.word.length * 12, fillAnswer.length * 12 + 20))}px` }}
/>
{!fillAnswer && (
<span
className="absolute inset-0 flex items-center justify-center text-gray-400 pointer-events-none"
style={{ paddingBottom: '8px' }}
>
____
</span>
)}
</span>
) : (
<span key={index}>{part}</span>
);
})}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3 mb-4">
{!showResult && fillAnswer.trim() && (
<button
onClick={handleFillAnswer}
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
</button>
)}
<button
onClick={() => setShowHint(!showHint)}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
{showHint ? '隱藏提示' : '顯示提示'}
</button>
</div>
{/* Hint Section */}
{showHint && (
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-semibold text-yellow-800 mb-2"></h4>
<p className="text-yellow-800">{currentCard.definition}</p>
</div>
)}
{showResult && (
<div className={`mt-6 p-6 rounded-lg w-full ${
fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()
? 'text-green-700'
: 'text-red-700'
}`}>
{fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase() ? '正確!' : '錯誤!'}
</p>
{fillAnswer.toLowerCase().trim() !== currentCard.word.toLowerCase() && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{currentCard.word}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-600">
<span className="mx-2">{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</p>
</div>
<div className="text-left">
</div>
</div>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'vocab-listening' ? (
/* Listening Test Mode - 聽力測試 */
<div className="relative">
{/* Error Report Button for Listening Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
()
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Content Sections */}
<div className="space-y-4 mb-8">
{/* Audio */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="flex items-center gap-3">
<span className="text-gray-700">{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
</div>
{/* Word Options */}
<div className="grid grid-cols-2 gap-3 mb-6">
{[currentCard.word, 'determine', 'achieve', 'consider'].map((word) => (
<button
key={word}
onClick={() => !showResult && handleListeningAnswer(word)}
disabled={showResult}
className={`p-4 text-center rounded-lg border-2 transition-all ${
showResult
? word === currentCard.word
? 'border-green-500 bg-green-50 text-green-700'
: word === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
{word}
</button>
))}
</div>
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === currentCard.word
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === currentCard.word
? 'text-green-700'
: 'text-red-700'
}`}>
{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}
</p>
{selectedAnswer !== currentCard.word && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{currentCard.word}</strong>
</p>
<div className="text-gray-600 text-left mt-1 flex items-center gap-2">
<span>{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
)}
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-speaking' ? (
/* Speaking Test Mode - 口說測試 */
<div className="relative">
{/* Error Report Button for Speaking Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
<div className="w-full">
<VoiceRecorder
targetText={currentCard.example}
targetTranslation={currentCard.exampleTranslation}
exampleImage={currentCard.exampleImage}
instructionText="請看例句圖片並大聲說出完整的例句:"
onRecordingComplete={() => {
// 簡化處理:直接顯示結果
handleSpeakingAnswer(currentCard.example)
}}
/>
</div>
{showResult && (
<div className="mt-6 p-6 rounded-lg bg-blue-50 border border-blue-200 w-full">
<p className="text-blue-700 text-left text-xl font-semibold mb-2">
</p>
<p className="text-gray-600 text-left">
...
</p>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-listening' ? (
/* Sentence Listening Test Mode - 例句聽力題 */
<div className="relative">
{/* Error Report Button */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
<div className="text-center mb-8">
<div className="mb-6">
<AudioPlayer text={currentCard.example} />
<p className="text-sm text-gray-500 mt-2">
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-3 mb-6">
{sentenceOptions.map((sentence, idx) => (
<button
key={idx}
onClick={() => !showResult && handleSentenceListeningAnswer(sentence)}
disabled={showResult}
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
showResult
? sentence === currentCard.example
? 'border-green-500 bg-green-50 text-green-700'
: sentence === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
<div className="text-sm text-gray-600 mb-1"> {String.fromCharCode(65 + idx)}:</div>
<div className="text-base">{sentence}</div>
</button>
))}
</div>
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === currentCard.example
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === currentCard.example
? 'text-green-700'
: 'text-red-700'
}`}>
{selectedAnswer === currentCard.example ? '正確!' : '錯誤!'}
</p>
{selectedAnswer !== currentCard.example && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">"{currentCard.example}"</strong>
</p>
<p className="text-gray-600 text-left mt-1">
{currentCard.exampleTranslation}
</p>
</div>
)}
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-reorder' ? (
/* Sentence Reorder Mode - 例句重組題 */
<div className="relative">
{/* Error Report Button */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Example Image */}
{currentCard.exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<img
src={currentCard.exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => setModalImage(currentCard.exampleImage || null)}
/>
</div>
</div>
)}
{/* Arranged Sentence Area */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left"></h3>
<div className="relative min-h-[120px] bg-gray-50 rounded-lg p-4 border-2 border-dashed border-gray-300">
{arrangedWords.length === 0 ? (
<div className="absolute inset-0 flex items-center justify-center text-gray-400 text-lg">
</div>
) : (
<div className="flex flex-wrap gap-2">
{arrangedWords.map((word, index) => (
<div
key={`arranged-${index}`}
className="inline-flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-blue-200 transition-colors"
onClick={() => handleRemoveFromArranged(word)}
>
{word}
<span className="ml-2 text-blue-600 hover:text-blue-800">×</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Shuffled Words */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left"></h3>
<div className="bg-white border border-gray-200 rounded-lg p-4 min-h-[80px]">
{shuffledWords.length === 0 ? (
<div className="text-center text-gray-400">
使
</div>
) : (
<div className="flex flex-wrap gap-2">
{shuffledWords.map((word, index) => (
<button
key={`shuffled-${index}`}
onClick={() => handleWordClick(word)}
className="bg-gray-100 text-gray-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-gray-200 active:bg-gray-300 transition-colors select-none"
>
{word}
</button>
))}
</div>
)}
</div>
</div>
{/* Control Buttons */}
<div className="flex gap-3 mb-6">
{arrangedWords.length > 0 && (
<button
onClick={handleCheckReorderAnswer}
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
</button>
)}
<button
onClick={handleResetReorder}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
</button>
</div>
{/* Result Feedback */}
{reorderResult !== null && (
<div className={`p-6 rounded-lg w-full mb-6 ${
reorderResult
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
reorderResult ? 'text-green-700' : 'text-red-700'
}`}>
{reorderResult ? '正確!' : '錯誤!'}
</p>
{!reorderResult && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">"{currentCard.example}"</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-600">
<strong></strong>{currentCard.exampleTranslation}
</p>
</div>
</div>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : null}
{/* Report Modal */}
{showReportModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="mb-4">
<p className="text-sm text-gray-600 mb-2">
{reportingCard?.word}
</p>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select
value={reportReason}
onChange={(e) => setReportReason(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
>
<option value=""></option>
<option value="translation"></option>
<option value="definition"></option>
<option value="pronunciation"></option>
<option value="example"></option>
<option value="image"></option>
<option value="other"></option>
</select>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowReportModal(false)}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50"
>
</button>
<button
onClick={handleReportSubmit}
disabled={!reportReason}
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
</div>
</div>
)}
{/* Image Modal */}
{modalImage && (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
onClick={() => setModalImage(null)}
>
<div className="relative max-w-4xl max-h-[90vh] mx-4">
<img
src={modalImage}
alt="放大圖片"
className="max-w-full max-h-full rounded-lg"
/>
<button
onClick={() => setModalImage(null)}
className="absolute top-4 right-4 bg-black bg-opacity-50 text-white p-2 rounded-full hover:bg-opacity-75"
>
</button>
</div>
</div>
)}
{/* Complete Modal */}
{showComplete && (
<LearningComplete
score={score}
mode={mode}
onRestart={handleRestart}
onBackToDashboard={() => router.push('/dashboard')}
/>
)}
{/* No Due Cards Modal */}
{showNoDueCards && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-8 max-w-md w-full mx-4 text-center">
<div className="text-6xl mb-4">📚</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
</h2>
<p className="text-gray-600 mb-6">
</p>
<div className="space-y-3 mb-6">
<div className="bg-blue-50 rounded-lg p-3 text-left">
<div className="font-medium text-blue-900 mb-1">💡 </div>
<ul className="text-blue-800 text-sm space-y-1">
<li> </li>
<li> </li>
<li> 調</li>
</ul>
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => router.push('/flashcards')}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
</button>
<button
onClick={() => router.push('/dashboard')}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors font-medium"
>
</button>
</div>
<button
onClick={handleRestart}
className="w-full mt-3 py-2 text-blue-600 hover:text-blue-800 transition-colors text-sm"
>
🔄
</button>
</div>
</div>
)}
</div>
<style jsx>{`
.card-container {
perspective: 1000px;
transition: height 0.3s ease;
overflow: visible;
position: relative;
}
.card {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.6s ease;
transform-style: preserve-3d;
}
.card.flipped {
transform: rotateY(180deg);
}
.card-front, .card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: stretch;
justify-content: center;
top: 0;
left: 0;
}
.card-back {
transform: rotateY(180deg);
}
.card-front > div {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
padding: 2rem;
box-sizing: border-box;
}
.card-back > div {
width: 100%;
padding: 1.5rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
`}</style>
</div>
)
}