feat: AI-powered quiz generation with caching

- Add Quiz model to store generated questions in DB
- Integrate OpenRouter API to generate quiz from lesson content
- Cache quiz results to avoid regenerating on every request
- Extract text from TipTap JSON and send to AI
- Fallback to default questions if AI fails or no API key

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-02-06 10:58:15 +00:00
parent bed7e440c1
commit f39680d714
2 changed files with 136 additions and 25 deletions

View File

@ -140,34 +140,132 @@ export class LessonsService {
}
async generateQuiz(lessonId: string): Promise<any> {
const lesson = await this.prisma.lesson.findUnique({ where: { id: lessonId } });
const lesson = await this.prisma.lesson.findUnique({
where: { id: lessonId },
include: { quiz: true },
});
if (!lesson) throw new NotFoundException('Lesson not found');
// Mock quiz - in production, parse lesson.content and call AI to generate questions
return {
questions: [
{
id: '1',
type: 'multiple_choice',
question: 'Какой из следующих вариантов верен?',
options: ['Вариант A', 'Вариант B', 'Вариант C', 'Вариант D'],
correctAnswer: 0,
// Return cached quiz if exists
if (lesson.quiz) {
return { questions: lesson.quiz.questions };
}
// Generate quiz using AI
const questions = await this.generateQuizWithAI(lesson.content, lesson.title);
// Save to database
await this.prisma.quiz.create({
data: { lessonId, questions: questions as any },
});
return { questions };
}
private async generateQuizWithAI(content: any, lessonTitle: string): Promise<any[]> {
try {
// Extract text from TipTap JSON content
const textContent = this.extractTextFromContent(content);
if (!textContent || textContent.length < 50) {
// Not enough content, return simple quiz
return this.getDefaultQuiz();
}
// Call OpenRouter to generate quiz
const apiKey = process.env.OPENROUTER_API_KEY;
const baseUrl = process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1';
const model = process.env.AI_MODEL_FREE || 'openai/gpt-4o-mini';
if (!apiKey) {
return this.getDefaultQuiz();
}
const prompt = `На основе следующего содержания урока "${lessonTitle}" сгенерируй 3-5 вопросов с вариантами ответов для теста.
Содержание урока:
${textContent.slice(0, 3000)}
Верни JSON массив вопросов в формате:
[
{
"id": "1",
"question": "Вопрос?",
"options": ["Вариант А", "Вариант Б", "Вариант В", "Вариант Г"],
"correctAnswer": 0
}
]
Только JSON, без markdown.`;
const response = await fetch(`${baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
{
id: '2',
type: 'multiple_choice',
question: 'Выберите правильный ответ:',
options: ['Ответ 1', 'Ответ 2', 'Ответ 3'],
correctAnswer: 1,
},
{
id: '3',
type: 'multiple_choice',
question: 'Что из перечисленного правильно?',
options: ['Первое', 'Второе', 'Третье', 'Четвёртое'],
correctAnswer: 2,
},
],
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
}),
});
if (!response.ok) {
return this.getDefaultQuiz();
}
const data: any = await response.json();
const aiResponse = data.choices?.[0]?.message?.content || '';
// Parse JSON from response
const jsonMatch = aiResponse.match(/\[[\s\S]*\]/);
if (jsonMatch) {
const questions = JSON.parse(jsonMatch[0]);
return questions;
}
return this.getDefaultQuiz();
} catch {
return this.getDefaultQuiz();
}
}
private extractTextFromContent(content: any): string {
if (!content || typeof content !== 'object') return '';
let text = '';
const traverse = (node: any) => {
if (node.type === 'text' && node.text) {
text += node.text + ' ';
}
if (node.content && Array.isArray(node.content)) {
node.content.forEach(traverse);
}
};
traverse(content);
return text.trim();
}
private getDefaultQuiz(): any[] {
return [
{
id: '1',
question: 'Вы изучили материал урока?',
options: ['Да, всё понятно', 'Частично', 'Нужно повторить', 'Нет'],
correctAnswer: 0,
},
{
id: '2',
question: 'Какие основные темы были рассмотрены?',
options: ['Теория и практика', 'Только теория', 'Только примеры', 'Не помню'],
correctAnswer: 0,
},
{
id: '3',
question: 'Готовы ли вы применить полученные знания?',
options: ['Да, готов', 'Нужна практика', 'Нужно повторить', 'Нет'],
correctAnswer: 0,
},
];
}
}