dramaling-vocab-learning/frontend/hooks/review/useReviewSession.ts

222 lines
6.3 KiB
TypeScript

import { useReducer, useEffect, useMemo } from 'react'
import {
INITIAL_TEST_ITEMS,
TestItem,
CardState,
sortTestItemsByPriority,
generateVocabOptions,
SIMPLE_CARDS
} from '@/lib/data/reviewSimpleData'
interface ReviewState {
testItems: TestItem[]
score: { correct: number; total: number }
isComplete: boolean
}
type ReviewAction =
| { type: 'LOAD_PROGRESS'; payload: ReviewState }
| { type: 'ANSWER_TEST_ITEM'; payload: { testItemId: string; confidence: number } }
| { type: 'SKIP_TEST_ITEM'; payload: { testItemId: string } }
| { type: 'RESTART' }
// 內部測驗項目更新函數
const updateTestItem = (
testItems: TestItem[],
testItemId: string,
updates: Partial<TestItem>
): TestItem[] => {
return testItems.map((item) =>
item.id === testItemId
? { ...item, ...updates }
: item
)
}
const reviewReducer = (state: ReviewState, action: ReviewAction): ReviewState => {
switch (action.type) {
case 'LOAD_PROGRESS':
return action.payload
case 'ANSWER_TEST_ITEM': {
const { testItemId, confidence } = action.payload
const isCorrect = confidence >= 1 // 修正:一般(1分)以上都算答對
const testItem = state.testItems.find(item => item.id === testItemId)
if (!testItem) return state
// 修正:只有答對才標記為完成,答錯只增加錯誤次數
const updatedTestItems = updateTestItem(state.testItems, testItemId,
isCorrect
? { isCompleted: true } // 答對:標記完成
: { wrongCount: testItem.wrongCount + 1 } // 答錯:只增加錯誤次數,不完成
)
const newScore = {
correct: state.score.correct + (isCorrect ? 1 : 0),
total: state.score.total + 1
}
const remainingTestItems = updatedTestItems.filter(item => !item.isCompleted)
const isComplete = remainingTestItems.length === 0
return {
testItems: updatedTestItems,
score: newScore,
isComplete
}
}
case 'SKIP_TEST_ITEM': {
const { testItemId } = action.payload
const testItem = state.testItems.find(item => item.id === testItemId)
if (!testItem) return state
const updatedTestItems = updateTestItem(state.testItems, testItemId, {
skipCount: testItem.skipCount + 1
})
const remainingTestItems = updatedTestItems.filter(item => !item.isCompleted)
const isComplete = remainingTestItems.length === 0
return {
testItems: updatedTestItems,
score: state.score,
isComplete
}
}
case 'RESTART':
return {
testItems: INITIAL_TEST_ITEMS,
score: { correct: 0, total: 0 },
isComplete: false
}
default:
return state
}
}
export function useReviewSession() {
// 使用 useReducer 統一狀態管理
const [state, dispatch] = useReducer(reviewReducer, {
testItems: INITIAL_TEST_ITEMS,
score: { correct: 0, total: 0 },
isComplete: false
})
const { testItems, score, isComplete } = state
// 智能排序獲取當前測驗項目 - 使用 useMemo 優化性能
const sortedTestItems = useMemo(() => sortTestItemsByPriority(testItems), [testItems])
const incompleteTestItems = useMemo(() =>
sortedTestItems.filter((item: TestItem) => !item.isCompleted),
[sortedTestItems]
)
const currentTestItem = incompleteTestItems[0] // 總是選擇優先級最高的未完成測驗項目
const currentCard = currentTestItem?.cardData // 當前詞卡數據
// localStorage進度保存和載入
useEffect(() => {
// 載入保存的進度
const savedProgress = localStorage.getItem('review-linear-progress')
if (savedProgress) {
try {
const parsed = JSON.parse(savedProgress)
const saveTime = new Date(parsed.timestamp)
const now = new Date()
const isToday = saveTime.toDateString() === now.toDateString()
if (isToday && parsed.testItems) {
dispatch({
type: 'LOAD_PROGRESS',
payload: {
testItems: parsed.testItems,
score: parsed.score || { correct: 0, total: 0 },
isComplete: parsed.isComplete || false
}
})
console.log('📖 載入保存的線性複習進度')
}
} catch (error) {
console.warn('進度載入失敗:', error)
localStorage.removeItem('review-linear-progress')
}
}
}, [])
// 保存進度到localStorage
const saveProgress = () => {
const progress = {
testItems,
score,
isComplete,
timestamp: new Date().toISOString()
}
localStorage.setItem('review-linear-progress', JSON.stringify(progress))
console.log('💾 線性進度已保存')
}
// 處理測驗項目答題
const handleAnswer = (confidence: number) => {
if (!currentTestItem) return
dispatch({
type: 'ANSWER_TEST_ITEM',
payload: { testItemId: currentTestItem.id, confidence }
})
// 保存進度
setTimeout(() => saveProgress(), 100)
}
// 處理測驗項目跳過
const handleSkip = () => {
if (!currentTestItem) return
dispatch({
type: 'SKIP_TEST_ITEM',
payload: { testItemId: currentTestItem.id }
})
// 保存進度
setTimeout(() => saveProgress(), 100)
}
// 重新開始 - 重置所有狀態
const handleRestart = () => {
dispatch({ type: 'RESTART' })
localStorage.removeItem('review-linear-progress')
console.log('🔄 線性複習進度已重置')
}
// 生成詞彙選擇選項 (僅當當前是詞彙選擇測驗時)
const vocabOptions = useMemo(() => {
if (currentTestItem?.testType === 'vocab-choice' && currentCard) {
return generateVocabOptions(currentCard.word, SIMPLE_CARDS)
}
return []
}, [currentTestItem, currentCard])
return {
// 狀態
testItems,
score,
isComplete,
currentTestItem,
currentCard,
vocabOptions,
sortedTestItems,
// 計算屬性
totalTestItems: testItems.length,
completedTestItems: testItems.filter(item => item.isCompleted).length,
// 動作
handleAnswer,
handleSkip,
handleRestart
}
}