246 lines
8.5 KiB
C#
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學習者使用基礎題型建立信心",
|
|
"簡單詞彙" => "簡單詞彙重點練習應用和拼寫",
|
|
"適中詞彙" => "適中詞彙進行全方位練習,包括口說",
|
|
"困難詞彙" => "困難詞彙回歸基礎重建記憶",
|
|
_ => "系統智能選擇最適合的複習方式"
|
|
};
|
|
}
|
|
} |