using DramaLing.Api.Models.DTOs; using System.Text.Json; using System.Text.RegularExpressions; 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[] _cefrLevels = { "A1", "A2", "B1", "B2", "C1", "C2" }; 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"] ?? "mock-api-key"; // For development without Gemini _logger.LogInformation("GeminiService initialized with API key: {ApiKeyStart}...", _apiKey.Length > 10 ? _apiKey.Substring(0, 10) : "mock"); _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); var prompt = BuildAnalysisPrompt(inputText, userLevel, options); var response = await CallGeminiAPI(prompt); var analysisData = ParseGeminiResponse(response, inputText, userLevel); var processingTime = (DateTime.UtcNow - startTime).TotalSeconds; analysisData.Metadata.ProcessingDate = DateTime.UtcNow; _logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime); return analysisData; } catch (Exception ex) { _logger.LogError(ex, "Error analyzing sentence: {Text}", inputText); throw; } } private string BuildAnalysisPrompt(string inputText, string userLevel, AnalysisOptions options) { var userIndex = Array.IndexOf(_cefrLevels, userLevel); var targetLevels = GetTargetLevels(userIndex); return $@" 請分析以下英文句子並以JSON格式回應: 句子: ""{inputText}"" 學習者程度: {userLevel} 請提供完整的分析,包含: 1. 語法檢查:檢查是否有語法錯誤,如有則提供修正建議 2. 詞彙分析:分析句子中每個有意義的詞彙 3. 中文翻譯:提供自然流暢的繁體中文翻譯 4. 慣用語識別:識別句子中的慣用語和片語 詞彙分析要求: - 為每個詞彙標註CEFR等級 (A1-C2) - 如果是慣用語,設置 isPhrase: true - 提供IPA發音標記 - 包含同義詞 - 提供適當的例句和翻譯 回應格式要求: {{ ""grammarCorrection"": {{ ""hasErrors"": boolean, ""correctedText"": ""修正後的句子"", ""corrections"": [ {{ ""error"": ""錯誤詞彙"", ""correction"": ""正確詞彙"", ""type"": ""錯誤類型"", ""explanation"": ""解釋"" }} ] }}, ""sentenceMeaning"": ""繁體中文翻譯"", ""vocabularyAnalysis"": {{ ""詞彙"": {{ ""word"": ""詞彙"", ""translation"": ""中文翻譯"", ""definition"": ""英文定義"", ""partOfSpeech"": ""詞性"", ""pronunciation"": ""IPA發音"", ""difficultyLevel"": ""CEFR等級"", ""isPhrase"": false, ""frequency"": ""使用頻率"", ""synonyms"": [""同義詞""], ""example"": ""例句"", ""exampleTranslation"": ""例句翻譯"", ""tags"": [""標籤""] }} }} }} 重要:回應必須是有效的JSON格式,不要包含任何其他文字。"; } private string[] GetTargetLevels(int userIndex) { var targets = new List(); if (userIndex + 1 < _cefrLevels.Length) targets.Add(_cefrLevels[userIndex + 1]); if (userIndex + 2 < _cefrLevels.Length) targets.Add(_cefrLevels[userIndex + 2]); return targets.ToArray(); } private async Task CallGeminiAPI(string prompt) { // 暫時使用模擬數據,稍後可替換為真實Gemini調用 if (_apiKey == "mock-api-key") { _logger.LogInformation("Using mock AI response for development"); await Task.Delay(1000); // 模擬API延遲 return GetMockResponse(); } try { var requestBody = new { contents = new[] { new { parts = new[] { new { text = prompt } } } }, generationConfig = new { temperature = 0.3, topK = 1, topP = 1, 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-pro:generateContent?key={_apiKey}", content); response.EnsureSuccessStatusCode(); var responseJson = await response.Content.ReadAsStringAsync(); var geminiResponse = JsonSerializer.Deserialize(responseJson); return geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text ?? string.Empty; } catch (Exception ex) { _logger.LogError(ex, "Gemini API call failed, falling back to mock response"); return GetMockResponse(); } } private string GetMockResponse() { return @"{ ""grammarCorrection"": { ""hasErrors"": true, ""correctedText"": ""She just joined the team, so let's cut her some slack until she gets used to the workflow."", ""corrections"": [ { ""error"": ""join"", ""correction"": ""joined"", ""type"": ""時態錯誤"", ""explanation"": ""第三人稱單數過去式應使用 'joined'"" }, { ""error"": ""get"", ""correction"": ""gets"", ""type"": ""時態錯誤"", ""explanation"": ""第三人稱單數現在式應使用 'gets'"" } ] }, ""sentenceMeaning"": ""她剛加入團隊,所以讓我們對她寬容一點,直到她習慣工作流程。"", ""vocabularyAnalysis"": { ""she"": { ""word"": ""she"", ""translation"": ""她"", ""definition"": ""female person pronoun"", ""partOfSpeech"": ""pronoun"", ""pronunciation"": ""/ʃiː/"", ""difficultyLevel"": ""A1"", ""isPhrase"": false, ""frequency"": ""very_high"", ""synonyms"": [""her""], ""example"": ""She is a teacher."", ""exampleTranslation"": ""她是一名老師。"", ""tags"": [""basic"", ""pronoun""] }, ""just"": { ""word"": ""just"", ""translation"": ""剛剛;僅僅"", ""definition"": ""recently; only"", ""partOfSpeech"": ""adverb"", ""pronunciation"": ""/dʒʌst/"", ""difficultyLevel"": ""A2"", ""isPhrase"": false, ""frequency"": ""high"", ""synonyms"": [""recently"", ""only"", ""merely""], ""example"": ""I just arrived."", ""exampleTranslation"": ""我剛到。"", ""tags"": [""time"", ""adverb""] }, ""joined"": { ""word"": ""joined"", ""translation"": ""加入"", ""definition"": ""became a member of (past tense of join)"", ""partOfSpeech"": ""verb"", ""pronunciation"": ""/dʒɔɪnd/"", ""difficultyLevel"": ""B1"", ""isPhrase"": false, ""frequency"": ""medium"", ""synonyms"": [""entered"", ""became part of""], ""example"": ""He joined the company last year."", ""exampleTranslation"": ""他去年加入了這家公司。"", ""tags"": [""work"", ""action""] }, ""the"": { ""word"": ""the"", ""translation"": ""定冠詞"", ""definition"": ""definite article"", ""partOfSpeech"": ""article"", ""pronunciation"": ""/ðə/"", ""difficultyLevel"": ""A1"", ""isPhrase"": false, ""frequency"": ""very_high"", ""synonyms"": [], ""example"": ""The cat is sleeping."", ""exampleTranslation"": ""貓在睡覺。"", ""tags"": [""basic""] }, ""team"": { ""word"": ""team"", ""translation"": ""團隊"", ""definition"": ""a group of people working together"", ""partOfSpeech"": ""noun"", ""pronunciation"": ""/tiːm/"", ""difficultyLevel"": ""A2"", ""isPhrase"": false, ""frequency"": ""high"", ""synonyms"": [""group"", ""crew""], ""example"": ""Our team works well together."", ""exampleTranslation"": ""我們的團隊合作得很好。"", ""tags"": [""work"", ""group""] }, ""so"": { ""word"": ""so"", ""translation"": ""所以;如此"", ""definition"": ""therefore; to such a degree"", ""partOfSpeech"": ""adverb"", ""pronunciation"": ""/soʊ/"", ""difficultyLevel"": ""A1"", ""isPhrase"": false, ""frequency"": ""very_high"", ""synonyms"": [""therefore"", ""thus""], ""example"": ""It was raining, so I stayed home."", ""exampleTranslation"": ""下雨了,所以我待在家裡。"", ""tags"": [""basic""] }, ""let's"": { ""word"": ""let's"", ""translation"": ""讓我們"", ""definition"": ""let us (contraction)"", ""partOfSpeech"": ""contraction"", ""pronunciation"": ""/lets/"", ""difficultyLevel"": ""A1"", ""isPhrase"": false, ""frequency"": ""high"", ""synonyms"": [""let us""], ""example"": ""Let's go to the park."", ""exampleTranslation"": ""我們去公園吧。"", ""tags"": [""basic""] }, ""cut"": { ""word"": ""cut"", ""translation"": ""切;削減"", ""definition"": ""to use a knife or other sharp tool to divide something"", ""partOfSpeech"": ""verb"", ""pronunciation"": ""/kʌt/"", ""difficultyLevel"": ""A2"", ""isPhrase"": false, ""frequency"": ""high"", ""synonyms"": [""slice"", ""chop"", ""reduce""], ""example"": ""Please cut the apple."", ""exampleTranslation"": ""請切蘋果。"", ""tags"": [""action""] }, ""her"": { ""word"": ""her"", ""translation"": ""她的;她"", ""definition"": ""belonging to or associated with a female"", ""partOfSpeech"": ""pronoun"", ""pronunciation"": ""/hər/"", ""difficultyLevel"": ""A1"", ""isPhrase"": false, ""frequency"": ""very_high"", ""synonyms"": [""hers""], ""example"": ""This is her book."", ""exampleTranslation"": ""這是她的書。"", ""tags"": [""basic"", ""pronoun""] }, ""some"": { ""word"": ""some"", ""translation"": ""一些"", ""definition"": ""an unspecified amount or number of"", ""partOfSpeech"": ""determiner"", ""pronunciation"": ""/sʌm/"", ""difficultyLevel"": ""A1"", ""isPhrase"": false, ""frequency"": ""very_high"", ""synonyms"": [""several"", ""a few""], ""example"": ""I need some help."", ""exampleTranslation"": ""我需要一些幫助。"", ""tags"": [""basic""] }, ""slack"": { ""word"": ""slack"", ""translation"": ""寬鬆;懈怠"", ""definition"": ""looseness; lack of tension"", ""partOfSpeech"": ""noun"", ""pronunciation"": ""/slæk/"", ""difficultyLevel"": ""B1"", ""isPhrase"": false, ""frequency"": ""medium"", ""synonyms"": [""looseness"", ""leeway""], ""example"": ""There's too much slack in this rope."", ""exampleTranslation"": ""這條繩子太鬆了。"", ""tags"": [""physical""] }, ""until"": { ""word"": ""until"", ""translation"": ""直到"", ""definition"": ""up to a particular time"", ""partOfSpeech"": ""preposition"", ""pronunciation"": ""/ʌnˈtɪl/"", ""difficultyLevel"": ""A2"", ""isPhrase"": false, ""frequency"": ""high"", ""synonyms"": [""till"", ""up to""], ""example"": ""Wait until tomorrow."", ""exampleTranslation"": ""等到明天。"", ""tags"": [""time""] }, ""gets"": { ""word"": ""gets"", ""translation"": ""變得;獲得"", ""definition"": ""becomes or obtains (third person singular)"", ""partOfSpeech"": ""verb"", ""pronunciation"": ""/ɡets/"", ""difficultyLevel"": ""A1"", ""isPhrase"": false, ""frequency"": ""very_high"", ""synonyms"": [""becomes"", ""obtains""], ""example"": ""It gets cold at night."", ""exampleTranslation"": ""晚上會變冷。"", ""tags"": [""basic""] }, ""used"": { ""word"": ""used"", ""translation"": ""習慣的"", ""definition"": ""familiar with something (used to)"", ""partOfSpeech"": ""adjective"", ""pronunciation"": ""/juːzd/"", ""difficultyLevel"": ""A2"", ""isPhrase"": false, ""frequency"": ""high"", ""synonyms"": [""accustomed"", ""familiar""], ""example"": ""I'm not used to this weather."", ""exampleTranslation"": ""我不習慣這種天氣。"", ""tags"": [""state""] }, ""to"": { ""word"": ""to"", ""translation"": ""到;向"", ""definition"": ""preposition expressing direction"", ""partOfSpeech"": ""preposition"", ""pronunciation"": ""/tu/"", ""difficultyLevel"": ""A1"", ""isPhrase"": false, ""frequency"": ""very_high"", ""synonyms"": [], ""example"": ""I'm going to school."", ""exampleTranslation"": ""我要去學校。"", ""tags"": [""basic""] }, ""workflow"": { ""word"": ""workflow"", ""translation"": ""工作流程"", ""definition"": ""the sequence of processes through which work passes"", ""partOfSpeech"": ""noun"", ""pronunciation"": ""/ˈwɜːrkfloʊ/"", ""difficultyLevel"": ""B2"", ""isPhrase"": false, ""frequency"": ""medium"", ""synonyms"": [""process"", ""procedure"", ""system""], ""example"": ""We need to improve our workflow."", ""exampleTranslation"": ""我們需要改善工作流程。"", ""tags"": [""work"", ""process""] }, ""cut someone some slack"": { ""word"": ""cut someone some slack"", ""translation"": ""對某人寬容一點"", ""definition"": ""to be more lenient or forgiving with someone"", ""partOfSpeech"": ""idiom"", ""pronunciation"": ""/kʌt ˈsʌmwʌn sʌm slæk/"", ""difficultyLevel"": ""B2"", ""isPhrase"": true, ""frequency"": ""medium"", ""synonyms"": [""be lenient"", ""be forgiving"", ""give leeway""], ""example"": ""Cut him some slack, he's new here."", ""exampleTranslation"": ""對他寬容一點,他是新來的。"", ""tags"": [""idiom"", ""workplace"", ""tolerance""] } } }"; } private SentenceAnalysisData ParseGeminiResponse(string response, string originalText, string userLevel) { try { // Clean the response to extract JSON var jsonMatch = Regex.Match(response, @"\{.*\}", RegexOptions.Singleline); if (!jsonMatch.Success) { throw new InvalidOperationException("Invalid JSON response from Gemini"); } var jsonResponse = jsonMatch.Value; var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var parsedResponse = JsonSerializer.Deserialize(jsonResponse, options) ?? throw new InvalidOperationException("Failed to parse Gemini response"); return ConvertToAnalysisData(parsedResponse, originalText, userLevel); } catch (Exception ex) { _logger.LogError(ex, "Error parsing Gemini response: {Response}", response); return CreateFallbackResponse(originalText, userLevel); } } private SentenceAnalysisData ConvertToAnalysisData(GeminiAnalysisResponse response, string originalText, string userLevel) { var analysisData = new SentenceAnalysisData { OriginalText = originalText, SentenceMeaning = response.SentenceMeaning ?? string.Empty, GrammarCorrection = response.GrammarCorrection != null ? new GrammarCorrectionDto { HasErrors = response.GrammarCorrection.HasErrors, CorrectedText = response.GrammarCorrection.CorrectedText ?? originalText, Corrections = response.GrammarCorrection.Corrections?.Select(c => new GrammarErrorDto { Error = c.Error ?? string.Empty, Correction = c.Correction ?? string.Empty, Type = c.Type ?? string.Empty, Explanation = c.Explanation ?? string.Empty, Severity = "medium" }).ToList() ?? new() } : null, Metadata = new AnalysisMetadata { UserLevel = userLevel, ProcessingDate = DateTime.UtcNow, AnalysisModel = "gemini-pro" } }; // Process vocabulary analysis if (response.VocabularyAnalysis != null) { foreach (var (word, analysis) in response.VocabularyAnalysis) { analysisData.VocabularyAnalysis[word] = new VocabularyAnalysisDto { Word = analysis.Word ?? word, Translation = analysis.Translation ?? string.Empty, Definition = analysis.Definition ?? string.Empty, PartOfSpeech = analysis.PartOfSpeech ?? string.Empty, Pronunciation = analysis.Pronunciation ?? $"/{word}/", DifficultyLevel = analysis.DifficultyLevel ?? "A1", IsPhrase = analysis.IsPhrase, Frequency = analysis.Frequency ?? "medium", Synonyms = analysis.Synonyms ?? new List(), Example = analysis.Example, ExampleTranslation = analysis.ExampleTranslation, Tags = analysis.Tags ?? new List() }; } } // Calculate statistics analysisData.Statistics = CalculateStatistics(analysisData.VocabularyAnalysis, userLevel); return analysisData; } private AnalysisStatistics CalculateStatistics(Dictionary vocabulary, string userLevel) { var userIndex = Array.IndexOf(_cefrLevels, userLevel); var stats = new AnalysisStatistics(); foreach (var word in vocabulary.Values) { var wordIndex = Array.IndexOf(_cefrLevels, word.DifficultyLevel); if (word.IsPhrase) { stats.Phrases++; } else if (wordIndex < userIndex) { stats.SimpleWords++; } else if (wordIndex == userIndex) { stats.ModerateWords++; } else { stats.DifficultWords++; } } stats.TotalWords = vocabulary.Count; stats.UniqueWords = vocabulary.Count; stats.AverageDifficulty = userLevel; // Simplified calculation return stats; } private SentenceAnalysisData CreateFallbackResponse(string originalText, string userLevel) { _logger.LogWarning("Using fallback response for text: {Text}", originalText); return new SentenceAnalysisData { OriginalText = originalText, SentenceMeaning = "分析過程中發生錯誤,請稍後再試。", Metadata = new AnalysisMetadata { UserLevel = userLevel, ProcessingDate = DateTime.UtcNow, AnalysisModel = "fallback" } }; } } // Gemini API response models internal class GeminiApiResponse { public List? Candidates { 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; } } // Internal models for Gemini response parsing internal class GeminiAnalysisResponse { public GeminiGrammarCorrection? GrammarCorrection { get; set; } public string? SentenceMeaning { get; set; } public Dictionary? VocabularyAnalysis { get; set; } } internal class GeminiGrammarCorrection { public bool HasErrors { get; set; } public string? CorrectedText { get; set; } public List? Corrections { get; set; } } internal class GeminiGrammarError { public string? Error { get; set; } public string? Correction { get; set; } public string? Type { get; set; } public string? Explanation { get; set; } } internal class GeminiVocabularyAnalysis { 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 IsPhrase { get; set; } public string? Frequency { get; set; } public List? Synonyms { get; set; } public string? Example { get; set; } public string? ExampleTranslation { get; set; } public List? Tags { get; set; } }