dramaling-vocab-learning/frontend/lib/services/flashcards.ts

481 lines
15 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.

// Flashcards API service
export interface ExampleImage {
id: string;
imageUrl: string;
isPrimary: boolean;
qualityScore?: number;
fileSize?: number;
createdAt: string;
}
export interface Flashcard {
id: string;
word: string;
translation: string;
definition: string;
partOfSpeech: string;
pronunciation: string;
example: string;
exampleTranslation?: string;
masteryLevel: number;
timesReviewed: number;
isFavorite: boolean;
nextReviewDate: string;
cefr: string;
createdAt: string;
updatedAt?: string;
// 新增圖片相關欄位
exampleImages: ExampleImage[];
hasExampleImage: boolean;
primaryImageUrl?: string;
// 測驗選項 (後端提供的混淆選項)
quizOptions?: string[];
}
export interface CreateFlashcardRequest {
word: string;
translation: string;
definition: string;
pronunciation: string;
partOfSpeech: string;
example: string;
exampleTranslation?: string;
cefr?: string; // A1, A2, B1, B2, C1, C2
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
import { flashcardsApiClient } from '@/lib/api/client'
import { getUserFriendlyErrorMessage } from '@/lib/api/errorHandler'
class FlashcardsService {
/**
* 統一的API請求處理 - 保持現有介面相容性
*/
private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const response = await flashcardsApiClient.request<T>(endpoint, {
...options,
context: 'FlashcardsService'
})
if (!response.success) {
// 統一錯誤處理但拋出Error以保持現有介面相容
const friendlyMessage = response.error ? getUserFriendlyErrorMessage(response.error) : '操作失敗'
throw new Error(friendlyMessage)
}
return response.data as T
}
// 詞卡查詢方法 (支援進階篩選、排序和分頁)
async getFlashcards(
search?: string,
favoritesOnly: boolean = false,
cefrLevel?: string,
partOfSpeech?: string,
masteryLevel?: string,
sortBy?: string,
sortOrder?: 'asc' | 'desc',
page?: number,
limit?: number
): Promise<ApiResponse<{ flashcards: Flashcard[], count: number }>> {
try {
const params = new URLSearchParams();
if (search) params.append('search', search);
if (favoritesOnly) params.append('favoritesOnly', 'true');
if (cefrLevel) params.append('cefrLevel', cefrLevel);
if (partOfSpeech) params.append('partOfSpeech', partOfSpeech);
if (masteryLevel) params.append('masteryLevel', masteryLevel);
// 排序和分頁參數
if (sortBy) params.append('sortBy', sortBy);
if (sortOrder) params.append('sortOrder', sortOrder);
if (page) params.append('page', page.toString());
if (limit) params.append('limit', limit.toString());
const queryString = params.toString();
const endpoint = `/flashcards${queryString ? `?${queryString}` : ''}`;
const response = await this.makeRequest<any>(endpoint);
// 轉換後端格式為前端標準 Flashcard 格式
if (response.success && response.data && response.data.flashcards) {
const flashcards = response.data.flashcards.map((card: any): Flashcard => ({
id: card.id,
word: card.word,
translation: card.translation,
definition: card.definition || '',
partOfSpeech: card.partOfSpeech || 'noun',
pronunciation: card.pronunciation || '',
example: card.example || '',
exampleTranslation: card.exampleTranslation,
masteryLevel: card.masteryLevel || 0,
timesReviewed: card.timesReviewed || 0,
isFavorite: card.isFavorite || false,
nextReviewDate: card.nextReviewDate || new Date().toISOString(),
cefr: card.cefr || 'A1', // 使用後端提供的 CEFR 字串
createdAt: card.createdAt,
updatedAt: card.updatedAt,
exampleImages: card.exampleImages || [],
hasExampleImage: card.hasExampleImage || false,
primaryImageUrl: card.primaryImageUrl
}));
return {
success: true,
data: {
flashcards,
count: response.data.count || flashcards.length
}
};
}
return response;
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch flashcards',
};
}
}
async createFlashcard(data: CreateFlashcardRequest): Promise<ApiResponse<Flashcard>> {
try {
return await this.makeRequest<ApiResponse<Flashcard>>('/flashcards', {
method: 'POST',
body: JSON.stringify(data),
});
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create flashcard',
};
}
}
async deleteFlashcard(id: string): Promise<ApiResponse<void>> {
try {
return await this.makeRequest<ApiResponse<void>>(`/flashcards/${id}`, {
method: 'DELETE',
});
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete flashcard',
};
}
}
async getFlashcard(id: string): Promise<ApiResponse<Flashcard>> {
try {
return await this.makeRequest<ApiResponse<Flashcard>>(`/flashcards/${id}`);
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get flashcard',
};
}
}
async updateFlashcard(id: string, data: CreateFlashcardRequest): Promise<ApiResponse<Flashcard>> {
try {
return await this.makeRequest<ApiResponse<Flashcard>>(`/flashcards/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update flashcard',
};
}
}
async toggleFavorite(id: string): Promise<ApiResponse<void>> {
try {
return await this.makeRequest<ApiResponse<void>>(`/flashcards/${id}/favorite`, {
method: 'POST',
});
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to toggle favorite',
};
}
}
// =====================================================
// 智能複習系統相關方法
// =====================================================
async getDueFlashcards(limit = 50): Promise<ApiResponse<Flashcard[]>> {
try {
console.log('🚀 API調用開始:', `/flashcards/due?limit=${limit}`);
const response = await this.makeRequest<{ success: boolean; data: any; count: number }>(`/flashcards/due?limit=${limit}`);
console.log('🔍 makeRequest回應:', response);
// 處理新的後端資料結構:{ flashcards: [...], count: number, metadata: {...} }
const flashcardsArray = response?.data?.flashcards || response?.data || [];
console.log('📊 flashcards資料:', typeof flashcardsArray, '長度:', flashcardsArray?.length);
if (!Array.isArray(flashcardsArray)) {
console.log('❌ flashcards不是數組:', flashcardsArray);
return {
success: false,
error: 'Invalid response data format',
};
}
// 轉換後端格式為前端期望格式
const flashcards = flashcardsArray.map((card: any) => ({
id: card.id,
word: card.word,
translation: card.translation,
definition: card.definition,
partOfSpeech: card.partOfSpeech,
pronunciation: card.pronunciation,
example: card.example,
exampleTranslation: card.exampleTranslation,
masteryLevel: card.masteryLevel || card.currentMasteryLevel || 0,
timesReviewed: card.timesReviewed || 0,
isFavorite: card.isFavorite || false,
nextReviewDate: card.nextReviewDate,
cefr: card.cefr || 'A2',
createdAt: card.createdAt,
updatedAt: card.updatedAt,
// 智能複習擴展欄位 (數值欄位已移除改用即時CEFR轉換)
baseMasteryLevel: card.baseMasteryLevel || card.masteryLevel || 0,
lastReviewDate: card.lastReviewDate || card.lastReviewedAt,
currentInterval: card.currentInterval || card.intervalDays || 1,
isOverdue: card.isOverdue || false,
overdueDays: card.overdueDays || 0,
// 圖片相關欄位
exampleImages: card.exampleImages || [],
hasExampleImage: card.hasExampleImage || false,
primaryImageUrl: card.primaryImageUrl,
// 測驗選項(新增:來自後端的 AI 生成混淆選項)
quizOptions: card.quizOptions || []
}));
console.log('✅ 數據轉換完成:', flashcards.length, '張詞卡');
console.log('🎯 首張詞卡的quizOptions:', flashcards[0]?.quizOptions);
return {
success: true,
data: flashcards
};
} catch (error) {
console.error('💥 API request failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get due flashcards',
};
}
}
async getNextReviewCard(): Promise<ApiResponse<Flashcard & { userLevel: number; wordLevel: number }>> {
try {
return await this.makeRequest<ApiResponse<Flashcard & { userLevel: number; wordLevel: number }>>('/flashcards/next-review');
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get next review card',
};
}
}
async getOptimalReviewMode(cardId: string, userCEFRLevel: string, wordCEFRLevel: string): Promise<ApiResponse<{ selectedMode: string }>> {
try {
const response = await this.makeRequest<{ success: boolean; data: any }>(`/flashcards/${cardId}/optimal-review-mode`, {
method: 'POST',
body: JSON.stringify({
userCEFRLevel,
wordCEFRLevel,
includeHistory: true
}),
});
return {
success: response.success,
data: {
selectedMode: response.data.selectedMode
}
};
} catch (error) {
console.error('Optimal review mode API failed, using fallback:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get optimal review mode',
};
}
}
async submitReview(id: string, reviewData: {
isCorrect: boolean;
confidenceLevel?: number;
questionType: string;
userAnswer?: string;
timeTaken?: number;
}): Promise<ApiResponse<{ newInterval: number; nextReviewDate: string; masteryLevel: number }>> {
try {
const response = await this.makeRequest<{ success: boolean; data: any }>(`/flashcards/${id}/review`, {
method: 'POST',
body: JSON.stringify({
...reviewData,
timestamp: Date.now()
}),
});
return {
success: response.success,
data: {
newInterval: response.data.newInterval || response.data.newIntervalDays || 1,
nextReviewDate: response.data.nextReviewDate,
masteryLevel: response.data.masteryLevel || response.data.newMasteryLevel || 0
}
};
} catch (error) {
console.error('Submit review API failed, using fallback:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to submit review',
};
}
}
async generateQuestionOptions(cardId: string, questionType: string): Promise<ApiResponse<{
options?: string[];
correctAnswer: string;
audioUrl?: string;
sentence?: string;
blankedSentence?: string;
scrambledWords?: string[];
}>> {
try {
return await this.makeRequest<ApiResponse<any>>(`/flashcards/${cardId}/question`, {
method: 'POST',
body: JSON.stringify({ questionType }),
});
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to generate question options',
};
}
}
/**
* 標記詞彙為已掌握簡化版API
*/
async markWordMastered(id: string): Promise<ApiResponse<{ nextReviewDate: string; intervalDays: number; successCount: number }>> {
try {
console.log('🎯 標記詞彙為已掌握:', id)
const response = await this.makeRequest<{ success: boolean; data: any }>(`/flashcards/${id}/mastered`, {
method: 'POST',
});
if (response.success && response.data) {
return {
success: true,
data: {
nextReviewDate: response.data.nextReviewDate,
intervalDays: response.data.intervalDays,
successCount: response.data.successCount
}
};
} else {
throw new Error('API 回應格式錯誤');
}
} catch (error) {
console.error('❌ 標記詞彙掌握失敗:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to mark word as mastered',
};
}
}
/**
* 獲取已完成的測驗記錄
*/
async getCompletedTests(cardIds?: string[]): Promise<{
success: boolean;
data: Array<{
flashcardId: string;
testType: string;
isCorrect: boolean;
completedAt: string;
userAnswer?: string;
}> | null;
error?: string;
}> {
try {
const params = cardIds && cardIds.length > 0 ? `?cardIds=${cardIds.join(',')}` : '';
const result = await this.makeRequest(`/review/completed-tests${params}`);
return {
success: true,
data: (result as any).data || [],
error: undefined
};
} catch (error) {
console.warn('Failed to get completed tests:', error);
return {
success: false,
data: [],
error: error instanceof Error ? error.message : 'Failed to get completed tests'
};
}
}
/**
* 記錄測驗完成狀態 (立即保存到ReviewRecord表)
*/
async recordTestCompletion(request: {
flashcardId: string;
testType: string;
isCorrect: boolean;
userAnswer?: string;
confidenceLevel?: number;
responseTimeMs?: number;
}): Promise<{ success: boolean; data: any | null; error?: string }> {
try {
const result = await this.makeRequest('/review/record-test', {
method: 'POST',
body: JSON.stringify({
flashcardId: request.flashcardId,
testType: request.testType,
isCorrect: request.isCorrect,
userAnswer: request.userAnswer,
confidenceLevel: request.confidenceLevel,
responseTimeMs: request.responseTimeMs || 2000
})
});
return {
success: true,
data: (result as any).data || result,
error: undefined
};
} catch (error) {
console.warn('Failed to record test completion:', error);
return {
success: false,
data: null,
error: error instanceof Error ? error.message : 'Failed to record test completion'
};
}
}
}
export const flashcardsService = new FlashcardsService();