dramaling-app/apps/web/src/stores/review.ts

349 lines
11 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.

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type {
VocabularyReviewData,
ReviewSession,
ReviewResponse,
WeaknessPattern
} from '@/utils/spacedRepetition'
import {
SpacedRepetitionAlgorithm,
createDefaultVocabularyReviewData
} from '@/utils/spacedRepetition'
export interface LearningPlan {
date: string
vocabulary: VocabularyReviewData[]
totalCount: number
estimatedTime: number // 分鐘
}
export interface ReviewStats {
todayCompleted: number
todayTotal: number
weeklyStreak: number
totalMastered: number
averageAccuracy: number
improvementTrend: number
nextReviewTime: Date | null
}
export const useReviewStore = defineStore('review', () => {
// 狀態
const vocabularyReviewData = ref<Map<string, VocabularyReviewData>>(new Map())
const reviewHistory = ref<ReviewSession[]>([])
const currentReviewSession = ref<ReviewSession | null>(null)
const learningPlan = ref<Map<string, LearningPlan>>(new Map())
const isLoading = ref(false)
const algorithm = new SpacedRepetitionAlgorithm()
// 計算屬性
const todaysReviewVocabulary = computed(() => {
const allVocabulary = Array.from(vocabularyReviewData.value.values())
return SpacedRepetitionAlgorithm.getTodaysReviewVocabulary(allVocabulary)
})
const reviewStats = computed((): ReviewStats => {
const todayTotal = todaysReviewVocabulary.value.length
const todayCompleted = reviewHistory.value.filter(session => {
const today = new Date()
today.setHours(0, 0, 0, 0)
const sessionDate = new Date(session.startTime)
sessionDate.setHours(0, 0, 0, 0)
return sessionDate.getTime() === today.getTime()
}).length
const allVocabulary = Array.from(vocabularyReviewData.value.values())
const totalMastered = allVocabulary.filter(v => v.masteryLevel >= 80).length
const efficiency = SpacedRepetitionAlgorithm.analyzeLearningEfficiency(reviewHistory.value)
// 計算連續學習天數
const weeklyStreak = calculateWeeklyStreak()
// 下次複習時間
const nextReviewTime = getNextReviewTime()
return {
todayCompleted,
todayTotal,
weeklyStreak,
totalMastered,
averageAccuracy: efficiency.averageAccuracy,
improvementTrend: efficiency.improvementTrend,
nextReviewTime
}
})
const urgentReviewVocabulary = computed(() => {
const today = new Date()
return todaysReviewVocabulary.value.filter(vocab => {
const overdueDays = Math.floor((today.getTime() - vocab.nextReviewDate.getTime()) / (24 * 60 * 60 * 1000))
return overdueDays > 2 // 過期超過2天視為緊急
})
})
const weaknessAnalysis = computed(() => {
const allPatterns = new Map<string, { severity: number, frequency: number }>()
Array.from(vocabularyReviewData.value.values()).forEach(vocab => {
vocab.weaknessPatterns.forEach(pattern => {
const existing = allPatterns.get(pattern.type) || { severity: 0, frequency: 0 }
allPatterns.set(pattern.type, {
severity: Math.max(existing.severity, pattern.severity),
frequency: existing.frequency + pattern.frequency
})
})
})
return Array.from(allPatterns.entries())
.map(([type, data]) => ({
type,
severity: data.severity,
frequency: data.frequency,
score: data.severity * Math.log(data.frequency + 1)
}))
.sort((a, b) => b.score - a.score)
.slice(0, 5) // 前5個最嚴重的薄弱點
})
// 方法
const initializeVocabularyReviewData = (vocabularyIds: string[]) => {
vocabularyIds.forEach(id => {
if (!vocabularyReviewData.value.has(id)) {
vocabularyReviewData.value.set(id, createDefaultVocabularyReviewData(id))
}
})
}
const startReviewSession = (vocabularyIds: string[]): string => {
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
currentReviewSession.value = {
vocabularyId: vocabularyIds[0], // 如果是批量複習,這裡需要調整
startTime: new Date(),
responses: [],
overallAccuracy: 0,
averageResponseTime: 0
}
return sessionId
}
const addReviewResponse = (response: Omit<ReviewResponse, 'timestamp'>) => {
if (!currentReviewSession.value) {
throw new Error('沒有活躍的複習會話')
}
const fullResponse: ReviewResponse = {
...response,
timestamp: new Date()
}
currentReviewSession.value.responses.push(fullResponse)
// 更新會話統計
updateSessionStats()
}
const completeReviewSession = () => {
if (!currentReviewSession.value) {
throw new Error('沒有活躍的複習會話')
}
currentReviewSession.value.endTime = new Date()
// 更新複習數據
const reviewData = vocabularyReviewData.value.get(currentReviewSession.value.vocabularyId)
if (reviewData) {
const updatedData = algorithm.calculateNextReview(reviewData, currentReviewSession.value)
vocabularyReviewData.value.set(reviewData.id, updatedData)
}
// 保存到歷史記錄
reviewHistory.value.push({ ...currentReviewSession.value })
// 清空當前會話
currentReviewSession.value = null
// 重新生成學習計劃
generateLearningPlan()
}
const updateSessionStats = () => {
if (!currentReviewSession.value) return
const responses = currentReviewSession.value.responses
const correctCount = responses.filter(r => r.isCorrect).length
const totalResponseTime = responses.reduce((sum, r) => sum + r.responseTime, 0)
currentReviewSession.value.overallAccuracy = responses.length > 0 ? correctCount / responses.length : 0
currentReviewSession.value.averageResponseTime = responses.length > 0 ? totalResponseTime / responses.length : 0
}
const generateLearningPlan = (daysAhead: number = 7) => {
const allVocabulary = Array.from(vocabularyReviewData.value.values())
const planMap = SpacedRepetitionAlgorithm.generateLearningPlan(allVocabulary, daysAhead)
learningPlan.value.clear()
planMap.forEach((vocabularyList, date) => {
const estimatedTime = vocabularyList.length * 2 // 每個詞彙平均2分鐘
learningPlan.value.set(date, {
date,
vocabulary: vocabularyList,
totalCount: vocabularyList.length,
estimatedTime
})
})
}
const calculateWeeklyStreak = (): number => {
if (reviewHistory.value.length === 0) return 0
const today = new Date()
let streak = 0
// 從今天開始往前檢查
for (let i = 0; i < 7; i++) {
const checkDate = new Date(today)
checkDate.setDate(today.getDate() - i)
checkDate.setHours(0, 0, 0, 0)
const nextDay = new Date(checkDate)
nextDay.setDate(checkDate.getDate() + 1)
const hasReviewOnDate = reviewHistory.value.some(session => {
const sessionDate = new Date(session.startTime)
return sessionDate >= checkDate && sessionDate < nextDay
})
if (hasReviewOnDate) {
streak++
} else if (i > 0) { // 今天沒複習不算打斷,其他天沒複習就算打斷
break
}
}
return streak
}
const getNextReviewTime = (): Date | null => {
const allVocabulary = Array.from(vocabularyReviewData.value.values())
if (allVocabulary.length === 0) return null
const nextReviews = allVocabulary
.filter(v => v.nextReviewDate > new Date())
.sort((a, b) => a.nextReviewDate.getTime() - b.nextReviewDate.getTime())
return nextReviews.length > 0 ? nextReviews[0].nextReviewDate : null
}
const getVocabularyReviewData = (vocabularyId: string): VocabularyReviewData | null => {
return vocabularyReviewData.value.get(vocabularyId) || null
}
const updateVocabularyReviewData = (data: VocabularyReviewData) => {
vocabularyReviewData.value.set(data.id, data)
}
const resetVocabularyProgress = (vocabularyId: string) => {
const defaultData = createDefaultVocabularyReviewData(vocabularyId)
vocabularyReviewData.value.set(vocabularyId, defaultData)
}
const getPersonalizedRecommendations = (): string[] => {
const recommendations: string[] = []
const stats = reviewStats.value
// 基於統計數據生成建議
if (stats.averageAccuracy < 0.7) {
recommendations.push('建議放慢學習節奏,專注於理解而不是數量')
}
if (stats.weeklyStreak === 0) {
recommendations.push('建立每日複習習慣即使只複習5個詞彙也有幫助')
}
if (urgentReviewVocabulary.value.length > 10) {
recommendations.push('有較多詞彙需要緊急複習,建議優先處理過期詞彙')
}
if (stats.improvementTrend < 0) {
recommendations.push('學習效果有下降趨勢,建議調整學習策略或休息一下')
}
// 基於薄弱點生成建議
const topWeakness = weaknessAnalysis.value[0]
if (topWeakness) {
const weaknessRecommendations = {
spelling: '建議加強拼寫練習,可以嘗試手寫練習',
meaning: '建議多做詞義辨析練習,建立詞彙語義網絡',
pronunciation: '建議多聽音頻,模仿正確發音',
usage: '建議多閱讀例句,理解詞彙在不同語境中的用法',
grammar: '建議複習相關語法規則,理解詞彙的語法功能'
}
recommendations.push(weaknessRecommendations[topWeakness.type as keyof typeof weaknessRecommendations])
}
return recommendations.slice(0, 3) // 最多返回3個建議
}
const exportReviewData = () => {
return {
vocabularyReviewData: Object.fromEntries(vocabularyReviewData.value),
reviewHistory: reviewHistory.value,
exportDate: new Date().toISOString()
}
}
const importReviewData = (data: any) => {
if (data.vocabularyReviewData) {
vocabularyReviewData.value = new Map(Object.entries(data.vocabularyReviewData))
}
if (data.reviewHistory) {
reviewHistory.value = data.reviewHistory.map((session: any) => ({
...session,
startTime: new Date(session.startTime),
endTime: session.endTime ? new Date(session.endTime) : undefined,
responses: session.responses.map((response: any) => ({
...response,
timestamp: new Date(response.timestamp)
}))
}))
}
generateLearningPlan()
}
// 初始化
generateLearningPlan()
return {
// 狀態
vocabularyReviewData,
reviewHistory,
currentReviewSession,
learningPlan,
isLoading,
// 計算屬性
todaysReviewVocabulary,
reviewStats,
urgentReviewVocabulary,
weaknessAnalysis,
// 方法
initializeVocabularyReviewData,
startReviewSession,
addReviewResponse,
completeReviewSession,
generateLearningPlan,
getVocabularyReviewData,
updateVocabularyReviewData,
resetVocabularyProgress,
getPersonalizedRecommendations,
exportReviewData,
importReviewData
}
})