// 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 { 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(endpoint: string, options: RequestInit = {}): Promise { const response = await flashcardsApiClient.request(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> { 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(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> { try { return await this.makeRequest>('/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> { try { return await this.makeRequest>(`/flashcards/${id}`, { method: 'DELETE', }); } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to delete flashcard', }; } } async getFlashcard(id: string): Promise> { try { return await this.makeRequest>(`/flashcards/${id}`); } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to get flashcard', }; } } async updateFlashcard(id: string, data: CreateFlashcardRequest): Promise> { try { return await this.makeRequest>(`/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> { try { return await this.makeRequest>(`/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> { 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> { try { return await this.makeRequest>('/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> { 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> { 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> { try { return await this.makeRequest>(`/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> { 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();