@studious-lms/server 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pipelines/aiLabChat.d.ts +9 -5
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +37 -144
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +413 -0
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -0
- package/dist/pipelines/aiLabChatContract.js +74 -0
- package/dist/pipelines/aiLabChatContract.js.map +1 -0
- package/dist/pipelines/labChatPrompt.d.ts +2 -0
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -0
- package/dist/pipelines/labChatPrompt.js +72 -0
- package/dist/pipelines/labChatPrompt.js.map +1 -0
- package/dist/routers/_app.d.ts +146 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/studentProgress.d.ts +75 -0
- package/dist/routers/studentProgress.d.ts.map +1 -0
- package/dist/routers/studentProgress.js +33 -0
- package/dist/routers/studentProgress.js.map +1 -0
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +31 -15
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +90 -48
- package/dist/services/message.js.map +1 -1
- package/dist/services/studentProgress.d.ts +45 -0
- package/dist/services/studentProgress.d.ts.map +1 -0
- package/dist/services/studentProgress.js +291 -0
- package/dist/services/studentProgress.js.map +1 -0
- package/package.json +2 -2
- package/sentry.properties +3 -0
- package/src/pipelines/aiLabChat.ts +37 -148
- package/src/pipelines/aiLabChatContract.ts +75 -0
- package/src/pipelines/labChatPrompt.ts +68 -0
- package/src/routers/_app.ts +4 -2
- package/src/routers/studentProgress.ts +47 -0
- package/src/services/labChat.ts +31 -22
- package/src/services/message.ts +97 -48
- package/src/services/studentProgress.ts +390 -0
- package/tests/lib/aiLabChatContract.test.ts +32 -0
- package/tests/pipelines/aiLabChat.test.ts +95 -0
- package/tests/routers/studentProgress.test.ts +283 -0
- package/tests/utils/aiLabChatPrompt.test.ts +18 -0
- package/vitest.unit.config.ts +7 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"studentProgress.js","sources":["services/studentProgress.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Student progress service - assignment recommendations and progress chat.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { inference } from \"../utils/inference.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { isTeacherInClass } from \"../models/class.js\";\nimport type { AssignmentType } from \"@prisma/client\";\n\ntype ProgressChatMessage = {\n role: \"user\" | \"assistant\";\n content: string;\n};\n\ntype GradeSubmission = {\n id: string;\n gradeReceived: number | null;\n submitted: boolean | null;\n returned: boolean | null;\n submittedAt: Date | null;\n assignment: {\n id: string;\n title: string;\n dueDate: Date;\n maxGrade: number | null;\n weight: number;\n type: AssignmentType;\n section: { id: string; name: string } | null;\n };\n};\n\nfunction calculatePercentage(submission: GradeSubmission) {\n if (submission.gradeReceived == null || !submission.assignment.maxGrade)\n return null;\n return (\n Math.round(\n (submission.gradeReceived / submission.assignment.maxGrade) * 1000,\n ) / 10\n );\n}\n\nfunction calculateTrend(submissions: GradeSubmission[]) {\n const graded = submissions\n .filter(\n (submission) =>\n submission.gradeReceived != null && submission.assignment.maxGrade,\n )\n .sort((a, b) => {\n const aTime = a.submittedAt?.getTime() ?? a.assignment.dueDate.getTime();\n const bTime = b.submittedAt?.getTime() ?? b.assignment.dueDate.getTime();\n return aTime - bTime;\n })\n .map((submission) => calculatePercentage(submission))\n .filter((value): value is number => value != null);\n\n if (graded.length < 2) return 0;\n const midpoint = Math.floor(graded.length / 2);\n const first = graded.slice(0, midpoint);\n const second = graded.slice(midpoint);\n const average = (values: number[]) =>\n values.reduce((sum, value) => sum + value, 0) / values.length;\n return Math.round((average(second) - average(first)) * 10) / 10;\n}\n\nfunction getOverallGrade(submissions: GradeSubmission[]) {\n let totalWeighted = 0;\n let totalWeight = 0;\n\n for (const submission of submissions) {\n if (\n submission.gradeReceived != null &&\n submission.assignment.maxGrade &&\n submission.assignment.weight\n ) {\n totalWeighted +=\n (submission.gradeReceived / submission.assignment.maxGrade) *\n submission.assignment.weight;\n totalWeight += submission.assignment.weight;\n }\n }\n\n return totalWeight > 0\n ? Math.round((totalWeighted / totalWeight) * 1000) / 10\n : null;\n}\n\nasync function loadStudentProgressContext(\n viewerId: string,\n classId: string,\n studentId: string,\n) {\n const isTeacher = await isTeacherInClass(classId, viewerId);\n if (viewerId !== studentId && !isTeacher) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"You can only view your own progress\",\n });\n }\n\n const [classData, student, submissions] = await Promise.all([\n prisma.class.findUnique({\n where: { id: classId },\n select: { id: true, name: true, subject: true },\n }),\n prisma.user.findFirst({\n where: {\n id: studentId,\n studentIn: { some: { id: classId } },\n },\n select: {\n id: true,\n username: true,\n profile: { select: { displayName: true } },\n },\n }),\n prisma.submission.findMany({\n where: {\n studentId,\n assignment: { classId, graded: true },\n },\n include: {\n assignment: {\n select: {\n id: true,\n title: true,\n dueDate: true,\n maxGrade: true,\n weight: true,\n type: true,\n section: { select: { id: true, name: true } },\n },\n },\n },\n orderBy: { assignment: { dueDate: \"asc\" } },\n }),\n ]);\n\n if (!classData) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Class not found\" });\n }\n\n if (!student) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Student not found in this class\",\n });\n }\n\n return { classData, student, submissions };\n}\n\nexport async function getStudentProgressRecommendations(\n viewerId: string,\n classId: string,\n studentId: string,\n) {\n const { student, submissions } = await loadStudentProgressContext(\n viewerId,\n classId,\n studentId,\n );\n\n const now = new Date();\n const recommendationCandidates = submissions\n .map((submission) => {\n const percentage = calculatePercentage(submission);\n const isMissing =\n !submission.submitted &&\n submission.assignment.dueDate.getTime() < now.getTime();\n const isLowScore = percentage != null && percentage < 70;\n const isUnreturned =\n Boolean(submission.submitted) &&\n submission.gradeReceived == null &&\n !submission.returned;\n const isUpcoming =\n !Boolean(submission.submitted) &&\n !submission.submittedAt &&\n !submission.returned &&\n submission.gradeReceived == null &&\n submission.assignment.dueDate.getTime() >= now.getTime();\n const reasons: string[] = [];\n let priorityScore = 0;\n\n if (isMissing) {\n reasons.push(\"Missing past-due work\");\n priorityScore += 100;\n }\n if (isLowScore) {\n reasons.push(`Scored ${percentage}%`);\n priorityScore += 85 - (percentage ?? 0);\n }\n if (isUnreturned) {\n reasons.push(\"Awaiting grade or feedback\");\n priorityScore += 15;\n }\n if (isUpcoming) {\n reasons.push(\"Upcoming graded assignment\");\n priorityScore += 5;\n }\n\n return {\n assignmentId: submission.assignment.id,\n submissionId: submission.id,\n title: submission.assignment.title,\n type: submission.assignment.type,\n sectionName: submission.assignment.section?.name ?? null,\n dueDate: submission.assignment.dueDate,\n gradeReceived: submission.gradeReceived,\n maxGrade: submission.assignment.maxGrade,\n percentage,\n submitted: Boolean(submission.submitted),\n returned: Boolean(submission.returned),\n reasons,\n priorityScore,\n };\n })\n .filter((candidate) => candidate.priorityScore > 0)\n .sort((a, b) => b.priorityScore - a.priorityScore)\n .slice(0, 5);\n\n const gradedPercentages = submissions\n .map(calculatePercentage)\n .filter((value): value is number => value != null);\n const lowScoreCount = gradedPercentages.filter(\n (percentage) => percentage < 70,\n ).length;\n const missingCount = submissions.filter(\n (submission) =>\n !submission.submitted &&\n submission.assignment.dueDate.getTime() < now.getTime(),\n ).length;\n const trend = calculateTrend(submissions);\n const overallGrade = getOverallGrade(submissions);\n\n const nextSteps = [\n missingCount > 0\n ? `Prioritize ${missingCount} missing assignment${missingCount === 1 ? \"\" : \"s\"}.`\n : null,\n lowScoreCount > 0\n ? `Review ${lowScoreCount} low-scoring assignment${lowScoreCount === 1 ? \"\" : \"s\"} before introducing new material.`\n : null,\n trend < -5\n ? \"Schedule a check-in because recent performance is trending down.\"\n : null,\n recommendationCandidates.length === 0\n ? \"No urgent assignment issues detected from the available grades.\"\n : null,\n ].filter((step): step is string => Boolean(step));\n\n return {\n student: {\n id: student.id,\n username: student.username,\n displayName: student.profile?.displayName ?? student.username,\n },\n summary: {\n overallGrade,\n trend,\n completedAssignments: submissions.filter((submission) =>\n Boolean(submission.submitted),\n ).length,\n totalAssignments: submissions.length,\n missingCount,\n lowScoreCount,\n },\n recommendations: recommendationCandidates.map(\n ({ priorityScore, ...candidate }) => candidate,\n ),\n nextSteps,\n };\n}\n\nfunction buildProgressSummary(submissions: GradeSubmission[]) {\n return submissions.map((submission) => ({\n title: submission.assignment.title,\n type: submission.assignment.type,\n dueDate: submission.assignment.dueDate.toISOString(),\n submitted: Boolean(submission.submitted),\n returned: Boolean(submission.returned),\n gradeReceived: submission.gradeReceived,\n maxGrade: submission.assignment.maxGrade,\n percentage: calculatePercentage(submission),\n section: submission.assignment.section?.name ?? null,\n }));\n}\n\nexport async function chatAboutStudentProgress(\n viewerId: string,\n input: {\n classId: string;\n studentId: string;\n message: string;\n history?: ProgressChatMessage[];\n },\n) {\n const { classData, student, submissions } = await loadStudentProgressContext(\n viewerId,\n input.classId,\n input.studentId,\n );\n\n const displayName = student.profile?.displayName ?? student.username;\n const summary = {\n overallGrade: getOverallGrade(submissions),\n trend: calculateTrend(submissions),\n assignments: buildProgressSummary(submissions),\n };\n\n const messages = [\n {\n role: \"system\" as const,\n content:\n \"You are an educational progress assistant for teachers and students. Use only the provided class and grade context. Be concise, specific, supportive, and avoid fabricating grades or assignments.\",\n },\n {\n role: \"user\" as const,\n content: JSON.stringify({\n class: classData,\n student: { id: student.id, username: student.username, displayName },\n progress: summary,\n }),\n },\n ...(input.history ?? []).slice(-8).map((message) => ({\n role: message.role,\n content: message.content,\n })),\n { role: \"user\" as const, content: input.message },\n ];\n\n try {\n const response = await inference<string>(messages);\n if (typeof response !== \"string\" || response.trim().length === 0) {\n throw new Error(\"Student progress chat returned an empty response\");\n }\n return { message: response, isFallback: false };\n } catch (error) {\n logger.error(\"Failed to generate student progress chat response\", {\n error,\n classId: input.classId,\n studentId: input.studentId,\n });\n\n const overall =\n summary.overallGrade == null\n ? \"not enough graded work\"\n : `${summary.overallGrade}%`;\n const missingItems = summary.assignments.filter(\n (assignment) =>\n !assignment.submitted &&\n new Date(assignment.dueDate).getTime() < Date.now(),\n ).length;\n const lowScores = summary.assignments.filter(\n (assignment) =>\n assignment.percentage != null && assignment.percentage < 70,\n ).length;\n const awaitingFeedback = summary.assignments.filter(\n (assignment) =>\n assignment.submitted &&\n assignment.gradeReceived == null &&\n !assignment.returned,\n ).length;\n const trendLabel =\n summary.overallGrade == null\n ? \"not enough graded work to determine a recent trend\"\n : summary.trend > 5\n ? \"improving\"\n : summary.trend < -5\n ? \"declining\"\n : \"stable\";\n const advice = [\n missingItems > 0\n ? `Review ${missingItems} missing assignment${missingItems === 1 ? \"\" : \"s\"}.`\n : null,\n lowScores > 0\n ? `Use targeted practice for ${lowScores} low-scoring assignment${lowScores === 1 ? \"\" : \"s\"}.`\n : null,\n awaitingFeedback > 0\n ? `Check back after feedback is returned for ${awaitingFeedback} submitted assignment${awaitingFeedback === 1 ? \"\" : \"s\"}.`\n : null,\n missingItems === 0 && lowScores === 0 && awaitingFeedback === 0\n ? \"Check upcoming work and ask for feedback as new grades are returned.\"\n : null,\n ].filter((item): item is string => Boolean(item));\n return {\n message: `${displayName}'s current overall progress is ${overall}, with ${trendLabel}. ${advice.join(\" \")}`,\n isFallback: true,\n };\n }\n}\n"],"names":[],"mappings":"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","debug_id":"e4b426af-1ccd-5152-a996-9a03a09b6503"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studious-lms/server",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Backend server for Studious application",
|
|
5
5
|
"main": "dist/exportType.js",
|
|
6
6
|
"types": "dist/exportType.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test:watch": "NODE_ENV=test vitest watch",
|
|
25
25
|
"test:coverage": "NODE_ENV=test vitest run --coverage",
|
|
26
26
|
"db:seed": "tsx src/seedDatabase.ts",
|
|
27
|
-
"sentry:sourcemaps": "sentry-cli sourcemaps inject
|
|
27
|
+
"sentry:sourcemaps": "sentry-cli sourcemaps inject ./dist && sentry-cli sourcemaps upload --org \"${SENTRY_ORG:-studious-lms}\" --project \"${SENTRY_PROJECT:-server}\" ./dist"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@google-cloud/storage": "^7.16.0",
|
|
@@ -8,80 +8,14 @@ import { GenerationStatus } from "@prisma/client";
|
|
|
8
8
|
import { pusher, teacherChannel } from "../lib/pusher.js";
|
|
9
9
|
import type { Assignment, Class, File, Section, User } from "@prisma/client";
|
|
10
10
|
import { inference, inferenceClient, sendAIMessage } from "../utils/inference.js";
|
|
11
|
-
import z from "zod";
|
|
12
11
|
import { logger } from "../utils/logger.js";
|
|
13
12
|
import { createPdf } from "../lib/jsonConversion.js";
|
|
14
13
|
import { v4 } from "uuid";
|
|
15
14
|
import { bucket } from "../lib/googleCloudStorage.js";
|
|
16
15
|
import OpenAI from "openai";
|
|
17
16
|
import { DocumentBlock } from "../lib/jsonStyles.js";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const labChatResponseSchema = z.object({
|
|
21
|
-
text: z.string(),
|
|
22
|
-
worksheetsToCreate: z.array(z.object({
|
|
23
|
-
title: z.string(),
|
|
24
|
-
questions: z.array(z.object({
|
|
25
|
-
type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']),
|
|
26
|
-
question: z.string(),
|
|
27
|
-
answer: z.string(),
|
|
28
|
-
options: z.array(z.object({
|
|
29
|
-
id: z.string(),
|
|
30
|
-
text: z.string(),
|
|
31
|
-
isCorrect: z.boolean(),
|
|
32
|
-
})).optional().default([]),
|
|
33
|
-
markScheme: z.array(z.object({
|
|
34
|
-
id: z.string(),
|
|
35
|
-
points: z.number(),
|
|
36
|
-
description: z.string(),
|
|
37
|
-
})).optional().default([]),
|
|
38
|
-
points: z.number().optional().default(0),
|
|
39
|
-
order: z.number(),
|
|
40
|
-
})),
|
|
41
|
-
})),
|
|
42
|
-
sectionsToCreate: z.array(z.object({
|
|
43
|
-
name: z.string(),
|
|
44
|
-
color: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
|
|
45
|
-
})),
|
|
46
|
-
assignmentsToCreate: z.array(z.object({
|
|
47
|
-
title: z.string(),
|
|
48
|
-
instructions: z.string(),
|
|
49
|
-
dueDate: z.string().datetime(),
|
|
50
|
-
acceptFiles: z.boolean(),
|
|
51
|
-
acceptExtendedResponse: z.boolean(),
|
|
52
|
-
acceptWorksheet: z.boolean(),
|
|
53
|
-
maxGrade: z.number(),
|
|
54
|
-
gradingBoundaryId: z.string().nullable().optional(),
|
|
55
|
-
markschemeId: z.string().nullable().optional(),
|
|
56
|
-
worksheetIds: z.array(z.string()),
|
|
57
|
-
studentIds: z.array(z.string()),
|
|
58
|
-
sectionId: z.string().nullable().optional(),
|
|
59
|
-
type: z.enum(['HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER']),
|
|
60
|
-
attachments: z.array(z.object({
|
|
61
|
-
id: z.string(),
|
|
62
|
-
})),
|
|
63
|
-
})).nullable().optional(),
|
|
64
|
-
docs: z.array(z.object({
|
|
65
|
-
title: z.string(),
|
|
66
|
-
blocks: z.array(z.object({
|
|
67
|
-
format: z.number().int().min(0).max(12),
|
|
68
|
-
content: z.union([z.string(), z.array(z.string())]),
|
|
69
|
-
metadata: z.object({
|
|
70
|
-
fontSize: z.number().min(6).nullable().optional(),
|
|
71
|
-
lineHeight: z.number().min(0.6).nullable().optional(),
|
|
72
|
-
paragraphSpacing: z.number().min(0).nullable().optional(),
|
|
73
|
-
indentWidth: z.number().min(0).nullable().optional(),
|
|
74
|
-
paddingX: z.number().min(0).nullable().optional(),
|
|
75
|
-
paddingY: z.number().min(0).nullable().optional(),
|
|
76
|
-
font: z.number().int().min(0).max(5).nullable().optional(),
|
|
77
|
-
color: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
|
|
78
|
-
background: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
|
|
79
|
-
align: z.enum(["left", "center", "right"]).nullable().optional(),
|
|
80
|
-
}).nullable().optional(),
|
|
81
|
-
})),
|
|
82
|
-
})).nullable().optional(),
|
|
83
|
-
});
|
|
84
|
-
|
|
17
|
+
import { type LabChatResponse, labChatArrayFieldInstructions, labChatResponseFormat, labChatResponseSchema } from "./aiLabChatContract.js";
|
|
18
|
+
import { buildLabChatSystemPrompt } from "./labChatPrompt.js";
|
|
85
19
|
|
|
86
20
|
/** Extended class data for AI context (schema-aware) */
|
|
87
21
|
type ClassContextData = {
|
|
@@ -91,8 +25,8 @@ type ClassContextData = {
|
|
|
91
25
|
gradingBoundaries: { id: string; structured: string }[];
|
|
92
26
|
worksheets: { id: string; name: string; questionCount: number }[];
|
|
93
27
|
files: File[];
|
|
94
|
-
students:
|
|
95
|
-
teachers:
|
|
28
|
+
students: { id: string; username: string; profile?: { displayName: string | null } | null }[];
|
|
29
|
+
teachers: { id: string; username: string; profile?: { displayName: string | null } | null }[];
|
|
96
30
|
assignments: (Assignment & {
|
|
97
31
|
section?: { id: string; name: string; order?: number | null } | null;
|
|
98
32
|
markScheme?: { id: string } | null;
|
|
@@ -169,8 +103,8 @@ Syllabus: ${cls.syllabus ? cls.syllabus.slice(0, 200) + (cls.syllabus.length > 2
|
|
|
169
103
|
SECTIONS (use sectionId when creating assignments):
|
|
170
104
|
${sectionList || " (none - suggest sectionsToCreate first)"}
|
|
171
105
|
|
|
172
|
-
MARK SCHEMES (use
|
|
173
|
-
${markSchemeList || " (none - suggest creating one or omit
|
|
106
|
+
MARK SCHEMES (use markSchemeId when creating assignments):
|
|
107
|
+
${markSchemeList || " (none - suggest creating one or omit markSchemeId)"}
|
|
174
108
|
|
|
175
109
|
GRADING BOUNDARIES (use gradingBoundaryId when creating assignments):
|
|
176
110
|
${gradingBoundaryList || " (none - suggest creating one or omit gradingBoundaryId)"}
|
|
@@ -322,12 +256,10 @@ export const generateAndSendLabIntroduction = async (
|
|
|
322
256
|
export const generateAndSendLabResponse = async (
|
|
323
257
|
labChatId: string,
|
|
324
258
|
teacherMessage: string,
|
|
325
|
-
conversationId: string,
|
|
326
259
|
emitOptions?: { classId: string; messageId: string }
|
|
327
260
|
): Promise<void> => {
|
|
328
261
|
try {
|
|
329
262
|
// Get lab context from database
|
|
330
|
-
|
|
331
263
|
const fullLabChat = await prisma.labChat.findUnique({
|
|
332
264
|
where: { id: labChatId },
|
|
333
265
|
include: {
|
|
@@ -344,6 +276,8 @@ export const generateAndSendLabIntroduction = async (
|
|
|
344
276
|
throw new Error('Lab chat not found');
|
|
345
277
|
}
|
|
346
278
|
|
|
279
|
+
const conversationId = fullLabChat.conversationId;
|
|
280
|
+
|
|
347
281
|
// Get recent conversation history
|
|
348
282
|
const recentMessages = await prisma.message.findMany({
|
|
349
283
|
where: {
|
|
@@ -370,67 +304,7 @@ export const generateAndSendLabIntroduction = async (
|
|
|
370
304
|
|
|
371
305
|
// Build conversation history as proper message objects
|
|
372
306
|
// Enhance the stored context with schema-aware instructions
|
|
373
|
-
const enhancedSystemPrompt =
|
|
374
|
-
|
|
375
|
-
IMPORTANT INSTRUCTIONS:
|
|
376
|
-
- Use the context information above (subject, topic, difficulty, objectives, etc.) as your foundation
|
|
377
|
-
- A separate CLASS CONTEXT message lists this class's sections, mark schemes, grading boundaries, worksheets, files, and students with their database IDs
|
|
378
|
-
- Do NOT ask teachers about technical details (hex codes, format numbers, IDs, schema fields). Use sensible defaults yourself.
|
|
379
|
-
- Only ask clarifying questions about content or pedagogy (e.g., topic scope, difficulty, number of questions). Never ask "what hex color?" or "which format?"
|
|
380
|
-
- When creating content, make reasonable choices: pick nice default colors, use standard formatting. Teachers care about the content, not implementation.
|
|
381
|
-
- Only output final course materials when you have sufficient details about the content itself
|
|
382
|
-
- Do not use markdown in your responses - use plain text only
|
|
383
|
-
- You are primarily a chatbot - only provide docs/assignments when the teacher explicitly requests them
|
|
384
|
-
- If the request is vague, ask 1-2 high-level clarifying questions (topic, scope, style) - never technical ones
|
|
385
|
-
|
|
386
|
-
CRITICAL: REFERENCING OBJECTS - NAMES vs IDs:
|
|
387
|
-
- In "text": Refer to objects by NAME (e.g., "Unit 1", "Biology Rubric", "Cell_Structure_Worksheet")
|
|
388
|
-
- In "assignmentsToCreate", "worksheetsToCreate", "sectionsToCreate": Use DATABASE IDs from the CLASS CONTEXT
|
|
389
|
-
* sectionId, gradingBoundaryId, markschemeId, worksheetIds, studentIds, attachments[].id must be real IDs from the context
|
|
390
|
-
* If the class has no sections/mark schemes/grading boundaries, use sectionsToCreate first, or omit optional IDs
|
|
391
|
-
|
|
392
|
-
RESPONSE FORMAT (JSON):
|
|
393
|
-
{ "text": string, "docs": null | array, "worksheetsToCreate": null | array, "sectionsToCreate": null | array, "assignmentsToCreate": null | array }
|
|
394
|
-
|
|
395
|
-
CRITICAL - "text" field rules:
|
|
396
|
-
- "text" must be a SHORT conversational summary (2-4 sentences). Plain text, no markdown.
|
|
397
|
-
- NEVER list assignment/worksheet fields in text (no "Type:", "dueDate:", "worksheetIds:", "sectionId:", etc.)
|
|
398
|
-
- NEVER dump schema or JSON-like output in text. The teacher sees the actual content in UI cards below.
|
|
399
|
-
- Good example: "I've created 4 assignments for Unit 1: Week 1 homework on the worksheet, Week 2 quiz, Week 3 lab activity, and Week 4 review test. You can create them below."
|
|
400
|
-
- Bad example: "Week 1 - Homework. Type: HOMEWORK. dueDate: 2026-03-10. worksheetIds: [...]" — NEVER do this.
|
|
401
|
-
|
|
402
|
-
- "docs": PDF documents when creating course materials (worksheets, handouts, answer keys)
|
|
403
|
-
- "worksheetsToCreate": Worksheets with questions when teacher wants structured assessments
|
|
404
|
-
- "sectionsToCreate": New sections when the class has none or teacher wants new units
|
|
405
|
-
- "assignmentsToCreate": Assignments when teacher explicitly requests them. Use IDs from CLASS CONTEXT. The structured data goes HERE only, not in text.
|
|
406
|
-
|
|
407
|
-
WHEN CREATING DOCUMENTS (docs):
|
|
408
|
-
- docs: [ { "title": string, "blocks": [ { "format": 0-12, "content": string | string[], "metadata"?: {...} } ] } ]
|
|
409
|
-
- Format: 0=H1, 1=H2, 2=H3, 3=H4, 4=H5, 5=H6, 6=PARAGRAPH, 7=BULLET, 8=NUMBERED, 9=TABLE, 10=IMAGE, 11=CODE_BLOCK, 12=QUOTE
|
|
410
|
-
- Bullets (7) and Numbered (8): content is array of strings; do NOT include * or 1. 2. 3. - renderer adds them
|
|
411
|
-
- Table (9) and Image (10) not supported - do not emit
|
|
412
|
-
- Colors: use sensible defaults (e.g. "#3B82F6" blue, "#10B981" green) - never ask the teacher
|
|
413
|
-
|
|
414
|
-
WHEN CREATING WORKSHEETS (worksheetsToCreate):
|
|
415
|
-
- Question types: MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER, LONG_ANSWER, MATH_EXPRESSION, ESSAY
|
|
416
|
-
- For MULTIPLE_CHOICE/TRUE_FALSE: options array with { id, text, isCorrect }
|
|
417
|
-
- For others: options can be empty; answer holds the key
|
|
418
|
-
- markScheme: array of { id, points, description } for rubric items
|
|
419
|
-
- points: total points per question; order: display order
|
|
420
|
-
|
|
421
|
-
WHEN CREATING SECTIONS (sectionsToCreate):
|
|
422
|
-
- Use when class has no sections or teacher wants new units (e.g., "Unit 1", "Chapter 3")
|
|
423
|
-
- color: pick a nice default (e.g. "#3B82F6") - do not ask
|
|
424
|
-
|
|
425
|
-
WHEN CREATING ASSIGNMENTS (assignmentsToCreate):
|
|
426
|
-
- Put ALL assignment data (title, type, dueDate, instructions, worksheetIds, etc.) ONLY in assignmentsToCreate. The "text" field gets a brief friendly summary only.
|
|
427
|
-
- Use IDs from CLASS CONTEXT. If class has no sections, suggest sectionsToCreate first.
|
|
428
|
-
- type: HOMEWORK | QUIZ | TEST | PROJECT | ESSAY | DISCUSSION | PRESENTATION | LAB | OTHER
|
|
429
|
-
- sectionId, gradingBoundaryId, markschemeId: use from context; omit if class has none (suggest creating first)
|
|
430
|
-
- studentIds: empty array = assign to all; otherwise list specific student IDs
|
|
431
|
-
- worksheetIds: IDs of existing worksheets; empty if using docs-only or new worksheets
|
|
432
|
-
- attachments[].id: file IDs from CLASS CONTEXT (PDFs, documents)
|
|
433
|
-
- acceptFiles, acceptExtendedResponse, acceptWorksheet: set based on assignment type`;
|
|
307
|
+
const enhancedSystemPrompt = buildLabChatSystemPrompt(fullLabChat.context);
|
|
434
308
|
|
|
435
309
|
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
|
436
310
|
{ role: 'system', content: enhancedSystemPrompt },
|
|
@@ -471,10 +345,18 @@ WHEN CREATING ASSIGNMENTS (assignmentsToCreate):
|
|
|
471
345
|
},
|
|
472
346
|
},
|
|
473
347
|
students: {
|
|
474
|
-
|
|
348
|
+
select: {
|
|
349
|
+
id: true,
|
|
350
|
+
username: true,
|
|
351
|
+
profile: { select: { displayName: true } },
|
|
352
|
+
},
|
|
475
353
|
},
|
|
476
354
|
teachers: {
|
|
477
|
-
|
|
355
|
+
select: {
|
|
356
|
+
id: true,
|
|
357
|
+
username: true,
|
|
358
|
+
profile: { select: { displayName: true } },
|
|
359
|
+
},
|
|
478
360
|
},
|
|
479
361
|
classFiles: {
|
|
480
362
|
include: {
|
|
@@ -529,7 +411,7 @@ REMINDER: Your "text" response must be a short, friendly summary (2-4 sentences)
|
|
|
529
411
|
// response_format: zodTextFormat(labChatResponseSchema, "lab_chat_response_format"),
|
|
530
412
|
// });
|
|
531
413
|
|
|
532
|
-
const response = await inference<
|
|
414
|
+
const response = await inference<LabChatResponse>(messages, labChatResponseSchema);
|
|
533
415
|
|
|
534
416
|
if (!response) {
|
|
535
417
|
throw new Error('No response generated from inference API');
|
|
@@ -635,6 +517,10 @@ REMINDER: Your "text" response must be a short, friendly summary (2-4 sentences)
|
|
|
635
517
|
}
|
|
636
518
|
|
|
637
519
|
if (emitOptions) {
|
|
520
|
+
await prisma.message.update({
|
|
521
|
+
where: { id: emitOptions.messageId },
|
|
522
|
+
data: { status: GenerationStatus.COMPLETED },
|
|
523
|
+
});
|
|
638
524
|
try {
|
|
639
525
|
await pusher.trigger(teacherChannel(emitOptions.classId), "lab-response-completed", {
|
|
640
526
|
labChatId,
|
|
@@ -643,10 +529,6 @@ REMINDER: Your "text" response must be a short, friendly summary (2-4 sentences)
|
|
|
643
529
|
} catch (broadcastError) {
|
|
644
530
|
logger.error("Failed to broadcast lab response completed:", { error: broadcastError });
|
|
645
531
|
}
|
|
646
|
-
await prisma.message.update({
|
|
647
|
-
where: { id: emitOptions.messageId },
|
|
648
|
-
data: { status: GenerationStatus.COMPLETED },
|
|
649
|
-
});
|
|
650
532
|
}
|
|
651
533
|
|
|
652
534
|
logger.info('AI response sent', { labChatId, conversationId });
|
|
@@ -663,20 +545,27 @@ REMINDER: Your "text" response must be a short, friendly summary (2-4 sentences)
|
|
|
663
545
|
});
|
|
664
546
|
|
|
665
547
|
if (emitOptions) {
|
|
666
|
-
|
|
548
|
+
try {
|
|
549
|
+
await prisma.message.update({
|
|
550
|
+
where: { id: emitOptions.messageId },
|
|
551
|
+
data: { status: GenerationStatus.FAILED },
|
|
552
|
+
});
|
|
553
|
+
} catch (statusError) {
|
|
554
|
+
logger.error("Failed to set message status FAILED:", {
|
|
555
|
+
error: statusError,
|
|
556
|
+
labChatId,
|
|
557
|
+
messageId: emitOptions.messageId,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
667
560
|
try {
|
|
668
561
|
await pusher.trigger(teacherChannel(emitOptions.classId), "lab-response-failed", {
|
|
669
562
|
labChatId,
|
|
670
563
|
messageId: emitOptions.messageId,
|
|
671
|
-
error:
|
|
564
|
+
error: "AI response generation failed",
|
|
672
565
|
});
|
|
673
566
|
} catch (broadcastError) {
|
|
674
567
|
logger.error("Failed to broadcast lab response failed:", { error: broadcastError });
|
|
675
568
|
}
|
|
676
|
-
await prisma.message.update({
|
|
677
|
-
where: { id: emitOptions.messageId },
|
|
678
|
-
data: { status: GenerationStatus.FAILED },
|
|
679
|
-
});
|
|
680
569
|
}
|
|
681
570
|
|
|
682
571
|
throw error; // Re-throw to see the full error in the calling function
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
|
|
3
|
+
export const labChatResponseSchema = z.object({
|
|
4
|
+
text: z.string(),
|
|
5
|
+
worksheetsToCreate: z.array(z.object({
|
|
6
|
+
title: z.string(),
|
|
7
|
+
questions: z.array(z.object({
|
|
8
|
+
type: z.enum(["MULTIPLE_CHOICE", "TRUE_FALSE", "SHORT_ANSWER", "LONG_ANSWER", "MATH_EXPRESSION", "ESSAY"]),
|
|
9
|
+
question: z.string(),
|
|
10
|
+
answer: z.string(),
|
|
11
|
+
options: z.array(z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
text: z.string(),
|
|
14
|
+
isCorrect: z.boolean(),
|
|
15
|
+
})).optional().default([]),
|
|
16
|
+
markScheme: z.array(z.object({
|
|
17
|
+
id: z.string(),
|
|
18
|
+
points: z.number(),
|
|
19
|
+
description: z.string(),
|
|
20
|
+
})).optional().default([]),
|
|
21
|
+
points: z.number().optional().default(0),
|
|
22
|
+
order: z.number(),
|
|
23
|
+
})),
|
|
24
|
+
})).default([]),
|
|
25
|
+
sectionsToCreate: z.array(z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
color: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
|
|
28
|
+
})).default([]),
|
|
29
|
+
assignmentsToCreate: z.array(z.object({
|
|
30
|
+
title: z.string(),
|
|
31
|
+
instructions: z.string(),
|
|
32
|
+
dueDate: z.string().datetime(),
|
|
33
|
+
acceptFiles: z.boolean(),
|
|
34
|
+
acceptExtendedResponse: z.boolean(),
|
|
35
|
+
acceptWorksheet: z.boolean(),
|
|
36
|
+
maxGrade: z.number(),
|
|
37
|
+
gradingBoundaryId: z.string().nullable().optional(),
|
|
38
|
+
markSchemeId: z.string().nullable().optional(),
|
|
39
|
+
worksheetIds: z.array(z.string()),
|
|
40
|
+
studentIds: z.array(z.string()),
|
|
41
|
+
sectionId: z.string().nullable().optional(),
|
|
42
|
+
type: z.enum(["HOMEWORK", "QUIZ", "TEST", "PROJECT", "ESSAY", "DISCUSSION", "PRESENTATION", "LAB", "OTHER"]),
|
|
43
|
+
attachments: z.array(z.object({
|
|
44
|
+
id: z.string(),
|
|
45
|
+
})),
|
|
46
|
+
})).nullable().optional(),
|
|
47
|
+
docs: z.array(z.object({
|
|
48
|
+
title: z.string(),
|
|
49
|
+
blocks: z.array(z.object({
|
|
50
|
+
format: z.number().int().min(0).max(12),
|
|
51
|
+
content: z.union([z.string(), z.array(z.string())]),
|
|
52
|
+
metadata: z.object({
|
|
53
|
+
fontSize: z.number().min(6).nullable().optional(),
|
|
54
|
+
lineHeight: z.number().min(0.6).nullable().optional(),
|
|
55
|
+
paragraphSpacing: z.number().min(0).nullable().optional(),
|
|
56
|
+
indentWidth: z.number().min(0).nullable().optional(),
|
|
57
|
+
paddingX: z.number().min(0).nullable().optional(),
|
|
58
|
+
paddingY: z.number().min(0).nullable().optional(),
|
|
59
|
+
font: z.number().int().min(0).max(5).nullable().optional(),
|
|
60
|
+
color: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
|
|
61
|
+
background: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
|
|
62
|
+
align: z.enum(["left", "center", "right"]).nullable().optional(),
|
|
63
|
+
}).nullable().optional(),
|
|
64
|
+
})),
|
|
65
|
+
})).nullable().optional(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export type LabChatResponse = z.infer<typeof labChatResponseSchema>;
|
|
69
|
+
|
|
70
|
+
export const labChatResponseFormat = `{ "text": string, "docs": null | array, "worksheetsToCreate": array, "sectionsToCreate": array, "assignmentsToCreate": null | array }`;
|
|
71
|
+
|
|
72
|
+
export const labChatArrayFieldInstructions = [
|
|
73
|
+
`- "worksheetsToCreate": always output an array. Use [] when there are no worksheets to create.`,
|
|
74
|
+
`- "sectionsToCreate": always output an array. Use [] when there are no sections to create.`,
|
|
75
|
+
].join("\n");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const buildLabChatSystemPrompt = (context: string): string => `${context}
|
|
2
|
+
|
|
3
|
+
IMPORTANT INSTRUCTIONS:
|
|
4
|
+
- Use the context information above (subject, topic, difficulty, objectives, etc.) as your foundation
|
|
5
|
+
- A separate CLASS CONTEXT message lists this class's sections, mark schemes, grading boundaries, worksheets, files, and students with their database IDs
|
|
6
|
+
- Do NOT ask teachers about technical details (hex codes, format numbers, IDs, schema fields). Use sensible defaults yourself.
|
|
7
|
+
- Only ask clarifying questions about content or pedagogy (e.g., topic scope, difficulty, number of questions). Never ask "what hex color?" or "which format?"
|
|
8
|
+
- When creating content, make reasonable choices: pick nice default colors, use standard formatting. Teachers care about the content, not implementation.
|
|
9
|
+
- Only output final course materials when you have sufficient details about the content itself
|
|
10
|
+
- Do not use markdown in your responses - use plain text only
|
|
11
|
+
- You are primarily a chatbot - only provide docs/assignments when the teacher explicitly requests them
|
|
12
|
+
- If the request is vague, ask 1-2 high-level clarifying questions (topic, scope, style) - never technical ones
|
|
13
|
+
|
|
14
|
+
CRITICAL: REFERENCING OBJECTS - NAMES vs IDs:
|
|
15
|
+
- In "text": Refer to objects by NAME (e.g., "Unit 1", "Biology Rubric", "Cell_Structure_Worksheet")
|
|
16
|
+
- In "assignmentsToCreate", "worksheetsToCreate", "sectionsToCreate": Use DATABASE IDs from the CLASS CONTEXT
|
|
17
|
+
* sectionId, gradingBoundaryId, markSchemeId, worksheetIds, studentIds, attachments[].id must be real IDs from the context
|
|
18
|
+
* If the class has no sections/mark schemes/grading boundaries, use sectionsToCreate first, or omit optional IDs
|
|
19
|
+
|
|
20
|
+
RESPONSE FORMAT (JSON):
|
|
21
|
+
{ "text": string, "docs": null | array, "worksheetsToCreate": array, "sectionsToCreate": array, "assignmentsToCreate": null | array }
|
|
22
|
+
|
|
23
|
+
CRITICAL ARRAY RULES:
|
|
24
|
+
- "worksheetsToCreate" must always be an array. Use [] when there are no worksheets to create.
|
|
25
|
+
- "sectionsToCreate" must always be an array. Use [] when there are no sections to create.
|
|
26
|
+
- Do not return null for "worksheetsToCreate" or "sectionsToCreate".
|
|
27
|
+
|
|
28
|
+
CRITICAL - "text" field rules:
|
|
29
|
+
- "text" must be a SHORT conversational summary (2-4 sentences). Plain text, no markdown.
|
|
30
|
+
- NEVER list assignment/worksheet fields in text (no "Type:", "dueDate:", "worksheetIds:", "sectionId:", etc.)
|
|
31
|
+
- NEVER dump schema or JSON-like output in text. The teacher sees the actual content in UI cards below.
|
|
32
|
+
- Good example: "I've created 4 assignments for Unit 1: Week 1 homework on the worksheet, Week 2 quiz, Week 3 lab activity, and Week 4 review test. You can create them below."
|
|
33
|
+
- Bad example: "Week 1 - Homework. Type: HOMEWORK. dueDate: 2026-03-10. worksheetIds: [...]" - NEVER do this.
|
|
34
|
+
|
|
35
|
+
- "docs": PDF documents when creating course materials (worksheets, handouts, answer keys)
|
|
36
|
+
- "worksheetsToCreate": Worksheets with questions when teacher wants structured assessments. Always return an array.
|
|
37
|
+
- "sectionsToCreate": New sections when the class has none or teacher wants new units. Always return an array.
|
|
38
|
+
- "assignmentsToCreate": Assignments when teacher explicitly requests them. Use IDs from CLASS CONTEXT. The structured data goes HERE only, not in text.
|
|
39
|
+
|
|
40
|
+
WHEN CREATING DOCUMENTS (docs):
|
|
41
|
+
- docs: [ { "title": string, "blocks": [ { "format": 0-12, "content": string | string[], "metadata"?: {...} } ] } ]
|
|
42
|
+
- Format: 0=H1, 1=H2, 2=H3, 3=H4, 4=H5, 5=H6, 6=PARAGRAPH, 7=BULLET, 8=NUMBERED, 9=TABLE, 10=IMAGE, 11=CODE_BLOCK, 12=QUOTE
|
|
43
|
+
- Bullets (7) and Numbered (8): content is array of strings; do NOT include * or 1. 2. 3. - renderer adds them
|
|
44
|
+
- Table (9) and Image (10) not supported - do not emit
|
|
45
|
+
- Colors: use sensible defaults (e.g. "#3B82F6" blue, "#10B981" green) - never ask the teacher
|
|
46
|
+
|
|
47
|
+
WHEN CREATING WORKSHEETS (worksheetsToCreate):
|
|
48
|
+
- Return an array every time, even when empty.
|
|
49
|
+
- Question types: MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER, LONG_ANSWER, MATH_EXPRESSION, ESSAY
|
|
50
|
+
- For MULTIPLE_CHOICE/TRUE_FALSE: options array with { id, text, isCorrect }
|
|
51
|
+
- For others: options can be empty; answer holds the key
|
|
52
|
+
- markScheme: array of { id, points, description } for rubric items
|
|
53
|
+
- points: total points per question; order: display order
|
|
54
|
+
|
|
55
|
+
WHEN CREATING SECTIONS (sectionsToCreate):
|
|
56
|
+
- Return an array every time, even when empty.
|
|
57
|
+
- Use when class has no sections or teacher wants new units (e.g., "Unit 1", "Chapter 3")
|
|
58
|
+
- color: pick a nice default (e.g. "#3B82F6") - do not ask
|
|
59
|
+
|
|
60
|
+
WHEN CREATING ASSIGNMENTS (assignmentsToCreate):
|
|
61
|
+
- Put ALL assignment data (title, type, dueDate, instructions, worksheetIds, etc.) ONLY in assignmentsToCreate. The "text" field gets a brief friendly summary only.
|
|
62
|
+
- Use IDs from CLASS CONTEXT. If class has no sections, suggest sectionsToCreate first.
|
|
63
|
+
- type: HOMEWORK | QUIZ | TEST | PROJECT | ESSAY | DISCUSSION | PRESENTATION | LAB | OTHER
|
|
64
|
+
- sectionId, gradingBoundaryId, markSchemeId: use from context; omit if class has none (suggest creating first)
|
|
65
|
+
- studentIds: empty array = assign to all; otherwise list specific student IDs
|
|
66
|
+
- worksheetIds: IDs of existing worksheets; empty if using docs-only or new worksheets
|
|
67
|
+
- attachments[].id: file IDs from CLASS CONTEXT (PDFs, documents)
|
|
68
|
+
- acceptFiles, acceptExtendedResponse, acceptWorksheet: set based on assignment type`;
|
package/src/routers/_app.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { newtonChatRouter } from "./newtonChat.js";
|
|
|
20
20
|
import { marketingRouter } from "./marketing.js";
|
|
21
21
|
import { worksheetRouter } from "./worksheet.js";
|
|
22
22
|
import { commentRouter } from "./comment.js";
|
|
23
|
+
import { studentProgressRouter } from "./studentProgress.js";
|
|
23
24
|
|
|
24
25
|
export const appRouter = createTRPCRouter({
|
|
25
26
|
class: classRouter,
|
|
@@ -41,7 +42,8 @@ export const appRouter = createTRPCRouter({
|
|
|
41
42
|
marketing: marketingRouter,
|
|
42
43
|
worksheet: worksheetRouter,
|
|
43
44
|
comment: commentRouter,
|
|
44
|
-
|
|
45
|
+
studentProgress: studentProgressRouter,
|
|
46
|
+
});
|
|
45
47
|
|
|
46
48
|
// Export type router type definition
|
|
47
49
|
export type AppRouter = typeof appRouter;
|
|
@@ -49,4 +51,4 @@ export type RouterInputs = inferRouterInputs<AppRouter>;
|
|
|
49
51
|
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
|
50
52
|
|
|
51
53
|
// Export caller
|
|
52
|
-
export const createCaller = createCallerFactory(appRouter);
|
|
54
|
+
export const createCaller = createCallerFactory(appRouter);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createTRPCRouter, protectedClassMemberProcedure } from "../trpc.js";
|
|
3
|
+
import {
|
|
4
|
+
chatAboutStudentProgress,
|
|
5
|
+
getStudentProgressRecommendations,
|
|
6
|
+
} from "../services/studentProgress.js";
|
|
7
|
+
|
|
8
|
+
const progressInputSchema = z.object({
|
|
9
|
+
classId: z.string(),
|
|
10
|
+
studentId: z.string(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const studentProgressRouter = createTRPCRouter({
|
|
14
|
+
getRecommendations: protectedClassMemberProcedure
|
|
15
|
+
.input(progressInputSchema)
|
|
16
|
+
.query(({ ctx, input }) =>
|
|
17
|
+
getStudentProgressRecommendations(
|
|
18
|
+
ctx.user!.id,
|
|
19
|
+
input.classId,
|
|
20
|
+
input.studentId,
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
|
|
24
|
+
chat: protectedClassMemberProcedure
|
|
25
|
+
.input(
|
|
26
|
+
progressInputSchema.extend({
|
|
27
|
+
message: z.string().min(1).max(4000),
|
|
28
|
+
history: z
|
|
29
|
+
.array(
|
|
30
|
+
z.object({
|
|
31
|
+
role: z.enum(["user", "assistant"]),
|
|
32
|
+
content: z.string().min(1).max(4000),
|
|
33
|
+
}),
|
|
34
|
+
)
|
|
35
|
+
.max(8)
|
|
36
|
+
.optional(),
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
.mutation(({ ctx, input }) =>
|
|
40
|
+
chatAboutStudentProgress(ctx.user!.id, {
|
|
41
|
+
classId: input.classId,
|
|
42
|
+
studentId: input.studentId,
|
|
43
|
+
message: input.message,
|
|
44
|
+
history: input.history,
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
47
|
+
});
|