@studious-lms/server 1.3.0 → 1.4.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.
Files changed (48) hide show
  1. package/dist/models/class.d.ts +24 -2
  2. package/dist/models/class.d.ts.map +1 -1
  3. package/dist/models/class.js +180 -81
  4. package/dist/models/class.js.map +1 -1
  5. package/dist/models/worksheet.d.ts +34 -34
  6. package/dist/pipelines/aiLabChat.d.ts +57 -2
  7. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  8. package/dist/pipelines/aiLabChat.js +252 -113
  9. package/dist/pipelines/aiLabChat.js.map +1 -1
  10. package/dist/pipelines/gradeWorksheet.d.ts +4 -4
  11. package/dist/routers/_app.d.ts +138 -56
  12. package/dist/routers/_app.d.ts.map +1 -1
  13. package/dist/routers/class.d.ts +24 -3
  14. package/dist/routers/class.d.ts.map +1 -1
  15. package/dist/routers/class.js +3 -3
  16. package/dist/routers/class.js.map +1 -1
  17. package/dist/routers/labChat.d.ts +10 -1
  18. package/dist/routers/labChat.d.ts.map +1 -1
  19. package/dist/routers/labChat.js +6 -3
  20. package/dist/routers/labChat.js.map +1 -1
  21. package/dist/routers/message.d.ts +11 -0
  22. package/dist/routers/message.d.ts.map +1 -1
  23. package/dist/routers/message.js +10 -3
  24. package/dist/routers/message.js.map +1 -1
  25. package/dist/routers/worksheet.d.ts +24 -24
  26. package/dist/services/class.d.ts +24 -2
  27. package/dist/services/class.d.ts.map +1 -1
  28. package/dist/services/class.js +18 -6
  29. package/dist/services/class.js.map +1 -1
  30. package/dist/services/labChat.d.ts +5 -1
  31. package/dist/services/labChat.d.ts.map +1 -1
  32. package/dist/services/labChat.js +96 -4
  33. package/dist/services/labChat.js.map +1 -1
  34. package/dist/services/message.d.ts +8 -0
  35. package/dist/services/message.d.ts.map +1 -1
  36. package/dist/services/message.js +74 -2
  37. package/dist/services/message.js.map +1 -1
  38. package/dist/services/worksheet.d.ts +18 -18
  39. package/package.json +1 -1
  40. package/prisma/schema.prisma +1 -1
  41. package/src/models/class.ts +189 -84
  42. package/src/pipelines/aiLabChat.ts +291 -118
  43. package/src/routers/class.ts +1 -1
  44. package/src/routers/labChat.ts +7 -0
  45. package/src/routers/message.ts +13 -0
  46. package/src/services/class.ts +14 -7
  47. package/src/services/labChat.ts +108 -2
  48. package/src/services/message.ts +93 -0
@@ -3,6 +3,7 @@
3
3
  * trigger AI responses, and broadcast via Pusher.
4
4
  */
5
5
  import { TRPCError } from "@trpc/server";
6
+ import { GenerationStatus } from "@prisma/client";
6
7
  import { prisma } from "../lib/prisma.js";
7
8
  import { chatChannel, teacherChannel, pusher } from "../lib/pusher.js";
8
9
  import {
@@ -166,7 +167,20 @@ export async function getLabChat(userId: string, labChatId: string) {
166
167
  });
167
168
  }
168
169
 
169
- return labChat;
170
+ const pendingMessage = await prisma.message.findFirst({
171
+ where: {
172
+ conversationId: labChat.conversationId,
173
+ status: GenerationStatus.PENDING,
174
+ senderId: { not: "AI_ASSISTANT" },
175
+ },
176
+ orderBy: { createdAt: "desc" },
177
+ select: { id: true },
178
+ });
179
+
180
+ return {
181
+ ...labChat,
182
+ pendingGenerationMessageId: pendingMessage?.id ?? null,
183
+ };
170
184
  }
171
185
 
172
186
  export async function listLabChats(userId: string, classId: string) {
@@ -282,10 +296,24 @@ export async function postToLabChat(
282
296
  }
283
297
 
284
298
  if (!isAIUser(userId)) {
299
+ await prisma.message.update({
300
+ where: { id: result.id },
301
+ data: { status: GenerationStatus.PENDING },
302
+ });
303
+ try {
304
+ await pusher.trigger(teacherChannel(labChat.classId), "lab-response-pending", {
305
+ labChatId,
306
+ messageId: result.id,
307
+ conversationId: labChat.conversationId,
308
+ });
309
+ } catch (error) {
310
+ console.error("Failed to broadcast lab response pending:", error);
311
+ }
285
312
  generateAndSendLabResponse(
286
313
  labChatId,
287
314
  content,
288
- labChat.conversationId
315
+ labChat.conversationId,
316
+ { classId: labChat.classId, messageId: result.id }
289
317
  );
290
318
  }
291
319
 
@@ -350,3 +378,81 @@ export async function deleteLabChat(userId: string, labChatId: string) {
350
378
 
351
379
  return { success: true };
352
380
  }
381
+
382
+ /** Rerun the last AI response for a lab chat. Deletes the last AI message and regenerates. */
383
+ export async function rerunLastResponse(userId: string, labChatId: string) {
384
+ const labChat = await findLabChatForPost(labChatId, userId);
385
+ if (!labChat) {
386
+ throw new TRPCError({
387
+ code: "FORBIDDEN",
388
+ message: "Lab chat not found or access denied",
389
+ });
390
+ }
391
+
392
+ const messages = await prisma.message.findMany({
393
+ where: { conversationId: labChat.conversationId },
394
+ orderBy: { createdAt: "desc" },
395
+ take: 10,
396
+ select: { id: true, content: true, senderId: true, createdAt: true },
397
+ });
398
+
399
+ const lastMessage = messages[0];
400
+ if (!lastMessage) {
401
+ throw new TRPCError({
402
+ code: "BAD_REQUEST",
403
+ message: "No messages to rerun",
404
+ });
405
+ }
406
+
407
+ if (!isAIUser(lastMessage.senderId)) {
408
+ throw new TRPCError({
409
+ code: "BAD_REQUEST",
410
+ message: "Last message is not from AI – nothing to rerun",
411
+ });
412
+ }
413
+
414
+ const teacherMessage = messages.find((m) => !isAIUser(m.senderId));
415
+ if (!teacherMessage) {
416
+ throw new TRPCError({
417
+ code: "BAD_REQUEST",
418
+ message: "No teacher message found to rerun from",
419
+ });
420
+ }
421
+
422
+ await prisma.message.delete({
423
+ where: { id: lastMessage.id },
424
+ });
425
+
426
+ try {
427
+ await pusher.trigger(chatChannel(labChat.conversationId), "message-deleted", {
428
+ messageId: lastMessage.id,
429
+ conversationId: labChat.conversationId,
430
+ senderId: lastMessage.senderId,
431
+ });
432
+ } catch (error) {
433
+ console.error("Failed to broadcast message deletion:", error);
434
+ }
435
+
436
+ await prisma.message.update({
437
+ where: { id: teacherMessage.id },
438
+ data: { status: GenerationStatus.PENDING },
439
+ });
440
+ try {
441
+ await pusher.trigger(teacherChannel(labChat.classId), "lab-response-pending", {
442
+ labChatId,
443
+ messageId: teacherMessage.id,
444
+ conversationId: labChat.conversationId,
445
+ });
446
+ } catch (error) {
447
+ console.error("Failed to broadcast lab response pending:", error);
448
+ }
449
+
450
+ generateAndSendLabResponse(
451
+ labChatId,
452
+ teacherMessage.content,
453
+ labChat.conversationId,
454
+ { classId: labChat.classId, messageId: teacherMessage.id }
455
+ );
456
+
457
+ return { success: true };
458
+ }
@@ -357,6 +357,99 @@ export async function deleteMessage(userId: string, messageId: string) {
357
357
  return { success: true, messageId };
358
358
  }
359
359
 
360
+ const CREATED_INDICES_KEY: Record<"assignment" | "worksheet" | "section", string> = {
361
+ assignment: "assignments",
362
+ worksheet: "worksheets",
363
+ section: "sections",
364
+ };
365
+
366
+ /** Mark an AI-suggested item as created. Stores in message meta for fast reads. */
367
+ export async function markSuggestionCreated(
368
+ userId: string,
369
+ input: {
370
+ messageId: string;
371
+ type: "assignment" | "worksheet" | "section";
372
+ index: number;
373
+ }
374
+ ) {
375
+ const { messageId, type, index } = input;
376
+
377
+ const existingMessage = await findMessageByIdMinimal(messageId);
378
+ if (!existingMessage) {
379
+ throw new TRPCError({
380
+ code: "NOT_FOUND",
381
+ message: "Message not found",
382
+ });
383
+ }
384
+
385
+ const membership = await findConversationMembership(
386
+ existingMessage.conversationId,
387
+ userId
388
+ );
389
+ if (!membership) {
390
+ throw new TRPCError({
391
+ code: "FORBIDDEN",
392
+ message: "Not a member of this conversation",
393
+ });
394
+ }
395
+
396
+ const currentMeta = (existingMessage.meta as Record<string, unknown>) ?? {};
397
+ const createdIndices = (currentMeta.createdIndices as Record<string, number[]>) ?? {};
398
+ const typeIndices = createdIndices[CREATED_INDICES_KEY[type]] ?? [];
399
+ if (typeIndices.includes(index)) return { success: true };
400
+
401
+ const newTypeIndices = [...typeIndices, index].sort((a, b) => a - b);
402
+ const newMeta = {
403
+ ...currentMeta,
404
+ createdIndices: {
405
+ ...createdIndices,
406
+ [CREATED_INDICES_KEY[type]]: newTypeIndices,
407
+ },
408
+ };
409
+
410
+ const updated = await prisma.message.update({
411
+ where: { id: messageId },
412
+ data: { meta: newMeta as object },
413
+ include: {
414
+ sender: {
415
+ select: {
416
+ id: true,
417
+ username: true,
418
+ profile: {
419
+ select: { displayName: true, profilePicture: true },
420
+ },
421
+ },
422
+ },
423
+ attachments: { select: { id: true, name: true, type: true } },
424
+ },
425
+ });
426
+
427
+ try {
428
+ await pusher.trigger(
429
+ chatChannel(existingMessage.conversationId),
430
+ "message-updated",
431
+ {
432
+ id: updated.id,
433
+ content: updated.content,
434
+ senderId: updated.senderId,
435
+ conversationId: updated.conversationId,
436
+ createdAt: updated.createdAt,
437
+ sender: updated.sender,
438
+ attachments: updated.attachments ?? [],
439
+ meta: newMeta,
440
+ mentionedUserIds: [] as string[],
441
+ }
442
+ );
443
+ } catch (error) {
444
+ logger.error("Failed to broadcast suggestion status via Pusher", {
445
+ error,
446
+ messageId,
447
+ });
448
+ }
449
+
450
+ return { success: true };
451
+ }
452
+
360
453
  export async function markAsRead(userId: string, conversationId: string) {
361
454
  const membership = await findConversationMembership(conversationId, userId);
362
455
  if (!membership) {