'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, isA1Learner } from '@/lib/utils/masteryCalculator' // 擴展的Flashcard接口,包含智能複習需要的欄位 interface ExtendedFlashcard extends Flashcard { userLevel?: number; // 學習者程度 (1-100) wordLevel?: number; // 詞彙難度 (1-100) nextReviewDate?: string; // 下次復習日期 currentInterval?: number; // 當前間隔天數 isOverdue?: boolean; // 是否逾期 overdueDays?: number; // 逾期天數 baseMasteryLevel?: number; // 基礎熟悉度 lastReviewDate?: string; // 最後復習日期 synonyms?: string[]; // 同義詞 (暫時保留mock格式) difficulty?: string; // CEFR等級 (暫時保留mock格式) exampleImage?: string; // 例句圖片 (暫時保留mock格式) } export default function LearnPage() { const router = useRouter() const [mounted, setMounted] = useState(false) // 智能複習狀態 const [currentCard, setCurrentCard] = useState(null) const [dueCards, setDueCards] = useState([]) 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(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(null) const [showReportModal, setShowReportModal] = useState(false) const [reportReason, setReportReason] = useState('') const [reportingCard, setReportingCard] = useState(null) const [showComplete, setShowComplete] = useState(false) const [cardHeight, setCardHeight] = useState(400) // 題型特定狀態 const [quizOptions, setQuizOptions] = useState([]) const [sentenceOptions, setSentenceOptions] = useState([]) // 例句重組狀態 const [shuffledWords, setShuffledWords] = useState([]) const [arrangedWords, setArrangedWords] = useState([]) const [reorderResult, setReorderResult] = useState(null) // Refs for measuring card content heights const cardFrontRef = useRef(null) const cardBackRef = useRef(null) const cardContainerRef = useRef(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) // 暫時使用mock data,等後端API就緒後替換 const mockDueCards: ExtendedFlashcard[] = [ { id: '1', word: 'brought', partOfSpeech: 'verb', 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: '他在我們的會議中提出了這件事,但沒有人同意。', masteryLevel: 65, timesReviewed: 3, isFavorite: false, nextReviewDate: new Date().toISOString().split('T')[0], // 今天到期 difficultyLevel: 'B1', createdAt: new Date().toISOString(), // 智能複習欄位 userLevel: 60, // 學習者程度 wordLevel: 70, // 詞彙難度 (困難詞彙) baseMasteryLevel: 75, lastReviewDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2天前 exampleImages: [], hasExampleImage: true, primaryImageUrl: '/images/examples/bring_up.png', synonyms: ['mentioned', 'raised', 'introduced'], difficulty: 'B1', exampleImage: '/images/examples/bring_up.png' }, { id: '2', word: 'simple', partOfSpeech: 'adjective', pronunciation: '/ˈsɪmpəl/', translation: '簡單的', definition: 'Easy to understand or do; not complex', example: 'This is a simple task that anyone can complete.', exampleTranslation: '這是一個任何人都能完成的簡單任務。', masteryLevel: 45, timesReviewed: 1, isFavorite: false, nextReviewDate: new Date().toISOString().split('T')[0], difficultyLevel: 'A2', createdAt: new Date().toISOString(), // 智能複習欄位 - A1學習者 userLevel: 15, // A1學習者 wordLevel: 25, baseMasteryLevel: 50, lastReviewDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), exampleImages: [], hasExampleImage: false, synonyms: ['easy', 'basic', 'straightforward'], difficulty: 'A2', exampleImage: '/images/examples/simple.png' } ]; setDueCards(mockDueCards); if (mockDueCards.length > 0) { await loadNextCardWithAutoMode(0); } } catch (error) { console.error('載入到期詞卡失敗:', error); } finally { setIsLoadingCard(false); } } // 智能載入下一張卡片並自動選擇模式 const loadNextCardWithAutoMode = async (cardIndex: number) => { try { setIsAutoSelecting(true); const card = dueCards[cardIndex]; if (!card) { setShowComplete(true); return; } setCurrentCard(card); setCurrentCardIndex(cardIndex); // 系統自動選擇最適合的複習模式 const selectedMode = await selectOptimalReviewMode(card); setMode(selectedMode); // 重置所有答題狀態 resetAllStates(); } catch (error) { console.error('載入卡片失敗:', error); } finally { setIsAutoSelecting(false); } } // 系統自動選擇最適合的複習模式 const selectOptimalReviewMode = async (card: ExtendedFlashcard): Promise => { // 暫時使用前端邏輯,後續整合後端API const userLevel = card.userLevel || 50; const wordLevel = card.wordLevel || 50; const availableModes = getReviewTypesByDifficulty(userLevel, wordLevel); // 映射到實際的模式名稱 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'; return modeMapping[selectedType] || 'flip-memory'; } // 重置所有答題狀態 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([]) setReorderResult(null) } }, [currentCard, mode]) // 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() - (currentCard.startTime || Date.now()) }); if (result.success && result.data) { // 更新卡片的熟悉度等資訊 setCurrentCard(prev => prev ? { ...prev, masteryLevel: result.data!.masteryLevel, nextReviewDate: result.data!.nextReviewDate } : null); } } 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) await loadDueCards(); // 重新載入到期詞卡 } // Show loading screen until mounted or while loading cards if (!mounted || isLoadingCard || !currentCard) { return (
{isAutoSelecting ? '系統正在選擇最適合的複習方式...' : '載入中...'}
) } return (
{/* Navigation */} router.push('/dashboard')} />
{/* Progress Bar */}
進度
{currentCardIndex + 1} / {dueCards.length}
{score.correct} / {score.total} {score.total > 0 && ( ({Math.round((score.correct / score.total) * 100)}%) )}
{/* Current Card Mastery Level */} {currentCard.baseMasteryLevel && currentCard.lastReviewDate && (
)} {/* System Auto-Selected Review Type Indicator */} {mode === 'flip-memory' ? ( /* Flip Card Mode */
{/* Error Report Button for Flip Mode */}
{/* Front */}
{/* Title and Instructions */}

翻卡記憶

{currentCard.difficulty}
{/* Instructions Test Action */}

點擊卡片翻面,根據你對單字的熟悉程度進行自我評估:

{/* Word Display */}

{currentCard.word}

{currentCard.pronunciation}
{/* Back */}
{/* Content Sections */}
{/* Definition */}

定義

{currentCard.definition}

{/* Example */}

例句

"{currentCard.example}"

"{currentCard.exampleTranslation}"

{/* Synonyms */}

同義詞

{currentCard.synonyms.map((synonym, index) => ( {synonym} ))}
{/* Navigation */}
) : mode === 'vocab-choice' ? ( /* Vocab Choice Mode - 詞彙選擇 */
{/* Error Report Button for Quiz Mode */}
{/* Title in top-left */}

詞彙選擇

{currentCard.difficulty}
{/* Instructions Test Action */}

請選擇符合上述定義的英文詞彙:

定義

{currentCard.definition}

{quizOptions.map((option, idx) => ( ))}
{showResult && (

{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}

{selectedAnswer !== currentCard.word && (

正確答案是:{currentCard.word}

)}
發音: {currentCard.pronunciation}
)}
{/* Navigation */}
) : mode === 'sentence-fill' ? ( /* Fill in the Blank Mode - 填空題 */
{/* Error Report Button for Fill Mode */}
{/* Title in top-left */}

例句填空

{currentCard.difficulty}
{/* Example Image */} {currentCard.exampleImage && (
Example illustration setModalImage(currentCard.exampleImage)} />
)} {/* Instructions Test Action */}

請點擊例句中的空白處輸入正確的單字:

{/* Example Sentence with Blanks */}
{currentCard.example.split(new RegExp(`(${currentCard.word})`, 'gi')).map((part, index) => { const isTargetWord = part.toLowerCase() === currentCard.word.toLowerCase(); return isTargetWord ? ( 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 && ( ____ )} ) : ( {part} ); })}
{/* Action Buttons */}
{!showResult && fillAnswer.trim() && ( )}
{/* Hint Section */} {showHint && (

詞彙定義:

{currentCard.definition}

)} {showResult && (

{fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase() ? '正確!' : '錯誤!'}

{fillAnswer.toLowerCase().trim() !== currentCard.word.toLowerCase() && (

正確答案是:{currentCard.word}

)}

{currentCard.pronunciation}

)}
{/* Navigation */}
) : mode === 'vocab-listening' ? ( /* Listening Test Mode - 聽力測試 */
{/* Error Report Button for Listening Mode */}
{/* Title in top-left */}

詞彙聽力 (暫時不上線)

{currentCard.difficulty}
{/* Instructions Test Action */}

請聽發音並選擇正確的英文單字:

{/* Content Sections */}
{/* Audio */}

發音

{currentCard.pronunciation}
{/* Word Options */}
{[currentCard.word, 'determine', 'achieve', 'consider'].map((word) => ( ))}
{showResult && (

{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}

{selectedAnswer !== currentCard.word && (

正確答案是:{currentCard.word}

發音:{currentCard.pronunciation}

)}
)}
{/* Navigation */}
) : mode === 'sentence-speaking' ? ( /* Speaking Test Mode - 口說測試 */
{/* Error Report Button for Speaking Mode */}
{/* Title in top-left */}

例句口說

{currentCard.difficulty}
{ // 簡化處理:直接顯示結果 handleSpeakingAnswer(currentCard.example) }} />
{showResult && (

錄音完成!

系統正在評估你的發音...

)}
{/* Navigation */}
) : mode === 'sentence-listening' ? ( /* Sentence Listening Test Mode - 例句聽力題 */
{/* Error Report Button */}
{/* Title in top-left */}

例句聽力

{currentCard.difficulty}
{/* Instructions Test Action */}

請聽例句並選擇正確的選項:

點擊播放聽例句

{sentenceOptions.map((sentence, idx) => ( ))}
{showResult && (

{selectedAnswer === currentCard.example ? '正確!' : '錯誤!'}

{selectedAnswer !== currentCard.example && (

正確答案是:"{currentCard.example}"

中文翻譯:{currentCard.exampleTranslation}

)}
)}
{/* Navigation */}
) : mode === 'sentence-reorder' ? ( /* Sentence Reorder Mode - 例句重組題 */
{/* Error Report Button */}
{/* Title in top-left */}

例句重組

{currentCard.difficulty}
{/* Example Image */} {currentCard.exampleImage && (
Example illustration setModalImage(currentCard.exampleImage)} />
)} {/* Arranged Sentence Area */}

重組區域:

{arrangedWords.length === 0 ? (
答案區
) : (
{arrangedWords.map((word, index) => (
handleRemoveFromArranged(word)} > {word} ×
))}
)}
{/* Instructions Test Action */}

點擊下方單字,依序重組成正確的句子:

{/* Shuffled Words */}

可用單字:

{shuffledWords.length === 0 ? (
所有單字都已使用
) : (
{shuffledWords.map((word, index) => ( ))}
)}
{/* Control Buttons */}
{arrangedWords.length > 0 && ( )}
{/* Result Feedback */} {reorderResult !== null && (

{reorderResult ? '正確!' : '錯誤!'}

{!reorderResult && (

正確答案是:"{currentCard.example}"

)}

中文翻譯:{currentCard.exampleTranslation}

)}
{/* Navigation */}
) : null} {/* Report Modal */} {showReportModal && (

回報錯誤

單字:{reportingCard?.word}

)} {/* Image Modal */} {modalImage && (
setModalImage(null)} >
放大圖片
)} {/* Complete Modal */} {showComplete && ( router.push('/dashboard')} /> )}
) }