dramaling-vocab-learning/backend/DramaLing.Api/Controllers/AIController.cs

377 lines
13 KiB
C#
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.

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using DramaLing.Api.Data;
using DramaLing.Api.Models.Entities;
using DramaLing.Api.Services;
using Microsoft.AspNetCore.Authorization;
namespace DramaLing.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class AIController : ControllerBase
{
private readonly DramaLingDbContext _context;
private readonly IAuthService _authService;
private readonly IGeminiService _geminiService;
private readonly ILogger<AIController> _logger;
public AIController(
DramaLingDbContext context,
IAuthService authService,
IGeminiService geminiService,
ILogger<AIController> logger)
{
_context = context;
_authService = authService;
_geminiService = geminiService;
_logger = logger;
}
/// <summary>
/// AI 生成詞卡 (支援 /frontend/app/generate/page.tsx)
/// </summary>
[HttpPost("generate")]
public async Task<ActionResult> GenerateCards([FromBody] GenerateCardsRequest request)
{
try
{
var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
if (userId == null)
return Unauthorized(new { Success = false, Error = "Invalid token" });
// 基本驗證
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 < 5 || request.CardCount > 20)
{
return BadRequest(new { Success = false, Error = "Card count must be between 5 and 20" });
}
// 檢查每日配額 (簡化版,未來可以基於用戶訂閱狀態)
var today = DateOnly.FromDateTime(DateTime.Today);
var todayStats = await _context.DailyStats
.FirstOrDefaultAsync(ds => ds.UserId == userId && ds.Date == today);
var todayApiCalls = todayStats?.AiApiCalls ?? 0;
var maxApiCalls = 10; // 免費用戶每日限制
if (todayApiCalls >= maxApiCalls)
{
return StatusCode(429, new
{
Success = false,
Error = "Daily AI generation limit exceeded"
});
}
// 建立生成任務 (簡化版,直接處理而不是非同步)
try
{
var generatedCards = await _geminiService.GenerateCardsAsync(
request.InputText,
request.ExtractionType,
request.CardCount);
if (generatedCards.Count == 0)
{
return StatusCode(500, new
{
Success = false,
Error = "AI generated no valid cards"
});
}
// 更新每日統計
if (todayStats == null)
{
todayStats = new DailyStats
{
Id = Guid.NewGuid(),
UserId = userId.Value,
Date = today
};
_context.DailyStats.Add(todayStats);
}
todayStats.AiApiCalls++;
todayStats.CardsGenerated += generatedCards.Count;
await _context.SaveChangesAsync();
return Ok(new
{
Success = true,
Data = new
{
TaskId = Guid.NewGuid(), // 模擬任務 ID
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 (Gemini API not configured)"
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in AI card generation");
return StatusCode(500, new
{
Success = false,
Error = "Failed to generate cards",
Timestamp = DateTime.UtcNow
});
}
}
/// <summary>
/// 保存生成的詞卡
/// </summary>
[HttpPost("generate/{taskId}/save")]
public async Task<ActionResult> SaveGeneratedCards(
Guid taskId,
[FromBody] SaveCardsRequest request)
{
try
{
var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
if (userId == null)
return Unauthorized(new { Success = false, Error = "Invalid token" });
// 基本驗證
if (request.CardSetId == Guid.Empty)
{
return BadRequest(new { Success = false, Error = "Card set ID is required" });
}
if (request.SelectedCards == null || request.SelectedCards.Count == 0)
{
return BadRequest(new { Success = false, Error = "Selected cards are required" });
}
// 驗證卡組是否屬於用戶
var cardSet = await _context.CardSets
.FirstOrDefaultAsync(cs => cs.Id == request.CardSetId && cs.UserId == userId);
if (cardSet == null)
{
return NotFound(new { Success = false, Error = "Card set not found" });
}
// 將生成的詞卡轉換為資料庫實體
var flashcardsToSave = request.SelectedCards.Select(card => new Flashcard
{
Id = Guid.NewGuid(),
UserId = userId.Value,
CardSetId = request.CardSetId,
Word = card.Word,
Translation = card.Translation,
Definition = card.Definition,
PartOfSpeech = card.PartOfSpeech,
Pronunciation = card.Pronunciation,
Example = card.Example,
ExampleTranslation = card.ExampleTranslation,
DifficultyLevel = card.DifficultyLevel
}).ToList();
_context.Flashcards.AddRange(flashcardsToSave);
await _context.SaveChangesAsync();
return Ok(new
{
Success = true,
Data = new
{
SavedCount = flashcardsToSave.Count,
Cards = flashcardsToSave.Select(f => new
{
f.Id,
f.Word,
f.Translation,
f.Definition
})
},
Message = $"Successfully saved {flashcardsToSave.Count} cards to your deck"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving generated cards");
return StatusCode(500, new
{
Success = false,
Error = "Failed to save cards",
Timestamp = DateTime.UtcNow
});
}
}
/// <summary>
/// 智能檢測詞卡內容
/// </summary>
[HttpPost("validate-card")]
public async Task<ActionResult> ValidateCard([FromBody] ValidateCardRequest request)
{
try
{
var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
if (userId == null)
return Unauthorized(new { Success = false, Error = "Invalid token" });
var flashcard = await _context.Flashcards
.FirstOrDefaultAsync(f => f.Id == request.FlashcardId && f.UserId == userId);
if (flashcard == null)
{
return NotFound(new { Success = false, Error = "Flashcard not found" });
}
try
{
var validationResult = await _geminiService.ValidateCardAsync(flashcard);
return Ok(new
{
Success = true,
Data = new
{
FlashcardId = request.FlashcardId,
ValidationResult = validationResult,
CheckedAt = DateTime.UtcNow
},
Message = "Card validation completed"
});
}
catch (InvalidOperationException ex) when (ex.Message.Contains("API key"))
{
// 模擬檢測結果
var mockResult = new ValidationResult
{
Issues = new List<ValidationIssue>(),
Suggestions = new List<string> { "詞卡內容看起來正確", "建議添加更多例句" },
OverallScore = 85,
Confidence = 0.7
};
return Ok(new
{
Success = true,
Data = new
{
FlashcardId = request.FlashcardId,
ValidationResult = mockResult,
CheckedAt = DateTime.UtcNow,
Note = "Mock validation (Gemini API not configured)"
},
Message = "Card validation completed (mock mode)"
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating card");
return StatusCode(500, new
{
Success = false,
Error = "Failed to validate card",
Timestamp = DateTime.UtcNow
});
}
}
/// <summary>
/// 生成模擬資料 (開發階段使用)
/// </summary>
private List<GeneratedCard> GenerateMockCards(int count)
{
var mockCards = new List<GeneratedCard>
{
new() {
Word = "accomplish",
PartOfSpeech = "verb",
Pronunciation = "/əˈkʌmplɪʃ/",
Translation = "完成、達成",
Definition = "To finish something successfully or to achieve something",
Synonyms = new() { "achieve", "complete" },
Example = "She accomplished her goal of learning English.",
ExampleTranslation = "她達成了學習英語的目標。",
DifficultyLevel = "B1"
},
new() {
Word = "negotiate",
PartOfSpeech = "verb",
Pronunciation = "/nɪˈɡəʊʃieɪt/",
Translation = "協商、談判",
Definition = "To discuss something with someone in order to reach an agreement",
Synonyms = new() { "bargain", "discuss" },
Example = "We need to negotiate a better deal.",
ExampleTranslation = "我們需要協商一個更好的交易。",
DifficultyLevel = "B2"
},
new() {
Word = "perspective",
PartOfSpeech = "noun",
Pronunciation = "/pərˈspektɪv/",
Translation = "觀點、看法",
Definition = "A particular way of considering something",
Synonyms = new() { "viewpoint", "opinion" },
Example = "From my perspective, this is the best solution.",
ExampleTranslation = "從我的觀點來看,這是最好的解決方案。",
DifficultyLevel = "B2"
}
};
return mockCards.Take(Math.Min(count, mockCards.Count)).ToList();
}
}
// Request DTOs
public class GenerateCardsRequest
{
public string InputText { get; set; } = string.Empty;
public string ExtractionType { get; set; } = "vocabulary"; // vocabulary, smart
public int CardCount { get; set; } = 10;
}
public class SaveCardsRequest
{
public Guid CardSetId { get; set; }
public List<GeneratedCard> SelectedCards { get; set; } = new();
}
public class ValidateCardRequest
{
public Guid FlashcardId { get; set; }
public Guid? ErrorReportId { get; set; }
}