diff --git a/frontend/app/learn/page.tsx b/frontend/app/learn/page.tsx index 39f7d80..329ebb1 100644 --- a/frontend/app/learn/page.tsx +++ b/frontend/app/learn/page.tsx @@ -54,6 +54,7 @@ export default function LearnPage() { const [reportReason, setReportReason] = useState('') const [reportingCard, setReportingCard] = useState(null) const [showComplete, setShowComplete] = useState(false) + const [showNoDueCards, setShowNoDueCards] = useState(false) const [cardHeight, setCardHeight] = useState(400) // 題型特定狀態 @@ -112,170 +113,38 @@ export default function LearnPage() { try { setIsLoadingCard(true) - // Mock data 展示四情境自動適配效果 - const mockDueCards: ExtendedFlashcard[] = [ - // 情境1: A1學習者 (userLevel ≤ 20) → 基礎3題型 - { - id: '1', - word: 'cat', - partOfSpeech: 'noun', - pronunciation: '/kæt/', - translation: '貓', - definition: 'A small animal that people keep as a pet', - example: 'The cat is sleeping on the sofa.', - exampleTranslation: '這隻貓正在沙發上睡覺。', - masteryLevel: 30, - timesReviewed: 1, - isFavorite: false, - nextReviewDate: new Date().toISOString().split('T')[0], - difficultyLevel: 'A1', - createdAt: new Date().toISOString(), - // A1學習者情境 - userLevel: 15, // A1學習者 - wordLevel: 20, // 基礎詞彙 - baseMasteryLevel: 35, - lastReviewDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), - exampleImages: [], - hasExampleImage: false, - synonyms: ['kitty', 'feline'], - difficulty: 'A1', - exampleImage: '' // 移除不存在的圖片 - }, + // 完全使用後端API數據 + const apiResult = await flashcardsService.getDueFlashcards(50); - // 情境2: 簡單詞彙 (學習者程度 > 詞彙程度) → 應用2題型 - { - id: '2', - word: 'happy', - partOfSpeech: 'adjective', - pronunciation: '/ˈhæpi/', - translation: '快樂的', - definition: 'Feeling pleasure and contentment', - example: 'She looks very happy today because of the good news.', - exampleTranslation: '她今天看起來很快樂,因為有好消息。', - masteryLevel: 85, - timesReviewed: 5, - isFavorite: true, - nextReviewDate: new Date().toISOString().split('T')[0], - difficultyLevel: 'A2', - createdAt: new Date().toISOString(), - // 簡單詞彙情境 (程度 > 難度) - userLevel: 70, // 中級學習者 - wordLevel: 35, // 簡單詞彙 (difficulty = -35) - baseMasteryLevel: 90, - lastReviewDate: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), - exampleImages: [], - hasExampleImage: true, - primaryImageUrl: '', - synonyms: ['joyful', 'glad', 'cheerful'], - difficulty: 'A2', - exampleImage: '' - }, + if (apiResult.success && apiResult.data && apiResult.data.length > 0) { + const cardsToUse = apiResult.data; + console.log('載入後端API數據:', cardsToUse.length, '張詞卡'); - // 情境3: 適中詞彙 (程度 ≈ 難度) → 全方位3題型 - { - id: '3', - word: 'determine', - partOfSpeech: 'verb', - pronunciation: '/dɪˈtɜːrmɪn/', - translation: '決定、確定', - definition: 'To decide or establish exactly what something is', - example: 'We need to determine the best solution for this problem.', - exampleTranslation: '我們需要確定這個問題的最佳解決方案。', - masteryLevel: 55, - timesReviewed: 3, - isFavorite: false, - nextReviewDate: new Date().toISOString().split('T')[0], - difficultyLevel: 'B1', - createdAt: new Date().toISOString(), - // 適中詞彙情境 (程度 ≈ 難度) - userLevel: 60, // 中級學習者 - wordLevel: 65, // 適中詞彙 (difficulty = +5) - baseMasteryLevel: 65, - lastReviewDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), - exampleImages: [], - hasExampleImage: true, - primaryImageUrl: '', - synonyms: ['decide', 'establish', 'figure out'], - difficulty: 'B1', - exampleImage: '' - }, + setDueCards(cardsToUse); - // 情境4: 困難詞彙 (程度 < 難度) → 基礎2題型 - { - id: '4', - word: 'sophisticated', - partOfSpeech: 'adjective', - pronunciation: '/səˈfɪstɪkeɪtɪd/', - translation: '精密的、複雜的', - definition: 'Having advanced knowledge, experience, or understanding', - example: 'The new software uses sophisticated algorithms to analyze data.', - exampleTranslation: '這個新軟體使用精密的算法來分析數據。', - masteryLevel: 25, - timesReviewed: 1, - isFavorite: false, - nextReviewDate: new Date().toISOString().split('T')[0], - difficultyLevel: 'C1', - createdAt: new Date().toISOString(), - // 困難詞彙情境 (程度 < 難度) - userLevel: 50, // 中級學習者 - wordLevel: 85, // 困難詞彙 (difficulty = +35) - baseMasteryLevel: 30, - lastReviewDate: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), - exampleImages: [], - hasExampleImage: true, - primaryImageUrl: '', - synonyms: ['advanced', 'complex', 'refined'], - difficulty: 'C1', - exampleImage: '' - }, - - // 額外情境: A1學習者遇到困難詞彙 → 自動保護 - { - id: '5', - word: 'dog', - partOfSpeech: 'noun', - pronunciation: '/dɔːg/', - translation: '狗', - definition: 'A four-legged animal that people keep as a pet', - example: 'My dog likes to play in the park.', - exampleTranslation: '我的狗喜歡在公園裡玩。', - masteryLevel: 20, - timesReviewed: 0, - isFavorite: false, - nextReviewDate: new Date().toISOString().split('T')[0], - difficultyLevel: 'A1', - createdAt: new Date().toISOString(), - // A1學習者保護測試 (即使是簡單詞彙也用基礎題型) - userLevel: 12, // A1初學者 - wordLevel: 15, // 簡單詞彙,但A1會被保護 - baseMasteryLevel: 25, - lastReviewDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), - exampleImages: [], - hasExampleImage: false, - synonyms: ['puppy', 'hound'], - difficulty: 'A1', - exampleImage: '' - } - ]; - - setDueCards(mockDueCards); - - // 直接設置第一張卡片,避免循環依賴 - if (mockDueCards.length > 0) { - const firstCard = mockDueCards[0]; + // 設置第一張卡片 + const firstCard = cardsToUse[0]; setCurrentCard(firstCard); setCurrentCardIndex(0); // 系統自動選擇模式 const selectedMode = await selectOptimalReviewMode(firstCard); setMode(selectedMode); - setIsAutoSelecting(false); // 確保設置為false + 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); } @@ -320,13 +189,43 @@ export default function LearnPage() { // 系統自動選擇最適合的複習模式 const selectOptimalReviewMode = async (card: ExtendedFlashcard): Promise => { - // 暫時使用前端邏輯,後續整合後端API - const userLevel = card.userLevel || 50; - const wordLevel = card.wordLevel || 50; + try { + // 使用CEFR字符串進行智能選擇 + const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2'; + const wordCEFRLevel = card.difficultyLevel || 'A2'; - const availableModes = getReviewTypesByDifficulty(userLevel, wordLevel); + 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', @@ -337,11 +236,121 @@ export default function LearnPage() { '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); @@ -431,9 +440,12 @@ export default function LearnPage() { const shuffled = [...words].sort(() => Math.random() - 0.5) setShuffledWords(shuffled) setArrangedWords([]) - setReorderResult(null) + // 只在卡片或模式切換時重置結果,不在其他狀態變化時重置 + if (reorderResult !== null) { + setReorderResult(null) + } } - }, [currentCard, mode]) + }, [currentCard, mode]) // 移除reorderResult依賴,避免循環重置 // Sentence reorder handlers const handleWordClick = (word: string) => { @@ -525,15 +537,19 @@ export default function LearnPage() { }); 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); + // 不中斷流程,允許用戶繼續學習 } } @@ -612,11 +628,12 @@ export default function LearnPage() { 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 || !currentCard) { + if (!mounted || isLoadingCard) { return (
@@ -626,6 +643,71 @@ export default function LearnPage() { ) } + // Show no due cards screen + if (showNoDueCards) { + return ( +
+ router.push('/dashboard')} + /> +
+
+
📚
+

+ 今日學習已完成! +

+

+ 目前沒有到期需要複習的詞卡。您可以: +

+ +
+
+
💡 建議行動
+
    +
  • • 前往詞卡管理頁面新增詞卡
  • +
  • • 查看學習統計和進度
  • +
  • • 調整學習目標和設定
  • +
+
+
+ +
+ + +
+ + +
+
+
+ ) + } + + // Show current card interface + if (!currentCard) { + return ( +
+
載入詞卡中...
+
+ ) + } + return (
{/* Navigation */} @@ -672,11 +754,13 @@ export default function LearnPage() {
當前情境
{currentCard && (() => { - const userLevel = currentCard.userLevel || 50; - const wordLevel = currentCard.wordLevel || 50; + const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2'; + const wordCEFRLevel = currentCard.difficultyLevel || 'A2'; + const userLevel = getCEFRToLevel(userCEFRLevel); + const wordLevel = getCEFRToLevel(wordCEFRLevel); const difficulty = wordLevel - userLevel; - if (userLevel <= 20) return 'A1學習者'; + if (userCEFRLevel === 'A1') return 'A1學習者'; if (difficulty < -10) return '簡單詞彙'; if (difficulty >= -10 && difficulty <= 10) return '適中詞彙'; return '困難詞彙'; @@ -684,29 +768,110 @@ export default function LearnPage() {
-
學習者程度
-
{currentCard?.userLevel || '--'}
+
學習者等級
+
{localStorage.getItem('userEnglishLevel') || 'A2'}
-
詞彙難度
-
{currentCard?.wordLevel || '--'}
+
詞彙等級
+
{currentCard?.difficultyLevel || 'A2'}
-
難度差異
+
等級差異
- {currentCard ? (currentCard.wordLevel || 50) - (currentCard.userLevel || 50) : '--'} + {currentCard ? (() => { + const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2'; + const wordCEFR = currentCard.difficultyLevel || 'A2'; + const diff = getCEFRToLevel(wordCEFR) - getCEFRToLevel(userCEFR); + return diff > 0 ? `+${diff}` : diff.toString(); + })() : '--'}
-
-
📚 演示說明
-
- • 點擊「下一張」可看到不同情境下系統自動選擇不同的複習題型
- • 卡片1-2: A1學習者 → 只會出現基礎題型 (翻卡、選擇、聽力)
- • 卡片3: 簡單詞彙 → 應用題型 (填空、重組)
- • 卡片4: 適中詞彙 → 全方位題型 (填空、重組、口說)
- • 卡片5: 困難詞彙 → 基礎題型 (翻卡、選擇) + {/* 當前選擇突出顯示 */} +
+
+
+ {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 ( + <> +
+ + 當前情境: {contextData?.icon} {context} + +
+ 可用題型: {contextData?.reviewTypes.join(' | ')} +
+
+ + ); + })()} +
+
+
系統已選擇
+
+ {getModeIcon(mode)} + {getModeLabel(mode)} +
+
+
+
+ + {/* 完整四情境對照表 */} +
+
📚 智能複習四情境對照表
+ +
+ + + + + + + + + + + + {currentCard && (() => { + const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2'; + const wordCEFR = currentCard.difficultyLevel || 'A2'; + const tableData = generateContextTable(userCEFR, wordCEFR); + + return tableData.map((row, index) => ( + + + + + + + + )); + })()} + +
情境類型建議複習方式學習目的判斷條件狀態
+ + {row.icon} {row.type} + + +
+ {row.reviewTypes.map((type, idx) => ( + {type} + ))} +
+
{row.purpose}{row.condition} + {row.isCurrent && ← 目前} +
+
+ +
+
🧠 智能適配說明:
+
系統根據您的CEFR等級和詞彙CEFR等級自動判斷學習情境,並智能選擇最適合的複習題型。
@@ -726,8 +891,8 @@ export default function LearnPage() { {/* System Auto-Selected Review Type Indicator */} {mode === 'flip-memory' ? ( @@ -1220,9 +1385,10 @@ export default function LearnPage() {

正確答案是:{currentCard.word}

-

- 發音:{currentCard.pronunciation} -

+
+ 發音:{currentCard.pronunciation} + +
)}
@@ -1669,6 +1835,54 @@ export default function LearnPage() { onBackToDashboard={() => router.push('/dashboard')} /> )} + + {/* No Due Cards Modal */} + {showNoDueCards && ( +
+
+
📚
+

+ 今日學習已完成! +

+

+ 目前沒有到期需要複習的詞卡。您可以: +

+ +
+
+
💡 建議行動
+
    +
  • • 前往詞卡管理頁面新增詞卡
  • +
  • • 查看學習統計和進度
  • +
  • • 調整學習目標和設定
  • +
+
+
+ +
+ + +
+ + +
+
+ )}