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

407 lines
13 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;
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 token = localStorage.getItem('auth_token');
const response = await fetch(`${this.baseURL}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...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 {
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);
console.log('📊 response.data類型:', typeof response.data, '長度:', response.data?.length);
if (!response.data || !Array.isArray(response.data)) {
console.log('❌ response.data不是數組:', response.data);
return {
success: false,
error: 'Invalid response data format',
};
}
// 轉換後端格式為前端期望格式
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,
// 智能複習擴展欄位 (數值欄位已移除改用即時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
}));
console.log('✅ 數據轉換完成:', flashcards.length, '張詞卡');
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',
};
}
}
/**
* 獲取已完成的測驗記錄
*/
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(`/study/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'
};
}
}
/**
* 記錄測驗完成狀態 (立即保存到StudyRecord表)
*/
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('/study/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();