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:
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,6 +241,7 @@ model Lesson {
|
||||
// Relations
|
||||
chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade)
|
||||
homework Homework[]
|
||||
quiz Quiz?
|
||||
|
||||
// Vector embedding for semantic search
|
||||
embedding Unsupported("vector(1536)")?
|
||||
@ -249,6 +250,18 @@ model Lesson {
|
||||
@@map("lessons")
|
||||
}
|
||||
|
||||
model Quiz {
|
||||
id String @id @default(uuid())
|
||||
lessonId String @unique @map("lesson_id")
|
||||
questions Json // Array of {id, question, options, correctAnswer}
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("quizzes")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// AI Course Generation
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user