using System.Text.Json; using System.Text; using DramaLing.Api.Models.Entities; namespace DramaLing.Api.Services; public interface IGeminiService { Task> GenerateCardsAsync(string inputText, string extractionType, int cardCount); Task ValidateCardAsync(Flashcard card); } public class GeminiService : IGeminiService { private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly string _apiKey; public GeminiService(HttpClient httpClient, IConfiguration configuration, ILogger logger) { _httpClient = httpClient; _configuration = configuration; _logger = logger; _apiKey = Environment.GetEnvironmentVariable("DRAMALING_GEMINI_API_KEY") ?? _configuration["AI:GeminiApiKey"] ?? ""; } public async Task> GenerateCardsAsync(string inputText, string extractionType, int cardCount) { try { if (string.IsNullOrEmpty(_apiKey)) { throw new InvalidOperationException("Gemini API key not configured"); } var prompt = BuildPrompt(inputText, extractionType, cardCount); var response = await CallGeminiApiAsync(prompt); return ParseGeneratedCards(response); } catch (Exception ex) { _logger.LogError(ex, "Error generating cards with Gemini API"); throw; } } public async Task ValidateCardAsync(Flashcard card) { try { if (string.IsNullOrEmpty(_apiKey)) { throw new InvalidOperationException("Gemini API key not configured"); } var prompt = BuildValidationPrompt(card); var response = await CallGeminiApiAsync(prompt); return ParseValidationResult(response); } catch (Exception ex) { _logger.LogError(ex, "Error validating card with Gemini API"); throw; } } private string BuildPrompt(string inputText, string extractionType, int cardCount) { var template = extractionType == "vocabulary" ? VocabularyExtractionPrompt : SmartExtractionPrompt; return template .Replace("{cardCount}", cardCount.ToString()) .Replace("{inputText}", inputText); } private string BuildValidationPrompt(Flashcard card) { return CardValidationPrompt .Replace("{word}", card.Word) .Replace("{translation}", card.Translation) .Replace("{definition}", card.Definition) .Replace("{partOfSpeech}", card.PartOfSpeech ?? "") .Replace("{pronunciation}", card.Pronunciation ?? "") .Replace("{example}", card.Example ?? ""); } private async Task CallGeminiApiAsync(string prompt) { var requestBody = new { contents = new[] { new { parts = new[] { new { text = prompt } } } } }; var json = JsonSerializer.Serialize(requestBody); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync( $"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={_apiKey}", content); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError("Gemini API error: {StatusCode} - {Content}", response.StatusCode, errorContent); throw new HttpRequestException($"Gemini API request failed: {response.StatusCode}"); } var responseContent = await response.Content.ReadAsStringAsync(); var geminiResponse = JsonSerializer.Deserialize(responseContent); if (geminiResponse.TryGetProperty("candidates", out var candidates) && candidates.GetArrayLength() > 0 && candidates[0].TryGetProperty("content", out var contentElement) && contentElement.TryGetProperty("parts", out var parts) && parts.GetArrayLength() > 0 && parts[0].TryGetProperty("text", out var textElement)) { return textElement.GetString() ?? ""; } throw new InvalidOperationException("Invalid response format from Gemini API"); } private List ParseGeneratedCards(string response) { try { // 清理回應文本 var cleanText = response.Trim(); cleanText = cleanText.Replace("```json", "").Replace("```", "").Trim(); // 如果不是以 { 開始,嘗試找到 JSON 部分 if (!cleanText.StartsWith("{")) { var jsonStart = cleanText.IndexOf("{"); if (jsonStart >= 0) { cleanText = cleanText[jsonStart..]; } } var jsonResponse = JsonSerializer.Deserialize(cleanText); if (!jsonResponse.TryGetProperty("cards", out var cardsElement) || cardsElement.ValueKind != JsonValueKind.Array) { throw new InvalidOperationException("Response does not contain cards array"); } var cards = new List(); foreach (var cardElement in cardsElement.EnumerateArray()) { var card = new GeneratedCard { Word = GetStringProperty(cardElement, "word"), PartOfSpeech = GetStringProperty(cardElement, "part_of_speech"), Pronunciation = GetStringProperty(cardElement, "pronunciation"), Translation = GetStringProperty(cardElement, "translation"), Definition = GetStringProperty(cardElement, "definition"), Synonyms = GetArrayProperty(cardElement, "synonyms"), Example = GetStringProperty(cardElement, "example"), ExampleTranslation = GetStringProperty(cardElement, "example_translation"), DifficultyLevel = GetStringProperty(cardElement, "difficulty_level") }; if (!string.IsNullOrEmpty(card.Word) && !string.IsNullOrEmpty(card.Translation)) { cards.Add(card); } } return cards; } catch (Exception ex) { _logger.LogError(ex, "Error parsing generated cards response: {Response}", response); throw new InvalidOperationException($"Failed to parse AI response: {ex.Message}"); } } private ValidationResult ParseValidationResult(string response) { try { var cleanText = response.Trim().Replace("```json", "").Replace("```", "").Trim(); var jsonResponse = JsonSerializer.Deserialize(cleanText); var issues = new List(); if (jsonResponse.TryGetProperty("issues", out var issuesElement)) { foreach (var issueElement in issuesElement.EnumerateArray()) { issues.Add(new ValidationIssue { Field = GetStringProperty(issueElement, "field"), Original = GetStringProperty(issueElement, "original"), Corrected = GetStringProperty(issueElement, "corrected"), Reason = GetStringProperty(issueElement, "reason"), Severity = GetStringProperty(issueElement, "severity") }); } } var suggestions = new List(); if (jsonResponse.TryGetProperty("suggestions", out var suggestionsElement)) { foreach (var suggestion in suggestionsElement.EnumerateArray()) { suggestions.Add(suggestion.GetString() ?? ""); } } return new ValidationResult { Issues = issues, Suggestions = suggestions, OverallScore = jsonResponse.TryGetProperty("overall_score", out var scoreElement) ? scoreElement.GetInt32() : 85, Confidence = jsonResponse.TryGetProperty("confidence", out var confidenceElement) ? confidenceElement.GetDouble() : 0.9 }; } catch (Exception ex) { _logger.LogError(ex, "Error parsing validation result: {Response}", response); throw new InvalidOperationException($"Failed to parse validation response: {ex.Message}"); } } private static string GetStringProperty(JsonElement element, string propertyName) { return element.TryGetProperty(propertyName, out var prop) ? prop.GetString() ?? "" : ""; } private static List GetArrayProperty(JsonElement element, string propertyName) { var result = new List(); if (element.TryGetProperty(propertyName, out var arrayElement) && arrayElement.ValueKind == JsonValueKind.Array) { foreach (var item in arrayElement.EnumerateArray()) { result.Add(item.GetString() ?? ""); } } return result; } // Prompt 模板 private const string VocabularyExtractionPrompt = @" 從以下英文文本中萃取 {cardCount} 個最重要的詞彙,為每個詞彙生成詞卡資料。 輸入文本: {inputText} 請按照以下 JSON 格式回應,不要包含任何其他文字或代碼塊標記: { ""cards"": [ { ""word"": ""單字原型"", ""part_of_speech"": ""詞性(n./v./adj./adv.等)"", ""pronunciation"": ""IPA音標"", ""translation"": ""繁體中文翻譯"", ""definition"": ""英文定義(保持A1-A2程度)"", ""synonyms"": [""同義詞1"", ""同義詞2""], ""example"": ""例句(使用原文中的句子或生成新句子)"", ""example_translation"": ""例句中文翻譯"", ""difficulty_level"": ""CEFR等級(A1/A2/B1/B2/C1/C2)"" } ] } 要求: 1. 選擇最有學習價值的詞彙 2. 定義要簡單易懂,適合英語學習者 3. 例句要實用且符合語境 4. 確保 JSON 格式正確 5. 同義詞最多2個,選擇常用的"; private const string SmartExtractionPrompt = @" 分析以下英文文本,識別片語、俚語和常用表達,生成 {cardCount} 個學習卡片: 輸入文本: {inputText} 重點關注: 1. 片語和俚語 2. 文化相關表達 3. 語境特定用法 4. 慣用語和搭配 請按照相同的 JSON 格式回應..."; private const string CardValidationPrompt = @" 請檢查以下詞卡內容的準確性: 單字: {word} 翻譯: {translation} 定義: {definition} 詞性: {partOfSpeech} 發音: {pronunciation} 例句: {example} 請按照以下 JSON 格式回應: { ""issues"": [], ""suggestions"": [], ""overall_score"": 85, ""confidence"": 0.9 }"; } // 支援類型 public class GeneratedCard { public string Word { get; set; } = string.Empty; public string PartOfSpeech { get; set; } = string.Empty; public string Pronunciation { get; set; } = string.Empty; public string Translation { get; set; } = string.Empty; public string Definition { get; set; } = string.Empty; public List Synonyms { get; set; } = new(); public string Example { get; set; } = string.Empty; public string ExampleTranslation { get; set; } = string.Empty; public string DifficultyLevel { get; set; } = string.Empty; } public class ValidationResult { public List Issues { get; set; } = new(); public List Suggestions { get; set; } = new(); public int OverallScore { get; set; } public double Confidence { get; set; } } public class ValidationIssue { public string Field { get; set; } = string.Empty; public string Original { get; set; } = string.Empty; public string Corrected { get; set; } = string.Empty; public string Reason { get; set; } = string.Empty; public string Severity { get; set; } = string.Empty; }