@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
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Auto-creates attendance records for events that don't have them yet.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
!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]="
|
|
6
|
+
!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]="234d526b-a633-54ce-87f8-d4e6af5df483")}catch(e){}}();
|
|
7
7
|
import { TRPCError } from "@trpc/server";
|
|
8
8
|
import { findClassWithStudents, findClassWithTeacher, findEventsByClassId, findAttendanceByEventId, findAttendanceByClassAndEvent, findManyAttendance, createAttendance, createAttendanceForEvent, updateAttendance, } from "../models/attendance.js";
|
|
9
|
+
import { sendToMultiple } from "./notification.js";
|
|
10
|
+
import { logger } from "../utils/logger.js";
|
|
9
11
|
/** Get attendance for a class (optionally filtered by event). Creates missing records. */
|
|
10
12
|
export async function getAttendance(classId, eventId) {
|
|
11
13
|
const classData = await findClassWithStudents(classId);
|
|
@@ -42,20 +44,82 @@ export async function updateAttendanceRecord(userId, classId, eventId, attendanc
|
|
|
42
44
|
const presentIds = attendance.present.map((s) => s.id);
|
|
43
45
|
const lateIds = attendance.late.map((s) => s.id);
|
|
44
46
|
const absentIds = attendance.absent.map((s) => s.id);
|
|
47
|
+
const previousLateIds = new Set(existing?.late.map((student) => student.id) ?? []);
|
|
48
|
+
const previousAbsentIds = new Set(existing?.absent.map((student) => student.id) ?? []);
|
|
49
|
+
const classStudentIds = new Set(classData.students.map((student) => student.id));
|
|
45
50
|
if (!existing) {
|
|
46
|
-
|
|
51
|
+
const created = await createAttendance({
|
|
47
52
|
classId,
|
|
48
53
|
eventId: eventId ?? null,
|
|
49
54
|
presentIds,
|
|
50
55
|
lateIds,
|
|
51
56
|
absentIds,
|
|
52
57
|
});
|
|
58
|
+
await notifyMarkedLateOrAbsent({
|
|
59
|
+
classId,
|
|
60
|
+
className: classData.name,
|
|
61
|
+
eventId: eventId ?? null,
|
|
62
|
+
eventName: created.event?.name ?? null,
|
|
63
|
+
classStudentIds,
|
|
64
|
+
lateIds,
|
|
65
|
+
absentIds,
|
|
66
|
+
previousLateIds,
|
|
67
|
+
previousAbsentIds,
|
|
68
|
+
});
|
|
69
|
+
return created;
|
|
53
70
|
}
|
|
54
|
-
|
|
71
|
+
const updated = await updateAttendance(existing.id, {
|
|
55
72
|
presentIds,
|
|
56
73
|
lateIds,
|
|
57
74
|
absentIds,
|
|
58
75
|
});
|
|
76
|
+
await notifyMarkedLateOrAbsent({
|
|
77
|
+
classId,
|
|
78
|
+
className: classData.name,
|
|
79
|
+
eventId: eventId ?? null,
|
|
80
|
+
eventName: existing.event?.name ?? null,
|
|
81
|
+
classStudentIds,
|
|
82
|
+
lateIds,
|
|
83
|
+
absentIds,
|
|
84
|
+
previousLateIds,
|
|
85
|
+
previousAbsentIds,
|
|
86
|
+
});
|
|
87
|
+
return updated;
|
|
88
|
+
}
|
|
89
|
+
/** Notify only students whose late or absent status is newly added. */
|
|
90
|
+
async function notifyMarkedLateOrAbsent(input) {
|
|
91
|
+
const lateReceiverIds = [...new Set(input.lateIds)].filter((id) => input.classStudentIds.has(id) && !input.previousLateIds.has(id));
|
|
92
|
+
const absentReceiverIds = [...new Set(input.absentIds)].filter((id) => input.classStudentIds.has(id) && !input.previousAbsentIds.has(id));
|
|
93
|
+
const link = input.eventId
|
|
94
|
+
? `/class/${input.classId}/attendance/${input.eventId}`
|
|
95
|
+
: `/class/${input.classId}/attendance`;
|
|
96
|
+
const attendanceLabel = input.eventName
|
|
97
|
+
? `"${input.eventName}" in ${input.className}`
|
|
98
|
+
: input.className;
|
|
99
|
+
await Promise.all([
|
|
100
|
+
sendAttendanceNotification({
|
|
101
|
+
receiverIds: lateReceiverIds,
|
|
102
|
+
title: `Marked late in ${input.className}`,
|
|
103
|
+
content: `You were marked late for ${attendanceLabel}.\n[View attendance](${link})`,
|
|
104
|
+
}),
|
|
105
|
+
sendAttendanceNotification({
|
|
106
|
+
receiverIds: absentReceiverIds,
|
|
107
|
+
title: `Marked absent in ${input.className}`,
|
|
108
|
+
content: `You were marked absent for ${attendanceLabel}.\n[View attendance](${link})`,
|
|
109
|
+
}),
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
/** Send attendance notifications without failing the attendance update itself. */
|
|
113
|
+
async function sendAttendanceNotification(input) {
|
|
114
|
+
if (input.receiverIds.length === 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await sendToMultiple(input);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
logger.error("Failed to send attendance notifications:", { error });
|
|
122
|
+
}
|
|
59
123
|
}
|
|
60
124
|
//# sourceMappingURL=attendance.js.map
|
|
61
|
-
//# debugId=
|
|
125
|
+
//# debugId=234d526b-a633-54ce-87f8-d4e6af5df483
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attendance.js","sources":["services/attendance.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Attendance service – get and update class attendance records.\n * Auto-creates attendance records for events that don't have them yet.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport {\n findClassWithStudents,\n findClassWithTeacher,\n findEventsByClassId,\n findAttendanceByEventId,\n findAttendanceByClassAndEvent,\n findManyAttendance,\n createAttendance,\n createAttendanceForEvent,\n updateAttendance,\n} from \"../models/attendance.js\";\n\n/** Get attendance for a class (optionally filtered by event). Creates missing records. */\nexport async function getAttendance(classId: string, eventId?: string) {\n const classData = await findClassWithStudents(classId);\n if (!classData) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"You are not authorized to view this class's attendance\",\n });\n }\n\n const events = await findEventsByClassId(classId);\n const studentIds = classData.students.map((s) => s.id);\n\n for (const event of events) {\n const existing = await findAttendanceByEventId(event.id);\n if (!existing) {\n await createAttendanceForEvent({\n classId,\n eventId: event.id,\n presentIds: studentIds,\n });\n }\n }\n\n return findManyAttendance(classId, eventId);\n}\n\n/** Update attendance record (present/late/absent). Teacher-only. */\nexport async function updateAttendanceRecord(\n userId: string,\n classId: string,\n eventId: string | null | undefined,\n attendance: {\n present: { id: string; username: string }[];\n late: { id: string; username: string }[];\n absent: { id: string; username: string }[];\n }\n) {\n const classData = await findClassWithTeacher(classId, userId);\n if (!classData) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"You are not authorized to update this class's attendance\",\n });\n }\n\n const existing = await findAttendanceByClassAndEvent(classId, eventId ?? null);\n\n const presentIds = attendance.present.map((s) => s.id);\n const lateIds = attendance.late.map((s) => s.id);\n const absentIds = attendance.absent.map((s) => s.id);\n\n if (!existing) {\n
|
|
1
|
+
{"version":3,"file":"attendance.js","sources":["services/attendance.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Attendance service – get and update class attendance records.\n * Auto-creates attendance records for events that don't have them yet.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport {\n findClassWithStudents,\n findClassWithTeacher,\n findEventsByClassId,\n findAttendanceByEventId,\n findAttendanceByClassAndEvent,\n findManyAttendance,\n createAttendance,\n createAttendanceForEvent,\n updateAttendance,\n} from \"../models/attendance.js\";\nimport { sendToMultiple } from \"./notification.js\";\nimport { logger } from \"../utils/logger.js\";\n\n/** Get attendance for a class (optionally filtered by event). Creates missing records. */\nexport async function getAttendance(classId: string, eventId?: string) {\n const classData = await findClassWithStudents(classId);\n if (!classData) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"You are not authorized to view this class's attendance\",\n });\n }\n\n const events = await findEventsByClassId(classId);\n const studentIds = classData.students.map((s) => s.id);\n\n for (const event of events) {\n const existing = await findAttendanceByEventId(event.id);\n if (!existing) {\n await createAttendanceForEvent({\n classId,\n eventId: event.id,\n presentIds: studentIds,\n });\n }\n }\n\n return findManyAttendance(classId, eventId);\n}\n\n/** Update attendance record (present/late/absent). Teacher-only. */\nexport async function updateAttendanceRecord(\n userId: string,\n classId: string,\n eventId: string | null | undefined,\n attendance: {\n present: { id: string; username: string }[];\n late: { id: string; username: string }[];\n absent: { id: string; username: string }[];\n }\n) {\n const classData = await findClassWithTeacher(classId, userId);\n if (!classData) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"You are not authorized to update this class's attendance\",\n });\n }\n\n const existing = await findAttendanceByClassAndEvent(classId, eventId ?? null);\n\n const presentIds = attendance.present.map((s) => s.id);\n const lateIds = attendance.late.map((s) => s.id);\n const absentIds = attendance.absent.map((s) => s.id);\n const previousLateIds = new Set(existing?.late.map((student) => student.id) ?? []);\n const previousAbsentIds = new Set(existing?.absent.map((student) => student.id) ?? []);\n const classStudentIds = new Set(classData.students.map((student) => student.id));\n\n if (!existing) {\n const created = await createAttendance({\n classId,\n eventId: eventId ?? null,\n presentIds,\n lateIds,\n absentIds,\n });\n await notifyMarkedLateOrAbsent({\n classId,\n className: classData.name,\n eventId: eventId ?? null,\n eventName: created.event?.name ?? null,\n classStudentIds,\n lateIds,\n absentIds,\n previousLateIds,\n previousAbsentIds,\n });\n return created;\n }\n\n const updated = await updateAttendance(existing.id, {\n presentIds,\n lateIds,\n absentIds,\n });\n await notifyMarkedLateOrAbsent({\n classId,\n className: classData.name,\n eventId: eventId ?? null,\n eventName: existing.event?.name ?? null,\n classStudentIds,\n lateIds,\n absentIds,\n previousLateIds,\n previousAbsentIds,\n });\n return updated;\n}\n\n/** Notify only students whose late or absent status is newly added. */\nasync function notifyMarkedLateOrAbsent(input: {\n classId: string;\n className: string;\n eventId: string | null;\n eventName: string | null;\n classStudentIds: Set<string>;\n lateIds: string[];\n absentIds: string[];\n previousLateIds: Set<string>;\n previousAbsentIds: Set<string>;\n}) {\n const lateReceiverIds = [...new Set(input.lateIds)].filter(\n (id) => input.classStudentIds.has(id) && !input.previousLateIds.has(id)\n );\n const absentReceiverIds = [...new Set(input.absentIds)].filter(\n (id) => input.classStudentIds.has(id) && !input.previousAbsentIds.has(id)\n );\n const link = input.eventId\n ? `/class/${input.classId}/attendance/${input.eventId}`\n : `/class/${input.classId}/attendance`;\n const attendanceLabel = input.eventName\n ? `\"${input.eventName}\" in ${input.className}`\n : input.className;\n\n await Promise.all([\n sendAttendanceNotification({\n receiverIds: lateReceiverIds,\n title: `Marked late in ${input.className}`,\n content: `You were marked late for ${attendanceLabel}.\\n[View attendance](${link})`,\n }),\n sendAttendanceNotification({\n receiverIds: absentReceiverIds,\n title: `Marked absent in ${input.className}`,\n content: `You were marked absent for ${attendanceLabel}.\\n[View attendance](${link})`,\n }),\n ]);\n}\n\n/** Send attendance notifications without failing the attendance update itself. */\nasync function sendAttendanceNotification(input: {\n receiverIds: string[];\n title: string;\n content: string;\n}) {\n if (input.receiverIds.length === 0) {\n return;\n }\n\n try {\n await sendToMultiple(input);\n } catch (error) {\n logger.error(\"Failed to send attendance notifications:\", { error });\n }\n}\n"],"names":[],"mappings":"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","debug_id":"234d526b-a633-54ce-87f8-d4e6af5df483"}
|
|
@@ -52,6 +52,22 @@ export declare function listConversations(userId: string): Promise<{
|
|
|
52
52
|
export declare function hideConversation(userId: string, conversationId: string): Promise<{
|
|
53
53
|
success: boolean;
|
|
54
54
|
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Search active school peers that can be selected for a new conversation.
|
|
57
|
+
*
|
|
58
|
+
* Blank or one-character input returns an empty list instead of searching all
|
|
59
|
+
* institution members, which keeps typeahead callers from issuing broad result
|
|
60
|
+
* queries.
|
|
61
|
+
*/
|
|
62
|
+
export declare function searchConversationMembers(userId: string, query: string): Promise<{
|
|
63
|
+
id: string;
|
|
64
|
+
username: string;
|
|
65
|
+
profile: {
|
|
66
|
+
displayName: string | null;
|
|
67
|
+
profilePicture: string | null;
|
|
68
|
+
profilePictureThumbnail: string | null;
|
|
69
|
+
} | null;
|
|
70
|
+
}[]>;
|
|
55
71
|
/** Create a DM or group conversation with the given members. */
|
|
56
72
|
export declare function createConversationRecord(userId: string, type: "DM" | "GROUP", name: string | undefined, memberIds: string[]): Promise<{
|
|
57
73
|
members: ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.d.ts","sourceRoot":"/","sources":["services/conversation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"conversation.d.ts","sourceRoot":"/","sources":["services/conversation.ts"],"names":[],"mappings":"AAyBA,gEAAgE;AAChE,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAuDrD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;;GAY5E;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;;;;;;;;KAe5E;AAED,gEAAgE;AAChE,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,GAAG,OAAO,EACpB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,SAAS,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEpB;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;GAavB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BvB;AAED,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;GAejB"}
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* Handles member management and unread counts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
!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]="
|
|
6
|
+
!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]="1d941fa8-dec4-5d6a-b549-daaf5adac742")}catch(e){}}();
|
|
7
7
|
import { TRPCError } from "@trpc/server";
|
|
8
|
-
import { findConversationsByUserId, countUnreadMessages, countUnreadMentions, findUserByUsername, findDmConversationsByUserId, findUsersByIdsOrUsernames, createConversation, findConversationByIdAndMember, createConversationMember, hideConversationMember, deleteConversationMember, } from "../models/conversation.js";
|
|
8
|
+
import { findConversationsByUserId, countUnreadMessages, countUnreadMentions, findUserByUsername, searchInstitutionConversationMembers, findDmConversationsByUserId, findUsersByIdsOrUsernames, createConversation, findConversationByIdAndMember, createConversationMember, hideConversationMember, deleteConversationMember, } from "../models/conversation.js";
|
|
9
|
+
/** Maximum number of member suggestions returned for conversation search. */
|
|
10
|
+
const MAX_MEMBER_SEARCH_RESULTS = 10;
|
|
11
|
+
/** Minimum non-whitespace characters required before running member search. */
|
|
12
|
+
const MIN_MEMBER_SEARCH_QUERY_LENGTH = 2;
|
|
9
13
|
/** List user's conversations with unread and mention counts. */
|
|
10
14
|
export async function listConversations(userId) {
|
|
11
15
|
const conversations = await findConversationsByUserId(userId);
|
|
@@ -53,6 +57,21 @@ export async function hideConversation(userId, conversationId) {
|
|
|
53
57
|
await hideConversationMember(userId, conversationId);
|
|
54
58
|
return { success: true };
|
|
55
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Search active school peers that can be selected for a new conversation.
|
|
62
|
+
*
|
|
63
|
+
* Blank or one-character input returns an empty list instead of searching all
|
|
64
|
+
* institution members, which keeps typeahead callers from issuing broad result
|
|
65
|
+
* queries.
|
|
66
|
+
*/
|
|
67
|
+
export async function searchConversationMembers(userId, query) {
|
|
68
|
+
const trimmedQuery = query.trim();
|
|
69
|
+
if (!trimmedQuery ||
|
|
70
|
+
trimmedQuery.length < MIN_MEMBER_SEARCH_QUERY_LENGTH) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
return searchInstitutionConversationMembers(userId, trimmedQuery, MAX_MEMBER_SEARCH_RESULTS);
|
|
74
|
+
}
|
|
56
75
|
/** Create a DM or group conversation with the given members. */
|
|
57
76
|
export async function createConversationRecord(userId, type, name, memberIds) {
|
|
58
77
|
if (type === "GROUP" && !name) {
|
|
@@ -153,4 +172,4 @@ export async function removeMemberFromConversation(userId, conversationId, membe
|
|
|
153
172
|
return conversation;
|
|
154
173
|
}
|
|
155
174
|
//# sourceMappingURL=conversation.js.map
|
|
156
|
-
//# debugId=
|
|
175
|
+
//# debugId=1d941fa8-dec4-5d6a-b549-daaf5adac742
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.js","sources":["services/conversation.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Conversation service – list, create, and manage DM/group conversations.\n * Handles member management and unread counts.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport {\n findConversationsByUserId,\n countUnreadMessages,\n countUnreadMentions,\n findUserByUsername,\n findDmConversationsByUserId,\n findUsersByIdsOrUsernames,\n createConversation,\n findConversationByIdAndMember,\n createConversationMember,\n hideConversationMember,\n deleteConversationMember,\n} from \"../models/conversation.js\";\n\n/** List user's conversations with unread and mention counts. */\nexport async function listConversations(userId: string) {\n const conversations = await findConversationsByUserId(userId);\n const visibleConversations = conversations.filter((conversation) => {\n const userMembership = conversation.members.find((member) => member.userId === userId);\n\n if (!userMembership?.hiddenAt) {\n return true;\n }\n\n return conversation.updatedAt > userMembership.hiddenAt;\n });\n\n const conversationsWithUnread = await Promise.all(\n visibleConversations.map(async (conversation) => {\n const userMembership = conversation.members.find(\n (m) => m.userId === userId\n );\n const lastViewedAt = userMembership?.lastViewedAt;\n const lastViewedMentionAt = userMembership?.lastViewedMentionAt;\n\n const unreadCount = await countUnreadMessages(\n conversation.id,\n userId,\n lastViewedAt ?? undefined\n );\n\n const mentionCutoffTime =\n lastViewedMentionAt && lastViewedAt\n ? lastViewedMentionAt > lastViewedAt\n ? lastViewedMentionAt\n : lastViewedAt\n : lastViewedMentionAt || lastViewedAt;\n\n const unreadMentionCount = await countUnreadMentions(\n conversation.id,\n userId,\n mentionCutoffTime ?? undefined\n );\n\n return {\n id: conversation.id,\n type: conversation.type,\n name: conversation.name,\n createdAt: conversation.createdAt,\n updatedAt: conversation.updatedAt,\n labChat: conversation.labChat,\n members: conversation.members,\n lastMessage: conversation.messages[0] || null,\n unreadCount,\n unreadMentionCount,\n };\n })\n );\n\n return conversationsWithUnread;\n}\n\nexport async function hideConversation(userId: string, conversationId: string) {\n const conversation = await findConversationByIdAndMember(conversationId, userId);\n if (!conversation) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Not a member of this conversation\",\n });\n }\n\n await hideConversationMember(userId, conversationId);\n\n return { success: true };\n}\n\n/** Create a DM or group conversation with the given members. */\nexport async function createConversationRecord(\n userId: string,\n type: \"DM\" | \"GROUP\",\n name: string | undefined,\n memberIds: string[]\n) {\n if (type === \"GROUP\" && !name) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Group conversations must have a name\",\n });\n }\n\n if (type === \"DM\" && memberIds.length !== 1) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"DM conversations must have exactly one other member\",\n });\n }\n\n if (type === \"DM\") {\n const [targetUser] = await findUsersByIdsOrUsernames([memberIds[0]]);\n if (!targetUser) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: `User not found`,\n });\n }\n\n const existingDMs = await findDmConversationsByUserId(userId);\n const existingDM = existingDMs.find((conv) => {\n const memberUserIds = conv.members.map((m) => m.userId);\n return (\n memberUserIds.length === 2 &&\n memberUserIds.includes(userId) &&\n memberUserIds.includes(targetUser.id)\n );\n });\n\n if (existingDM) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: `A conversation with ${targetUser.username} already exists`,\n });\n }\n }\n\n const members = await findUsersByIdsOrUsernames(memberIds);\n type Member = (typeof members)[number];\n const uniqueMembers = Array.from(\n new Map(members.map((m) => [m.id, m] as [string, Member])).values()\n );\n\n if (uniqueMembers.length !== memberIds.length) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"One or more members not found\",\n });\n }\n\n const toAddIds = memberIds\n .map((id) =>\n uniqueMembers.find((m) => m.id === id || m.username === id)?.id\n )\n .filter(Boolean) as string[];\n\n return createConversation({\n type,\n name,\n members: [\n { userId, role: type === \"GROUP\" ? \"ADMIN\" : \"MEMBER\" },\n ...toAddIds.map((uid) => ({ userId: uid, role: \"MEMBER\" })),\n ],\n });\n}\n\nexport async function getConversation(\n userId: string,\n conversationId: string\n) {\n const conversation = await findConversationByIdAndMember(\n conversationId,\n userId\n );\n if (!conversation) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Conversation not found or access denied\",\n });\n }\n return conversation;\n}\n\nexport async function addMemberToConversation(\n userId: string,\n conversationId: string,\n memberUsername: string\n) {\n const conversation = await findConversationByIdAndMember(\n conversationId,\n userId\n );\n if (!conversation) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Conversation not found or access denied\",\n });\n }\n\n const member = await findUserByUsername(memberUsername);\n if (!member) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Member not found\",\n });\n }\n\n await createConversationMember({\n userId: member.id,\n conversationId,\n role: \"MEMBER\",\n });\n\n return conversation;\n}\n\nexport async function removeMemberFromConversation(\n userId: string,\n conversationId: string,\n memberId: string\n) {\n const conversation = await findConversationByIdAndMember(\n conversationId,\n userId\n );\n if (!conversation) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Conversation not found or access denied\",\n });\n }\n\n await deleteConversationMember(memberId, conversationId);\n return conversation;\n}\n"],"names":[],"mappings":"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","debug_id":"1e016f9f-4be7-566a-a3f9-ad63c2a0ada1"}
|
|
1
|
+
{"version":3,"file":"conversation.js","sources":["services/conversation.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Conversation service – list, create, and manage DM/group conversations.\n * Handles member management and unread counts.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport {\n findConversationsByUserId,\n countUnreadMessages,\n countUnreadMentions,\n findUserByUsername,\n searchInstitutionConversationMembers,\n findDmConversationsByUserId,\n findUsersByIdsOrUsernames,\n createConversation,\n findConversationByIdAndMember,\n createConversationMember,\n hideConversationMember,\n deleteConversationMember,\n} from \"../models/conversation.js\";\n\n/** Maximum number of member suggestions returned for conversation search. */\nconst MAX_MEMBER_SEARCH_RESULTS = 10;\n/** Minimum non-whitespace characters required before running member search. */\nconst MIN_MEMBER_SEARCH_QUERY_LENGTH = 2;\n\n/** List user's conversations with unread and mention counts. */\nexport async function listConversations(userId: string) {\n const conversations = await findConversationsByUserId(userId);\n const visibleConversations = conversations.filter((conversation) => {\n const userMembership = conversation.members.find((member) => member.userId === userId);\n\n if (!userMembership?.hiddenAt) {\n return true;\n }\n\n return conversation.updatedAt > userMembership.hiddenAt;\n });\n\n const conversationsWithUnread = await Promise.all(\n visibleConversations.map(async (conversation) => {\n const userMembership = conversation.members.find(\n (m) => m.userId === userId\n );\n const lastViewedAt = userMembership?.lastViewedAt;\n const lastViewedMentionAt = userMembership?.lastViewedMentionAt;\n\n const unreadCount = await countUnreadMessages(\n conversation.id,\n userId,\n lastViewedAt ?? undefined\n );\n\n const mentionCutoffTime =\n lastViewedMentionAt && lastViewedAt\n ? lastViewedMentionAt > lastViewedAt\n ? lastViewedMentionAt\n : lastViewedAt\n : lastViewedMentionAt || lastViewedAt;\n\n const unreadMentionCount = await countUnreadMentions(\n conversation.id,\n userId,\n mentionCutoffTime ?? undefined\n );\n\n return {\n id: conversation.id,\n type: conversation.type,\n name: conversation.name,\n createdAt: conversation.createdAt,\n updatedAt: conversation.updatedAt,\n labChat: conversation.labChat,\n members: conversation.members,\n lastMessage: conversation.messages[0] || null,\n unreadCount,\n unreadMentionCount,\n };\n })\n );\n\n return conversationsWithUnread;\n}\n\nexport async function hideConversation(userId: string, conversationId: string) {\n const conversation = await findConversationByIdAndMember(conversationId, userId);\n if (!conversation) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Not a member of this conversation\",\n });\n }\n\n await hideConversationMember(userId, conversationId);\n\n return { success: true };\n}\n\n/**\n * Search active school peers that can be selected for a new conversation.\n *\n * Blank or one-character input returns an empty list instead of searching all\n * institution members, which keeps typeahead callers from issuing broad result\n * queries.\n */\nexport async function searchConversationMembers(userId: string, query: string) {\n const trimmedQuery = query.trim();\n\n if (\n !trimmedQuery ||\n trimmedQuery.length < MIN_MEMBER_SEARCH_QUERY_LENGTH\n ) {\n return [];\n }\n\n return searchInstitutionConversationMembers(\n userId,\n trimmedQuery,\n MAX_MEMBER_SEARCH_RESULTS\n );\n}\n\n/** Create a DM or group conversation with the given members. */\nexport async function createConversationRecord(\n userId: string,\n type: \"DM\" | \"GROUP\",\n name: string | undefined,\n memberIds: string[]\n) {\n if (type === \"GROUP\" && !name) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Group conversations must have a name\",\n });\n }\n\n if (type === \"DM\" && memberIds.length !== 1) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"DM conversations must have exactly one other member\",\n });\n }\n\n if (type === \"DM\") {\n const [targetUser] = await findUsersByIdsOrUsernames([memberIds[0]]);\n if (!targetUser) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: `User not found`,\n });\n }\n\n const existingDMs = await findDmConversationsByUserId(userId);\n const existingDM = existingDMs.find((conv) => {\n const memberUserIds = conv.members.map((m) => m.userId);\n return (\n memberUserIds.length === 2 &&\n memberUserIds.includes(userId) &&\n memberUserIds.includes(targetUser.id)\n );\n });\n\n if (existingDM) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: `A conversation with ${targetUser.username} already exists`,\n });\n }\n }\n\n const members = await findUsersByIdsOrUsernames(memberIds);\n type Member = (typeof members)[number];\n const uniqueMembers = Array.from(\n new Map(members.map((m) => [m.id, m] as [string, Member])).values()\n );\n\n if (uniqueMembers.length !== memberIds.length) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"One or more members not found\",\n });\n }\n\n const toAddIds = memberIds\n .map((id) =>\n uniqueMembers.find((m) => m.id === id || m.username === id)?.id\n )\n .filter(Boolean) as string[];\n\n return createConversation({\n type,\n name,\n members: [\n { userId, role: type === \"GROUP\" ? \"ADMIN\" : \"MEMBER\" },\n ...toAddIds.map((uid) => ({ userId: uid, role: \"MEMBER\" })),\n ],\n });\n}\n\nexport async function getConversation(\n userId: string,\n conversationId: string\n) {\n const conversation = await findConversationByIdAndMember(\n conversationId,\n userId\n );\n if (!conversation) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Conversation not found or access denied\",\n });\n }\n return conversation;\n}\n\nexport async function addMemberToConversation(\n userId: string,\n conversationId: string,\n memberUsername: string\n) {\n const conversation = await findConversationByIdAndMember(\n conversationId,\n userId\n );\n if (!conversation) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Conversation not found or access denied\",\n });\n }\n\n const member = await findUserByUsername(memberUsername);\n if (!member) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Member not found\",\n });\n }\n\n await createConversationMember({\n userId: member.id,\n conversationId,\n role: \"MEMBER\",\n });\n\n return conversation;\n}\n\nexport async function removeMemberFromConversation(\n userId: string,\n conversationId: string,\n memberId: string\n) {\n const conversation = await findConversationByIdAndMember(\n conversationId,\n userId\n );\n if (!conversation) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Conversation not found or access denied\",\n });\n }\n\n await deleteConversationMember(memberId, conversationId);\n return conversation;\n}\n"],"names":[],"mappings":"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","debug_id":"1d941fa8-dec4-5d6a-b549-daaf5adac742"}
|
|
@@ -152,6 +152,7 @@ export declare function postToLabChat(userId: string, input: {
|
|
|
152
152
|
content: string;
|
|
153
153
|
senderId: string;
|
|
154
154
|
conversationId: string;
|
|
155
|
+
status: import(".prisma/client").$Enums.JobStatus | null;
|
|
155
156
|
createdAt: Date;
|
|
156
157
|
sender: {
|
|
157
158
|
id: string;
|
|
@@ -171,7 +172,13 @@ export declare function postToLabChat(userId: string, input: {
|
|
|
171
172
|
export declare function deleteLabChat(userId: string, labChatId: string): Promise<{
|
|
172
173
|
success: boolean;
|
|
173
174
|
}>;
|
|
174
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Rerun the last AI response in a lab chat.
|
|
177
|
+
*
|
|
178
|
+
* The AI message bubble is reused (not deleted): we flip it back to
|
|
179
|
+
* PENDING and clear its content so the existing message id stays stable
|
|
180
|
+
* for the client. The pipeline then refills it.
|
|
181
|
+
*/
|
|
175
182
|
export declare function rerunLastResponse(userId: string, labChatId: string): Promise<{
|
|
176
183
|
success: boolean;
|
|
177
184
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"labChat.d.ts","sourceRoot":"/","sources":["services/labChat.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"labChat.d.ts","sourceRoot":"/","sources":["services/labChat.ts"],"names":[],"mappings":"AA4BA,sFAAsF;AACtF,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiI3D;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCjE;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqBjE;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IACL,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;;;;;;;;;;;;;;;;;;;;;GA+JF;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;GAiDpE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;GAiGxE"}
|
package/dist/services/labChat.js
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
* trigger AI responses, and broadcast via Pusher.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
!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]="
|
|
6
|
+
!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]="ed2a451c-ef85-522a-a235-58f49dd2708f")}catch(e){}}();
|
|
7
7
|
import { TRPCError } from "@trpc/server";
|
|
8
8
|
import { JobStatus } from "@prisma/client";
|
|
9
9
|
import { prisma } from "../lib/prisma.js";
|
|
10
10
|
import { chatChannel, teacherChannel, pusher } from "../lib/pusher.js";
|
|
11
11
|
import { findClassWithTeachersForLab, findLabChatByMember, findLabChatByTeacher, findLabChatById, findClassMembership, findLabChatsByClass, findLabChatForPost, findLabChatForDelete, findMentionedMemberships, } from "../models/labChat.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { getAIUserId, isAIUser } from "../utils/aiUser.js";
|
|
13
|
+
import { createPendingAIMessage } from "../utils/inference.js";
|
|
14
14
|
import { logger } from "../utils/logger.js";
|
|
15
|
+
import { enqueueRunJob } from "../workers/queues.js";
|
|
15
16
|
import { claimMessageAttachments, ensureMessageHasContentOrAttachments, } from "./message.js";
|
|
16
17
|
/** Create a lab chat with conversation and teacher members. Broadcasts via Pusher. */
|
|
17
18
|
export async function createLabChat(userId, input) {
|
|
@@ -103,7 +104,20 @@ export async function createLabChat(userId, input) {
|
|
|
103
104
|
});
|
|
104
105
|
return labChat;
|
|
105
106
|
});
|
|
106
|
-
|
|
107
|
+
// Create the AI introduction placeholder *outside* the transaction (the
|
|
108
|
+
// helper triggers Pusher and we don't want a tx rollback to leave a
|
|
109
|
+
// dangling broadcast). The pipeline will fill this row in.
|
|
110
|
+
try {
|
|
111
|
+
const aiPlaceholder = await createPendingAIMessage(result.conversationId);
|
|
112
|
+
await enqueueRunJob("AI_LAB_CHAT", { conversationId: result.conversationId, aiMessageId: aiPlaceholder.id }, { classId, userId });
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger.error("Failed to enqueue lab chat introduction", {
|
|
116
|
+
error,
|
|
117
|
+
labChatId: result.id,
|
|
118
|
+
conversationId: result.conversationId,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
107
121
|
try {
|
|
108
122
|
await pusher.trigger(teacherChannel(classId), "lab-chat-created", {
|
|
109
123
|
id: result.id,
|
|
@@ -140,11 +154,13 @@ export async function getLabChat(userId, labChatId) {
|
|
|
140
154
|
message: "Lab chat not found or access denied",
|
|
141
155
|
});
|
|
142
156
|
}
|
|
157
|
+
// The AI placeholder row IS the pending generation marker now — its
|
|
158
|
+
// status flips PENDING → COMPLETED / FAILED as the pipeline runs.
|
|
143
159
|
const pendingMessage = await prisma.message.findFirst({
|
|
144
160
|
where: {
|
|
145
161
|
conversationId: labChat.conversationId,
|
|
146
162
|
status: JobStatus.PENDING,
|
|
147
|
-
senderId:
|
|
163
|
+
senderId: getAIUserId(),
|
|
148
164
|
},
|
|
149
165
|
orderBy: { createdAt: "desc" },
|
|
150
166
|
select: { id: true },
|
|
@@ -201,7 +217,6 @@ export async function postToLabChat(userId, input) {
|
|
|
201
217
|
content: normalizedContent,
|
|
202
218
|
senderId: userId,
|
|
203
219
|
conversationId: labChat.conversationId,
|
|
204
|
-
...(isAIUser(userId) ? {} : { status: JobStatus.PENDING }),
|
|
205
220
|
},
|
|
206
221
|
});
|
|
207
222
|
await claimMessageAttachments(tx, userId, labChat.conversationId, message.id, fileIds);
|
|
@@ -244,6 +259,7 @@ export async function postToLabChat(userId, input) {
|
|
|
244
259
|
content: result.content,
|
|
245
260
|
senderId: result.senderId,
|
|
246
261
|
conversationId: result.conversationId,
|
|
262
|
+
status: result.status,
|
|
247
263
|
createdAt: result.createdAt,
|
|
248
264
|
sender: result.sender,
|
|
249
265
|
attachments: result.attachments ?? [],
|
|
@@ -255,31 +271,53 @@ export async function postToLabChat(userId, input) {
|
|
|
255
271
|
console.error("Failed to broadcast lab chat message:", error);
|
|
256
272
|
}
|
|
257
273
|
if (!isAIUser(userId)) {
|
|
274
|
+
// Create the AI placeholder post-tx so a transaction rollback can never
|
|
275
|
+
// leave a dangling PENDING bubble. The placeholder broadcast IS the
|
|
276
|
+
// "AI is thinking..." indicator inline in the chat; the teacher-channel
|
|
277
|
+
// event below drives the lab list "AI busy in this lab" pill.
|
|
278
|
+
let aiPlaceholderId = null;
|
|
279
|
+
try {
|
|
280
|
+
const aiPlaceholder = await createPendingAIMessage(labChat.conversationId);
|
|
281
|
+
aiPlaceholderId = aiPlaceholder.id;
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
logger.error("Failed to create AI placeholder for lab response", {
|
|
285
|
+
error,
|
|
286
|
+
labChatId,
|
|
287
|
+
teacherMessageId: result.id,
|
|
288
|
+
conversationId: labChat.conversationId,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
258
291
|
try {
|
|
259
292
|
await pusher.trigger(teacherChannel(labChat.classId), "lab-response-pending", {
|
|
260
293
|
labChatId,
|
|
261
|
-
messageId: result.id,
|
|
294
|
+
messageId: aiPlaceholderId ?? result.id,
|
|
262
295
|
conversationId: labChat.conversationId,
|
|
263
296
|
});
|
|
264
297
|
}
|
|
265
298
|
catch (error) {
|
|
266
299
|
console.error("Failed to broadcast lab response pending:", error);
|
|
267
300
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
error
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
301
|
+
if (aiPlaceholderId) {
|
|
302
|
+
try {
|
|
303
|
+
await enqueueRunJob("AI_LAB_CHAT", { conversationId: labChat.conversationId, aiMessageId: aiPlaceholderId }, { classId: labChat.classId, userId });
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
logger.error("Failed to enqueue lab response", {
|
|
307
|
+
error,
|
|
308
|
+
labChatId,
|
|
309
|
+
aiMessageId: aiPlaceholderId,
|
|
310
|
+
conversationId: labChat.conversationId,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
277
314
|
}
|
|
278
315
|
return {
|
|
279
316
|
id: result.id,
|
|
280
317
|
content: result.content,
|
|
281
318
|
senderId: result.senderId,
|
|
282
319
|
conversationId: result.conversationId,
|
|
320
|
+
status: result.status,
|
|
283
321
|
createdAt: result.createdAt,
|
|
284
322
|
sender: result.sender,
|
|
285
323
|
attachments: result.attachments ?? [],
|
|
@@ -330,7 +368,13 @@ export async function deleteLabChat(userId, labChatId) {
|
|
|
330
368
|
}
|
|
331
369
|
return { success: true };
|
|
332
370
|
}
|
|
333
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* Rerun the last AI response in a lab chat.
|
|
373
|
+
*
|
|
374
|
+
* The AI message bubble is reused (not deleted): we flip it back to
|
|
375
|
+
* PENDING and clear its content so the existing message id stays stable
|
|
376
|
+
* for the client. The pipeline then refills it.
|
|
377
|
+
*/
|
|
334
378
|
export async function rerunLastResponse(userId, labChatId) {
|
|
335
379
|
const labChat = await findLabChatForPost(labChatId, userId);
|
|
336
380
|
if (!labChat) {
|
|
@@ -365,45 +409,60 @@ export async function rerunLastResponse(userId, labChatId) {
|
|
|
365
409
|
message: "No teacher message found to rerun from",
|
|
366
410
|
});
|
|
367
411
|
}
|
|
368
|
-
await prisma
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
412
|
+
const reusedAiMessage = await prisma.message.update({
|
|
413
|
+
where: { id: lastMessage.id },
|
|
414
|
+
data: {
|
|
415
|
+
content: "",
|
|
416
|
+
status: JobStatus.PENDING,
|
|
417
|
+
meta: {},
|
|
418
|
+
},
|
|
419
|
+
select: { id: true, conversationId: true, status: true, content: true, senderId: true, createdAt: true },
|
|
420
|
+
});
|
|
421
|
+
// Tell the chat UI the existing AI bubble is regenerating — content
|
|
422
|
+
// cleared, status flipped back to PENDING, same id.
|
|
377
423
|
try {
|
|
378
|
-
await pusher.trigger(chatChannel(labChat.conversationId), "message-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
senderId:
|
|
424
|
+
await pusher.trigger(chatChannel(labChat.conversationId), "message-updated", {
|
|
425
|
+
id: reusedAiMessage.id,
|
|
426
|
+
content: reusedAiMessage.content,
|
|
427
|
+
senderId: reusedAiMessage.senderId,
|
|
428
|
+
conversationId: reusedAiMessage.conversationId,
|
|
429
|
+
status: reusedAiMessage.status,
|
|
430
|
+
createdAt: reusedAiMessage.createdAt,
|
|
431
|
+
sender: {
|
|
432
|
+
id: reusedAiMessage.senderId,
|
|
433
|
+
username: 'Newton_AI',
|
|
434
|
+
profile: { displayName: 'Newton AI', profilePicture: null },
|
|
435
|
+
},
|
|
436
|
+
mentionedUserIds: [],
|
|
437
|
+
meta: {},
|
|
438
|
+
attachments: [],
|
|
382
439
|
});
|
|
383
440
|
}
|
|
384
441
|
catch (error) {
|
|
385
|
-
console.error("Failed to broadcast message
|
|
442
|
+
console.error("Failed to broadcast AI message rerun reset:", error);
|
|
386
443
|
}
|
|
387
444
|
try {
|
|
388
445
|
await pusher.trigger(teacherChannel(labChat.classId), "lab-response-pending", {
|
|
389
446
|
labChatId,
|
|
390
|
-
messageId:
|
|
447
|
+
messageId: reusedAiMessage.id,
|
|
391
448
|
conversationId: labChat.conversationId,
|
|
392
449
|
});
|
|
393
450
|
}
|
|
394
451
|
catch (error) {
|
|
395
452
|
console.error("Failed to broadcast lab response pending:", error);
|
|
396
453
|
}
|
|
397
|
-
|
|
398
|
-
classId: labChat.classId,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
error,
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
454
|
+
try {
|
|
455
|
+
await enqueueRunJob("AI_LAB_CHAT", { conversationId: labChat.conversationId, aiMessageId: reusedAiMessage.id }, { classId: labChat.classId, userId });
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
logger.error("Failed to enqueue lab response rerun", {
|
|
459
|
+
error,
|
|
460
|
+
labChatId,
|
|
461
|
+
aiMessageId: reusedAiMessage.id,
|
|
462
|
+
conversationId: labChat.conversationId,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
406
465
|
return { success: true };
|
|
407
466
|
}
|
|
408
467
|
//# sourceMappingURL=labChat.js.map
|
|
409
|
-
//# debugId=
|
|
468
|
+
//# debugId=ed2a451c-ef85-522a-a235-58f49dd2708f
|