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

246 lines
8.5 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 ILogger<QuestionGeneratorService> _logger;
public QuestionGeneratorService(
DramaLingDbContext context,
ILogger<QuestionGeneratorService> logger)
{
_context = context;
_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)
{
// 從相同用戶的其他詞卡中選擇3個干擾選項
var distractors = await _context.Flashcards
.Where(f => f.UserId == flashcard.UserId &&
f.Id != flashcard.Id &&
!f.IsArchived)
.OrderBy(x => Guid.NewGuid()) // 隨機排序
.Take(3)
.Select(f => f.Word)
.ToListAsync();
// 如果沒有足夠的詞卡,添加一些預設選項
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));
distractors.AddRange(availableDefaults.Take(3 - distractors.Count));
}
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 "困難詞彙";
}
/// <summary>
/// 獲取選擇原因說明
/// </summary>
private string GetSelectionReason(string selectedMode, int userLevel, int wordLevel)
{
var context = GetAdaptationContext(userLevel, wordLevel);
return context switch
{
"A1學習者" => "A1學習者使用基礎題型建立信心",
"簡單詞彙" => "簡單詞彙重點練習應用和拼寫",
"適中詞彙" => "適中詞彙進行全方位練習,包括口說",
"困難詞彙" => "困難詞彙回歸基礎重建記憶",
_ => "系統智能選擇最適合的複習方式"
};
}
}