@studious-lms/server 1.4.9 → 1.6.0
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/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +2 -3
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +4 -5
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/lib/pusher.d.ts +7 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +9 -3
- package/dist/lib/pusher.js.map +1 -1
- package/dist/models/attendance.d.ts +16 -7
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +17 -2
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/auth.d.ts +1 -0
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +3 -2
- package/dist/models/auth.js.map +1 -1
- package/dist/models/comment.d.ts +0 -5
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/conversation.d.ts +17 -16
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +2 -1
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/message.d.ts +6 -3
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/school.d.ts +0 -22
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +2 -14
- package/dist/models/school.js.map +1 -1
- package/dist/models/worksheet.d.ts +76 -19
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +2 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +20 -9
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +63 -75
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts +13 -4
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +39 -27
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +5 -3
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +60 -24
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +34 -2
- package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -1
- package/dist/pipelines/pdfToWorksheet.js +205 -32
- package/dist/pipelines/pdfToWorksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +106 -48
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +0 -4
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +0 -8
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +64 -4
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/auth.d.ts +1 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +0 -3
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/conversation.d.ts +2 -1
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/labChat.d.ts +3 -1
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/message.d.ts +3 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/newtonChat.d.ts +1 -0
- package/dist/routers/newtonChat.d.ts.map +1 -1
- package/dist/routers/school.d.ts +6 -0
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +7 -2
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/worksheet.d.ts +37 -7
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +16 -6
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/announcement.js +4 -4
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +16 -6
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts.map +1 -1
- package/dist/services/attendance.js +68 -4
- package/dist/services/attendance.js.map +1 -1
- package/dist/services/auth.d.ts +1 -0
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/comment.d.ts +0 -3
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/conversation.d.ts +2 -1
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/labChat.d.ts +10 -2
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +103 -44
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +3 -0
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +20 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +1 -0
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +40 -17
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/school.d.ts +6 -0
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +71 -6
- package/dist/services/school.js.map +1 -1
- package/dist/services/worksheet.d.ts +48 -11
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +73 -29
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/generateUsername.d.ts +8 -0
- package/dist/utils/generateUsername.d.ts.map +1 -0
- package/dist/utils/generateUsername.js +103 -0
- package/dist/utils/generateUsername.js.map +1 -0
- package/dist/utils/inference.d.ts +50 -1
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +167 -2
- package/dist/utils/inference.js.map +1 -1
- package/dist/workers/connection.d.ts +3 -0
- package/dist/workers/connection.d.ts.map +1 -0
- package/dist/workers/connection.js +33 -0
- package/dist/workers/connection.js.map +1 -0
- package/dist/workers/index.d.ts +10 -0
- package/dist/workers/index.d.ts.map +1 -0
- package/dist/workers/index.js +63 -0
- package/dist/workers/index.js.map +1 -0
- package/dist/workers/processors/aiLabChat.d.ts +7 -0
- package/dist/workers/processors/aiLabChat.d.ts.map +1 -0
- package/dist/workers/processors/aiLabChat.js +52 -0
- package/dist/workers/processors/aiLabChat.js.map +1 -0
- package/dist/workers/processors/aiNewtonChat.d.ts +7 -0
- package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -0
- package/dist/workers/processors/aiNewtonChat.js +57 -0
- package/dist/workers/processors/aiNewtonChat.js.map +1 -0
- package/dist/workers/processors/createRunJobWorker.d.ts +46 -0
- package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -0
- package/dist/workers/processors/createRunJobWorker.js +118 -0
- package/dist/workers/processors/createRunJobWorker.js.map +1 -0
- package/dist/workers/processors/gradeWorksheet.d.ts +6 -0
- package/dist/workers/processors/gradeWorksheet.d.ts.map +1 -0
- package/dist/workers/processors/gradeWorksheet.js +19 -0
- package/dist/workers/processors/gradeWorksheet.js.map +1 -0
- package/dist/workers/processors/pdfToWorksheet.d.ts +8 -0
- package/dist/workers/processors/pdfToWorksheet.d.ts.map +1 -0
- package/dist/workers/processors/pdfToWorksheet.js +20 -0
- package/dist/workers/processors/pdfToWorksheet.js.map +1 -0
- package/dist/workers/processors/studentProgressChat.d.ts +8 -0
- package/dist/workers/processors/studentProgressChat.d.ts.map +1 -0
- package/dist/workers/processors/studentProgressChat.js +27 -0
- package/dist/workers/processors/studentProgressChat.js.map +1 -0
- package/dist/workers/queues.d.ts +85 -0
- package/dist/workers/queues.d.ts.map +1 -0
- package/dist/workers/queues.js +119 -0
- package/dist/workers/queues.js.map +1 -0
- package/package.json +6 -3
- package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +166 -0
- package/prisma/schema.prisma +465 -387
- package/src/index.ts +12 -1
- package/src/lib/config/cors.ts +0 -1
- package/src/lib/fileUpload.ts +2 -4
- package/src/lib/pusher.ts +7 -1
- package/src/models/attendance.ts +15 -0
- package/src/models/auth.ts +1 -0
- package/src/models/school.ts +0 -14
- package/src/models/worksheet.ts +2 -2
- package/src/pipelines/aiLabChat.ts +71 -79
- package/src/pipelines/aiNewtonChat.ts +47 -31
- package/src/pipelines/gradeWorksheet.ts +63 -25
- package/src/pipelines/pdfToWorksheet.ts +272 -34
- package/src/routers/assignment.ts +74 -3
- package/src/routers/school.ts +5 -0
- package/src/routers/worksheet.ts +25 -4
- package/src/services/announcement.ts +2 -2
- package/src/services/assignment.ts +18 -4
- package/src/services/attendance.ts +87 -2
- package/src/services/labChat.ts +108 -48
- package/src/services/message.ts +19 -0
- package/src/services/newtonChat.ts +46 -25
- package/src/services/school.ts +89 -6
- package/src/services/worksheet.ts +87 -28
- package/src/utils/generateUsername.ts +108 -0
- package/src/utils/inference.ts +209 -1
- package/src/workers/connection.ts +37 -0
- package/src/workers/index.ts +71 -0
- package/src/workers/processors/aiLabChat.ts +65 -0
- package/src/workers/processors/aiNewtonChat.ts +75 -0
- package/src/workers/processors/createRunJobWorker.ts +164 -0
- package/src/workers/processors/gradeWorksheet.ts +16 -0
- package/src/workers/processors/pdfToWorksheet.ts +17 -0
- package/src/workers/processors/studentProgressChat.ts +28 -0
- package/src/workers/queues.ts +174 -0
- package/tests/{globalSetup.ts → integration/globalSetup.ts} +4 -4
- package/tests/{helpers.ts → integration/helpers.ts} +2 -2
- package/tests/{pipelines → integration/pipelines}/pdfToWorksheet.test.ts +55 -43
- package/tests/{routers → integration/routers}/attendance.test.ts +61 -2
- package/tests/{routers → integration/routers}/auth.test.ts +3 -3
- package/tests/{routers → integration/routers}/class.test.ts +3 -17
- package/tests/{routers → integration/routers}/conversation.test.ts +1 -1
- package/tests/{routers → integration/routers}/event.test.ts +1 -1
- package/tests/{routers → integration/routers}/message.test.ts +1 -1
- package/tests/{routers → integration/routers}/newtonChat.test.ts +1 -1
- package/tests/{routers → integration/routers}/studentProgress.test.ts +1 -1
- package/tests/{user.test.ts → integration/routers/user.test.ts} +2 -2
- package/tests/integration/routers/worksheet.test.ts +448 -0
- package/tests/{setup.ts → integration/setup.ts} +4 -4
- package/tests/{lib → unit/lib}/cors.test.ts +1 -1
- package/tests/{middleware → unit/middleware}/security.test.ts +1 -1
- package/tests/{pipelines → unit/pipelines}/aiLabChat.test.ts +1 -1
- package/tests/{lib → unit/pipelines}/aiLabChatContract.test.ts +1 -1
- package/tests/{utils/aiLabChatPrompt.test.ts → unit/pipelines/labChatPrompt.test.ts} +2 -2
- package/tests/{utils → unit/services}/studentProgress.test.ts +5 -5
- package/tests/{utils → unit/utils}/aiUser.test.ts +1 -1
- package/tests/{utils → unit/utils}/generateInviteCode.test.ts +1 -1
- package/tests/{utils → unit/utils}/inference.test.ts +6 -6
- package/tests/{utils → unit/utils}/logger.test.ts +1 -1
- package/tests/{utils → unit/utils}/prismaErrorHandler.test.ts +1 -1
- package/tests/{utils → unit/utils}/prismaWrapper.test.ts +1 -1
- package/vitest.config.ts +11 -3
- package/vitest.unit.config.ts +10 -10
- package/tests/routers/worksheet.test.ts +0 -194
- package/tests/startup.test.ts +0 -5
- package/tests/worksheet.test.ts +0 -181
- /package/tests/{routers → integration/routers}/agenda.test.ts +0 -0
- /package/tests/{routers → integration/routers}/announcement.test.ts +0 -0
- /package/tests/{routers → integration/routers}/assignment.test.ts +0 -0
- /package/tests/{routers → integration/routers}/comment.test.ts +0 -0
- /package/tests/{routers → integration/routers}/folder.test.ts +0 -0
- /package/tests/{routers → integration/routers}/labChat.test.ts +0 -0
- /package/tests/{routers → integration/routers}/marketing.test.ts +0 -0
- /package/tests/{routers → integration/routers}/notification.test.ts +0 -0
- /package/tests/{routers → integration/routers}/search.test.ts +0 -0
- /package/tests/{routers → integration/routers}/section.test.ts +0 -0
- /package/tests/{server → unit/middleware}/rateLimit.test.ts +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Grade worksheet pipeline – AI-powered grading of worksheet responses.
|
|
3
3
|
* Grades questions via inference API, broadcasts status via Pusher (pending/completed/failed/cancelled).
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { JobStatus, WorksheetQuestionType } from "@prisma/client";
|
|
6
6
|
import { prisma } from "../lib/prisma.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
8
|
import z from "zod";
|
|
@@ -113,11 +113,11 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
|
|
|
113
113
|
const updatedStudentQuestionProgress = await prisma.studentQuestionProgress.update({
|
|
114
114
|
where: { id: studentQuestionProgress.id, status: {
|
|
115
115
|
not: {
|
|
116
|
-
in: [
|
|
116
|
+
in: [JobStatus.CANCELLED],
|
|
117
117
|
},
|
|
118
118
|
} },
|
|
119
119
|
data: {
|
|
120
|
-
status:
|
|
120
|
+
status: JobStatus.COMPLETED,
|
|
121
121
|
isCorrect: (apiResponse as { isCorrect: boolean }).isCorrect,
|
|
122
122
|
points: (apiResponse as { points: number }).points,
|
|
123
123
|
markschemeState: (apiResponse as {
|
|
@@ -143,13 +143,23 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
|
|
|
143
143
|
return updatedStudentQuestionProgress;
|
|
144
144
|
} catch (error) {
|
|
145
145
|
logger.error('Failed to grade worksheet response', { error, worksheetResponseId });
|
|
146
|
+
const { count } = await prisma.studentQuestionProgress.updateMany({
|
|
147
|
+
where: {
|
|
148
|
+
id: studentQuestionProgress.id,
|
|
149
|
+
status: { not: JobStatus.CANCELLED },
|
|
150
|
+
},
|
|
151
|
+
data: { status: JobStatus.FAILED },
|
|
152
|
+
});
|
|
153
|
+
if (count === 0) {
|
|
154
|
+
logger.info('Skipping failed status update for cancelled worksheet question', {
|
|
155
|
+
worksheetResponseId,
|
|
156
|
+
worksheetQuestionProgressId,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
146
160
|
pusher.trigger(worksheetChannel(worksheetResponse.id), `set-failed`, {
|
|
147
161
|
id: studentQuestionProgress.id,
|
|
148
162
|
});
|
|
149
|
-
await prisma.studentQuestionProgress.update({
|
|
150
|
-
where: { id: studentQuestionProgress.id },
|
|
151
|
-
data: { status: GenerationStatus.FAILED },
|
|
152
|
-
});
|
|
153
163
|
throw error;
|
|
154
164
|
}
|
|
155
165
|
}
|
|
@@ -160,7 +170,7 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
|
|
|
160
170
|
* @returns updated worksheet response
|
|
161
171
|
*/
|
|
162
172
|
|
|
163
|
-
const DO_NOT_INFERENCE_STATUSES = [
|
|
173
|
+
const DO_NOT_INFERENCE_STATUSES = [JobStatus.CANCELLED, JobStatus.PENDING, JobStatus.COMPLETED];
|
|
164
174
|
|
|
165
175
|
export const gradeWorksheetPipeline = async (worksheetResponseId: string) => {
|
|
166
176
|
logger.info('Grading worksheet response', { worksheetResponseId });
|
|
@@ -200,17 +210,20 @@ export const gradeWorksheetPipeline = async (worksheetResponseId: string) => {
|
|
|
200
210
|
for (const response of worksheetResponse.responses) {
|
|
201
211
|
logger.info('Grading question', { questionId: response.questionId });
|
|
202
212
|
|
|
203
|
-
const
|
|
204
|
-
where: {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
const { count } = await prisma.studentQuestionProgress.updateMany({
|
|
214
|
+
where: {
|
|
215
|
+
id: response.id,
|
|
216
|
+
status: {
|
|
217
|
+
not: {
|
|
218
|
+
in: DO_NOT_INFERENCE_STATUSES,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
data: { status: JobStatus.PENDING },
|
|
210
223
|
});
|
|
211
224
|
|
|
212
|
-
if (
|
|
213
|
-
|
|
225
|
+
if (count === 0) {
|
|
226
|
+
continue;
|
|
214
227
|
}
|
|
215
228
|
|
|
216
229
|
await gradeWorksheetQuestion(worksheetResponseId, response.id);
|
|
@@ -233,7 +246,7 @@ export const cancelGradePipeline = async (worksheetResponseId: string, worksheet
|
|
|
233
246
|
}
|
|
234
247
|
const updatedStudentQuestionProgress = await prisma.studentQuestionProgress.update({
|
|
235
248
|
where: { id: worksheetQuestionProgressId },
|
|
236
|
-
data: { status:
|
|
249
|
+
data: { status: JobStatus.CANCELLED },
|
|
237
250
|
});
|
|
238
251
|
|
|
239
252
|
await removeAllPreviousAIComments(worksheetQuestionProgressId);
|
|
@@ -255,26 +268,51 @@ export const regradeWorksheetPipeline = async (worksheetResponseId: string, work
|
|
|
255
268
|
},
|
|
256
269
|
});
|
|
257
270
|
|
|
258
|
-
await removeAllPreviousAIComments(worksheetQuestionProgressId);
|
|
259
|
-
|
|
260
271
|
if (!worksheetResponse) {
|
|
261
272
|
logger.error('Worksheet response not found');
|
|
262
273
|
throw new Error('Worksheet response not found');
|
|
263
274
|
}
|
|
264
275
|
|
|
265
|
-
const
|
|
276
|
+
const { count } = await prisma.studentQuestionProgress.updateMany({
|
|
277
|
+
where: {
|
|
278
|
+
id: worksheetQuestionProgressId,
|
|
279
|
+
status: { not: JobStatus.CANCELLED },
|
|
280
|
+
},
|
|
281
|
+
data: { status: JobStatus.PENDING },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (count === 0) {
|
|
285
|
+
logger.info('Skipping regrade for cancelled worksheet question', {
|
|
286
|
+
worksheetResponseId,
|
|
287
|
+
worksheetQuestionProgressId,
|
|
288
|
+
});
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const updatedStudentQuestionProgress = await prisma.studentQuestionProgress.findUnique({
|
|
266
293
|
where: { id: worksheetQuestionProgressId },
|
|
267
|
-
data: { status: GenerationStatus.PENDING },
|
|
268
294
|
});
|
|
269
295
|
|
|
296
|
+
await removeAllPreviousAIComments(worksheetQuestionProgressId);
|
|
297
|
+
|
|
270
298
|
await gradeWorksheetQuestion(worksheetResponseId, worksheetQuestionProgressId);
|
|
271
299
|
|
|
272
300
|
return updatedStudentQuestionProgress;
|
|
273
301
|
} catch (error) {
|
|
274
|
-
await prisma.studentQuestionProgress.
|
|
275
|
-
where: {
|
|
276
|
-
|
|
302
|
+
const { count } = await prisma.studentQuestionProgress.updateMany({
|
|
303
|
+
where: {
|
|
304
|
+
id: worksheetQuestionProgressId,
|
|
305
|
+
status: { not: JobStatus.CANCELLED },
|
|
306
|
+
},
|
|
307
|
+
data: { status: JobStatus.FAILED },
|
|
277
308
|
});
|
|
309
|
+
if (count === 0) {
|
|
310
|
+
logger.info('Skipping failed status update for cancelled worksheet question regrade', {
|
|
311
|
+
worksheetResponseId,
|
|
312
|
+
worksheetQuestionProgressId,
|
|
313
|
+
});
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
278
316
|
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
279
317
|
where: { id: worksheetResponseId, },
|
|
280
318
|
include: {
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PDF URL → worksheet: send a remote PDF URL to structured inference,
|
|
3
3
|
* then persist a Worksheet and WorksheetQuestion rows.
|
|
4
|
+
*
|
|
5
|
+
* Normally driven by a BullMQ worker, so the producer (e.g.
|
|
6
|
+
* `createWorksheetFromUploadedPdf`) creates a placeholder Worksheet row up
|
|
7
|
+
* front and passes its id in via `worksheetId`. The pipeline broadcasts
|
|
8
|
+
* `Worksheet.status` transitions on the per-worksheet job Pusher channel
|
|
9
|
+
* (`set-pending` / `set-completed` / `set-failed` / `set-cancelled`) — same
|
|
10
|
+
* convention as `gradeWorksheet`.
|
|
11
|
+
*
|
|
12
|
+
* The legacy `pdfToWorksheet(gcsUrl, classId)` signature is preserved for
|
|
13
|
+
* direct/synchronous callers (such as tests): when no `worksheetId` is
|
|
14
|
+
* supplied the pipeline creates the row itself.
|
|
4
15
|
*/
|
|
5
16
|
import { z } from "zod";
|
|
6
17
|
import type { ResponseInput } from "openai/resources/responses/responses.js";
|
|
18
|
+
import { JobStatus, Prisma } from "@prisma/client";
|
|
7
19
|
import { prisma } from "../lib/prisma.js";
|
|
8
20
|
import { inference } from "../utils/inference.js";
|
|
9
21
|
import { logger } from "../utils/logger.js";
|
|
22
|
+
import { pusher, worksheetJobChannel } from "../lib/pusher.js";
|
|
10
23
|
|
|
11
24
|
/** Restrict remote fetches to Google-hosted object URLs. */
|
|
12
25
|
function isProbablyGoogleCloudUrl(url: string): boolean {
|
|
@@ -41,6 +54,61 @@ function sanitizeUrlForLogs(url: string): string {
|
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Pusher event names mirror `gradeWorksheet`'s convention so clients can
|
|
59
|
+
* share the same JobStatus → event-name mapping.
|
|
60
|
+
*/
|
|
61
|
+
const STATUS_EVENT_NAMES: Record<JobStatus, string> = {
|
|
62
|
+
NOT_STARTED: 'set-not-started',
|
|
63
|
+
PENDING: 'set-pending',
|
|
64
|
+
COMPLETED: 'set-completed',
|
|
65
|
+
FAILED: 'set-failed',
|
|
66
|
+
CANCELLED: 'set-cancelled',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Realtime payload for the per-worksheet job channel.
|
|
71
|
+
*
|
|
72
|
+
* On COMPLETED we ship the new title and question count so clients can
|
|
73
|
+
* patch their card optimistically and side-step replica lag on the next
|
|
74
|
+
* refetch. Error strings are intentionally NOT broadcast — they may carry
|
|
75
|
+
* server internals; the full error stays on `RunJob.error` for ops only.
|
|
76
|
+
*/
|
|
77
|
+
export interface WorksheetJobStatusEvent {
|
|
78
|
+
worksheetId: string;
|
|
79
|
+
status: JobStatus;
|
|
80
|
+
/** Filled on COMPLETED so clients can avoid a replica-lagged refetch. */
|
|
81
|
+
name?: string;
|
|
82
|
+
questionCount?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fire-and-forget broadcast of a `Worksheet.status` transition. We never let
|
|
87
|
+
* a Pusher outage break the actual pipeline: the persisted `Worksheet.status`
|
|
88
|
+
* is the source of truth, the channel just nudges clients.
|
|
89
|
+
*/
|
|
90
|
+
async function emitStatus(
|
|
91
|
+
worksheetId: string | undefined,
|
|
92
|
+
status: JobStatus,
|
|
93
|
+
extras: Pick<WorksheetJobStatusEvent, 'name' | 'questionCount'> = {},
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
if (!worksheetId) return;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await pusher.trigger(
|
|
99
|
+
worksheetJobChannel(worksheetId),
|
|
100
|
+
STATUS_EVENT_NAMES[status],
|
|
101
|
+
{ worksheetId, status, ...extras } satisfies WorksheetJobStatusEvent,
|
|
102
|
+
);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.warn('pdfToWorksheet: failed to broadcast status update', {
|
|
105
|
+
worksheetId,
|
|
106
|
+
status,
|
|
107
|
+
error: error instanceof Error ? error.message : String(error),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
44
112
|
/** LLM output shape (validated by inference). */
|
|
45
113
|
const worksheetDraftSchema = z.object({
|
|
46
114
|
title: z.string(),
|
|
@@ -104,20 +172,74 @@ const schemaInstructions = `Return ONLY valid JSON matching this schema:
|
|
|
104
172
|
Rules: "questions" is an array ([] if none). Non-MCQ "options" can be []. Use "order" 1,2,3,… in reading order. No markdown or extra keys.
|
|
105
173
|
If no answer is provided for a multiple-choice or true/false question, determine the answer from the options.`;
|
|
106
174
|
|
|
175
|
+
export interface PdfToWorksheetOptions {
|
|
176
|
+
/**
|
|
177
|
+
* Pre-existing Worksheet row id. When supplied, the pipeline fills in this
|
|
178
|
+
* row instead of creating a new one and broadcasts progress on its job
|
|
179
|
+
* channel. This is the path used by the BullMQ worker.
|
|
180
|
+
*/
|
|
181
|
+
worksheetId?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
107
184
|
/**
|
|
108
185
|
* Sends a PDF URL to the model for remote retrieval, extracts a worksheet via
|
|
109
|
-
* structured inference, and inserts worksheet + questions
|
|
186
|
+
* structured inference, and inserts (or back-fills) worksheet + questions.
|
|
187
|
+
*
|
|
188
|
+
* When called with `options.worksheetId` the pipeline:
|
|
189
|
+
* - keeps that row's id stable (the client already routed to it),
|
|
190
|
+
* - drives `status` PENDING → COMPLETED / FAILED,
|
|
191
|
+
* - fans out `set-*` Pusher events on `worksheetJobChannel`.
|
|
110
192
|
*/
|
|
111
|
-
export async function pdfToWorksheet(
|
|
112
|
-
|
|
193
|
+
export async function pdfToWorksheet(
|
|
194
|
+
gcsUrl: string,
|
|
195
|
+
classId: string,
|
|
196
|
+
options: PdfToWorksheetOptions = {},
|
|
197
|
+
) {
|
|
198
|
+
const sourceUrl = sanitizeUrlForLogs(gcsUrl);
|
|
199
|
+
const placeholderWorksheetId = options.worksheetId;
|
|
200
|
+
|
|
201
|
+
await emitStatus(placeholderWorksheetId, JobStatus.PENDING);
|
|
113
202
|
|
|
114
203
|
if (!isProbablyGoogleCloudUrl(gcsUrl)) {
|
|
115
204
|
logger.warn("pdfToWorksheet: rejected unsupported PDF URL", {
|
|
116
|
-
sanitizedUrl,
|
|
205
|
+
sanitizedUrl: sourceUrl,
|
|
117
206
|
});
|
|
207
|
+
await markWorksheetFailed(
|
|
208
|
+
placeholderWorksheetId,
|
|
209
|
+
"Only Google Cloud Storage URLs are supported",
|
|
210
|
+
);
|
|
118
211
|
throw new Error("Only Google Cloud Storage URLs are supported");
|
|
119
212
|
}
|
|
120
213
|
|
|
214
|
+
// Look for a previously-completed worksheet pointing at the same source PDF.
|
|
215
|
+
// If there is one (and it's not the placeholder we were handed) we reuse it,
|
|
216
|
+
// mark the placeholder CANCELLED so the editor stops spinning, and return
|
|
217
|
+
// the canonical worksheet.
|
|
218
|
+
const existingForSource = await prisma.worksheet.findUnique({
|
|
219
|
+
where: { classId_sourceUrl: { classId, sourceUrl } },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (existingForSource && existingForSource.id !== placeholderWorksheetId) {
|
|
223
|
+
logger.info("pdfToWorksheet: reusing existing worksheet for source PDF", {
|
|
224
|
+
worksheetId: existingForSource.id,
|
|
225
|
+
classId,
|
|
226
|
+
sourceUrl,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (placeholderWorksheetId) {
|
|
230
|
+
await prisma.worksheet
|
|
231
|
+
.update({
|
|
232
|
+
where: { id: placeholderWorksheetId },
|
|
233
|
+
data: { status: JobStatus.CANCELLED },
|
|
234
|
+
})
|
|
235
|
+
.catch(() => undefined);
|
|
236
|
+
|
|
237
|
+
await emitStatus(placeholderWorksheetId, JobStatus.CANCELLED);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return existingForSource;
|
|
241
|
+
}
|
|
242
|
+
|
|
121
243
|
const filename = getFilenameFromUrl(gcsUrl);
|
|
122
244
|
const prompt = `Read the attached PDF and convert it into a single worksheet JSON object.\n\n${schemaInstructions}`;
|
|
123
245
|
|
|
@@ -132,46 +254,162 @@ export async function pdfToWorksheet(gcsUrl: string, classId: string) {
|
|
|
132
254
|
},
|
|
133
255
|
];
|
|
134
256
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
257
|
+
let draft: WorksheetDraft | null;
|
|
258
|
+
try {
|
|
259
|
+
draft = await inference<WorksheetDraft>(
|
|
260
|
+
input,
|
|
261
|
+
worksheetDraftSchema,
|
|
262
|
+
{
|
|
263
|
+
reason: 'pdf_to_worksheet_draft',
|
|
264
|
+
metadata: {
|
|
265
|
+
classId,
|
|
266
|
+
filename,
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
272
|
+
await markWorksheetFailed(placeholderWorksheetId, message);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
146
275
|
|
|
147
276
|
if (!draft) {
|
|
148
277
|
logger.warn("pdfToWorksheet: no model output", { filename });
|
|
278
|
+
await markWorksheetFailed(
|
|
279
|
+
placeholderWorksheetId,
|
|
280
|
+
"Model returned no parseable worksheet output",
|
|
281
|
+
);
|
|
149
282
|
return null;
|
|
150
283
|
}
|
|
151
284
|
|
|
152
|
-
|
|
153
|
-
const worksheet = await tx
|
|
154
|
-
|
|
155
|
-
|
|
285
|
+
try {
|
|
286
|
+
const worksheet = await prisma.$transaction(async (tx) => {
|
|
287
|
+
const created = placeholderWorksheetId
|
|
288
|
+
? await tx.worksheet.update({
|
|
289
|
+
where: { id: placeholderWorksheetId },
|
|
290
|
+
data: {
|
|
291
|
+
classId,
|
|
292
|
+
name: draft!.title,
|
|
293
|
+
sourceUrl,
|
|
294
|
+
status: JobStatus.PENDING,
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
: await tx.worksheet.create({
|
|
298
|
+
data: {
|
|
299
|
+
classId,
|
|
300
|
+
name: draft!.title,
|
|
301
|
+
sourceUrl,
|
|
302
|
+
status: JobStatus.PENDING,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
156
305
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
306
|
+
if (draft!.questions.length > 0) {
|
|
307
|
+
// When re-running against an existing placeholder, drop any prior
|
|
308
|
+
// questions so we don't accumulate duplicates from retries.
|
|
309
|
+
if (placeholderWorksheetId) {
|
|
310
|
+
await tx.worksheetQuestion.deleteMany({
|
|
311
|
+
where: { worksheetId: created.id },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await tx.worksheetQuestion.createMany({
|
|
316
|
+
data: draft!.questions.map((question) => ({
|
|
317
|
+
worksheetId: created.id,
|
|
318
|
+
type: question.type,
|
|
319
|
+
question: question.question,
|
|
320
|
+
answer: question.answer,
|
|
321
|
+
points: Math.round(question.points ?? 0),
|
|
322
|
+
options: question.options ?? [],
|
|
323
|
+
markScheme: (question.markScheme ?? []).map((item) => ({
|
|
324
|
+
...item,
|
|
325
|
+
points: Math.round(item.points),
|
|
326
|
+
})),
|
|
327
|
+
order: Math.round(question.order),
|
|
169
328
|
})),
|
|
170
|
-
|
|
171
|
-
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return tx.worksheet.update({
|
|
333
|
+
where: { id: created.id },
|
|
334
|
+
data: { status: JobStatus.COMPLETED },
|
|
172
335
|
});
|
|
173
|
-
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await emitStatus(placeholderWorksheetId, JobStatus.COMPLETED, {
|
|
339
|
+
name: worksheet.name,
|
|
340
|
+
questionCount: draft.questions.length,
|
|
341
|
+
});
|
|
174
342
|
|
|
175
343
|
return worksheet;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (
|
|
346
|
+
error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
347
|
+
error.code === "P2002"
|
|
348
|
+
) {
|
|
349
|
+
const worksheet = await prisma.worksheet.findUnique({
|
|
350
|
+
where: {
|
|
351
|
+
classId_sourceUrl: {
|
|
352
|
+
classId,
|
|
353
|
+
sourceUrl,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (worksheet) {
|
|
359
|
+
logger.info("pdfToWorksheet: reused worksheet created by concurrent retry", {
|
|
360
|
+
worksheetId: worksheet.id,
|
|
361
|
+
classId,
|
|
362
|
+
sourceUrl,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
if (placeholderWorksheetId && placeholderWorksheetId !== worksheet.id) {
|
|
366
|
+
await prisma.worksheet
|
|
367
|
+
.update({
|
|
368
|
+
where: { id: placeholderWorksheetId },
|
|
369
|
+
data: { status: JobStatus.CANCELLED },
|
|
370
|
+
})
|
|
371
|
+
.catch(() => undefined);
|
|
372
|
+
|
|
373
|
+
await emitStatus(placeholderWorksheetId, JobStatus.CANCELLED);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return worksheet;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
381
|
+
await markWorksheetFailed(placeholderWorksheetId, message);
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function markWorksheetFailed(
|
|
387
|
+
worksheetId: string | undefined,
|
|
388
|
+
errorMessage: string,
|
|
389
|
+
) {
|
|
390
|
+
if (!worksheetId) return;
|
|
391
|
+
|
|
392
|
+
await prisma.worksheet
|
|
393
|
+
.update({
|
|
394
|
+
where: { id: worksheetId },
|
|
395
|
+
data: { status: JobStatus.FAILED },
|
|
396
|
+
})
|
|
397
|
+
.catch((updateError) => {
|
|
398
|
+
logger.error('pdfToWorksheet: failed to mark worksheet FAILED', {
|
|
399
|
+
worksheetId,
|
|
400
|
+
error:
|
|
401
|
+
updateError instanceof Error
|
|
402
|
+
? updateError.message
|
|
403
|
+
: String(updateError),
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// The full error is logged + persisted on `RunJob.error`; the broadcast
|
|
408
|
+
// intentionally carries no error payload to avoid leaking internals.
|
|
409
|
+
logger.error('pdfToWorksheet: marking worksheet FAILED', {
|
|
410
|
+
worksheetId,
|
|
411
|
+
error: errorMessage,
|
|
176
412
|
});
|
|
413
|
+
|
|
414
|
+
await emitStatus(worksheetId, JobStatus.FAILED);
|
|
177
415
|
}
|
|
@@ -5,7 +5,6 @@ import { prisma } from "../lib/prisma.js";
|
|
|
5
5
|
import { createDirectUploadFiles, type DirectUploadFile, confirmDirectUpload, updateUploadProgress, type UploadedFile } from "../lib/fileUpload.js";
|
|
6
6
|
import { deleteFile } from "../lib/googleCloudStorage.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
|
-
import { gradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
|
|
9
8
|
import {
|
|
10
9
|
classMemberOrSchoolAdminWhere,
|
|
11
10
|
classTeacherOrSchoolAdminWhere,
|
|
@@ -35,6 +34,15 @@ import {
|
|
|
35
34
|
} from "../services/assignment.js";
|
|
36
35
|
import { sendToMultiple } from "../services/notification.js";
|
|
37
36
|
|
|
37
|
+
function getCommenterName(comment: {
|
|
38
|
+
author: {
|
|
39
|
+
username: string;
|
|
40
|
+
profile: { displayName: string | null } | null;
|
|
41
|
+
};
|
|
42
|
+
}) {
|
|
43
|
+
return comment.author.profile?.displayName || comment.author.username;
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
// DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
|
|
39
47
|
// Use directFileSchema instead
|
|
40
48
|
|
|
@@ -369,7 +377,18 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
369
377
|
.mutation(async ({ ctx, input }) => {
|
|
370
378
|
const assignment = await prisma.assignment.findFirst({
|
|
371
379
|
where: { id: input.assignmentId, classId: input.classId },
|
|
372
|
-
select: {
|
|
380
|
+
select: {
|
|
381
|
+
id: true,
|
|
382
|
+
title: true,
|
|
383
|
+
class: {
|
|
384
|
+
select: {
|
|
385
|
+
id: true,
|
|
386
|
+
name: true,
|
|
387
|
+
students: { select: { id: true } },
|
|
388
|
+
teachers: { select: { id: true } },
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
373
392
|
});
|
|
374
393
|
if (!assignment) {
|
|
375
394
|
throw new TRPCError({
|
|
@@ -405,6 +424,24 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
405
424
|
include: commentInclude,
|
|
406
425
|
});
|
|
407
426
|
|
|
427
|
+
const authorId = ctx.user!.id;
|
|
428
|
+
const receiverIds = Array.from(
|
|
429
|
+
new Set([
|
|
430
|
+
...assignment.class.students.map((s) => s.id),
|
|
431
|
+
...assignment.class.teachers.map((t) => t.id),
|
|
432
|
+
])
|
|
433
|
+
).filter((id) => id !== authorId);
|
|
434
|
+
|
|
435
|
+
if (receiverIds.length > 0) {
|
|
436
|
+
sendToMultiple({
|
|
437
|
+
receiverIds,
|
|
438
|
+
title: `New comment on ${assignment.title}`,
|
|
439
|
+
content: `${getCommenterName(comment)} commented in ${assignment.class.name}:\n\n${input.content}\n\n[View assignment](/class/${assignment.class.id}/assignments/${assignment.id})`,
|
|
440
|
+
}).catch((error) => {
|
|
441
|
+
logger.error("Failed to send assignment comment notifications:", error);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
408
445
|
return { comment };
|
|
409
446
|
}),
|
|
410
447
|
|
|
@@ -458,7 +495,23 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
458
495
|
{ assignment: { class: classTeacherOrSchoolAdminWhere(ctx.user!.id) } },
|
|
459
496
|
],
|
|
460
497
|
},
|
|
461
|
-
select: {
|
|
498
|
+
select: {
|
|
499
|
+
id: true,
|
|
500
|
+
studentId: true,
|
|
501
|
+
assignment: {
|
|
502
|
+
select: {
|
|
503
|
+
id: true,
|
|
504
|
+
title: true,
|
|
505
|
+
class: {
|
|
506
|
+
select: {
|
|
507
|
+
id: true,
|
|
508
|
+
name: true,
|
|
509
|
+
teachers: { select: { id: true } },
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
462
515
|
});
|
|
463
516
|
if (!submission) {
|
|
464
517
|
throw new TRPCError({
|
|
@@ -493,6 +546,24 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
493
546
|
include: commentInclude,
|
|
494
547
|
});
|
|
495
548
|
|
|
549
|
+
const authorId = ctx.user!.id;
|
|
550
|
+
const receiverIds = Array.from(
|
|
551
|
+
new Set([
|
|
552
|
+
submission.studentId,
|
|
553
|
+
...submission.assignment.class.teachers.map((t) => t.id),
|
|
554
|
+
])
|
|
555
|
+
).filter((id) => id !== authorId);
|
|
556
|
+
|
|
557
|
+
if (receiverIds.length > 0) {
|
|
558
|
+
sendToMultiple({
|
|
559
|
+
receiverIds,
|
|
560
|
+
title: `New private comment on ${submission.assignment.title}`,
|
|
561
|
+
content: `${getCommenterName(comment)} commented on a submission in ${submission.assignment.class.name}:\n\n${input.content}\n\n[View submission](/class/${submission.assignment.class.id}/assignments/${submission.assignment.id}/submissions/${submission.id})`,
|
|
562
|
+
}).catch((error) => {
|
|
563
|
+
logger.error("Failed to send submission comment notifications:", error);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
496
567
|
return { comment };
|
|
497
568
|
}),
|
|
498
569
|
|
package/src/routers/school.ts
CHANGED
|
@@ -104,6 +104,11 @@ export const schoolRouter = createTRPCRouter({
|
|
|
104
104
|
schoolId: z.string(),
|
|
105
105
|
email: z.string().email(),
|
|
106
106
|
role: schoolRoleSchema,
|
|
107
|
+
username: z
|
|
108
|
+
.string()
|
|
109
|
+
.min(3, "Username must be at least 3 characters")
|
|
110
|
+
.optional(),
|
|
111
|
+
displayName: z.string().min(1).optional(),
|
|
107
112
|
})
|
|
108
113
|
)
|
|
109
114
|
.mutation(({ ctx, input }) => createInvite(ctx.user!.id, input)),
|
package/src/routers/worksheet.ts
CHANGED
|
@@ -127,8 +127,18 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
127
127
|
),
|
|
128
128
|
|
|
129
129
|
createFromPdf: protectedTeacherProcedure
|
|
130
|
-
.input(
|
|
131
|
-
|
|
130
|
+
.input(
|
|
131
|
+
z.object({
|
|
132
|
+
classId: z.string(),
|
|
133
|
+
url: z.string().url(),
|
|
134
|
+
name: z.string().optional(),
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
.mutation(({ ctx, input }) =>
|
|
138
|
+
createWorksheetFromPdf(input.classId, input.url, ctx.user!.id, {
|
|
139
|
+
name: input.name,
|
|
140
|
+
}),
|
|
141
|
+
),
|
|
132
142
|
|
|
133
143
|
getWorksheetUploadUrls: protectedTeacherProcedure
|
|
134
144
|
.input(getWorksheetUploadUrlsSchema)
|
|
@@ -190,9 +200,20 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
190
200
|
}),
|
|
191
201
|
|
|
192
202
|
createFromUploadedPdf: protectedTeacherProcedure
|
|
193
|
-
.input(
|
|
203
|
+
.input(
|
|
204
|
+
z.object({
|
|
205
|
+
classId: z.string(),
|
|
206
|
+
fileId: z.string(),
|
|
207
|
+
name: z.string().optional(),
|
|
208
|
+
}),
|
|
209
|
+
)
|
|
194
210
|
.mutation(({ ctx, input }) =>
|
|
195
|
-
createWorksheetFromUploadedPdf(
|
|
211
|
+
createWorksheetFromUploadedPdf(
|
|
212
|
+
input.classId,
|
|
213
|
+
input.fileId,
|
|
214
|
+
ctx.user!.id,
|
|
215
|
+
{ name: input.name },
|
|
216
|
+
)
|
|
196
217
|
),
|
|
197
218
|
|
|
198
219
|
addQuestion: protectedProcedure
|