using DramaLing.Api.Models.DTOs; using System.Text.Json; using System.Text; namespace DramaLing.Api.Services; public interface IGeminiService { Task AnalyzeSentenceAsync(string inputText, string userLevel, AnalysisOptions options); } public class GeminiService : IGeminiService { private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly string _apiKey; public GeminiService(HttpClient httpClient, IConfiguration configuration, ILogger logger) { _httpClient = httpClient; _logger = logger; _apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY") ?? configuration["AI:GeminiApiKey"] ?? configuration["Gemini:ApiKey"] ?? throw new InvalidOperationException("Gemini API Key not configured"); _logger.LogInformation("GeminiService initialized with API key: {ApiKeyStart}...", _apiKey.Length > 10 ? _apiKey.Substring(0, 10) : "[key-not-set]"); _httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0"); } public async Task AnalyzeSentenceAsync(string inputText, string userLevel, AnalysisOptions options) { var startTime = DateTime.UtcNow; try { _logger.LogInformation("Starting sentence analysis for text: {Text}, UserLevel: {UserLevel}", inputText.Substring(0, Math.Min(50, inputText.Length)), userLevel); // 符合產品需求規格的結構化 prompt var prompt = $@"You are an English learning assistant. Analyze this sentence for a {userLevel} CEFR level learner and return ONLY a valid JSON response. **Input Sentence**: ""{inputText}"" **Learner Level**: {userLevel} **Required JSON Structure:** {{ ""sentenceTranslation"": ""Traditional Chinese translation of the entire sentence"", ""hasGrammarErrors"": true/false, ""grammarCorrections"": [ {{ ""original"": ""incorrect text"", ""corrected"": ""correct text"", ""type"": ""error type (tense/subject-verb/preposition/word-order)"", ""explanation"": ""brief explanation in Traditional Chinese"" }} ], ""vocabularyAnalysis"": {{ ""word1"": {{ ""word"": ""the word"", ""translation"": ""Traditional Chinese translation"", ""definition"": ""English definition"", ""partOfSpeech"": ""noun/verb/adjective/etc"", ""pronunciation"": ""/phonetic/"", ""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"", ""frequency"": ""high/medium/low"", ""synonyms"": [""synonym1"", ""synonym2""], ""example"": ""example sentence"", ""exampleTranslation"": ""Traditional Chinese example translation"" }} }}, ""idioms"": [ {{ ""idiom"": ""idiomatic expression"", ""translation"": ""Traditional Chinese meaning"", ""definition"": ""English explanation"", ""pronunciation"": ""/phonetic notation/"", ""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"", ""frequency"": ""high/medium/low"", ""synonyms"": [""synonym1"", ""synonym2""], ""example"": ""usage example"", ""exampleTranslation"": ""Traditional Chinese example"" }} ] }} **Analysis Guidelines:** 1. **Grammar Check**: Detect tense errors, subject-verb agreement, preposition usage, word order 2. **Vocabulary Analysis**: Include ALL significant words (exclude articles: a, an, the) 3. **CEFR Levels**: Assign accurate A1-C2 levels for each word 4. **Idioms**: Identify any idiomatic expressions or phrasal verbs 5. **Translations**: Use Traditional Chinese (Taiwan standard) **IMPORTANT**: Return ONLY the JSON object, no additional text or explanation."; var aiResponse = await CallGeminiAPI(prompt); _logger.LogInformation("Gemini AI response received: {ResponseLength} characters", aiResponse?.Length ?? 0); if (string.IsNullOrWhiteSpace(aiResponse)) { throw new InvalidOperationException("Gemini API returned empty response"); } // 檢查是否是安全過濾的回退訊息 if (aiResponse.Contains("temporarily unavailable due to safety filtering")) { // 這是安全過濾的情況,但我們仍然要處理它而不是拋出異常 _logger.LogWarning("Using safety filtering fallback response"); } // 直接使用 AI 的回應創建分析數據 var analysisData = CreateAnalysisFromAIResponse(inputText, userLevel, aiResponse); var processingTime = (DateTime.UtcNow - startTime).TotalSeconds; _logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime); return analysisData; } catch (Exception ex) { _logger.LogError(ex, "Error analyzing sentence: {Text}", inputText); throw; } } private SentenceAnalysisData CreateAnalysisFromAIResponse(string inputText, string userLevel, string aiResponse) { _logger.LogInformation("Creating analysis from AI response: {ResponsePreview}...", aiResponse.Substring(0, Math.Min(100, aiResponse.Length))); try { // 清理 AI 回應以確保是純 JSON var cleanJson = aiResponse.Trim(); if (cleanJson.StartsWith("```json")) { cleanJson = cleanJson.Substring(7); } if (cleanJson.EndsWith("```")) { cleanJson = cleanJson.Substring(0, cleanJson.Length - 3); } // 解析 AI 回應的 JSON var aiAnalysis = JsonSerializer.Deserialize(cleanJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (aiAnalysis == null) { throw new InvalidOperationException("Failed to parse AI response JSON"); } // 轉換為 DTO 結構 var analysisData = new SentenceAnalysisData { OriginalText = inputText, SentenceMeaning = aiAnalysis.SentenceTranslation ?? "", VocabularyAnalysis = ConvertVocabularyAnalysis(aiAnalysis.VocabularyAnalysis ?? new()), Idioms = ConvertIdioms(aiAnalysis.Idioms ?? new()), GrammarCorrection = ConvertGrammarCorrection(aiAnalysis), Metadata = new AnalysisMetadata { UserLevel = userLevel, ProcessingDate = DateTime.UtcNow, AnalysisModel = "gemini-1.5-flash", AnalysisVersion = "2.0" } }; // 計算統計 analysisData.Statistics = CalculateStatistics(analysisData.VocabularyAnalysis, analysisData.Idioms, userLevel); return analysisData; } catch (JsonException ex) { _logger.LogError(ex, "Failed to parse AI response as JSON: {Response}", aiResponse); // 回退到舊的處理方式 return CreateFallbackAnalysis(inputText, userLevel, aiResponse); } } private Dictionary ConvertVocabularyAnalysis(Dictionary aiVocab) { var result = new Dictionary(); foreach (var kvp in aiVocab) { var aiWord = kvp.Value; result[kvp.Key] = new VocabularyAnalysisDto { Word = aiWord.Word ?? kvp.Key, Translation = aiWord.Translation ?? "", Definition = aiWord.Definition ?? "", PartOfSpeech = aiWord.PartOfSpeech ?? "unknown", Pronunciation = aiWord.Pronunciation ?? $"/{kvp.Key}/", DifficultyLevel = aiWord.DifficultyLevel ?? "A2", Frequency = aiWord.Frequency ?? "medium", Synonyms = aiWord.Synonyms ?? new List(), Example = aiWord.Example, ExampleTranslation = aiWord.ExampleTranslation, Tags = new List { "ai-analyzed", "gemini" } }; } return result; } private List ConvertIdioms(List aiIdioms) { var result = new List(); foreach (var aiIdiom in aiIdioms) { result.Add(new IdiomDto { Idiom = aiIdiom.Idiom ?? "", Translation = aiIdiom.Translation ?? "", Definition = aiIdiom.Definition ?? "", Pronunciation = aiIdiom.Pronunciation ?? "", DifficultyLevel = aiIdiom.DifficultyLevel ?? "B2", Frequency = aiIdiom.Frequency ?? "medium", Synonyms = aiIdiom.Synonyms ?? new List(), Example = aiIdiom.Example, ExampleTranslation = aiIdiom.ExampleTranslation }); } return result; } private GrammarCorrectionDto? ConvertGrammarCorrection(AiAnalysisResponse aiAnalysis) { if (!aiAnalysis.HasGrammarErrors || aiAnalysis.GrammarCorrections == null || !aiAnalysis.GrammarCorrections.Any()) { return null; } var corrections = aiAnalysis.GrammarCorrections.Select(gc => new GrammarErrorDto { Error = gc.Original ?? "", Correction = gc.Corrected ?? "", Type = gc.Type ?? "grammar", Explanation = gc.Explanation ?? "", Severity = "medium", Position = new ErrorPosition { Start = 0, End = 0 } // 簡化處理 }).ToList(); return new GrammarCorrectionDto { HasErrors = true, CorrectedText = string.Join(" ", corrections.Select(c => c.Correction)), Corrections = corrections }; } private SentenceAnalysisData CreateFallbackAnalysis(string inputText, string userLevel, string aiResponse) { _logger.LogWarning("Using fallback analysis due to JSON parsing failure"); return new SentenceAnalysisData { OriginalText = inputText, SentenceMeaning = aiResponse, VocabularyAnalysis = CreateBasicVocabularyFromText(inputText, aiResponse), Metadata = new AnalysisMetadata { UserLevel = userLevel, ProcessingDate = DateTime.UtcNow, AnalysisModel = "gemini-1.5-flash-fallback", AnalysisVersion = "1.0" }, Statistics = new AnalysisStatistics() }; } private Dictionary CreateBasicVocabularyFromText(string inputText, string aiResponse) { // 從 AI 回應中提取真實的詞彙翻譯 var words = inputText.Split(' ', StringSplitOptions.RemoveEmptyEntries); var result = new Dictionary(); foreach (var word in words.Take(15)) { var cleanWord = word.ToLower().Trim('.', ',', '!', '?', ';', ':', '"', '\''); if (string.IsNullOrEmpty(cleanWord) || cleanWord.Length < 2) continue; result[cleanWord] = new VocabularyAnalysisDto { Word = cleanWord, Translation = ExtractTranslationFromAI(cleanWord, aiResponse), Definition = $"Please refer to the AI analysis above for detailed definition.", PartOfSpeech = "unknown", Pronunciation = $"/{cleanWord}/", DifficultyLevel = EstimateBasicDifficulty(cleanWord), Frequency = "medium", Synonyms = new List(), Example = null, ExampleTranslation = null, Tags = new List { "fallback-analysis" } }; } return result; } private string ExtractTranslationFromAI(string word, string aiResponse) { // 嘗試從 AI 回應中提取該詞的翻譯 // 這是簡化版本,真正的版本應該解析完整的 JSON if (aiResponse.Contains(word, StringComparison.OrdinalIgnoreCase)) { return $"{word} translation from AI"; } return $"{word} - 請查看完整分析"; } private string EstimateBasicDifficulty(string word) { // 基本詞彙列表(這是最小的 fallback 邏輯) var a1Words = new[] { "i", "you", "he", "she", "it", "we", "they", "the", "a", "an", "is", "are", "was", "were", "have", "do", "go", "get", "see", "know" }; var a2Words = new[] { "make", "think", "come", "take", "use", "work", "call", "try", "ask", "need", "feel", "become", "leave", "put", "say", "tell", "turn", "move" }; var lowerWord = word.ToLower(); if (a1Words.Contains(lowerWord)) return "A1"; if (a2Words.Contains(lowerWord)) return "A2"; if (word.Length <= 4) return "A2"; if (word.Length <= 6) return "B1"; return "B2"; } private AnalysisStatistics CalculateStatistics(Dictionary vocabulary, List idioms, string userLevel) { var stats = new AnalysisStatistics { TotalWords = vocabulary.Count, UniqueWords = vocabulary.Count, SimpleWords = vocabulary.Count(kvp => kvp.Value.DifficultyLevel == "A1"), ModerateWords = vocabulary.Count(kvp => kvp.Value.DifficultyLevel == "A2"), DifficultWords = vocabulary.Count(kvp => !new[] { "A1", "A2" }.Contains(kvp.Value.DifficultyLevel)), Idioms = idioms.Count, // 基於實際 idioms 陣列計算 AverageDifficulty = userLevel }; return stats; } private async Task CallGeminiAPI(string prompt) { try { var requestBody = new { contents = new[] { new { parts = new[] { new { text = prompt } } } }, generationConfig = new { temperature = 0.7, topK = 40, topP = 0.95, maxOutputTokens = 2000 } }; 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:generateContent?key={_apiKey}", content); response.EnsureSuccessStatusCode(); var responseJson = await response.Content.ReadAsStringAsync(); _logger.LogInformation("Raw Gemini API response: {Response}", responseJson.Substring(0, Math.Min(500, responseJson.Length))); // 先嘗試使用動態解析來避免反序列化問題 using var document = JsonDocument.Parse(responseJson); var root = document.RootElement; string aiText = string.Empty; // 檢查是否有 candidates 陣列 if (root.TryGetProperty("candidates", out var candidatesElement) && candidatesElement.ValueKind == JsonValueKind.Array) { _logger.LogInformation("Found candidates array with {Count} items", candidatesElement.GetArrayLength()); var firstCandidate = candidatesElement.EnumerateArray().FirstOrDefault(); if (firstCandidate.ValueKind != JsonValueKind.Undefined) { if (firstCandidate.TryGetProperty("content", out var contentElement)) { if (contentElement.TryGetProperty("parts", out var partsElement) && partsElement.ValueKind == JsonValueKind.Array) { var firstPart = partsElement.EnumerateArray().FirstOrDefault(); if (firstPart.TryGetProperty("text", out var textElement)) { aiText = textElement.GetString() ?? string.Empty; _logger.LogInformation("Successfully extracted text: {Length} characters", aiText.Length); } } } } } // 檢查是否有安全過濾 if (root.TryGetProperty("promptFeedback", out var feedbackElement)) { _logger.LogWarning("Gemini prompt feedback received: {Feedback}", feedbackElement.ToString()); } _logger.LogInformation("Gemini API returned: {ResponseLength} characters", aiText.Length); if (!string.IsNullOrEmpty(aiText)) { _logger.LogInformation("AI text preview: {Preview}", aiText.Substring(0, Math.Min(200, aiText.Length))); } // 如果沒有獲取到文本且有安全過濾回饋,返回友好訊息 if (string.IsNullOrWhiteSpace(aiText) && root.TryGetProperty("promptFeedback", out _)) { return "The content analysis is temporarily unavailable due to safety filtering. Please try with different content."; } return aiText; } catch (Exception ex) { _logger.LogError(ex, "Gemini API call failed"); throw; } } } // Gemini API response models internal class GeminiApiResponse { public List? Candidates { get; set; } public object? PromptFeedback { get; set; } } internal class GeminiCandidate { public GeminiContent? Content { get; set; } } internal class GeminiContent { public List? Parts { get; set; } } internal class GeminiPart { public string? Text { get; set; } } // AI Response JSON Models internal class AiAnalysisResponse { public string? SentenceTranslation { get; set; } public bool HasGrammarErrors { get; set; } public List? GrammarCorrections { get; set; } public Dictionary? VocabularyAnalysis { get; set; } public List? Idioms { get; set; } } internal class AiGrammarCorrection { public string? Original { get; set; } public string? Corrected { get; set; } public string? Type { get; set; } public string? Explanation { get; set; } } internal class AiVocabularyAnalysis { public string? Word { get; set; } public string? Translation { get; set; } public string? Definition { get; set; } public string? PartOfSpeech { get; set; } public string? Pronunciation { get; set; } public string? DifficultyLevel { get; set; } public string? Frequency { get; set; } public List? Synonyms { get; set; } public string? Example { get; set; } public string? ExampleTranslation { get; set; } } internal class AiIdiom { public string? Idiom { get; set; } public string? Translation { get; set; } public string? Definition { get; set; } public string? Pronunciation { get; set; } public string? DifficultyLevel { get; set; } public string? Frequency { get; set; } public List? Synonyms { get; set; } public string? Example { get; set; } public string? ExampleTranslation { get; set; } }