dramaling-vocab-learning/backend/DramaLing.Api/Services/QuestionGeneratorService.cs

284 lines
10 KiB
C#

using DramaLing.Api.Data;
using DramaLing.Api.Models.DTOs.SpacedRepetition;
using DramaLing.Api.Models.Entities;
using Microsoft.EntityFrameworkCore;
namespace DramaLing.Api.Services;
/// <summary>
/// 題目生成服務介面
/// </summary>
public interface IQuestionGeneratorService
{
Task<QuestionData> GenerateQuestionAsync(Guid flashcardId, string questionType);
}
/// <summary>
/// 題目生成服務實現
/// </summary>
public class QuestionGeneratorService : IQuestionGeneratorService
{
private readonly DramaLingDbContext _context;
private readonly IOptionsVocabularyService _optionsVocabularyService;
private readonly ILogger<QuestionGeneratorService> _logger;
public QuestionGeneratorService(
DramaLingDbContext context,
IOptionsVocabularyService optionsVocabularyService,
ILogger<QuestionGeneratorService> logger)
{
_context = context;
_optionsVocabularyService = optionsVocabularyService;
_logger = logger;
}
/// <summary>
/// 根據題型生成對應的題目數據
/// </summary>
public async Task<QuestionData> GenerateQuestionAsync(Guid flashcardId, string questionType)
{
var flashcard = await _context.Flashcards.FindAsync(flashcardId);
if (flashcard == null)
throw new ArgumentException($"Flashcard {flashcardId} not found");
_logger.LogInformation("Generating {QuestionType} question for flashcard {FlashcardId}, word: {Word}",
questionType, flashcardId, flashcard.Word);
return questionType switch
{
"vocab-choice" => await GenerateVocabChoiceAsync(flashcard),
"sentence-fill" => GenerateFillBlankQuestion(flashcard),
"sentence-reorder" => GenerateReorderQuestion(flashcard),
"sentence-listening" => await GenerateSentenceListeningAsync(flashcard),
_ => new QuestionData
{
QuestionType = questionType,
CorrectAnswer = flashcard.Word
}
};
}
/// <summary>
/// 生成詞彙選擇題選項
/// </summary>
private async Task<QuestionData> GenerateVocabChoiceAsync(Flashcard flashcard)
{
var distractors = new List<string>();
// 🆕 優先嘗試使用智能詞彙庫生成選項
try
{
// 直接使用 Flashcard 的屬性
var cefrLevel = flashcard.DifficultyLevel ?? "B1"; // 預設為 B1
var partOfSpeech = flashcard.PartOfSpeech ?? "noun"; // 預設為 noun
_logger.LogDebug("Attempting to generate smart distractors for '{Word}' (CEFR: {CEFR}, PartOfSpeech: {PartOfSpeech})",
flashcard.Word, cefrLevel, partOfSpeech);
// 檢查詞彙庫是否有足夠詞彙
var hasSufficientVocab = await _optionsVocabularyService.HasSufficientVocabularyAsync(cefrLevel, partOfSpeech);
if (hasSufficientVocab)
{
var smartDistractors = await _optionsVocabularyService.GenerateDistractorsAsync(
flashcard.Word, cefrLevel, partOfSpeech, 3);
if (smartDistractors.Any())
{
distractors.AddRange(smartDistractors);
_logger.LogInformation("Successfully generated {Count} smart distractors for '{Word}'",
smartDistractors.Count, flashcard.Word);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to generate smart distractors for '{Word}', falling back to user vocabulary",
flashcard.Word);
}
// 🔄 回退機制:如果智能詞彙庫無法提供足夠選項,使用原有邏輯
if (distractors.Count < 3)
{
_logger.LogInformation("Using fallback method for '{Word}' (current distractors: {Count})",
flashcard.Word, distractors.Count);
var userDistractors = await _context.Flashcards
.Where(f => f.UserId == flashcard.UserId &&
f.Id != flashcard.Id &&
!f.IsArchived &&
!distractors.Contains(f.Word)) // 避免重複
.OrderBy(x => Guid.NewGuid())
.Take(3 - distractors.Count)
.Select(f => f.Word)
.ToListAsync();
distractors.AddRange(userDistractors);
// 如果還是不夠,使用預設選項
while (distractors.Count < 3)
{
var defaultOptions = new[] { "example", "sample", "test", "word", "basic", "simple", "common", "usual" };
var availableDefaults = defaultOptions
.Where(opt => opt != flashcard.Word && !distractors.Contains(opt));
var neededCount = 3 - distractors.Count;
distractors.AddRange(availableDefaults.Take(neededCount));
// 防止無限循環
if (!availableDefaults.Any())
break;
}
}
var options = new List<string> { flashcard.Word };
options.AddRange(distractors.Take(3));
// 隨機打亂選項順序
var shuffledOptions = options.OrderBy(x => Guid.NewGuid()).ToArray();
return new QuestionData
{
QuestionType = "vocab-choice",
Options = shuffledOptions,
CorrectAnswer = flashcard.Word
};
}
/// <summary>
/// 生成填空題
/// </summary>
private QuestionData GenerateFillBlankQuestion(Flashcard flashcard)
{
if (string.IsNullOrEmpty(flashcard.Example))
{
throw new ArgumentException($"Flashcard {flashcard.Id} has no example sentence for fill-blank question");
}
// 在例句中將目標詞彙替換為空白
var blankedSentence = flashcard.Example.Replace(flashcard.Word, "______", StringComparison.OrdinalIgnoreCase);
// 如果沒有替換成功,嘗試其他變化形式
if (blankedSentence == flashcard.Example)
{
// TODO: 未來可以實現更智能的詞形變化識別
blankedSentence = flashcard.Example + " (請填入: " + flashcard.Word + ")";
}
return new QuestionData
{
QuestionType = "sentence-fill",
BlankedSentence = blankedSentence,
CorrectAnswer = flashcard.Word,
Sentence = flashcard.Example
};
}
/// <summary>
/// 生成例句重組題
/// </summary>
private QuestionData GenerateReorderQuestion(Flashcard flashcard)
{
if (string.IsNullOrEmpty(flashcard.Example))
{
throw new ArgumentException($"Flashcard {flashcard.Id} has no example sentence for reorder question");
}
// 將例句拆分為單字並打亂順序
var words = flashcard.Example
.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.Select(word => word.Trim('.',',','!','?',';',':')) // 移除標點符號
.Where(word => !string.IsNullOrEmpty(word))
.ToArray();
// 隨機打亂順序
var scrambledWords = words.OrderBy(x => Guid.NewGuid()).ToArray();
return new QuestionData
{
QuestionType = "sentence-reorder",
ScrambledWords = scrambledWords,
CorrectAnswer = flashcard.Example,
Sentence = flashcard.Example
};
}
/// <summary>
/// 生成例句聽力題選項
/// </summary>
private async Task<QuestionData> GenerateSentenceListeningAsync(Flashcard flashcard)
{
if (string.IsNullOrEmpty(flashcard.Example))
{
throw new ArgumentException($"Flashcard {flashcard.Id} has no example sentence for listening question");
}
// 從其他詞卡中選擇3個例句作為干擾選項
var distractorSentences = await _context.Flashcards
.Where(f => f.UserId == flashcard.UserId &&
f.Id != flashcard.Id &&
!f.IsArchived &&
!string.IsNullOrEmpty(f.Example))
.OrderBy(x => Guid.NewGuid())
.Take(3)
.Select(f => f.Example!)
.ToListAsync();
// 如果沒有足夠的例句,添加預設選項
while (distractorSentences.Count < 3)
{
var defaultSentences = new[]
{
"This is a simple example sentence.",
"I think this is a good opportunity.",
"She decided to take a different approach.",
"They managed to solve the problem quickly."
};
var availableDefaults = defaultSentences
.Where(sent => sent != flashcard.Example && !distractorSentences.Contains(sent));
distractorSentences.AddRange(availableDefaults.Take(3 - distractorSentences.Count));
}
var options = new List<string> { flashcard.Example };
options.AddRange(distractorSentences.Take(3));
// 隨機打亂選項順序
var shuffledOptions = options.OrderBy(x => Guid.NewGuid()).ToArray();
return new QuestionData
{
QuestionType = "sentence-listening",
Options = shuffledOptions,
CorrectAnswer = flashcard.Example,
Sentence = flashcard.Example,
AudioUrl = $"/audio/sentences/{flashcard.Id}.mp3" // 未來音頻服務用
};
}
/// <summary>
/// 判斷是否為A1學習者
/// </summary>
public bool IsA1Learner(int userLevel) => userLevel <= 20; // 固定A1門檻
/// <summary>
/// 獲取適配情境描述
/// </summary>
public string GetAdaptationContext(int userLevel, int wordLevel)
{
var difficulty = wordLevel - userLevel;
if (userLevel <= 20) // 固定A1門檻
return "A1學習者";
if (difficulty < -10)
return "簡單詞彙";
if (difficulty >= -10 && difficulty <= 10)
return "適中詞彙";
return "困難詞彙";
}
}