using DramaLing.Api.Data; using DramaLing.Api.Models.Configuration; using DramaLing.Api.Models.Entities; using DramaLing.Api.Services.Monitoring; using DramaLing.Api.Services.AI.Gemini; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using System.Diagnostics; using System.Text.Json; using DramaLing.Api.Contracts.Services.Core; namespace DramaLing.Api.Services; /// /// 選項詞彙庫服務實作 /// 提供基於 CEFR 等級、詞性和字數的智能選項生成 /// public class OptionsVocabularyService : IOptionsVocabularyService { private readonly DramaLingDbContext _context; private readonly IMemoryCache _cache; private readonly ILogger _logger; private readonly OptionsVocabularyOptions _options; private readonly OptionsVocabularyMetrics _metrics; private readonly IGeminiClient _geminiClient; public OptionsVocabularyService( DramaLingDbContext context, IMemoryCache cache, ILogger logger, IOptions options, OptionsVocabularyMetrics metrics, IGeminiClient geminiClient) { _context = context; _cache = cache; _logger = logger; _options = options.Value; _metrics = metrics; _geminiClient = geminiClient; } /// /// 生成智能干擾選項 /// public async Task> GenerateDistractorsAsync( string targetWord, string cefrLevel, string partOfSpeech, int count = 3) { var distractorsWithDetails = await GenerateDistractorsWithDetailsAsync( targetWord, cefrLevel, partOfSpeech, count); return distractorsWithDetails.Select(v => v.Word).ToList(); } /// /// 生成智能干擾選項(含詳細資訊) /// public async Task> GenerateDistractorsWithDetailsAsync( string targetWord, string cefrLevel, string partOfSpeech, int count = 3) { var stopwatch = Stopwatch.StartNew(); try { // 記錄請求指標 _metrics.RecordGenerationRequest(cefrLevel, partOfSpeech, count); _logger.LogInformation("Generating {Count} distractors for word '{Word}' (CEFR: {CEFR}, PartOfSpeech: {PartOfSpeech})", count, targetWord, cefrLevel, partOfSpeech); // 1. 首先檢查資料庫中是否已有該詞彙的混淆選項 var cacheKey = $"quiz_options_{targetWord.ToLowerInvariant()}_{cefrLevel}_{partOfSpeech}"; if (_cache.TryGetValue(cacheKey, out List? cachedOptions) && cachedOptions != null) { _logger.LogInformation("✅ Using cached distractors for '{Word}': {Distractors}", targetWord, string.Join(", ", cachedOptions.Select(d => d.Word))); return cachedOptions.Take(count).ToList(); } // 2. 檢查資料庫中是否有現有選項 var targetWordLower = targetWord.ToLower(); var existingOptions = await _context.OptionsVocabularies .Where(ov => ov.CEFRLevel == cefrLevel && ov.PartOfSpeech == partOfSpeech && ov.IsActive && ov.Word.ToLower() != targetWordLower) .Take(count * 2) // 取更多選項以供隨機選擇 .ToListAsync(); if (existingOptions.Count >= count) { var selectedExisting = existingOptions .OrderBy(x => Guid.NewGuid()) // 隨機排序 .Take(count) .ToList(); // 快取結果 _cache.Set(cacheKey, selectedExisting, TimeSpan.FromHours(1)); _logger.LogInformation("📚 Using existing database distractors for '{Word}': {Distractors}", targetWord, string.Join(", ", selectedExisting.Select(d => d.Word))); return selectedExisting; } _logger.LogInformation("💡 Insufficient database options ({ExistingCount}/{RequiredCount}), proceeding to AI generation for '{Word}'", existingOptions.Count, count, targetWord); // 3. 使用 AI 生成新的混淆選項 var aiGeneratedOptions = await GenerateOptionsWithAI(targetWord, cefrLevel, partOfSpeech, count); // 4. 保存新生成的選項到資料庫 if (aiGeneratedOptions.Any()) { await _context.OptionsVocabularies.AddRangeAsync(aiGeneratedOptions); await _context.SaveChangesAsync(); // 快取結果 _cache.Set(cacheKey, aiGeneratedOptions, TimeSpan.FromHours(1)); } _logger.LogInformation("🤖 Successfully generated {Count} AI distractors for '{Word}': {Distractors}", aiGeneratedOptions.Count, targetWord, string.Join(", ", aiGeneratedOptions.Select(d => d.Word))); // 記錄生成完成指標 stopwatch.Stop(); _metrics.RecordGenerationDuration(stopwatch.Elapsed, aiGeneratedOptions.Count); return aiGeneratedOptions; } catch (Exception ex) { _logger.LogError(ex, "Error generating distractors for word '{Word}'", targetWord); _metrics.RecordError("generation_failed", "GenerateDistractorsWithDetailsAsync"); // 降級到固定選項 return GetFallbackDistractors(targetWord, cefrLevel, partOfSpeech, count); } } /// /// 檢查詞彙庫是否有足夠的詞彙支援選項生成 /// public async Task HasSufficientVocabularyAsync(string cefrLevel, string partOfSpeech) { try { var allowedLevels = GetAllowedCEFRLevels(cefrLevel); var count = await _context.OptionsVocabularies .Where(v => v.IsActive && allowedLevels.Contains(v.CEFRLevel) && v.PartOfSpeech == partOfSpeech) .CountAsync(); var hasSufficient = count >= _options.MinimumVocabularyThreshold; _logger.LogDebug("Vocabulary count for CEFR: {CEFR}, PartOfSpeech: {PartOfSpeech}: {Count} (sufficient: {HasSufficient})", cefrLevel, partOfSpeech, count, hasSufficient); return hasSufficient; } catch (Exception ex) { _logger.LogError(ex, "Error checking vocabulary sufficiency for CEFR: {CEFR}, PartOfSpeech: {PartOfSpeech}", cefrLevel, partOfSpeech); return false; } } /// /// 獲取允許的 CEFR 等級(包含相鄰等級) /// private static List GetAllowedCEFRLevels(string targetLevel) { var levels = new[] { "A1", "A2", "B1", "B2", "C1", "C2" }; var targetIndex = Array.IndexOf(levels, targetLevel); if (targetIndex == -1) { // 如果不是標準 CEFR 等級,只返回原等級 return new List { targetLevel }; } var allowed = new List { targetLevel }; // 加入相鄰等級(允許難度稍有差異) if (targetIndex > 0) allowed.Add(levels[targetIndex - 1]); if (targetIndex < levels.Length - 1) allowed.Add(levels[targetIndex + 1]); return allowed; } /// /// 獲取允許的 CEFR 數字等級(包含相鄰等級) /// private static List GetAllowedCEFRLevelsNumeric(int targetLevelNumeric) { var allowed = new List { targetLevelNumeric }; // 加入相鄰等級(允許難度稍有差異) if (targetLevelNumeric > 1) allowed.Add(targetLevelNumeric - 1); if (targetLevelNumeric < 6) allowed.Add(targetLevelNumeric + 1); return allowed; } /// /// 使用 AI 生成混淆選項 /// private async Task> GenerateOptionsWithAI( string targetWord, string cefrLevel, string partOfSpeech, int count) { try { var prompt = $@"Generate {count} vocabulary words that would be good wrong answers for a multiple choice quiz about the word ""{targetWord}"". Requirements: - CEFR level: {cefrLevel} - Part of speech: {partOfSpeech} - The words should be plausible but clearly different from ""{targetWord}"" - Avoid obviously wrong answers - Make the words challenging but fair for language learners Please respond with ONLY a JSON array of strings, like: [""word1"", ""word2"", ""word3""]"; var response = await _geminiClient.CallGeminiAPIAsync(prompt); // 解析 AI 回應 var cleanedResponse = ExtractJsonFromResponse(response); var generatedWords = JsonSerializer.Deserialize(cleanedResponse); if (generatedWords == null || generatedWords.Length == 0) { _logger.LogWarning("AI returned empty or invalid response for word '{Word}'", targetWord); return GetFallbackDistractors(targetWord, cefrLevel, partOfSpeech, count); } // 轉換為 OptionsVocabulary 實體,並過濾掉與目標詞彙相同的詞彙 var targetWordLower = targetWord.ToLower(); var filteredWords = generatedWords .Where(word => !string.IsNullOrWhiteSpace(word) && word.Trim().ToLower() != targetWordLower) .Take(count); var options = filteredWords.Select(word => new OptionsVocabulary { Id = Guid.NewGuid(), Word = word.Trim(), CEFRLevel = cefrLevel, PartOfSpeech = partOfSpeech, IsActive = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }).ToList(); _logger.LogInformation("Filtered AI generated words from {OriginalCount} to {FilteredCount} options for '{TargetWord}'", generatedWords.Length, options.Count, targetWord); // 計算字數長度 foreach (var option in options) { option.CalculateWordLength(); } return options; } catch (Exception ex) { _logger.LogError(ex, "AI generation failed for word '{Word}', falling back to fixed options", targetWord); return GetFallbackDistractors(targetWord, cefrLevel, partOfSpeech, count); } } /// /// 降級固定選項(當 AI 生成失敗時使用) /// private List GetFallbackDistractors( string targetWord, string cefrLevel, string partOfSpeech, int count) { // 根據詞性提供不同的固定選項(避免常見目標詞彙) var fallbackWords = partOfSpeech.ToLower() switch { "verb" => new[] { "accomplish", "construct", "transform", "establish", "generate", "implement", "maintain", "organize", "validate", "coordinate" }, "noun" => new[] { "solution", "approach", "framework", "component", "structure", "mechanism", "principle", "strategy", "technique", "procedure" }, "adjective" => new[] { "efficient", "comprehensive", "innovative", "sophisticated", "fundamental", "essential", "remarkable", "outstanding", "significant", "substantial" }, "adverb" => new[] { "thoroughly", "efficiently", "precisely", "consistently", "systematically", "effectively", "accurately", "appropriately", "specifically", "particularly" }, _ => new[] { "element", "aspect", "feature", "component", "factor", "criteria", "parameter", "attribute", "characteristic", "specification" } }; var targetWordLower = targetWord.ToLower(); var selectedWords = fallbackWords .Where(word => word.ToLower() != targetWordLower) .Take(count * 2) // 取更多選項用於隨機選擇 .OrderBy(x => Guid.NewGuid()) // 隨機排序 .Take(count); _logger.LogInformation("Using fallback distractors for '{TargetWord}': {FallbackWords}", targetWord, string.Join(", ", selectedWords)); return selectedWords .Select(word => new OptionsVocabulary { Id = Guid.NewGuid(), Word = word, CEFRLevel = cefrLevel, PartOfSpeech = partOfSpeech, IsActive = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }) .ToList(); } /// /// 從 AI 回應中提取 JSON 內容 /// private string ExtractJsonFromResponse(string response) { // 移除可能的 markdown 代碼塊標記 var cleaned = response.Replace("```json", "").Replace("```", "").Trim(); // 找到第一個 [ 和最後一個 ] var startIndex = cleaned.IndexOf('['); var endIndex = cleaned.LastIndexOf(']'); if (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex) { return cleaned.Substring(startIndex, endIndex - startIndex + 1); } return cleaned; } }