394 lines
12 KiB
TypeScript
394 lines
12 KiB
TypeScript
import { create } from 'zustand'
|
|
import { subscribeWithSelector } from 'zustand/middleware'
|
|
import { getReviewTypesByCEFR } from '@/lib/utils/cefrUtils'
|
|
|
|
// 複習模式類型
|
|
export type ReviewMode = 'flip-memory' | 'vocab-choice' | 'vocab-listening' | 'sentence-listening' | 'sentence-fill' | 'sentence-reorder' | 'sentence-speaking'
|
|
|
|
// 測驗項目接口
|
|
export interface TestItem {
|
|
id: string
|
|
cardId: string
|
|
word: string
|
|
testType: ReviewMode
|
|
testName: string
|
|
isCompleted: boolean
|
|
isCurrent: boolean
|
|
order: number
|
|
// 新增狀態欄位
|
|
isSkipped: boolean
|
|
isIncorrect: boolean
|
|
priority: number
|
|
skippedAt?: number
|
|
lastAttemptAt?: number
|
|
}
|
|
|
|
// 測驗隊列狀態接口
|
|
interface TestQueueState {
|
|
// 測驗隊列狀態
|
|
testItems: TestItem[]
|
|
currentTestIndex: number
|
|
completedTests: number
|
|
totalTests: number
|
|
currentMode: ReviewMode
|
|
|
|
// 新增跳過隊列管理
|
|
skippedTests: Set<string>
|
|
priorityQueue: TestItem[]
|
|
|
|
// Actions
|
|
setTestItems: (items: TestItem[]) => void
|
|
setCurrentTestIndex: (index: number) => void
|
|
setCompletedTests: (completed: number) => void
|
|
setTotalTests: (total: number) => void
|
|
setCurrentMode: (mode: ReviewMode) => void
|
|
initializeTestQueue: (dueCards: any[], completedTests: any[]) => void
|
|
goToNextTest: () => void
|
|
skipCurrentTest: () => void
|
|
markTestCompleted: (testIndex: number) => void
|
|
markTestIncorrect: (testIndex: number) => void
|
|
resetQueue: () => void
|
|
|
|
// 新增智能隊列管理方法
|
|
reorderByPriority: () => void
|
|
getNextPriorityTest: () => TestItem | null
|
|
isAllTestsCompleted: () => boolean
|
|
getTestStats: () => {
|
|
total: number
|
|
completed: number
|
|
skipped: number
|
|
incorrect: number
|
|
remaining: number
|
|
}
|
|
}
|
|
|
|
// 工具函數
|
|
function getTestTypeName(testType: string): string {
|
|
const names = {
|
|
'flip-memory': '翻卡記憶',
|
|
'vocab-choice': '詞彙選擇',
|
|
'sentence-fill': '例句填空',
|
|
'sentence-reorder': '例句重組',
|
|
'vocab-listening': '詞彙聽力',
|
|
'sentence-listening': '例句聽力',
|
|
'sentence-speaking': '例句口說'
|
|
}
|
|
return names[testType as keyof typeof names] || testType
|
|
}
|
|
|
|
// 優先級演算法
|
|
function calculateTestPriority(test: TestItem): number {
|
|
const now = Date.now()
|
|
|
|
// 基礎優先級分數
|
|
let priority = 0
|
|
|
|
// 1. 未嘗試的測驗有最高優先級
|
|
if (!test.isCompleted && !test.isSkipped && !test.isIncorrect) {
|
|
priority = 100
|
|
}
|
|
// 2. 答錯的測驗需要重複練習
|
|
else if (test.isIncorrect) {
|
|
priority = 20
|
|
// 如果是最近答錯的,稍微降低優先級避免連續重複
|
|
if (test.lastAttemptAt && (now - test.lastAttemptAt) < 60000) { // 1分鐘內
|
|
priority = 15
|
|
}
|
|
}
|
|
// 3. 跳過的測驗排在最後
|
|
else if (test.isSkipped) {
|
|
priority = 10
|
|
// 跳過時間越久,優先級稍微提高
|
|
if (test.skippedAt) {
|
|
const timeSinceSkipped = now - test.skippedAt
|
|
const hours = timeSinceSkipped / (1000 * 60 * 60)
|
|
priority += Math.min(hours * 0.5, 5) // 最多增加5分
|
|
}
|
|
}
|
|
|
|
return priority
|
|
}
|
|
|
|
function reorderTestItems(testItems: TestItem[]): TestItem[] {
|
|
// 更新每個測驗的優先級
|
|
const itemsWithPriority = testItems.map(item => ({
|
|
...item,
|
|
priority: calculateTestPriority(item)
|
|
}))
|
|
|
|
// 按優先級排序
|
|
return itemsWithPriority.sort((a, b) => {
|
|
// 1. 優先級分數高的在前
|
|
if (b.priority !== a.priority) {
|
|
return b.priority - a.priority
|
|
}
|
|
|
|
// 2. 相同優先級時,按原始順序
|
|
return a.order - b.order
|
|
})
|
|
}
|
|
|
|
export const useTestQueueStore = create<TestQueueState>()(
|
|
subscribeWithSelector((set, get) => ({
|
|
// 初始狀態
|
|
testItems: [],
|
|
currentTestIndex: 0,
|
|
completedTests: 0,
|
|
totalTests: 0,
|
|
currentMode: 'flip-memory',
|
|
skippedTests: new Set<string>(),
|
|
priorityQueue: [],
|
|
|
|
// Actions
|
|
setTestItems: (items) => set({ testItems: items }),
|
|
|
|
setCurrentTestIndex: (index) => set({ currentTestIndex: index }),
|
|
|
|
setCompletedTests: (completed) => set({ completedTests: completed }),
|
|
|
|
setTotalTests: (total) => set({ totalTests: total }),
|
|
|
|
setCurrentMode: (mode) => set({ currentMode: mode }),
|
|
|
|
initializeTestQueue: (dueCards = [], completedTests = []) => {
|
|
const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2'
|
|
let remainingTestItems: TestItem[] = []
|
|
let order = 1
|
|
|
|
dueCards.forEach(card => {
|
|
const wordCEFRLevel = card.difficultyLevel || 'A2'
|
|
const allTestTypes = getReviewTypesByCEFR(userCEFRLevel, wordCEFRLevel)
|
|
|
|
const completedTestTypes = completedTests
|
|
.filter(ct => ct.flashcardId === card.id)
|
|
.map(ct => ct.testType)
|
|
|
|
const remainingTestTypes = allTestTypes.filter(testType =>
|
|
!completedTestTypes.includes(testType)
|
|
)
|
|
|
|
console.log(`🎯 詞卡 ${card.word}: 總共${allTestTypes.length}個測驗, 已完成${completedTestTypes.length}個, 剩餘${remainingTestTypes.length}個`)
|
|
|
|
remainingTestTypes.forEach(testType => {
|
|
remainingTestItems.push({
|
|
id: `${card.id}-${testType}`,
|
|
cardId: card.id,
|
|
word: card.word,
|
|
testType: testType as ReviewMode,
|
|
testName: getTestTypeName(testType),
|
|
isCompleted: false,
|
|
isCurrent: false,
|
|
order,
|
|
// 新增狀態欄位
|
|
isSkipped: false,
|
|
isIncorrect: false,
|
|
priority: 100 // 新測驗預設最高優先級
|
|
})
|
|
order++
|
|
})
|
|
})
|
|
|
|
if (remainingTestItems.length === 0) {
|
|
console.log('🎉 所有測驗都已完成!')
|
|
return
|
|
}
|
|
|
|
// 標記第一個測驗為當前
|
|
remainingTestItems[0].isCurrent = true
|
|
|
|
set({
|
|
testItems: remainingTestItems,
|
|
totalTests: remainingTestItems.length,
|
|
currentTestIndex: 0,
|
|
completedTests: 0,
|
|
currentMode: remainingTestItems[0].testType,
|
|
skippedTests: new Set<string>(),
|
|
priorityQueue: reorderTestItems(remainingTestItems)
|
|
})
|
|
|
|
console.log('📝 剩餘測驗項目:', remainingTestItems.length, '個')
|
|
},
|
|
|
|
goToNextTest: () => {
|
|
const { testItems, currentTestIndex } = get()
|
|
|
|
if (currentTestIndex + 1 < testItems.length) {
|
|
const nextIndex = currentTestIndex + 1
|
|
const updatedTestItems = testItems.map((item, index) => ({
|
|
...item,
|
|
isCurrent: index === nextIndex
|
|
}))
|
|
|
|
const nextTestItem = updatedTestItems[nextIndex]
|
|
|
|
set({
|
|
testItems: updatedTestItems,
|
|
currentTestIndex: nextIndex,
|
|
currentMode: nextTestItem.testType
|
|
})
|
|
|
|
console.log(`🔄 載入下一個測驗: ${nextTestItem.word} - ${nextTestItem.testType}`)
|
|
} else {
|
|
console.log('🎉 所有測驗完成!')
|
|
}
|
|
},
|
|
|
|
skipCurrentTest: () => {
|
|
const { testItems, currentTestIndex, skippedTests } = get()
|
|
const currentTest = testItems[currentTestIndex]
|
|
|
|
if (!currentTest) return
|
|
|
|
// 標記測驗為跳過狀態
|
|
const updatedTest = {
|
|
...currentTest,
|
|
isSkipped: true,
|
|
skippedAt: Date.now(),
|
|
isCurrent: false,
|
|
priority: calculateTestPriority({ ...currentTest, isSkipped: true })
|
|
}
|
|
|
|
// 更新跳過測驗集合
|
|
const newSkippedTests = new Set(skippedTests)
|
|
newSkippedTests.add(currentTest.id)
|
|
|
|
// 重新排序隊列
|
|
const updatedItems = testItems.map((item, index) =>
|
|
index === currentTestIndex ? updatedTest : item
|
|
)
|
|
const reorderedItems = reorderTestItems(updatedItems)
|
|
|
|
// 找到下一個高優先級測驗
|
|
const nextTestIndex = reorderedItems.findIndex(item =>
|
|
!item.isCompleted && item.id !== currentTest.id
|
|
)
|
|
|
|
// 標記新的當前測驗
|
|
if (nextTestIndex >= 0) {
|
|
reorderedItems[nextTestIndex].isCurrent = true
|
|
}
|
|
|
|
set({
|
|
testItems: reorderedItems,
|
|
currentTestIndex: Math.max(0, nextTestIndex),
|
|
skippedTests: newSkippedTests,
|
|
priorityQueue: reorderedItems,
|
|
currentMode: nextTestIndex >= 0 ? reorderedItems[nextTestIndex].testType : 'flip-memory'
|
|
})
|
|
|
|
console.log(`⏭️ 跳過測驗: ${currentTest.word} - ${currentTest.testType}`)
|
|
},
|
|
|
|
markTestCompleted: (testIndex) => {
|
|
const { testItems, skippedTests } = get()
|
|
const completedTest = testItems[testIndex]
|
|
|
|
const updatedTestItems = testItems.map((item, index) =>
|
|
index === testIndex
|
|
? { ...item, isCompleted: true, isCurrent: false, priority: 0 }
|
|
: item
|
|
)
|
|
|
|
// 從跳過列表中移除(如果存在)
|
|
const newSkippedTests = new Set(skippedTests)
|
|
if (completedTest) {
|
|
newSkippedTests.delete(completedTest.id)
|
|
}
|
|
|
|
set({
|
|
testItems: updatedTestItems,
|
|
completedTests: get().completedTests + 1,
|
|
skippedTests: newSkippedTests,
|
|
priorityQueue: reorderTestItems(updatedTestItems)
|
|
})
|
|
},
|
|
|
|
markTestIncorrect: (testIndex) => {
|
|
const { testItems } = get()
|
|
const incorrectTest = testItems[testIndex]
|
|
|
|
if (!incorrectTest) return
|
|
|
|
const updatedTest = {
|
|
...incorrectTest,
|
|
isIncorrect: true,
|
|
lastAttemptAt: Date.now(),
|
|
isCurrent: false,
|
|
priority: calculateTestPriority({ ...incorrectTest, isIncorrect: true })
|
|
}
|
|
|
|
const updatedItems = testItems.map((item, index) =>
|
|
index === testIndex ? updatedTest : item
|
|
)
|
|
const reorderedItems = reorderTestItems(updatedItems)
|
|
|
|
// 找到下一個測驗
|
|
const nextTestIndex = reorderedItems.findIndex(item =>
|
|
!item.isCompleted && item.id !== incorrectTest.id
|
|
)
|
|
|
|
if (nextTestIndex >= 0) {
|
|
reorderedItems[nextTestIndex].isCurrent = true
|
|
}
|
|
|
|
set({
|
|
testItems: reorderedItems,
|
|
currentTestIndex: Math.max(0, nextTestIndex),
|
|
priorityQueue: reorderedItems,
|
|
currentMode: nextTestIndex >= 0 ? reorderedItems[nextTestIndex].testType : 'flip-memory'
|
|
})
|
|
|
|
console.log(`❌ 測驗答錯: ${incorrectTest.word} - ${incorrectTest.testType}`)
|
|
},
|
|
|
|
resetQueue: () => set({
|
|
testItems: [],
|
|
currentTestIndex: 0,
|
|
completedTests: 0,
|
|
totalTests: 0,
|
|
currentMode: 'flip-memory',
|
|
skippedTests: new Set<string>(),
|
|
priorityQueue: []
|
|
}),
|
|
|
|
// 新增智能隊列管理方法
|
|
reorderByPriority: () => {
|
|
const { testItems } = get()
|
|
const reorderedItems = reorderTestItems(testItems)
|
|
|
|
// 找到當前應該顯示的測驗
|
|
const currentTestIndex = reorderedItems.findIndex(item => item.isCurrent)
|
|
const nextAvailableIndex = reorderedItems.findIndex(item => !item.isCompleted)
|
|
|
|
set({
|
|
testItems: reorderedItems,
|
|
priorityQueue: reorderedItems,
|
|
currentTestIndex: Math.max(0, currentTestIndex >= 0 ? currentTestIndex : nextAvailableIndex)
|
|
})
|
|
},
|
|
|
|
getNextPriorityTest: () => {
|
|
const { testItems } = get()
|
|
return testItems.find(item => !item.isCompleted && !item.isCurrent) || null
|
|
},
|
|
|
|
isAllTestsCompleted: () => {
|
|
const { testItems } = get()
|
|
return testItems.length > 0 && testItems.every(item => item.isCompleted)
|
|
},
|
|
|
|
getTestStats: () => {
|
|
const { testItems } = get()
|
|
|
|
const stats = {
|
|
total: testItems.length,
|
|
completed: testItems.filter(item => item.isCompleted).length,
|
|
skipped: testItems.filter(item => item.isSkipped && !item.isCompleted).length,
|
|
incorrect: testItems.filter(item => item.isIncorrect && !item.isCompleted).length,
|
|
remaining: testItems.filter(item => !item.isCompleted).length
|
|
}
|
|
|
|
return stats
|
|
}
|
|
}))
|
|
) |