dramaling-vocab-learning/frontend/lib/services/review/reviewService.ts

175 lines
5.3 KiB
TypeScript

import { flashcardsService } from '@/lib/services/flashcards'
import { ExtendedFlashcard } from '@/lib/types/review'
import { TestItem } from '@/store/review/useTestQueueStore'
import { isTestMode, getMockCompletedTests } from '@/lib/mock/reviewMockData'
// 複習會話服務
export class ReviewService {
// 數據轉換:將 Flashcard 轉換為 ExtendedFlashcard
static transformToExtendedFlashcard(flashcard: any): ExtendedFlashcard {
return {
...flashcard,
// 確保必填欄位有預設值
nextReviewDate: flashcard.nextReviewDate || new Date().toISOString(),
// 複習相關的額外欄位
currentInterval: flashcard.currentInterval || 0,
isOverdue: false,
overdueDays: 0,
baseMasteryLevel: flashcard.masteryLevel || 0,
lastReviewDate: flashcard.lastReviewDate || undefined,
// 內容擴展
synonyms: flashcard.synonyms || [],
exampleImage: flashcard.primaryImageUrl || undefined,
// 複習統計
reviewCount: flashcard.timesReviewed || 0,
successRate: 0
}
}
// 載入到期詞卡
static async loadDueCards(limit = 50): Promise<ExtendedFlashcard[]> {
try {
const result = await flashcardsService.getDueFlashcards(limit)
if (result.success && result.data) {
// 轉換為 ExtendedFlashcard
return result.data.map(this.transformToExtendedFlashcard)
} else {
throw new Error(result.error || '載入詞卡失敗')
}
} catch (error) {
console.error('載入到期詞卡失敗:', error)
throw error
}
}
// 載入已完成的測驗
static async loadCompletedTests(cardIds: string[]): Promise<any[]> {
try {
// 🧪 測試模式:使用 Mock 數據
if (isTestMode()) {
console.log('🧪 [測試模式] 使用 Mock 已完成測驗數據')
const mockTests = getMockCompletedTests()
// 模擬 API 延遲
await new Promise(resolve => setTimeout(resolve, 200))
console.log('✅ [測試模式] 載入Mock已完成測驗成功:', mockTests.length, '項測驗')
return mockTests
}
// 🌐 正常模式:使用後端 API
const result = await flashcardsService.getCompletedTests(cardIds)
if (result.success && result.data) {
return result.data
} else {
console.warn('載入已完成測驗失敗:', result.error)
return []
}
} catch (error) {
console.error('載入已完成測驗異常:', error)
return []
}
}
// 記錄測驗結果
static async recordTestResult(params: {
flashcardId: string
testType: string
isCorrect: boolean
userAnswer?: string
confidenceLevel?: number
responseTimeMs?: number
}): Promise<boolean> {
try {
const result = await flashcardsService.recordTestCompletion({
...params,
responseTimeMs: params.responseTimeMs || 2000
})
if (result.success) {
return true
} else {
console.error('記錄測驗結果失敗:', result.error)
return false
}
} catch (error) {
console.error('記錄測驗結果異常:', error)
return false
}
}
// 生成測驗選項
static async generateTestOptions(
cardId: string,
testType: string,
count = 4
): Promise<string[]> {
try {
// 這裡可以呼叫後端API生成選項
// 或者使用本地邏輯生成
// 暫時使用簡單的佔位符邏輯
return Array.from({ length: count }, (_, i) => `選項 ${i + 1}`)
} catch (error) {
console.error('生成測驗選項失敗:', error)
return []
}
}
// 驗證學習會話完整性
static validateSession(
cards: ExtendedFlashcard[],
testItems: TestItem[]
): { isValid: boolean; errors: string[] } {
const errors: string[] = []
// 檢查詞卡是否存在
if (!cards || cards.length === 0) {
errors.push('沒有可用的詞卡')
}
// 檢查測驗項目
if (!testItems || testItems.length === 0) {
errors.push('沒有可用的測驗項目')
}
// 檢查測驗項目和詞卡的一致性
if (cards && testItems) {
const cardIds = new Set(cards.map(c => c.id))
const testCardIds = new Set(testItems.map(t => t.cardId))
for (const testCardId of testCardIds) {
if (!cardIds.has(testCardId)) {
errors.push(`測驗項目引用了不存在的詞卡: ${testCardId}`)
}
}
}
return {
isValid: errors.length === 0,
errors
}
}
// 計算學習統計
static calculateStats(testItems: TestItem[], score: { correct: number; total: number }) {
const completed = testItems.filter(item => item.isCompleted).length
const total = testItems.length
const progressPercentage = total > 0 ? (completed / total) * 100 : 0
const accuracyPercentage = score.total > 0 ? (score.correct / score.total) * 100 : 0
return {
completed,
total,
remaining: total - completed,
progressPercentage: Math.round(progressPercentage),
accuracyPercentage: Math.round(accuracyPercentage),
estimatedTimeRemaining: Math.max(0, (total - completed) * 30) // 假設每個測驗30秒
}
}
}