266 lines
8.1 KiB
TypeScript
266 lines
8.1 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];
|
|
return await this.makeRequest<ApiResponse<Flashcard[]>>(`/flashcards/due?date=${today}&limit=${limit}`);
|
|
} catch (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, userLevel: number, wordLevel: number): Promise<ApiResponse<{ selectedMode: string }>> {
|
|
try {
|
|
return await this.makeRequest<ApiResponse<{ selectedMode: string }>>(`/flashcards/${cardId}/optimal-review-mode`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
userLevel,
|
|
wordLevel,
|
|
includeHistory: true
|
|
}),
|
|
});
|
|
} catch (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 {
|
|
return await this.makeRequest<ApiResponse<{ newInterval: number; nextReviewDate: string; masteryLevel: number }>>(`/flashcards/${id}/review`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
...reviewData,
|
|
timestamp: Date.now()
|
|
}),
|
|
});
|
|
} catch (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(); |