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

321 lines
10 KiB
TypeScript

// 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;
difficultyLevel: string;
createdAt: string;
updatedAt?: string;
// 新增圖片相關欄位
exampleImages: ExampleImage[];
hasExampleImage: boolean;
primaryImageUrl?: string;
}
export interface CreateFlashcardRequest {
word: string;
translation: string;
definition: string;
pronunciation: string;
partOfSpeech: string;
example: string;
exampleTranslation?: string;
difficultyLevel?: string; // A1, A2, B1, B2, C1, C2
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
class FlashcardsService {
private readonly baseURL = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api`;
private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const response = await fetch(`${this.baseURL}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Network error' }));
throw new Error(errorData.error || errorData.details || `HTTP ${response.status}`);
}
return response.json();
}
// 詞卡查詢方法 (支援進階篩選、排序和分頁)
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}` : ''}`;
return await this.makeRequest<ApiResponse<{ flashcards: Flashcard[], count: number }>>(endpoint);
} 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 {
const today = new Date().toISOString().split('T')[0];
const response = await this.makeRequest<{ success: boolean; data: any[]; count: number }>(`/flashcards/due?date=${today}&limit=${limit}`);
// 轉換後端格式為前端期望格式
const flashcards = response.data.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,
difficultyLevel: card.difficultyLevel || 'A2',
createdAt: card.createdAt,
updatedAt: card.updatedAt,
// 智能複習擴展欄位
userLevel: card.userLevel || 50,
wordLevel: card.wordLevel || 50,
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
}));
return {
success: true,
data: flashcards
};
} catch (error) {
console.error('API request failed, using fallback:', 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',
};
}
}
}
export const flashcardsService = new FlashcardsService();