diff --git a/backend/DramaLing.Api/Controllers/AIController.cs b/backend/DramaLing.Api/Controllers/AIController.cs index 11959be..ac02770 100644 --- a/backend/DramaLing.Api/Controllers/AIController.cs +++ b/backend/DramaLing.Api/Controllers/AIController.cs @@ -29,6 +29,88 @@ public class AIController : ControllerBase _logger = logger; } + /// + /// AI 生成詞卡測試端點 (開發用,無需認證) + /// + [HttpPost("test/generate")] + [AllowAnonymous] + public async Task TestGenerateCards([FromBody] GenerateCardsRequest request) + { + try + { + // 基本驗證 + if (string.IsNullOrWhiteSpace(request.InputText)) + { + return BadRequest(new { Success = false, Error = "Input text is required" }); + } + + if (request.InputText.Length > 5000) + { + return BadRequest(new { Success = false, Error = "Input text must be less than 5000 characters" }); + } + + if (!new[] { "vocabulary", "smart" }.Contains(request.ExtractionType)) + { + return BadRequest(new { Success = false, Error = "Invalid extraction type" }); + } + + if (request.CardCount < 1 || request.CardCount > 20) + { + return BadRequest(new { Success = false, Error = "Card count must be between 1 and 20" }); + } + + // 測試模式:直接使用模擬資料 + try + { + var generatedCards = await _geminiService.GenerateCardsAsync( + request.InputText, + request.ExtractionType, + request.CardCount); + + return Ok(new + { + Success = true, + Data = new + { + TaskId = Guid.NewGuid(), + Status = "completed", + GeneratedCards = generatedCards + }, + Message = $"Successfully generated {generatedCards.Count} cards" + }); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("API key")) + { + _logger.LogWarning("Gemini API key not configured, using mock data"); + + // 返回模擬資料 + var mockCards = GenerateMockCards(request.CardCount); + return Ok(new + { + Success = true, + Data = new + { + TaskId = Guid.NewGuid(), + Status = "completed", + GeneratedCards = mockCards + }, + Message = $"Generated {mockCards.Count} mock cards (Test mode)" + }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in AI card generation test"); + return StatusCode(500, new + { + Success = false, + Error = "Failed to generate cards", + Details = ex.Message, + Timestamp = DateTime.UtcNow + }); + } + } + /// /// AI 生成詞卡 (支援 /frontend/app/generate/page.tsx) /// @@ -155,6 +237,100 @@ public class AIController : ControllerBase } } + /// + /// 測試版保存生成的詞卡 (無需認證) + /// + [HttpPost("test/save")] + [AllowAnonymous] + public async Task TestSaveCards([FromBody] TestSaveCardsRequest request) + { + try + { + // 基本驗證 + if (request.SelectedCards == null || request.SelectedCards.Count == 0) + { + return BadRequest(new { Success = false, Error = "Selected cards are required" }); + } + + // 創建或使用預設卡組 + var defaultCardSet = await _context.CardSets + .FirstOrDefaultAsync(cs => cs.IsDefault); + + if (defaultCardSet == null) + { + // 創建預設卡組 + defaultCardSet = new CardSet + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), // 測試用戶 ID + Name = "AI 生成詞卡", + Description = "通過 AI 智能生成的詞卡集合", + Color = "#3B82F6", + IsDefault = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + _context.CardSets.Add(defaultCardSet); + } + + // 將生成的詞卡轉換為資料庫實體 + var flashcardsToSave = request.SelectedCards.Select(card => new Flashcard + { + Id = Guid.NewGuid(), + UserId = defaultCardSet.UserId, + CardSetId = defaultCardSet.Id, + Word = card.Word, + Translation = card.Translation, + Definition = card.Definition, + PartOfSpeech = card.PartOfSpeech, + Pronunciation = card.Pronunciation, + Example = card.Example, + ExampleTranslation = card.ExampleTranslation, + DifficultyLevel = card.DifficultyLevel, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }).ToList(); + + _context.Flashcards.AddRange(flashcardsToSave); + + // 更新卡組計數 + defaultCardSet.CardCount += flashcardsToSave.Count; + defaultCardSet.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return Ok(new + { + Success = true, + Data = new + { + SavedCount = flashcardsToSave.Count, + CardSetId = defaultCardSet.Id, + CardSetName = defaultCardSet.Name, + Cards = flashcardsToSave.Select(f => new + { + f.Id, + f.Word, + f.Translation, + f.Definition + }) + }, + Message = $"Successfully saved {flashcardsToSave.Count} cards to deck '{defaultCardSet.Name}'" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving generated cards"); + return StatusCode(500, new + { + Success = false, + Error = "Failed to save cards", + Details = ex.Message, + Timestamp = DateTime.UtcNow + }); + } + } + /// /// 保存生成的詞卡 /// @@ -374,4 +550,9 @@ public class ValidateCardRequest { public Guid FlashcardId { get; set; } public Guid? ErrorReportId { get; set; } +} + +public class TestSaveCardsRequest +{ + public List SelectedCards { get; set; } = new(); } \ No newline at end of file diff --git a/backend/DramaLing.Api/DramaLing.Api.csproj b/backend/DramaLing.Api/DramaLing.Api.csproj index 2111157..c49ef9a 100644 --- a/backend/DramaLing.Api/DramaLing.Api.csproj +++ b/backend/DramaLing.Api/DramaLing.Api.csproj @@ -1,23 +1,24 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - - - - - - + + + + net8.0 + enable + enable + d32b5470-3ba2-442c-8352-dd968fd406d8 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/GeminiService.cs b/backend/DramaLing.Api/Services/GeminiService.cs index 0c11ff5..cd528e1 100644 --- a/backend/DramaLing.Api/Services/GeminiService.cs +++ b/backend/DramaLing.Api/Services/GeminiService.cs @@ -30,7 +30,7 @@ public class GeminiService : IGeminiService { try { - if (string.IsNullOrEmpty(_apiKey)) + if (string.IsNullOrEmpty(_apiKey) || _apiKey == "your-gemini-api-key-here") { throw new InvalidOperationException("Gemini API key not configured"); } @@ -51,7 +51,7 @@ public class GeminiService : IGeminiService { try { - if (string.IsNullOrEmpty(_apiKey)) + if (string.IsNullOrEmpty(_apiKey) || _apiKey == "your-gemini-api-key-here") { throw new InvalidOperationException("Gemini API key not configured"); } diff --git a/frontend/app/generate/page.tsx b/frontend/app/generate/page.tsx index d659fdc..6836b1b 100644 --- a/frontend/app/generate/page.tsx +++ b/frontend/app/generate/page.tsx @@ -86,19 +86,116 @@ function GenerateContent() { } ] - const handleGenerate = () => { + const handleGenerate = async () => { + if (!textInput.trim()) return + setIsGenerating(true) - // Simulate AI generation - setTimeout(() => { + + try { + const response = await fetch('http://localhost:5000/api/ai/test/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + inputText: textInput, + extractionType: extractionType, + cardCount: cardCount + }) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result = await response.json() + + if (result.success) { + // 將後端格式轉換為前端格式 + const convertedCards = result.data.generatedCards.map((card: any, index: number) => ({ + id: index + 1, + word: card.word, + partOfSpeech: card.partOfSpeech, + pronunciation: { + us: card.pronunciation || '/unknown/', + uk: card.pronunciation || '/unknown/' + }, + translation: card.translation, + definition: card.definition, + synonyms: card.synonyms || [], + antonyms: [], // 後端暫不提供,使用空陣列 + originalExample: card.example || '', + originalExampleTranslation: card.exampleTranslation || '', + generatedExample: { + sentence: card.example || '', + translation: card.exampleTranslation || '', + imageUrl: '/images/examples/placeholder.png', // 佔位圖 + audioUrl: '#' + }, + difficulty: card.difficultyLevel || 'B1' + })) + + setGeneratedCards(convertedCards) + setShowPreview(true) + } else { + throw new Error(result.error || 'Generation failed') + } + } catch (error) { + console.error('Error generating cards:', error) + alert(`生成詞卡時發生錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`) + // 錯誤時使用模擬資料作為備用 setGeneratedCards(mockGeneratedCards) setShowPreview(true) + } finally { setIsGenerating(false) - }, 2000) + } } - const handleSaveCards = () => { - // Mock save action - alert('詞卡已保存到您的卡組!') + const handleSaveCards = async () => { + if (generatedCards.length === 0) return + + try { + // 將前端格式轉換為後端格式 + const cardsToSave = generatedCards.map(card => ({ + word: card.word, + partOfSpeech: card.partOfSpeech, + pronunciation: card.pronunciation.us, // 使用美式發音 + translation: card.translation, + definition: card.definition, + synonyms: card.synonyms, + example: card.originalExample || card.generatedExample.sentence, + exampleTranslation: card.originalExampleTranslation || card.generatedExample.translation, + difficultyLevel: card.difficulty + })) + + const response = await fetch('http://localhost:5000/api/ai/test/save', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + selectedCards: cardsToSave + }) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result = await response.json() + + if (result.success) { + alert(`🎉 成功保存 ${result.data.savedCount} 張詞卡到「${result.data.cardSetName}」!`) + // 可選:清空生成的詞卡或跳轉到詞卡列表 + // setGeneratedCards([]) + // setShowPreview(false) + } else { + throw new Error(result.error || 'Save failed') + } + } catch (error) { + console.error('Error saving cards:', error) + alert(`❌ 保存詞卡時發生錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`) + } } const toggleImageForCard = (cardId: number) => {