diff --git a/backend/DramaLing.Api/Services/GeminiService.cs b/backend/DramaLing.Api/Services/GeminiService.cs index 7a9623b..41e1945 100644 --- a/backend/DramaLing.Api/Services/GeminiService.cs +++ b/backend/DramaLing.Api/Services/GeminiService.cs @@ -40,8 +40,58 @@ public class GeminiService : IGeminiService _logger.LogInformation("Starting sentence analysis for text: {Text}, UserLevel: {UserLevel}", inputText.Substring(0, Math.Min(50, inputText.Length)), userLevel); - // 使用簡單的 prompt 直接調用 Gemini API - var prompt = $"Translate this English sentence to Traditional Chinese and provide grammar analysis: \"{inputText}\""; + // 符合產品需求規格的結構化 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); @@ -52,6 +102,13 @@ public class GeminiService : IGeminiService 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); @@ -72,47 +129,129 @@ public class GeminiService : IGeminiService _logger.LogInformation("Creating analysis from AI response: {ResponsePreview}...", aiResponse.Substring(0, Math.Min(100, aiResponse.Length))); - // 直接使用 AI 回應作為分析結果 - var analysisData = new SentenceAnalysisData + 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, // 直接使用 AI 的完整回應作為分析結果 + SentenceMeaning = aiResponse, VocabularyAnalysis = CreateBasicVocabularyFromText(inputText, aiResponse), Metadata = new AnalysisMetadata { UserLevel = userLevel, ProcessingDate = DateTime.UtcNow, - AnalysisModel = "gemini-1.5-flash", + AnalysisModel = "gemini-1.5-flash-fallback", AnalysisVersion = "1.0" - } + }, + Statistics = new AnalysisStatistics() }; - - // 檢查是否有語法錯誤(基於 AI 回應) - if (aiResponse.ToLower().Contains("error") || aiResponse.ToLower().Contains("incorrect") || - aiResponse.ToLower().Contains("should be") || aiResponse.ToLower().Contains("錯誤")) - { - analysisData.GrammarCorrection = new GrammarCorrectionDto - { - HasErrors = true, - CorrectedText = inputText, // 保持原文,讓用戶看到 AI 的建議 - Corrections = new List - { - new GrammarErrorDto - { - Error = "AI detected grammar issues", - Correction = "See AI analysis above", - Type = "AI Grammar Check", - Explanation = aiResponse, // 直接使用 AI 的解釋 - Severity = "medium" - } - } - }; - } - - // 計算統計(使用統一的慣用語術語) - analysisData.Statistics = CalculateStatistics(analysisData.VocabularyAnalysis, userLevel); - - return analysisData; } private Dictionary CreateBasicVocabularyFromText(string inputText, string aiResponse) @@ -199,11 +338,37 @@ public class GeminiService : IGeminiService 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; } @@ -219,6 +384,7 @@ public class GeminiService : IGeminiService internal class GeminiApiResponse { public List? Candidates { get; set; } + public object? PromptFeedback { get; set; } } internal class GeminiCandidate @@ -234,4 +400,46 @@ internal class GeminiContent 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; } } \ No newline at end of file