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"", ""isIdiom"": false, ""frequency"": ""high/medium/low"", ""synonyms"": [""synonym1"", ""synonym2""], ""example"": ""example sentence"", ""exampleTranslation"": ""Traditional Chinese example translation"" }} }}, ""idioms"": [ {{ ""phrase"": ""idiomatic expression"", ""translation"": ""Traditional Chinese meaning"", ""definition"": ""English explanation"", ""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()), GrammarCorrection = ConvertGrammarCorrection(aiAnalysis), Metadata = new AnalysisMetadata { UserLevel = userLevel, ProcessingDate = DateTime.UtcNow, AnalysisModel = "gemini-1.5-flash", AnalysisVersion = "2.0" } }; // 計算統計 analysisData.Statistics = CalculateStatistics(analysisData.VocabularyAnalysis, 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", IsIdiom = aiWord.IsIdiom, Frequency = aiWord.Frequency ?? "medium", Synonyms = aiWord.Synonyms ?? new List(), Example = aiWord.Example, ExampleTranslation = aiWord.ExampleTranslation, Tags = new List { "ai-analyzed", "gemini" } }; } 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) { 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 = $"{cleanWord} - AI分析請查看上方詳細說明", Definition = $"Definition in AI analysis above", PartOfSpeech = "word", Pronunciation = $"/{cleanWord}/", DifficultyLevel = EstimateBasicDifficulty(cleanWord), IsIdiom = false, // 統一使用 IsIdiom Frequency = "medium", Synonyms = new List(), Example = $"See AI analysis for {cleanWord}", ExampleTranslation = "詳見上方AI分析", Tags = new List { "ai-analyzed" } }; } return result; } private string EstimateBasicDifficulty(string word) { var basicWords = new[] { "i", "you", "he", "she", "it", "we", "they", "the", "a", "an", "is", "are", "was", "were" }; return basicWords.Contains(word.ToLower()) ? "A1" : "A2"; } private AnalysisStatistics CalculateStatistics(Dictionary vocabulary, 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 = vocabulary.Count(kvp => kvp.Value.IsIdiom), // 統一使用 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))); var geminiResponse = JsonSerializer.Deserialize(responseJson); // Check for safety filter blocking if (geminiResponse?.PromptFeedback != null) { _logger.LogWarning("Gemini prompt feedback received: {Feedback}", JsonSerializer.Serialize(geminiResponse.PromptFeedback)); } // 詳細調試信息 _logger.LogInformation("Gemini response candidates count: {Count}", geminiResponse?.Candidates?.Count ?? 0); var firstCandidate = geminiResponse?.Candidates?.FirstOrDefault(); _logger.LogInformation("First candidate content: {HasContent}", firstCandidate?.Content != null); _logger.LogInformation("First candidate parts count: {Count}", firstCandidate?.Content?.Parts?.Count ?? 0); // Try to get response text var aiText = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text ?? string.Empty; _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 no text but we have a successful response, it might be safety filtered if (string.IsNullOrWhiteSpace(aiText) && geminiResponse?.PromptFeedback != null) { // Return a fallback response instead of throwing 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 bool IsIdiom { 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? Phrase { get; set; } public string? Translation { get; set; } public string? Definition { get; set; } public string? Example { get; set; } public string? ExampleTranslation { get; set; } }