558 lines
19 KiB
C#
558 lines
19 KiB
C#
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 生成詞卡測試端點 (開發用,無需認證)
|
||
/// </summary>
|
||
[HttpPost("test/generate")]
|
||
[AllowAnonymous]
|
||
public async Task<ActionResult> 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
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <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("test/save")]
|
||
[AllowAnonymous]
|
||
public async Task<ActionResult> 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
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <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; }
|
||
}
|
||
|
||
public class TestSaveCardsRequest
|
||
{
|
||
public List<GeneratedCard> SelectedCards { get; set; } = new();
|
||
} |