You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
162 lines
5.2 KiB
162 lines
5.2 KiB
import { Router, Response } from "express"; |
|
import prisma from "../lib/prisma"; |
|
import { getDeepSeekClient, getPrompt } from "../lib/deepseek"; |
|
import type { AuthRequest } from "../middleware/auth"; |
|
|
|
const router = Router(); |
|
|
|
const DEFAULT_TEST_PROMPT = `Ты — составитель тестов. Сгенерируй тест из РОВНО 10 вопросов. |
|
|
|
Требования к вопросам: |
|
- Вопросы должны проверять ПОНИМАНИЕ темы, а не запоминание фактов |
|
- Сложность: 3 лёгких, 4 средних, 3 сложных |
|
- Каждый вопрос имеет 4 варианта ответа (a, b, c, d) и ровно один правильный |
|
- Неправильные варианты должны быть правдоподобными (не абсурдными) |
|
- Вопросы на русском языке |
|
|
|
ВАЖНО: Верни ТОЛЬКО JSON-массив, без markdown-обёрток (\`\`\`json), без пояснений, ЧИСТЫЙ JSON: |
|
[{"question":"текст вопроса","options":{"a":"вариант А","b":"вариант Б","c":"вариант В","d":"вариант Г"},"correct":"a"}]`; |
|
|
|
/** Контекст пары: id назначенного ученика (общие тесты для наставника и ученика). */ |
|
function pairStudentScopeId(req: AuthRequest): number { |
|
return req.studentId!; |
|
} |
|
|
|
const authorSelect = { username: true, displayName: true } as const; |
|
|
|
router.get("/", async (req: AuthRequest, res: Response) => { |
|
const scopeId = pairStudentScopeId(req); |
|
const tests = await prisma.test.findMany({ |
|
where: { studentId: scopeId }, |
|
orderBy: { createdAt: "desc" }, |
|
include: { |
|
author: { select: authorSelect }, |
|
results: { |
|
where: { studentId: scopeId }, |
|
orderBy: { createdAt: "asc" }, |
|
}, |
|
}, |
|
}); |
|
res.json(tests); |
|
}); |
|
|
|
router.get("/:id", async (req: AuthRequest, res: Response) => { |
|
const id = parseInt(req.params.id as string, 10); |
|
const scopeId = pairStudentScopeId(req); |
|
const test = await prisma.test.findFirst({ |
|
where: { id, studentId: scopeId }, |
|
include: { |
|
author: { select: authorSelect }, |
|
results: { |
|
where: { studentId: scopeId }, |
|
orderBy: { createdAt: "asc" }, |
|
}, |
|
}, |
|
}); |
|
|
|
if (!test) { |
|
res.status(404).json({ error: "Test not found" }); |
|
return; |
|
} |
|
|
|
res.json(test); |
|
}); |
|
|
|
router.post("/generate", async (req: AuthRequest, res: Response) => { |
|
const { topic, fromQuestions } = req.body; |
|
const scopeId = pairStudentScopeId(req); |
|
const questionBankStudentId = req.studentId!; |
|
|
|
const client = await getDeepSeekClient(); |
|
const systemPrompt = await getPrompt("prompt_test", DEFAULT_TEST_PROMPT); |
|
|
|
let userMessage: string; |
|
|
|
if (fromQuestions) { |
|
const questions = await prisma.question.findMany({ |
|
where: { studentId: questionBankStudentId }, |
|
orderBy: { createdAt: "desc" }, |
|
take: 20, |
|
}); |
|
const questionTexts = questions.map((q: { text: string }) => q.text).join("\n"); |
|
userMessage = `Составь тест на основе этих вопросов, которые задавал пользователь ранее:\n${questionTexts}`; |
|
} else { |
|
if (!topic) { |
|
res.status(400).json({ error: "Topic is required" }); |
|
return; |
|
} |
|
userMessage = `Тема: ${topic}`; |
|
} |
|
|
|
try { |
|
const response = await client.chat.completions.create({ |
|
model: "deepseek-chat", |
|
messages: [ |
|
{ role: "system", content: systemPrompt }, |
|
{ role: "user", content: userMessage }, |
|
], |
|
}); |
|
|
|
const raw = response.choices[0]?.message?.content || "[]"; |
|
const jsonMatch = raw.match(/\[[\s\S]*\]/); |
|
const questionsJson = jsonMatch ? jsonMatch[0] : "[]"; |
|
|
|
const test = await prisma.test.create({ |
|
data: { |
|
topic: topic || "По прошлым вопросам", |
|
questions: questionsJson, |
|
studentId: scopeId, |
|
authorId: req.user!.id, |
|
}, |
|
include: { |
|
author: { select: authorSelect }, |
|
results: true, |
|
}, |
|
}); |
|
|
|
res.json(test); |
|
} catch (err: any) { |
|
res.status(500).json({ error: err.message }); |
|
} |
|
}); |
|
|
|
router.post("/:id/submit", async (req: AuthRequest, res: Response) => { |
|
const id = parseInt(req.params.id as string, 10); |
|
const scopeId = pairStudentScopeId(req); |
|
const { answers } = req.body as { answers: Record<string, string> }; |
|
|
|
const test = await prisma.test.findFirst({ where: { id, studentId: scopeId } }); |
|
if (!test) { |
|
res.status(404).json({ error: "Test not found" }); |
|
return; |
|
} |
|
|
|
const questions = JSON.parse(test.questions); |
|
let score = 0; |
|
const total = questions.length; |
|
|
|
for (const q of questions as { question: string; correct: string }[]) { |
|
if (answers[q.question] === q.correct) { |
|
score++; |
|
} |
|
} |
|
|
|
if (req.user!.role === "TUTOR") { |
|
res.json({ score, total, persisted: false as const }); |
|
return; |
|
} |
|
|
|
const result = await prisma.testResult.create({ |
|
data: { |
|
testId: id, |
|
studentId: req.user!.id, |
|
answers: JSON.stringify(answers), |
|
score, |
|
total, |
|
}, |
|
}); |
|
|
|
res.json({ ...result, persisted: true as const }); |
|
}); |
|
|
|
export default router;
|
|
|