@studious-lms/server 1.3.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/models/class.d.ts +24 -2
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +180 -81
- package/dist/models/class.js.map +1 -1
- package/dist/models/worksheet.d.ts +34 -34
- package/dist/pipelines/aiLabChat.d.ts +61 -2
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +204 -172
- 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/gradeWorksheet.d.ts +4 -4
- 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 +284 -56
- 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/class.d.ts +24 -3
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +3 -3
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/labChat.d.ts +10 -1
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +6 -3
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/message.d.ts +11 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +10 -3
- package/dist/routers/message.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/routers/worksheet.d.ts +24 -24
- package/dist/services/class.d.ts +24 -2
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +18 -6
- package/dist/services/class.js.map +1 -1
- package/dist/services/labChat.d.ts +5 -1
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +112 -4
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +8 -0
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +116 -2
- 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/dist/services/worksheet.d.ts +18 -18
- package/package.json +2 -2
- package/prisma/schema.prisma +1 -1
- package/sentry.properties +3 -0
- package/src/models/class.ts +189 -84
- package/src/pipelines/aiLabChat.ts +246 -184
- package/src/pipelines/aiLabChatContract.ts +75 -0
- package/src/pipelines/labChatPrompt.ts +68 -0
- package/src/routers/_app.ts +4 -2
- package/src/routers/class.ts +1 -1
- package/src/routers/labChat.ts +7 -0
- package/src/routers/message.ts +13 -0
- package/src/routers/studentProgress.ts +47 -0
- package/src/services/class.ts +14 -7
- package/src/services/labChat.ts +120 -5
- package/src/services/message.ts +142 -0
- 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,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Student progress service - assignment recommendations and progress chat.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e4b426af-1ccd-5152-a996-9a03a09b6503")}catch(e){}}();
|
|
6
|
+
import { TRPCError } from "@trpc/server";
|
|
7
|
+
import { prisma } from "../lib/prisma.js";
|
|
8
|
+
import { inference } from "../utils/inference.js";
|
|
9
|
+
import { logger } from "../utils/logger.js";
|
|
10
|
+
import { isTeacherInClass } from "../models/class.js";
|
|
11
|
+
function calculatePercentage(submission) {
|
|
12
|
+
if (submission.gradeReceived == null || !submission.assignment.maxGrade)
|
|
13
|
+
return null;
|
|
14
|
+
return (Math.round((submission.gradeReceived / submission.assignment.maxGrade) * 1000) / 10);
|
|
15
|
+
}
|
|
16
|
+
function calculateTrend(submissions) {
|
|
17
|
+
const graded = submissions
|
|
18
|
+
.filter((submission) => submission.gradeReceived != null && submission.assignment.maxGrade)
|
|
19
|
+
.sort((a, b) => {
|
|
20
|
+
const aTime = a.submittedAt?.getTime() ?? a.assignment.dueDate.getTime();
|
|
21
|
+
const bTime = b.submittedAt?.getTime() ?? b.assignment.dueDate.getTime();
|
|
22
|
+
return aTime - bTime;
|
|
23
|
+
})
|
|
24
|
+
.map((submission) => calculatePercentage(submission))
|
|
25
|
+
.filter((value) => value != null);
|
|
26
|
+
if (graded.length < 2)
|
|
27
|
+
return 0;
|
|
28
|
+
const midpoint = Math.floor(graded.length / 2);
|
|
29
|
+
const first = graded.slice(0, midpoint);
|
|
30
|
+
const second = graded.slice(midpoint);
|
|
31
|
+
const average = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
32
|
+
return Math.round((average(second) - average(first)) * 10) / 10;
|
|
33
|
+
}
|
|
34
|
+
function getOverallGrade(submissions) {
|
|
35
|
+
let totalWeighted = 0;
|
|
36
|
+
let totalWeight = 0;
|
|
37
|
+
for (const submission of submissions) {
|
|
38
|
+
if (submission.gradeReceived != null &&
|
|
39
|
+
submission.assignment.maxGrade &&
|
|
40
|
+
submission.assignment.weight) {
|
|
41
|
+
totalWeighted +=
|
|
42
|
+
(submission.gradeReceived / submission.assignment.maxGrade) *
|
|
43
|
+
submission.assignment.weight;
|
|
44
|
+
totalWeight += submission.assignment.weight;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return totalWeight > 0
|
|
48
|
+
? Math.round((totalWeighted / totalWeight) * 1000) / 10
|
|
49
|
+
: null;
|
|
50
|
+
}
|
|
51
|
+
async function loadStudentProgressContext(viewerId, classId, studentId) {
|
|
52
|
+
const isTeacher = await isTeacherInClass(classId, viewerId);
|
|
53
|
+
if (viewerId !== studentId && !isTeacher) {
|
|
54
|
+
throw new TRPCError({
|
|
55
|
+
code: "UNAUTHORIZED",
|
|
56
|
+
message: "You can only view your own progress",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const [classData, student, submissions] = await Promise.all([
|
|
60
|
+
prisma.class.findUnique({
|
|
61
|
+
where: { id: classId },
|
|
62
|
+
select: { id: true, name: true, subject: true },
|
|
63
|
+
}),
|
|
64
|
+
prisma.user.findFirst({
|
|
65
|
+
where: {
|
|
66
|
+
id: studentId,
|
|
67
|
+
studentIn: { some: { id: classId } },
|
|
68
|
+
},
|
|
69
|
+
select: {
|
|
70
|
+
id: true,
|
|
71
|
+
username: true,
|
|
72
|
+
profile: { select: { displayName: true } },
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
prisma.submission.findMany({
|
|
76
|
+
where: {
|
|
77
|
+
studentId,
|
|
78
|
+
assignment: { classId, graded: true },
|
|
79
|
+
},
|
|
80
|
+
include: {
|
|
81
|
+
assignment: {
|
|
82
|
+
select: {
|
|
83
|
+
id: true,
|
|
84
|
+
title: true,
|
|
85
|
+
dueDate: true,
|
|
86
|
+
maxGrade: true,
|
|
87
|
+
weight: true,
|
|
88
|
+
type: true,
|
|
89
|
+
section: { select: { id: true, name: true } },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
orderBy: { assignment: { dueDate: "asc" } },
|
|
94
|
+
}),
|
|
95
|
+
]);
|
|
96
|
+
if (!classData) {
|
|
97
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Class not found" });
|
|
98
|
+
}
|
|
99
|
+
if (!student) {
|
|
100
|
+
throw new TRPCError({
|
|
101
|
+
code: "NOT_FOUND",
|
|
102
|
+
message: "Student not found in this class",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return { classData, student, submissions };
|
|
106
|
+
}
|
|
107
|
+
export async function getStudentProgressRecommendations(viewerId, classId, studentId) {
|
|
108
|
+
const { student, submissions } = await loadStudentProgressContext(viewerId, classId, studentId);
|
|
109
|
+
const now = new Date();
|
|
110
|
+
const recommendationCandidates = submissions
|
|
111
|
+
.map((submission) => {
|
|
112
|
+
const percentage = calculatePercentage(submission);
|
|
113
|
+
const isMissing = !submission.submitted &&
|
|
114
|
+
submission.assignment.dueDate.getTime() < now.getTime();
|
|
115
|
+
const isLowScore = percentage != null && percentage < 70;
|
|
116
|
+
const isUnreturned = Boolean(submission.submitted) &&
|
|
117
|
+
submission.gradeReceived == null &&
|
|
118
|
+
!submission.returned;
|
|
119
|
+
const isUpcoming = !Boolean(submission.submitted) &&
|
|
120
|
+
!submission.submittedAt &&
|
|
121
|
+
!submission.returned &&
|
|
122
|
+
submission.gradeReceived == null &&
|
|
123
|
+
submission.assignment.dueDate.getTime() >= now.getTime();
|
|
124
|
+
const reasons = [];
|
|
125
|
+
let priorityScore = 0;
|
|
126
|
+
if (isMissing) {
|
|
127
|
+
reasons.push("Missing past-due work");
|
|
128
|
+
priorityScore += 100;
|
|
129
|
+
}
|
|
130
|
+
if (isLowScore) {
|
|
131
|
+
reasons.push(`Scored ${percentage}%`);
|
|
132
|
+
priorityScore += 85 - (percentage ?? 0);
|
|
133
|
+
}
|
|
134
|
+
if (isUnreturned) {
|
|
135
|
+
reasons.push("Awaiting grade or feedback");
|
|
136
|
+
priorityScore += 15;
|
|
137
|
+
}
|
|
138
|
+
if (isUpcoming) {
|
|
139
|
+
reasons.push("Upcoming graded assignment");
|
|
140
|
+
priorityScore += 5;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
assignmentId: submission.assignment.id,
|
|
144
|
+
submissionId: submission.id,
|
|
145
|
+
title: submission.assignment.title,
|
|
146
|
+
type: submission.assignment.type,
|
|
147
|
+
sectionName: submission.assignment.section?.name ?? null,
|
|
148
|
+
dueDate: submission.assignment.dueDate,
|
|
149
|
+
gradeReceived: submission.gradeReceived,
|
|
150
|
+
maxGrade: submission.assignment.maxGrade,
|
|
151
|
+
percentage,
|
|
152
|
+
submitted: Boolean(submission.submitted),
|
|
153
|
+
returned: Boolean(submission.returned),
|
|
154
|
+
reasons,
|
|
155
|
+
priorityScore,
|
|
156
|
+
};
|
|
157
|
+
})
|
|
158
|
+
.filter((candidate) => candidate.priorityScore > 0)
|
|
159
|
+
.sort((a, b) => b.priorityScore - a.priorityScore)
|
|
160
|
+
.slice(0, 5);
|
|
161
|
+
const gradedPercentages = submissions
|
|
162
|
+
.map(calculatePercentage)
|
|
163
|
+
.filter((value) => value != null);
|
|
164
|
+
const lowScoreCount = gradedPercentages.filter((percentage) => percentage < 70).length;
|
|
165
|
+
const missingCount = submissions.filter((submission) => !submission.submitted &&
|
|
166
|
+
submission.assignment.dueDate.getTime() < now.getTime()).length;
|
|
167
|
+
const trend = calculateTrend(submissions);
|
|
168
|
+
const overallGrade = getOverallGrade(submissions);
|
|
169
|
+
const nextSteps = [
|
|
170
|
+
missingCount > 0
|
|
171
|
+
? `Prioritize ${missingCount} missing assignment${missingCount === 1 ? "" : "s"}.`
|
|
172
|
+
: null,
|
|
173
|
+
lowScoreCount > 0
|
|
174
|
+
? `Review ${lowScoreCount} low-scoring assignment${lowScoreCount === 1 ? "" : "s"} before introducing new material.`
|
|
175
|
+
: null,
|
|
176
|
+
trend < -5
|
|
177
|
+
? "Schedule a check-in because recent performance is trending down."
|
|
178
|
+
: null,
|
|
179
|
+
recommendationCandidates.length === 0
|
|
180
|
+
? "No urgent assignment issues detected from the available grades."
|
|
181
|
+
: null,
|
|
182
|
+
].filter((step) => Boolean(step));
|
|
183
|
+
return {
|
|
184
|
+
student: {
|
|
185
|
+
id: student.id,
|
|
186
|
+
username: student.username,
|
|
187
|
+
displayName: student.profile?.displayName ?? student.username,
|
|
188
|
+
},
|
|
189
|
+
summary: {
|
|
190
|
+
overallGrade,
|
|
191
|
+
trend,
|
|
192
|
+
completedAssignments: submissions.filter((submission) => Boolean(submission.submitted)).length,
|
|
193
|
+
totalAssignments: submissions.length,
|
|
194
|
+
missingCount,
|
|
195
|
+
lowScoreCount,
|
|
196
|
+
},
|
|
197
|
+
recommendations: recommendationCandidates.map(({ priorityScore, ...candidate }) => candidate),
|
|
198
|
+
nextSteps,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function buildProgressSummary(submissions) {
|
|
202
|
+
return submissions.map((submission) => ({
|
|
203
|
+
title: submission.assignment.title,
|
|
204
|
+
type: submission.assignment.type,
|
|
205
|
+
dueDate: submission.assignment.dueDate.toISOString(),
|
|
206
|
+
submitted: Boolean(submission.submitted),
|
|
207
|
+
returned: Boolean(submission.returned),
|
|
208
|
+
gradeReceived: submission.gradeReceived,
|
|
209
|
+
maxGrade: submission.assignment.maxGrade,
|
|
210
|
+
percentage: calculatePercentage(submission),
|
|
211
|
+
section: submission.assignment.section?.name ?? null,
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
export async function chatAboutStudentProgress(viewerId, input) {
|
|
215
|
+
const { classData, student, submissions } = await loadStudentProgressContext(viewerId, input.classId, input.studentId);
|
|
216
|
+
const displayName = student.profile?.displayName ?? student.username;
|
|
217
|
+
const summary = {
|
|
218
|
+
overallGrade: getOverallGrade(submissions),
|
|
219
|
+
trend: calculateTrend(submissions),
|
|
220
|
+
assignments: buildProgressSummary(submissions),
|
|
221
|
+
};
|
|
222
|
+
const messages = [
|
|
223
|
+
{
|
|
224
|
+
role: "system",
|
|
225
|
+
content: "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.",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
role: "user",
|
|
229
|
+
content: JSON.stringify({
|
|
230
|
+
class: classData,
|
|
231
|
+
student: { id: student.id, username: student.username, displayName },
|
|
232
|
+
progress: summary,
|
|
233
|
+
}),
|
|
234
|
+
},
|
|
235
|
+
...(input.history ?? []).slice(-8).map((message) => ({
|
|
236
|
+
role: message.role,
|
|
237
|
+
content: message.content,
|
|
238
|
+
})),
|
|
239
|
+
{ role: "user", content: input.message },
|
|
240
|
+
];
|
|
241
|
+
try {
|
|
242
|
+
const response = await inference(messages);
|
|
243
|
+
if (typeof response !== "string" || response.trim().length === 0) {
|
|
244
|
+
throw new Error("Student progress chat returned an empty response");
|
|
245
|
+
}
|
|
246
|
+
return { message: response, isFallback: false };
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
logger.error("Failed to generate student progress chat response", {
|
|
250
|
+
error,
|
|
251
|
+
classId: input.classId,
|
|
252
|
+
studentId: input.studentId,
|
|
253
|
+
});
|
|
254
|
+
const overall = summary.overallGrade == null
|
|
255
|
+
? "not enough graded work"
|
|
256
|
+
: `${summary.overallGrade}%`;
|
|
257
|
+
const missingItems = summary.assignments.filter((assignment) => !assignment.submitted &&
|
|
258
|
+
new Date(assignment.dueDate).getTime() < Date.now()).length;
|
|
259
|
+
const lowScores = summary.assignments.filter((assignment) => assignment.percentage != null && assignment.percentage < 70).length;
|
|
260
|
+
const awaitingFeedback = summary.assignments.filter((assignment) => assignment.submitted &&
|
|
261
|
+
assignment.gradeReceived == null &&
|
|
262
|
+
!assignment.returned).length;
|
|
263
|
+
const trendLabel = summary.overallGrade == null
|
|
264
|
+
? "not enough graded work to determine a recent trend"
|
|
265
|
+
: summary.trend > 5
|
|
266
|
+
? "improving"
|
|
267
|
+
: summary.trend < -5
|
|
268
|
+
? "declining"
|
|
269
|
+
: "stable";
|
|
270
|
+
const advice = [
|
|
271
|
+
missingItems > 0
|
|
272
|
+
? `Review ${missingItems} missing assignment${missingItems === 1 ? "" : "s"}.`
|
|
273
|
+
: null,
|
|
274
|
+
lowScores > 0
|
|
275
|
+
? `Use targeted practice for ${lowScores} low-scoring assignment${lowScores === 1 ? "" : "s"}.`
|
|
276
|
+
: null,
|
|
277
|
+
awaitingFeedback > 0
|
|
278
|
+
? `Check back after feedback is returned for ${awaitingFeedback} submitted assignment${awaitingFeedback === 1 ? "" : "s"}.`
|
|
279
|
+
: null,
|
|
280
|
+
missingItems === 0 && lowScores === 0 && awaitingFeedback === 0
|
|
281
|
+
? "Check upcoming work and ask for feedback as new grades are returned."
|
|
282
|
+
: null,
|
|
283
|
+
].filter((item) => Boolean(item));
|
|
284
|
+
return {
|
|
285
|
+
message: `${displayName}'s current overall progress is ${overall}, with ${trendLabel}. ${advice.join(" ")}`,
|
|
286
|
+
isFallback: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=studentProgress.js.map
|
|
291
|
+
//# debugId=e4b426af-1ccd-5152-a996-9a03a09b6503
|
|
@@ -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":"AAAA;;GAEG;;;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAyBtD,SAAS,mBAAmB,CAAC,UAA2B;IACtD,IAAI,UAAU,CAAC,aAAa,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ;QACrE,OAAO,IAAI,CAAC;IACd,OAAO,CACL,IAAI,CAAC,KAAK,CACR,CAAC,UAAU,CAAC,aAAa,GAAG,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI,CACnE,GAAG,EAAE,CACP,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,WAA8B;IACpD,MAAM,MAAM,GAAG,WAAW;SACvB,MAAM,CACL,CAAC,UAAU,EAAE,EAAE,CACb,UAAU,CAAC,aAAa,IAAI,IAAI,IAAI,UAAU,CAAC,UAAU,CAAC,QAAQ,CACrE;SACA,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACb,MAAM,KAAK,GAAG,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACzE,MAAM,KAAK,GAAG,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACzE,OAAO,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC,CAAC;SACD,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;SACpD,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;IAErD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,CAAC,MAAgB,EAAE,EAAE,CACnC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IAChE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;AAClE,CAAC;AAED,SAAS,eAAe,CAAC,WAA8B;IACrD,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,IACE,UAAU,CAAC,aAAa,IAAI,IAAI;YAChC,UAAU,CAAC,UAAU,CAAC,QAAQ;YAC9B,UAAU,CAAC,UAAU,CAAC,MAAM,EAC5B,CAAC;YACD,aAAa;gBACX,CAAC,UAAU,CAAC,aAAa,GAAG,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC;oBAC3D,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC;YAC/B,WAAW,IAAI,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,OAAO,WAAW,GAAG,CAAC;QACpB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,aAAa,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE;QACvD,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,KAAK,UAAU,0BAA0B,CACvC,QAAgB,EAChB,OAAe,EACf,SAAiB;IAEjB,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC5D,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;QACzC,MAAM,IAAI,SAAS,CAAC;YAClB,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,qCAAqC;SAC/C,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;YACtB,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;YACtB,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;SAChD,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;YACpB,KAAK,EAAE;gBACL,EAAE,EAAE,SAAS;gBACb,SAAS,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE;aACrC;YACD,MAAM,EAAE;gBACN,EAAE,EAAE,IAAI;gBACR,QAAQ,EAAE,IAAI;gBACd,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE;aAC3C;SACF,CAAC;QACF,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC;YACzB,KAAK,EAAE;gBACL,SAAS;gBACT,UAAU,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE;aACtC;YACD,OAAO,EAAE;gBACP,UAAU,EAAE;oBACV,MAAM,EAAE;wBACN,EAAE,EAAE,IAAI;wBACR,KAAK,EAAE,IAAI;wBACX,OAAO,EAAE,IAAI;wBACb,QAAQ,EAAE,IAAI;wBACd,MAAM,EAAE,IAAI;wBACZ,IAAI,EAAE,IAAI;wBACV,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;qBAC9C;iBACF;aACF;YACD,OAAO,EAAE,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;SAC5C,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,SAAS,CAAC;YAClB,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,iCAAiC;SAC3C,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,QAAgB,EAChB,OAAe,EACf,SAAiB;IAEjB,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,0BAA0B,CAC/D,QAAQ,EACR,OAAO,EACP,SAAS,CACV,CAAC;IAEF,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,wBAAwB,GAAG,WAAW;SACzC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE;QAClB,MAAM,UAAU,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;QACnD,MAAM,SAAS,GACb,CAAC,UAAU,CAAC,SAAS;YACrB,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;QAC1D,MAAM,UAAU,GAAG,UAAU,IAAI,IAAI,IAAI,UAAU,GAAG,EAAE,CAAC;QACzD,MAAM,YAAY,GAChB,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC;YAC7B,UAAU,CAAC,aAAa,IAAI,IAAI;YAChC,CAAC,UAAU,CAAC,QAAQ,CAAC;QACvB,MAAM,UAAU,GACd,CAAC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC;YAC9B,CAAC,UAAU,CAAC,WAAW;YACvB,CAAC,UAAU,CAAC,QAAQ;YACpB,UAAU,CAAC,aAAa,IAAI,IAAI;YAChC,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;QAC3D,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,IAAI,aAAa,GAAG,CAAC,CAAC;QAEtB,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;YACtC,aAAa,IAAI,GAAG,CAAC;QACvB,CAAC;QACD,IAAI,UAAU,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,UAAU,UAAU,GAAG,CAAC,CAAC;YACtC,aAAa,IAAI,EAAE,GAAG,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,YAAY,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;YAC3C,aAAa,IAAI,EAAE,CAAC;QACtB,CAAC;QACD,IAAI,UAAU,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;YAC3C,aAAa,IAAI,CAAC,CAAC;QACrB,CAAC;QAED,OAAO;YACL,YAAY,EAAE,UAAU,CAAC,UAAU,CAAC,EAAE;YACtC,YAAY,EAAE,UAAU,CAAC,EAAE;YAC3B,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK;YAClC,IAAI,EAAE,UAAU,CAAC,UAAU,CAAC,IAAI;YAChC,WAAW,EAAE,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,IAAI,IAAI;YACxD,OAAO,EAAE,UAAU,CAAC,UAAU,CAAC,OAAO;YACtC,aAAa,EAAE,UAAU,CAAC,aAAa;YACvC,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,QAAQ;YACxC,UAAU;YACV,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC;YACxC,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC;YACtC,OAAO;YACP,aAAa;SACd,CAAC;IACJ,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,aAAa,GAAG,CAAC,CAAC;SAClD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,GAAG,CAAC,CAAC,aAAa,CAAC;SACjD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEf,MAAM,iBAAiB,GAAG,WAAW;SAClC,GAAG,CAAC,mBAAmB,CAAC;SACxB,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;IACrD,MAAM,aAAa,GAAG,iBAAiB,CAAC,MAAM,CAC5C,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,GAAG,EAAE,CAChC,CAAC,MAAM,CAAC;IACT,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,CACrC,CAAC,UAAU,EAAE,EAAE,CACb,CAAC,UAAU,CAAC,SAAS;QACrB,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAC1D,CAAC,MAAM,CAAC;IACT,MAAM,KAAK,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAElD,MAAM,SAAS,GAAG;QAChB,YAAY,GAAG,CAAC;YACd,CAAC,CAAC,cAAc,YAAY,sBAAsB,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG;YAClF,CAAC,CAAC,IAAI;QACR,aAAa,GAAG,CAAC;YACf,CAAC,CAAC,UAAU,aAAa,0BAA0B,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,mCAAmC;YACpH,CAAC,CAAC,IAAI;QACR,KAAK,GAAG,CAAC,CAAC;YACR,CAAC,CAAC,kEAAkE;YACpE,CAAC,CAAC,IAAI;QACR,wBAAwB,CAAC,MAAM,KAAK,CAAC;YACnC,CAAC,CAAC,iEAAiE;YACnE,CAAC,CAAC,IAAI;KACT,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAElD,OAAO;QACL,OAAO,EAAE;YACP,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,WAAW,EAAE,OAAO,CAAC,OAAO,EAAE,WAAW,IAAI,OAAO,CAAC,QAAQ;SAC9D;QACD,OAAO,EAAE;YACP,YAAY;YACZ,KAAK;YACL,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CACtD,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAC9B,CAAC,MAAM;YACR,gBAAgB,EAAE,WAAW,CAAC,MAAM;YACpC,YAAY;YACZ,aAAa;SACd;QACD,eAAe,EAAE,wBAAwB,CAAC,GAAG,CAC3C,CAAC,EAAE,aAAa,EAAE,GAAG,SAAS,EAAE,EAAE,EAAE,CAAC,SAAS,CAC/C;QACD,SAAS;KACV,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,WAA8B;IAC1D,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QACtC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK;QAClC,IAAI,EAAE,UAAU,CAAC,UAAU,CAAC,IAAI;QAChC,OAAO,EAAE,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,WAAW,EAAE;QACpD,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC;QACxC,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC;QACtC,aAAa,EAAE,UAAU,CAAC,aAAa;QACvC,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,QAAQ;QACxC,UAAU,EAAE,mBAAmB,CAAC,UAAU,CAAC;QAC3C,OAAO,EAAE,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,IAAI,IAAI;KACrD,CAAC,CAAC,CAAC;AACN,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,QAAgB,EAChB,KAKC;IAED,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,0BAA0B,CAC1E,QAAQ,EACR,KAAK,CAAC,OAAO,EACb,KAAK,CAAC,SAAS,CAChB,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,WAAW,IAAI,OAAO,CAAC,QAAQ,CAAC;IACrE,MAAM,OAAO,GAAG;QACd,YAAY,EAAE,eAAe,CAAC,WAAW,CAAC;QAC1C,KAAK,EAAE,cAAc,CAAC,WAAW,CAAC;QAClC,WAAW,EAAE,oBAAoB,CAAC,WAAW,CAAC;KAC/C,CAAC;IAEF,MAAM,QAAQ,GAAG;QACf;YACE,IAAI,EAAE,QAAiB;YACvB,OAAO,EACL,oMAAoM;SACvM;QACD;YACE,IAAI,EAAE,MAAe;YACrB,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;gBACtB,KAAK,EAAE,SAAS;gBAChB,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE;gBACpE,QAAQ,EAAE,OAAO;aAClB,CAAC;SACH;QACD,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YACnD,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,OAAO,EAAE,OAAO,CAAC,OAAO;SACzB,CAAC,CAAC;QACH,EAAE,IAAI,EAAE,MAAe,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE;KAClD,CAAC;IAEF,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAS,QAAQ,CAAC,CAAC;QACnD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QACtE,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IAClD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,mDAAmD,EAAE;YAChE,KAAK;YACL,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,SAAS,EAAE,KAAK,CAAC,SAAS;SAC3B,CAAC,CAAC;QAEH,MAAM,OAAO,GACX,OAAO,CAAC,YAAY,IAAI,IAAI;YAC1B,CAAC,CAAC,wBAAwB;YAC1B,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,GAAG,CAAC;QACjC,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CAC7C,CAAC,UAAU,EAAE,EAAE,CACb,CAAC,UAAU,CAAC,SAAS;YACrB,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CACtD,CAAC,MAAM,CAAC;QACT,MAAM,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CAC1C,CAAC,UAAU,EAAE,EAAE,CACb,UAAU,CAAC,UAAU,IAAI,IAAI,IAAI,UAAU,CAAC,UAAU,GAAG,EAAE,CAC9D,CAAC,MAAM,CAAC;QACT,MAAM,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CACjD,CAAC,UAAU,EAAE,EAAE,CACb,UAAU,CAAC,SAAS;YACpB,UAAU,CAAC,aAAa,IAAI,IAAI;YAChC,CAAC,UAAU,CAAC,QAAQ,CACvB,CAAC,MAAM,CAAC;QACT,MAAM,UAAU,GACd,OAAO,CAAC,YAAY,IAAI,IAAI;YAC1B,CAAC,CAAC,oDAAoD;YACtD,CAAC,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC;gBACnB,CAAC,CAAC,WAAW;gBACb,CAAC,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC;oBAClB,CAAC,CAAC,WAAW;oBACb,CAAC,CAAC,QAAQ,CAAC;QACjB,MAAM,MAAM,GAAG;YACb,YAAY,GAAG,CAAC;gBACd,CAAC,CAAC,UAAU,YAAY,sBAAsB,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG;gBAC9E,CAAC,CAAC,IAAI;YACR,SAAS,GAAG,CAAC;gBACX,CAAC,CAAC,6BAA6B,SAAS,0BAA0B,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG;gBAC/F,CAAC,CAAC,IAAI;YACR,gBAAgB,GAAG,CAAC;gBAClB,CAAC,CAAC,6CAA6C,gBAAgB,wBAAwB,gBAAgB,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG;gBAC3H,CAAC,CAAC,IAAI;YACR,YAAY,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,IAAI,gBAAgB,KAAK,CAAC;gBAC7D,CAAC,CAAC,sEAAsE;gBACxE,CAAC,CAAC,IAAI;SACT,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAClD,OAAO;YACL,OAAO,EAAE,GAAG,WAAW,kCAAkC,OAAO,UAAU,UAAU,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;YAC3G,UAAU,EAAE,IAAI;SACjB,CAAC;IACJ,CAAC;AACH,CAAC","debug_id":"e4b426af-1ccd-5152-a996-9a03a09b6503"}
|
|
@@ -18,10 +18,10 @@ export declare function getWorksheet(worksheetId: string): Promise<{
|
|
|
18
18
|
updatedAt: Date;
|
|
19
19
|
order: number | null;
|
|
20
20
|
markScheme: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
21
|
-
question: string;
|
|
22
|
-
points: number;
|
|
23
21
|
worksheetId: string;
|
|
22
|
+
question: string;
|
|
24
23
|
answer: string;
|
|
24
|
+
points: number;
|
|
25
25
|
}[];
|
|
26
26
|
} & {
|
|
27
27
|
id: string;
|
|
@@ -81,10 +81,10 @@ export declare function addQuestionToWorksheet(worksheetId: string, data: {
|
|
|
81
81
|
updatedAt: Date;
|
|
82
82
|
order: number | null;
|
|
83
83
|
markScheme: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
84
|
-
question: string;
|
|
85
|
-
points: number;
|
|
86
84
|
worksheetId: string;
|
|
85
|
+
question: string;
|
|
87
86
|
answer: string;
|
|
87
|
+
points: number;
|
|
88
88
|
}>;
|
|
89
89
|
export declare function reorderWorksheetQuestions(worksheetId: string, movedId: string, position: "before" | "after", targetId: string): Promise<{
|
|
90
90
|
id: string;
|
|
@@ -104,10 +104,10 @@ export declare function updateWorksheetQuestionRecord(worksheetId: string, quest
|
|
|
104
104
|
updatedAt: Date;
|
|
105
105
|
order: number | null;
|
|
106
106
|
markScheme: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
107
|
-
question: string;
|
|
108
|
-
points: number;
|
|
109
107
|
worksheetId: string;
|
|
108
|
+
question: string;
|
|
110
109
|
answer: string;
|
|
110
|
+
points: number;
|
|
111
111
|
}>;
|
|
112
112
|
export declare function deleteWorksheetQuestionRecord(worksheetId: string, questionId: string): Promise<{
|
|
113
113
|
options: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
@@ -117,10 +117,10 @@ export declare function deleteWorksheetQuestionRecord(worksheetId: string, quest
|
|
|
117
117
|
updatedAt: Date;
|
|
118
118
|
order: number | null;
|
|
119
119
|
markScheme: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
120
|
-
question: string;
|
|
121
|
-
points: number;
|
|
122
120
|
worksheetId: string;
|
|
121
|
+
question: string;
|
|
123
122
|
answer: string;
|
|
123
|
+
points: number;
|
|
124
124
|
}>;
|
|
125
125
|
export declare function getWorksheetSubmission(worksheetId: string, submissionId: string): Promise<{
|
|
126
126
|
responses: ({
|
|
@@ -134,12 +134,12 @@ export declare function getWorksheetSubmission(worksheetId: string, submissionId
|
|
|
134
134
|
updatedAt: Date | null;
|
|
135
135
|
feedback: string | null;
|
|
136
136
|
studentId: string;
|
|
137
|
+
points: number;
|
|
137
138
|
response: string;
|
|
138
139
|
isCorrect: boolean;
|
|
139
140
|
markschemeState: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
140
|
-
points: number;
|
|
141
|
-
questionId: string;
|
|
142
141
|
studentWorksheetResponseId: string | null;
|
|
142
|
+
questionId: string;
|
|
143
143
|
})[];
|
|
144
144
|
} & {
|
|
145
145
|
id: string;
|
|
@@ -159,12 +159,12 @@ export declare function answerWorksheetQuestion(worksheetResponseId: string, que
|
|
|
159
159
|
updatedAt: Date | null;
|
|
160
160
|
feedback: string | null;
|
|
161
161
|
studentId: string;
|
|
162
|
+
points: number;
|
|
162
163
|
response: string;
|
|
163
164
|
isCorrect: boolean;
|
|
164
165
|
markschemeState: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
165
|
-
points: number;
|
|
166
|
-
questionId: string;
|
|
167
166
|
studentWorksheetResponseId: string | null;
|
|
167
|
+
questionId: string;
|
|
168
168
|
}[];
|
|
169
169
|
} & {
|
|
170
170
|
id: string;
|
|
@@ -183,12 +183,12 @@ export declare function cancelGrading(worksheetResponseId: string, progressId: s
|
|
|
183
183
|
updatedAt: Date | null;
|
|
184
184
|
feedback: string | null;
|
|
185
185
|
studentId: string;
|
|
186
|
+
points: number;
|
|
186
187
|
response: string;
|
|
187
188
|
isCorrect: boolean;
|
|
188
189
|
markschemeState: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
189
|
-
points: number;
|
|
190
|
-
questionId: string;
|
|
191
190
|
studentWorksheetResponseId: string | null;
|
|
191
|
+
questionId: string;
|
|
192
192
|
}>;
|
|
193
193
|
export declare function regradeQuestion(worksheetResponseId: string, progressId: string): Promise<{
|
|
194
194
|
status: import(".prisma/client").$Enums.GenerationStatus | null;
|
|
@@ -197,12 +197,12 @@ export declare function regradeQuestion(worksheetResponseId: string, progressId:
|
|
|
197
197
|
updatedAt: Date | null;
|
|
198
198
|
feedback: string | null;
|
|
199
199
|
studentId: string;
|
|
200
|
+
points: number;
|
|
200
201
|
response: string;
|
|
201
202
|
isCorrect: boolean;
|
|
202
203
|
markschemeState: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
203
|
-
points: number;
|
|
204
|
-
questionId: string;
|
|
205
204
|
studentWorksheetResponseId: string | null;
|
|
205
|
+
questionId: string;
|
|
206
206
|
}>;
|
|
207
207
|
export declare function gradeAnswer(questionId: string, studentWorksheetResponseId: string, data: {
|
|
208
208
|
responseId?: string;
|
|
@@ -218,12 +218,12 @@ export declare function gradeAnswer(questionId: string, studentWorksheetResponse
|
|
|
218
218
|
updatedAt: Date | null;
|
|
219
219
|
feedback: string | null;
|
|
220
220
|
studentId: string;
|
|
221
|
+
points: number;
|
|
221
222
|
response: string;
|
|
222
223
|
isCorrect: boolean;
|
|
223
224
|
markschemeState: import("@prisma/client/runtime/library.js").JsonValue | null;
|
|
224
|
-
points: number;
|
|
225
|
-
questionId: string;
|
|
226
225
|
studentWorksheetResponseId: string | null;
|
|
226
|
+
questionId: string;
|
|
227
227
|
}>;
|
|
228
228
|
export declare function addCommentToResponse(responseId: string, comment: string, authorId: string): Promise<{
|
|
229
229
|
status: import(".prisma/client").$Enums.GenerationStatus | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studious-lms/server",
|
|
3
|
-
"version": "1.
|
|
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",
|
package/prisma/schema.prisma
CHANGED
|
@@ -478,7 +478,7 @@ model LabChat {
|
|
|
478
478
|
conversationId String @unique
|
|
479
479
|
createdById String // Teacher who created the lab
|
|
480
480
|
createdAt DateTime @default(now())
|
|
481
|
-
updatedAt DateTime @updatedAt
|
|
481
|
+
updatedAt DateTime @updatedAt
|
|
482
482
|
class Class @relation("ClassLabChats", fields: [classId], references: [id], onDelete: Cascade)
|
|
483
483
|
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
484
484
|
createdBy User @relation("CreatedLabChats", fields: [createdById], references: [id], onDelete: NoAction)
|