diff --git a/backend/DramaLing.Api/Controllers/AIController.cs b/backend/DramaLing.Api/Controllers/AIController.cs deleted file mode 100644 index 8ebb48e..0000000 --- a/backend/DramaLing.Api/Controllers/AIController.cs +++ /dev/null @@ -1,188 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using DramaLing.Api.Models.DTOs; -using DramaLing.Api.Services; -using System.Diagnostics; -using System.Security.Claims; - -namespace DramaLing.Api.Controllers; - -[ApiController] -[Route("api/ai")] -public class AIController : ControllerBase -{ - private readonly IGeminiService _geminiService; - private readonly IAnalysisCacheService _cacheService; - private readonly IUsageTrackingService _usageTrackingService; - private readonly ILogger _logger; - - public AIController( - IGeminiService geminiService, - IAnalysisCacheService cacheService, - IUsageTrackingService usageTrackingService, - ILogger logger) - { - _geminiService = geminiService; - _cacheService = cacheService; - _usageTrackingService = usageTrackingService; - _logger = logger; - } - - /// - /// 智能分析英文句子 - /// - /// 分析請求 - /// 分析結果 - [HttpPost("analyze-sentence")] - public async Task> AnalyzeSentence( - [FromBody] SentenceAnalysisRequest request) - { - var requestId = Guid.NewGuid().ToString(); - var stopwatch = Stopwatch.StartNew(); - - try - { - // For testing without auth - use dummy user ID - var userId = "test-user-id"; - - _logger.LogInformation("Processing sentence analysis request {RequestId} for user {UserId}", - requestId, userId); - - // Input validation - if (!ModelState.IsValid) - { - return BadRequest(CreateErrorResponse("INVALID_INPUT", "輸入格式錯誤", - ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(), - requestId)); - } - - // For testing - skip usage limits - // var userGuid = Guid.Parse(userId); - // var canUseService = await _usageTrackingService.CheckUsageLimitAsync(userGuid); - // if (!canUseService) - // { - // return StatusCode(429, CreateErrorResponse("RATE_LIMIT_EXCEEDED", "已超過每日使用限制", - // new { limit = 5, resetTime = DateTime.UtcNow.Date.AddDays(1) }, - // requestId)); - // } - - // Check cache first - var cachedResult = await _cacheService.GetCachedAnalysisAsync(request.InputText); - if (cachedResult != null) - { - _logger.LogInformation("Returning cached result for request {RequestId}", requestId); - - // Parse cached result - var cachedData = System.Text.Json.JsonSerializer.Deserialize(cachedResult.AnalysisResult); - if (cachedData != null) - { - return Ok(new SentenceAnalysisResponse - { - Success = true, - ProcessingTime = stopwatch.Elapsed.TotalSeconds, - Data = cachedData - }); - } - } - - // Perform AI analysis - var options = request.Options ?? new AnalysisOptions(); - var analysisData = await _geminiService.AnalyzeSentenceAsync( - request.InputText, request.UserLevel, options); - - // Cache the result - await _cacheService.SetCachedAnalysisAsync(request.InputText, analysisData, TimeSpan.FromHours(24)); - - // Skip usage tracking for testing - // await _usageTrackingService.RecordSentenceAnalysisAsync(userGuid); - - stopwatch.Stop(); - analysisData.Metadata.ProcessingDate = DateTime.UtcNow; - - _logger.LogInformation("Sentence analysis completed for request {RequestId} in {ElapsedMs}ms", - requestId, stopwatch.ElapsedMilliseconds); - - return Ok(new SentenceAnalysisResponse - { - Success = true, - ProcessingTime = stopwatch.Elapsed.TotalSeconds, - Data = analysisData - }); - } - catch (ArgumentException ex) - { - _logger.LogWarning(ex, "Invalid input for request {RequestId}", requestId); - return BadRequest(CreateErrorResponse("INVALID_INPUT", ex.Message, null, requestId)); - } - catch (InvalidOperationException ex) - { - _logger.LogError(ex, "AI service error for request {RequestId}", requestId); - return StatusCode(500, CreateErrorResponse("AI_SERVICE_ERROR", "AI服務暫時不可用", null, requestId)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected error processing request {RequestId}", requestId); - return StatusCode(500, CreateErrorResponse("INTERNAL_ERROR", "伺服器內部錯誤", null, requestId)); - } - } - - /// - /// 健康檢查端點 - /// - [HttpGet("health")] - [AllowAnonymous] - public ActionResult GetHealth() - { - return Ok(new - { - Status = "Healthy", - Service = "AI Analysis Service", - Timestamp = DateTime.UtcNow, - Version = "1.0" - }); - } - - private string GetCurrentUserId() - { - var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier) - ?? User.FindFirst("sub") - ?? User.FindFirst("user_id"); - - if (userIdClaim?.Value == null) - { - throw new UnauthorizedAccessException("用戶ID未找到"); - } - - return userIdClaim.Value; - } - - private ApiErrorResponse CreateErrorResponse(string code, string message, object? details, string requestId) - { - var suggestions = GetSuggestionsForError(code); - - return new ApiErrorResponse - { - Success = false, - Error = new ApiError - { - Code = code, - Message = message, - Details = details, - Suggestions = suggestions - }, - RequestId = requestId, - Timestamp = DateTime.UtcNow - }; - } - - private List GetSuggestionsForError(string errorCode) - { - return errorCode switch - { - "INVALID_INPUT" => new List { "請檢查輸入格式", "確保文本長度在限制內" }, - "RATE_LIMIT_EXCEEDED" => new List { "升級到Premium帳戶以獲得無限使用", "明天重新嘗試" }, - "AI_SERVICE_ERROR" => new List { "請稍後重試", "如果問題持續,請聯繫客服" }, - _ => new List { "請稍後重試" } - }; - } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Models/DTOs/AIAnalysisDto.cs b/backend/DramaLing.Api/Models/DTOs/AIAnalysisDto.cs index 9934174..a9aeca6 100644 --- a/backend/DramaLing.Api/Models/DTOs/AIAnalysisDto.cs +++ b/backend/DramaLing.Api/Models/DTOs/AIAnalysisDto.cs @@ -22,7 +22,7 @@ public class AnalysisOptions public bool IncludeGrammarCheck { get; set; } = true; public bool IncludeVocabularyAnalysis { get; set; } = true; public bool IncludeTranslation { get; set; } = true; - public bool IncludePhraseDetection { get; set; } = true; + public bool IncludeIdiomDetection { get; set; } = true; public bool IncludeExamples { get; set; } = true; } @@ -76,7 +76,7 @@ public class VocabularyAnalysisDto public string PartOfSpeech { get; set; } = string.Empty; public string Pronunciation { get; set; } = string.Empty; public string DifficultyLevel { get; set; } = string.Empty; - public bool IsPhrase { get; set; } + public bool IsIdiom { get; set; } public string Frequency { get; set; } = string.Empty; public List Synonyms { get; set; } = new(); public string? Example { get; set; } @@ -91,7 +91,7 @@ public class AnalysisStatistics public int SimpleWords { get; set; } public int ModerateWords { get; set; } public int DifficultWords { get; set; } - public int Phrases { get; set; } + public int Idioms { get; set; } public string AverageDifficulty { get; set; } = string.Empty; } diff --git a/backend/DramaLing.Api/Program.cs b/backend/DramaLing.Api/Program.cs index 8a7e514..66aebcd 100644 --- a/backend/DramaLing.Api/Program.cs +++ b/backend/DramaLing.Api/Program.cs @@ -36,13 +36,12 @@ else // Custom Services builder.Services.AddScoped(); builder.Services.AddHttpClient(); -builder.Services.AddScoped(); +// 快取系統已移除,每次都直接調用 AI API builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -// Background Services -builder.Services.AddHostedService(); +// Background Services (快取清理服務已移除) // Authentication - 從環境變數讀取 JWT 配置 var supabaseUrl = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_URL") diff --git a/backend/DramaLing.Api/Services/GeminiService.cs b/backend/DramaLing.Api/Services/GeminiService.cs index f9b0652..7a9623b 100644 --- a/backend/DramaLing.Api/Services/GeminiService.cs +++ b/backend/DramaLing.Api/Services/GeminiService.cs @@ -1,6 +1,5 @@ using DramaLing.Api.Models.DTOs; using System.Text.Json; -using System.Text.RegularExpressions; using System.Text; namespace DramaLing.Api.Services; @@ -14,7 +13,6 @@ 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) @@ -25,10 +23,10 @@ public class GeminiService : IGeminiService _apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY") ?? configuration["AI:GeminiApiKey"] ?? configuration["Gemini:ApiKey"] - ?? "mock-api-key"; // For development without Gemini + ?? throw new InvalidOperationException("Gemini API Key not configured"); _logger.LogInformation("GeminiService initialized with API key: {ApiKeyStart}...", - _apiKey.Length > 10 ? _apiKey.Substring(0, 10) : "mock"); + _apiKey.Length > 10 ? _apiKey.Substring(0, 10) : "[key-not-set]"); _httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0"); } @@ -42,13 +40,22 @@ public class GeminiService : IGeminiService _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); + // 使用簡單的 prompt 直接調用 Gemini API + var prompt = $"Translate this English sentence to Traditional Chinese and provide grammar analysis: \"{inputText}\""; + + 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"); + } + + // 直接使用 AI 的回應創建分析數據 + var analysisData = CreateAnalysisFromAIResponse(inputText, userLevel, aiResponse); var processingTime = (DateTime.UtcNow - startTime).TotalSeconds; - analysisData.Metadata.ProcessingDate = DateTime.UtcNow; - _logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime); return analysisData; @@ -60,89 +67,108 @@ public class GeminiService : IGeminiService } } - private string BuildAnalysisPrompt(string inputText, string userLevel, AnalysisOptions options) + private SentenceAnalysisData CreateAnalysisFromAIResponse(string inputText, string userLevel, string aiResponse) { - var userIndex = Array.IndexOf(_cefrLevels, userLevel); - var targetLevels = GetTargetLevels(userIndex); + _logger.LogInformation("Creating analysis from AI response: {ResponsePreview}...", + aiResponse.Substring(0, Math.Min(100, aiResponse.Length))); - return $@" -請分析以下英文句子並以JSON格式回應: -句子: ""{inputText}"" -學習者程度: {userLevel} + // 直接使用 AI 回應作為分析結果 + var analysisData = new SentenceAnalysisData + { + OriginalText = inputText, + SentenceMeaning = aiResponse, // 直接使用 AI 的完整回應作為分析結果 + VocabularyAnalysis = CreateBasicVocabularyFromText(inputText, aiResponse), + Metadata = new AnalysisMetadata + { + UserLevel = userLevel, + ProcessingDate = DateTime.UtcNow, + AnalysisModel = "gemini-1.5-flash", + AnalysisVersion = "1.0" + } + }; -請提供完整的分析,包含: + // 檢查是否有語法錯誤(基於 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" + } + } + }; + } -1. 語法檢查:檢查是否有語法錯誤,如有則提供修正建議 -2. 詞彙分析:分析句子中每個有意義的詞彙 -3. 中文翻譯:提供自然流暢的繁體中文翻譯 -4. 慣用語識別:識別句子中的慣用語和片語 + // 計算統計(使用統一的慣用語術語) + analysisData.Statistics = CalculateStatistics(analysisData.VocabularyAnalysis, userLevel); -詞彙分析要求: -- 為每個詞彙標註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格式,不要包含任何其他文字。"; + return analysisData; } - private string[] GetTargetLevels(int userIndex) + private Dictionary CreateBasicVocabularyFromText(string inputText, string aiResponse) { - var targets = new List(); + var words = inputText.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var result = new Dictionary(); - if (userIndex + 1 < _cefrLevels.Length) - targets.Add(_cefrLevels[userIndex + 1]); + foreach (var word in words.Take(15)) + { + var cleanWord = word.ToLower().Trim('.', ',', '!', '?', ';', ':', '"', '\''); + if (string.IsNullOrEmpty(cleanWord) || cleanWord.Length < 2) continue; - if (userIndex + 2 < _cefrLevels.Length) - targets.Add(_cefrLevels[userIndex + 2]); + 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 targets.ToArray(); + 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) { - // 暫時使用模擬數據,稍後可替換為真實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 @@ -159,9 +185,9 @@ public class GeminiService : IGeminiService }, generationConfig = new { - temperature = 0.3, - topK = 1, - topP = 1, + temperature = 0.7, + topK = 40, + topP = 0.95, maxOutputTokens = 2000 } }; @@ -169,422 +195,24 @@ public class GeminiService : IGeminiService 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); + 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(); var geminiResponse = JsonSerializer.Deserialize(responseJson); - return geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text ?? string.Empty; + var aiText = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text ?? string.Empty; + + _logger.LogInformation("Gemini API returned: {ResponseLength} characters", aiText.Length); + + return aiText; } catch (Exception ex) { - _logger.LogError(ex, "Gemini API call failed, falling back to mock response"); - return GetMockResponse(); + _logger.LogError(ex, "Gemini API call failed"); + throw; } } - - 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 @@ -606,43 +234,4 @@ internal class GeminiContent 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; } } \ No newline at end of file