@studious-lms/server 1.5.0 → 1.7.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 +3 -2
- package/dist/lib/config/cors.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/conversation.d.ts +33 -17
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/conversation.js +50 -2
- package/dist/models/conversation.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 +62 -74
- 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/pdfToWorksheet.d.ts +31 -2
- package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -1
- package/dist/pipelines/pdfToWorksheet.js +160 -28
- package/dist/pipelines/pdfToWorksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +48 -4
- package/dist/routers/_app.d.ts.map +1 -1
- 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/conversation.d.ts +15 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +6 -3
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/labChat.d.ts +1 -0
- 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/worksheet.d.ts +4 -2
- 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/conversation.d.ts +16 -0
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/conversation.js +22 -3
- package/dist/services/conversation.js.map +1 -1
- package/dist/services/labChat.d.ts +8 -1
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +101 -42
- 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 +8 -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/worksheet.d.ts +15 -6
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +58 -12
- package/dist/services/worksheet.js.map +1 -1
- 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/index.js +13 -3
- package/dist/workers/index.js.map +1 -1
- 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 +5 -6
- package/dist/workers/processors/pdfToWorksheet.d.ts.map +1 -1
- package/dist/workers/processors/pdfToWorksheet.js +15 -95
- package/dist/workers/processors/pdfToWorksheet.js.map +1 -1
- 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 +20 -4
- package/dist/workers/queues.d.ts.map +1 -1
- package/dist/workers/queues.js +3 -3
- package/dist/workers/queues.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +12 -1
- package/src/lib/config/cors.ts +1 -0
- package/src/lib/pusher.ts +7 -1
- package/src/models/attendance.ts +15 -0
- package/src/models/conversation.ts +54 -0
- package/src/pipelines/aiLabChat.ts +70 -78
- package/src/pipelines/aiNewtonChat.ts +47 -31
- package/src/pipelines/pdfToWorksheet.ts +224 -32
- package/src/routers/assignment.ts +74 -3
- package/src/routers/conversation.ts +7 -0
- package/src/routers/worksheet.ts +25 -4
- package/src/services/announcement.ts +2 -2
- package/src/services/assignment.ts +17 -4
- package/src/services/attendance.ts +87 -2
- package/src/services/conversation.ts +30 -0
- package/src/services/labChat.ts +106 -46
- package/src/services/message.ts +6 -0
- package/src/services/newtonChat.ts +45 -25
- package/src/services/worksheet.ts +71 -10
- package/src/utils/inference.ts +209 -1
- package/src/workers/index.ts +11 -1
- 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 +12 -114
- package/src/workers/processors/studentProgressChat.ts +28 -0
- package/src/workers/queues.ts +20 -5
- package/tests/integration/routers/attendance.test.ts +60 -1
- package/tests/integration/routers/conversation.test.ts +121 -2
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
countUnreadMessages,
|
|
9
9
|
countUnreadMentions,
|
|
10
10
|
findUserByUsername,
|
|
11
|
+
searchInstitutionConversationMembers,
|
|
11
12
|
findDmConversationsByUserId,
|
|
12
13
|
findUsersByIdsOrUsernames,
|
|
13
14
|
createConversation,
|
|
@@ -17,6 +18,11 @@ import {
|
|
|
17
18
|
deleteConversationMember,
|
|
18
19
|
} from "../models/conversation.js";
|
|
19
20
|
|
|
21
|
+
/** Maximum number of member suggestions returned for conversation search. */
|
|
22
|
+
const MAX_MEMBER_SEARCH_RESULTS = 10;
|
|
23
|
+
/** Minimum non-whitespace characters required before running member search. */
|
|
24
|
+
const MIN_MEMBER_SEARCH_QUERY_LENGTH = 2;
|
|
25
|
+
|
|
20
26
|
/** List user's conversations with unread and mention counts. */
|
|
21
27
|
export async function listConversations(userId: string) {
|
|
22
28
|
const conversations = await findConversationsByUserId(userId);
|
|
@@ -89,6 +95,30 @@ export async function hideConversation(userId: string, conversationId: string) {
|
|
|
89
95
|
return { success: true };
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Search active school peers that can be selected for a new conversation.
|
|
100
|
+
*
|
|
101
|
+
* Blank or one-character input returns an empty list instead of searching all
|
|
102
|
+
* institution members, which keeps typeahead callers from issuing broad result
|
|
103
|
+
* queries.
|
|
104
|
+
*/
|
|
105
|
+
export async function searchConversationMembers(userId: string, query: string) {
|
|
106
|
+
const trimmedQuery = query.trim();
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
!trimmedQuery ||
|
|
110
|
+
trimmedQuery.length < MIN_MEMBER_SEARCH_QUERY_LENGTH
|
|
111
|
+
) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return searchInstitutionConversationMembers(
|
|
116
|
+
userId,
|
|
117
|
+
trimmedQuery,
|
|
118
|
+
MAX_MEMBER_SEARCH_RESULTS
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
92
122
|
/** Create a DM or group conversation with the given members. */
|
|
93
123
|
export async function createConversationRecord(
|
|
94
124
|
userId: string,
|
package/src/services/labChat.ts
CHANGED
|
@@ -17,9 +17,10 @@ import {
|
|
|
17
17
|
findLabChatForDelete,
|
|
18
18
|
findMentionedMemberships,
|
|
19
19
|
} from "../models/labChat.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { getAIUserId, isAIUser } from "../utils/aiUser.js";
|
|
21
|
+
import { createPendingAIMessage } from "../utils/inference.js";
|
|
22
22
|
import { logger } from "../utils/logger.js";
|
|
23
|
+
import { enqueueRunJob } from "../workers/queues.js";
|
|
23
24
|
import {
|
|
24
25
|
claimMessageAttachments,
|
|
25
26
|
ensureMessageHasContentOrAttachments,
|
|
@@ -125,12 +126,23 @@ export async function createLabChat(
|
|
|
125
126
|
return labChat;
|
|
126
127
|
});
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
// Create the AI introduction placeholder *outside* the transaction (the
|
|
130
|
+
// helper triggers Pusher and we don't want a tx rollback to leave a
|
|
131
|
+
// dangling broadcast). The pipeline will fill this row in.
|
|
132
|
+
try {
|
|
133
|
+
const aiPlaceholder = await createPendingAIMessage(result.conversationId);
|
|
134
|
+
await enqueueRunJob(
|
|
135
|
+
"AI_LAB_CHAT",
|
|
136
|
+
{ conversationId: result.conversationId, aiMessageId: aiPlaceholder.id },
|
|
137
|
+
{ classId, userId },
|
|
138
|
+
);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.error("Failed to enqueue lab chat introduction", {
|
|
141
|
+
error,
|
|
142
|
+
labChatId: result.id,
|
|
143
|
+
conversationId: result.conversationId,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
134
146
|
|
|
135
147
|
try {
|
|
136
148
|
await pusher.trigger(teacherChannel(classId), "lab-chat-created", {
|
|
@@ -172,11 +184,13 @@ export async function getLabChat(userId: string, labChatId: string) {
|
|
|
172
184
|
});
|
|
173
185
|
}
|
|
174
186
|
|
|
187
|
+
// The AI placeholder row IS the pending generation marker now — its
|
|
188
|
+
// status flips PENDING → COMPLETED / FAILED as the pipeline runs.
|
|
175
189
|
const pendingMessage = await prisma.message.findFirst({
|
|
176
190
|
where: {
|
|
177
191
|
conversationId: labChat.conversationId,
|
|
178
192
|
status: JobStatus.PENDING,
|
|
179
|
-
senderId:
|
|
193
|
+
senderId: getAIUserId(),
|
|
180
194
|
},
|
|
181
195
|
orderBy: { createdAt: "desc" },
|
|
182
196
|
select: { id: true },
|
|
@@ -252,7 +266,6 @@ export async function postToLabChat(
|
|
|
252
266
|
content: normalizedContent,
|
|
253
267
|
senderId: userId,
|
|
254
268
|
conversationId: labChat.conversationId,
|
|
255
|
-
...(isAIUser(userId) ? {} : { status: JobStatus.PENDING }),
|
|
256
269
|
},
|
|
257
270
|
});
|
|
258
271
|
|
|
@@ -309,6 +322,7 @@ export async function postToLabChat(
|
|
|
309
322
|
content: result.content,
|
|
310
323
|
senderId: result.senderId,
|
|
311
324
|
conversationId: result.conversationId,
|
|
325
|
+
status: result.status,
|
|
312
326
|
createdAt: result.createdAt,
|
|
313
327
|
sender: result.sender,
|
|
314
328
|
attachments: result.attachments ?? [],
|
|
@@ -321,26 +335,49 @@ export async function postToLabChat(
|
|
|
321
335
|
}
|
|
322
336
|
|
|
323
337
|
if (!isAIUser(userId)) {
|
|
338
|
+
// Create the AI placeholder post-tx so a transaction rollback can never
|
|
339
|
+
// leave a dangling PENDING bubble. The placeholder broadcast IS the
|
|
340
|
+
// "AI is thinking..." indicator inline in the chat; the teacher-channel
|
|
341
|
+
// event below drives the lab list "AI busy in this lab" pill.
|
|
342
|
+
let aiPlaceholderId: string | null = null;
|
|
343
|
+
try {
|
|
344
|
+
const aiPlaceholder = await createPendingAIMessage(labChat.conversationId);
|
|
345
|
+
aiPlaceholderId = aiPlaceholder.id;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
logger.error("Failed to create AI placeholder for lab response", {
|
|
348
|
+
error,
|
|
349
|
+
labChatId,
|
|
350
|
+
teacherMessageId: result.id,
|
|
351
|
+
conversationId: labChat.conversationId,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
324
355
|
try {
|
|
325
356
|
await pusher.trigger(teacherChannel(labChat.classId), "lab-response-pending", {
|
|
326
357
|
labChatId,
|
|
327
|
-
messageId: result.id,
|
|
358
|
+
messageId: aiPlaceholderId ?? result.id,
|
|
328
359
|
conversationId: labChat.conversationId,
|
|
329
360
|
});
|
|
330
361
|
} catch (error) {
|
|
331
362
|
console.error("Failed to broadcast lab response pending:", error);
|
|
332
363
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
364
|
+
|
|
365
|
+
if (aiPlaceholderId) {
|
|
366
|
+
try {
|
|
367
|
+
await enqueueRunJob(
|
|
368
|
+
"AI_LAB_CHAT",
|
|
369
|
+
{ conversationId: labChat.conversationId, aiMessageId: aiPlaceholderId },
|
|
370
|
+
{ classId: labChat.classId, userId },
|
|
371
|
+
);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
logger.error("Failed to enqueue lab response", {
|
|
374
|
+
error,
|
|
375
|
+
labChatId,
|
|
376
|
+
aiMessageId: aiPlaceholderId,
|
|
377
|
+
conversationId: labChat.conversationId,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
344
381
|
}
|
|
345
382
|
|
|
346
383
|
return {
|
|
@@ -348,6 +385,7 @@ export async function postToLabChat(
|
|
|
348
385
|
content: result.content,
|
|
349
386
|
senderId: result.senderId,
|
|
350
387
|
conversationId: result.conversationId,
|
|
388
|
+
status: result.status,
|
|
351
389
|
createdAt: result.createdAt,
|
|
352
390
|
sender: result.sender,
|
|
353
391
|
attachments: result.attachments ?? [],
|
|
@@ -406,7 +444,13 @@ export async function deleteLabChat(userId: string, labChatId: string) {
|
|
|
406
444
|
return { success: true };
|
|
407
445
|
}
|
|
408
446
|
|
|
409
|
-
/**
|
|
447
|
+
/**
|
|
448
|
+
* Rerun the last AI response in a lab chat.
|
|
449
|
+
*
|
|
450
|
+
* The AI message bubble is reused (not deleted): we flip it back to
|
|
451
|
+
* PENDING and clear its content so the existing message id stays stable
|
|
452
|
+
* for the client. The pipeline then refills it.
|
|
453
|
+
*/
|
|
410
454
|
export async function rerunLastResponse(userId: string, labChatId: string) {
|
|
411
455
|
const labChat = await findLabChatForPost(labChatId, userId);
|
|
412
456
|
if (!labChat) {
|
|
@@ -446,46 +490,62 @@ export async function rerunLastResponse(userId: string, labChatId: string) {
|
|
|
446
490
|
});
|
|
447
491
|
}
|
|
448
492
|
|
|
449
|
-
await prisma
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
493
|
+
const reusedAiMessage = await prisma.message.update({
|
|
494
|
+
where: { id: lastMessage.id },
|
|
495
|
+
data: {
|
|
496
|
+
content: "",
|
|
497
|
+
status: JobStatus.PENDING,
|
|
498
|
+
meta: {},
|
|
499
|
+
},
|
|
500
|
+
select: { id: true, conversationId: true, status: true, content: true, senderId: true, createdAt: true },
|
|
501
|
+
});
|
|
458
502
|
|
|
503
|
+
// Tell the chat UI the existing AI bubble is regenerating — content
|
|
504
|
+
// cleared, status flipped back to PENDING, same id.
|
|
459
505
|
try {
|
|
460
|
-
await pusher.trigger(chatChannel(labChat.conversationId), "message-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
senderId:
|
|
506
|
+
await pusher.trigger(chatChannel(labChat.conversationId), "message-updated", {
|
|
507
|
+
id: reusedAiMessage.id,
|
|
508
|
+
content: reusedAiMessage.content,
|
|
509
|
+
senderId: reusedAiMessage.senderId,
|
|
510
|
+
conversationId: reusedAiMessage.conversationId,
|
|
511
|
+
status: reusedAiMessage.status,
|
|
512
|
+
createdAt: reusedAiMessage.createdAt,
|
|
513
|
+
sender: {
|
|
514
|
+
id: reusedAiMessage.senderId,
|
|
515
|
+
username: 'Newton_AI',
|
|
516
|
+
profile: { displayName: 'Newton AI', profilePicture: null },
|
|
517
|
+
},
|
|
518
|
+
mentionedUserIds: [],
|
|
519
|
+
meta: {},
|
|
520
|
+
attachments: [],
|
|
464
521
|
});
|
|
465
522
|
} catch (error) {
|
|
466
|
-
console.error("Failed to broadcast message
|
|
523
|
+
console.error("Failed to broadcast AI message rerun reset:", error);
|
|
467
524
|
}
|
|
468
525
|
try {
|
|
469
526
|
await pusher.trigger(teacherChannel(labChat.classId), "lab-response-pending", {
|
|
470
527
|
labChatId,
|
|
471
|
-
messageId:
|
|
528
|
+
messageId: reusedAiMessage.id,
|
|
472
529
|
conversationId: labChat.conversationId,
|
|
473
530
|
});
|
|
474
531
|
} catch (error) {
|
|
475
532
|
console.error("Failed to broadcast lab response pending:", error);
|
|
476
533
|
}
|
|
477
534
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
535
|
+
try {
|
|
536
|
+
await enqueueRunJob(
|
|
537
|
+
"AI_LAB_CHAT",
|
|
538
|
+
{ conversationId: labChat.conversationId, aiMessageId: reusedAiMessage.id },
|
|
539
|
+
{ classId: labChat.classId, userId },
|
|
540
|
+
);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
logger.error("Failed to enqueue lab response rerun", {
|
|
483
543
|
error,
|
|
484
544
|
labChatId,
|
|
485
|
-
|
|
545
|
+
aiMessageId: reusedAiMessage.id,
|
|
486
546
|
conversationId: labChat.conversationId,
|
|
487
|
-
})
|
|
488
|
-
|
|
547
|
+
});
|
|
548
|
+
}
|
|
489
549
|
|
|
490
550
|
return { success: true };
|
|
491
551
|
}
|
package/src/services/message.ts
CHANGED
|
@@ -192,6 +192,7 @@ export async function listMessages(
|
|
|
192
192
|
conversationId: message.conversationId,
|
|
193
193
|
createdAt: message.createdAt,
|
|
194
194
|
sender: message.sender,
|
|
195
|
+
status: message.status,
|
|
195
196
|
attachments: message.attachments.map((a) => ({
|
|
196
197
|
id: a.id,
|
|
197
198
|
name: a.name,
|
|
@@ -316,6 +317,7 @@ export async function sendMessage(
|
|
|
316
317
|
content: result.content,
|
|
317
318
|
senderId: result.senderId,
|
|
318
319
|
conversationId: result.conversationId,
|
|
320
|
+
status: result.status,
|
|
319
321
|
createdAt: result.createdAt,
|
|
320
322
|
sender: result.sender,
|
|
321
323
|
attachments: result.attachments ?? [],
|
|
@@ -336,6 +338,7 @@ export async function sendMessage(
|
|
|
336
338
|
content: result.content,
|
|
337
339
|
senderId: result.senderId,
|
|
338
340
|
conversationId: result.conversationId,
|
|
341
|
+
status: result.status,
|
|
339
342
|
createdAt: result.createdAt,
|
|
340
343
|
sender: result.sender,
|
|
341
344
|
attachments: result.attachments ?? [],
|
|
@@ -440,6 +443,7 @@ export async function updateMessage(
|
|
|
440
443
|
content: updatedMessage.content,
|
|
441
444
|
senderId: updatedMessage.senderId,
|
|
442
445
|
conversationId: updatedMessage.conversationId,
|
|
446
|
+
status: updatedMessage.status,
|
|
443
447
|
createdAt: updatedMessage.createdAt,
|
|
444
448
|
sender: updatedMessage.sender,
|
|
445
449
|
attachments: updatedMessage.attachments ?? [],
|
|
@@ -460,6 +464,7 @@ export async function updateMessage(
|
|
|
460
464
|
content: updatedMessage.content,
|
|
461
465
|
senderId: updatedMessage.senderId,
|
|
462
466
|
conversationId: updatedMessage.conversationId,
|
|
467
|
+
status: updatedMessage.status,
|
|
463
468
|
createdAt: updatedMessage.createdAt,
|
|
464
469
|
sender: updatedMessage.sender,
|
|
465
470
|
mentionedUserIds,
|
|
@@ -633,6 +638,7 @@ export async function markSuggestionCreated(
|
|
|
633
638
|
content: updated.content,
|
|
634
639
|
senderId: updated.senderId,
|
|
635
640
|
conversationId: updated.conversationId,
|
|
641
|
+
status: updated.status,
|
|
636
642
|
createdAt: updated.createdAt,
|
|
637
643
|
sender: updated.sender,
|
|
638
644
|
attachments: updated.attachments ?? [],
|
|
@@ -6,7 +6,8 @@ import { TRPCError } from "@trpc/server";
|
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
7
|
import { chatChannel, pusher } from "../lib/pusher.js";
|
|
8
8
|
import { isAIUser } from "../utils/aiUser.js";
|
|
9
|
-
import {
|
|
9
|
+
import { createPendingAIMessage } from "../utils/inference.js";
|
|
10
|
+
import { enqueueRunJob } from "../workers/queues.js";
|
|
10
11
|
import {
|
|
11
12
|
claimMessageAttachments,
|
|
12
13
|
ensureMessageHasContentOrAttachments,
|
|
@@ -44,7 +45,7 @@ export async function getTutorConversation(
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
const
|
|
48
|
+
const { chat, isNew } = await prisma.$transaction(async (tx) => {
|
|
48
49
|
const existingNewtonChat = await tx.newtonChat.findFirst({
|
|
49
50
|
where: { submissionId: submission.id },
|
|
50
51
|
include: {
|
|
@@ -60,7 +61,7 @@ export async function getTutorConversation(
|
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
if (existingNewtonChat && existingNewtonChat.conversation.members.length > 0) {
|
|
63
|
-
return existingNewtonChat;
|
|
64
|
+
return { chat: existingNewtonChat, isNew: false };
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
if (existingNewtonChat) {
|
|
@@ -71,7 +72,7 @@ export async function getTutorConversation(
|
|
|
71
72
|
role: "MEMBER",
|
|
72
73
|
},
|
|
73
74
|
});
|
|
74
|
-
return existingNewtonChat;
|
|
75
|
+
return { chat: existingNewtonChat, isNew: false };
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
const conversation = await tx.conversation.create({
|
|
@@ -98,23 +99,31 @@ export async function getTutorConversation(
|
|
|
98
99
|
},
|
|
99
100
|
});
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
return { chat: newtonChat, isNew: true };
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (isNew) {
|
|
106
|
+
// Create the AI introduction placeholder and enqueue OUTSIDE the
|
|
107
|
+
// transaction — a tx rollback would otherwise orphan a job pointing at
|
|
108
|
+
// a non-existent NewtonChat / Message row.
|
|
109
|
+
try {
|
|
110
|
+
const aiPlaceholder = await createPendingAIMessage(chat.conversationId);
|
|
111
|
+
await enqueueRunJob(
|
|
112
|
+
"AI_NEWTON_CHAT",
|
|
113
|
+
{ conversationId: chat.conversationId, aiMessageId: aiPlaceholder.id },
|
|
114
|
+
{ userId },
|
|
115
|
+
);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.error("Failed to enqueue Newton introduction", {
|
|
107
118
|
error,
|
|
108
|
-
newtonChatId:
|
|
119
|
+
newtonChatId: chat.id,
|
|
109
120
|
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return newtonChat;
|
|
113
|
-
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
114
123
|
|
|
115
124
|
return {
|
|
116
|
-
conversationId:
|
|
117
|
-
newtonChatId:
|
|
125
|
+
conversationId: chat.conversationId,
|
|
126
|
+
newtonChatId: chat.id,
|
|
118
127
|
};
|
|
119
128
|
}
|
|
120
129
|
|
|
@@ -218,6 +227,7 @@ export async function postToNewtonChat(
|
|
|
218
227
|
content: result.content,
|
|
219
228
|
senderId: result.senderId,
|
|
220
229
|
conversationId: result.conversationId,
|
|
230
|
+
status: result.status,
|
|
221
231
|
createdAt: result.createdAt,
|
|
222
232
|
sender: result.sender,
|
|
223
233
|
attachments: result.attachments ?? [],
|
|
@@ -230,14 +240,23 @@ export async function postToNewtonChat(
|
|
|
230
240
|
}
|
|
231
241
|
|
|
232
242
|
if (!isAIUser(userId)) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
newtonChat.conversationId
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
// Create the AI placeholder post-tx (so a rollback can't broadcast a
|
|
244
|
+
// ghost bubble); the pipeline fills it in.
|
|
245
|
+
try {
|
|
246
|
+
const aiPlaceholder = await createPendingAIMessage(newtonChat.conversationId);
|
|
247
|
+
await enqueueRunJob(
|
|
248
|
+
"AI_NEWTON_CHAT",
|
|
249
|
+
{ conversationId: newtonChat.conversationId, aiMessageId: aiPlaceholder.id },
|
|
250
|
+
{ userId },
|
|
251
|
+
);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
logger.error("Failed to enqueue Newton response", {
|
|
254
|
+
error,
|
|
255
|
+
newtonChatId,
|
|
256
|
+
studentMessageId: result.id,
|
|
257
|
+
conversationId: newtonChat.conversationId,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
241
260
|
}
|
|
242
261
|
|
|
243
262
|
return {
|
|
@@ -245,6 +264,7 @@ export async function postToNewtonChat(
|
|
|
245
264
|
content: result.content,
|
|
246
265
|
senderId: result.senderId,
|
|
247
266
|
conversationId: result.conversationId,
|
|
267
|
+
status: result.status,
|
|
248
268
|
createdAt: result.createdAt,
|
|
249
269
|
sender: result.sender,
|
|
250
270
|
attachments: result.attachments ?? [],
|
|
@@ -7,7 +7,8 @@ import { JobStatus, type WorksheetQuestionType } from "@prisma/client";
|
|
|
7
7
|
import { prisma } from "../lib/prisma.js";
|
|
8
8
|
import { getFileUrl } from "../lib/fileUpload.js";
|
|
9
9
|
import { cancelGradePipeline, regradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
|
|
10
|
-
import {
|
|
10
|
+
import { enqueueRunJob } from "../workers/queues.js";
|
|
11
|
+
import { logger } from "../utils/logger.js";
|
|
11
12
|
import {
|
|
12
13
|
findWorksheetByIdMinimal,
|
|
13
14
|
findWorksheetByIdForClassMember,
|
|
@@ -170,21 +171,77 @@ export async function duplicateWorksheetRecord(
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
174
|
+
* Kick off PDF-to-worksheet conversion asynchronously.
|
|
175
|
+
*
|
|
176
|
+
* We synchronously create a placeholder Worksheet row (status PENDING) so the
|
|
177
|
+
* caller has an id to navigate to immediately, then enqueue a BullMQ job that
|
|
178
|
+
* fills in the real title and questions. Progress is broadcast on the
|
|
179
|
+
* per-worksheet job Pusher channel; clients should subscribe there to drive
|
|
180
|
+
* loading UI.
|
|
175
181
|
*/
|
|
176
182
|
export async function createWorksheetFromPdf(
|
|
177
183
|
classId: string,
|
|
178
|
-
url: string
|
|
184
|
+
url: string,
|
|
185
|
+
userId?: string,
|
|
186
|
+
options: { name?: string } = {},
|
|
179
187
|
) {
|
|
180
|
-
const
|
|
181
|
-
|
|
188
|
+
const placeholder = await prisma.worksheet.create({
|
|
189
|
+
data: {
|
|
190
|
+
classId,
|
|
191
|
+
name: options.name?.trim() || "Converting PDF…",
|
|
192
|
+
status: JobStatus.PENDING,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const { runJobId } = await enqueueRunJob(
|
|
198
|
+
"PDF_TO_WORKSHEET",
|
|
199
|
+
{ gcsUrl: url, classId, worksheetId: placeholder.id },
|
|
200
|
+
{ classId, userId: userId ?? null },
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await prisma.worksheet
|
|
204
|
+
.update({
|
|
205
|
+
where: { id: placeholder.id },
|
|
206
|
+
data: { runJobId },
|
|
207
|
+
})
|
|
208
|
+
.catch((linkError) => {
|
|
209
|
+
logger.error(
|
|
210
|
+
"createWorksheetFromPdf: failed to link RunJob to Worksheet",
|
|
211
|
+
{
|
|
212
|
+
worksheetId: placeholder.id,
|
|
213
|
+
runJobId,
|
|
214
|
+
error:
|
|
215
|
+
linkError instanceof Error
|
|
216
|
+
? linkError.message
|
|
217
|
+
: String(linkError),
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
...placeholder,
|
|
224
|
+
runJobId,
|
|
225
|
+
};
|
|
226
|
+
} catch (error) {
|
|
227
|
+
await prisma.worksheet
|
|
228
|
+
.update({
|
|
229
|
+
where: { id: placeholder.id },
|
|
230
|
+
data: { status: JobStatus.FAILED },
|
|
231
|
+
})
|
|
232
|
+
.catch(() => undefined);
|
|
233
|
+
|
|
234
|
+
logger.error("createWorksheetFromPdf: failed to enqueue conversion", {
|
|
235
|
+
worksheetId: placeholder.id,
|
|
236
|
+
classId,
|
|
237
|
+
error: error instanceof Error ? error.message : String(error),
|
|
238
|
+
});
|
|
239
|
+
|
|
182
240
|
throw new TRPCError({
|
|
183
241
|
code: "INTERNAL_SERVER_ERROR",
|
|
184
|
-
message: "Failed to
|
|
242
|
+
message: "Failed to start worksheet conversion",
|
|
185
243
|
});
|
|
186
244
|
}
|
|
187
|
-
return created;
|
|
188
245
|
}
|
|
189
246
|
|
|
190
247
|
/**
|
|
@@ -193,7 +250,8 @@ export async function createWorksheetFromPdf(
|
|
|
193
250
|
export async function createWorksheetFromUploadedPdf(
|
|
194
251
|
classId: string,
|
|
195
252
|
fileId: string,
|
|
196
|
-
userId: string
|
|
253
|
+
userId: string,
|
|
254
|
+
options: { name?: string } = {},
|
|
197
255
|
) {
|
|
198
256
|
const file = await prisma.file.findFirst({
|
|
199
257
|
where: {
|
|
@@ -205,6 +263,7 @@ export async function createWorksheetFromUploadedPdf(
|
|
|
205
263
|
id: true,
|
|
206
264
|
path: true,
|
|
207
265
|
type: true,
|
|
266
|
+
name: true,
|
|
208
267
|
},
|
|
209
268
|
});
|
|
210
269
|
|
|
@@ -225,7 +284,9 @@ export async function createWorksheetFromUploadedPdf(
|
|
|
225
284
|
}
|
|
226
285
|
|
|
227
286
|
const signedUrl = await getFileUrl(file.path);
|
|
228
|
-
return createWorksheetFromPdf(classId, signedUrl
|
|
287
|
+
return createWorksheetFromPdf(classId, signedUrl, userId, {
|
|
288
|
+
name: options.name ?? file.name ?? undefined,
|
|
289
|
+
});
|
|
229
290
|
}
|
|
230
291
|
|
|
231
292
|
export async function addQuestionToWorksheet(
|