@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { chatAboutStudentProgress } from '../../services/studentProgress.js';
|
|
2
|
+
import { createRunJobWorker } from './createRunJobWorker.js';
|
|
3
|
+
|
|
4
|
+
export const studentProgressChatWorker = createRunJobWorker({
|
|
5
|
+
type: 'STUDENT_PROGRESS_CHAT',
|
|
6
|
+
workerOptions: {
|
|
7
|
+
concurrency: 4,
|
|
8
|
+
limiter: { max: 20, duration: 60_000 },
|
|
9
|
+
},
|
|
10
|
+
run: async ({ classId, studentId, message }, ctx) => {
|
|
11
|
+
// `chatAboutStudentProgress` runs an authorization check using the viewer
|
|
12
|
+
// id, so callers MUST pass `userId` via `enqueueRunJob` options for the
|
|
13
|
+
// service to receive it here.
|
|
14
|
+
if (!ctx.userId) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'STUDENT_PROGRESS_CHAT requires `userId` on the RunJob (set via enqueueRunJob options)',
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const response = await chatAboutStudentProgress(ctx.userId, {
|
|
21
|
+
classId,
|
|
22
|
+
studentId,
|
|
23
|
+
message,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return response;
|
|
27
|
+
},
|
|
28
|
+
});
|
package/src/workers/queues.ts
CHANGED
|
@@ -26,15 +26,30 @@ const DEFAULT_JOB_OPTIONS: JobsOptions = {
|
|
|
26
26
|
* The queue name string must match what the corresponding processor passes
|
|
27
27
|
* to `new Worker(<name>, ...)`.
|
|
28
28
|
*/
|
|
29
|
-
type JobInputs = {
|
|
30
|
-
|
|
29
|
+
export type JobInputs = {
|
|
30
|
+
/**
|
|
31
|
+
* `worksheetId` is the placeholder Worksheet row created synchronously by
|
|
32
|
+
* the producer (so the client has an id to navigate to). The pipeline fills
|
|
33
|
+
* that row in asynchronously and broadcasts progress on the per-worksheet
|
|
34
|
+
* job channel.
|
|
35
|
+
*/
|
|
36
|
+
PDF_TO_WORKSHEET: { gcsUrl: string; classId: string; worksheetId: string };
|
|
31
37
|
GRADE_WORKSHEET: { worksheetResponseId: string };
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
/**
|
|
39
|
+
* `aiMessageId` is the placeholder AI Message row created synchronously by
|
|
40
|
+
* the producer before enqueue (so the client renders a PENDING bubble in
|
|
41
|
+
* the timeline). The pipeline fills in the content and flips the row to
|
|
42
|
+
* COMPLETED / FAILED. Used for both intros and replies — single shape.
|
|
43
|
+
*/
|
|
44
|
+
AI_LAB_CHAT: { conversationId: string; aiMessageId: string };
|
|
45
|
+
/**
|
|
46
|
+
* `aiMessageId` placeholder pattern, same as AI_LAB_CHAT — see above.
|
|
47
|
+
*/
|
|
48
|
+
AI_NEWTON_CHAT: { conversationId: string; aiMessageId: string };
|
|
34
49
|
STUDENT_PROGRESS_CHAT: { classId: string; studentId: string; message: string };
|
|
35
50
|
};
|
|
36
51
|
|
|
37
|
-
const QUEUE_NAMES: Record<RunJobType, string> = {
|
|
52
|
+
export const QUEUE_NAMES: Record<RunJobType, string> = {
|
|
38
53
|
PDF_TO_WORKSHEET: 'pdfToWorksheet',
|
|
39
54
|
GRADE_WORKSHEET: 'gradeWorksheet',
|
|
40
55
|
AI_LAB_CHAT: 'aiLabChat',
|
|
@@ -117,6 +117,66 @@ describe('Attendance Router', () => {
|
|
|
117
117
|
expect(updatedRecord?.late.some((l: any) => l.id === student2.id)).toBe(true);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
test('should notify students newly marked late or absent', async () => {
|
|
121
|
+
await prisma.notification.deleteMany({
|
|
122
|
+
where: { receiverId: student2.id },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await user1Caller.attendance.update({
|
|
126
|
+
classId: testClass.id,
|
|
127
|
+
eventId: testEvent.event.id,
|
|
128
|
+
attendance: {
|
|
129
|
+
present: [{ id: student1.id, username: student1.username }],
|
|
130
|
+
late: [{ id: student2.id, username: student2.username }],
|
|
131
|
+
absent: [],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
let notifications = await user2Caller.notification.list();
|
|
136
|
+
expect(
|
|
137
|
+
notifications.some(
|
|
138
|
+
(notification: any) =>
|
|
139
|
+
notification.title === `Marked late in ${testClass.name}` &&
|
|
140
|
+
notification.content.includes(testEvent.event.name) &&
|
|
141
|
+
notification.content.includes(`/class/${testClass.id}/attendance/${testEvent.event.id}`)
|
|
142
|
+
)
|
|
143
|
+
).toBe(true);
|
|
144
|
+
|
|
145
|
+
await user1Caller.attendance.update({
|
|
146
|
+
classId: testClass.id,
|
|
147
|
+
eventId: testEvent.event.id,
|
|
148
|
+
attendance: {
|
|
149
|
+
present: [{ id: student1.id, username: student1.username }],
|
|
150
|
+
late: [{ id: student2.id, username: student2.username }],
|
|
151
|
+
absent: [],
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
notifications = await user2Caller.notification.list();
|
|
156
|
+
expect(
|
|
157
|
+
notifications.filter(
|
|
158
|
+
(notification: any) => notification.title === `Marked late in ${testClass.name}`
|
|
159
|
+
)
|
|
160
|
+
).toHaveLength(1);
|
|
161
|
+
|
|
162
|
+
await user1Caller.attendance.update({
|
|
163
|
+
classId: testClass.id,
|
|
164
|
+
eventId: testEvent.event.id,
|
|
165
|
+
attendance: {
|
|
166
|
+
present: [{ id: student1.id, username: student1.username }],
|
|
167
|
+
late: [],
|
|
168
|
+
absent: [{ id: student2.id, username: student2.username }],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
notifications = await user2Caller.notification.list();
|
|
173
|
+
expect(
|
|
174
|
+
notifications.some(
|
|
175
|
+
(notification: any) => notification.title === `Marked absent in ${testClass.name}`
|
|
176
|
+
)
|
|
177
|
+
).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
120
180
|
test('should update attendance without event', async () => {
|
|
121
181
|
const result = await user1Caller.attendance.update({
|
|
122
182
|
classId: testClass.id,
|
|
@@ -157,4 +217,3 @@ describe('Attendance Router', () => {
|
|
|
157
217
|
})));
|
|
158
218
|
});
|
|
159
219
|
});
|
|
160
|
-
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { test, expect, describe, beforeAll } from 'vitest';
|
|
1
|
+
import { test, expect, describe, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
import { prisma } from '../../../src/lib/prisma';
|
|
3
3
|
import { user1Caller, user2Caller, user3Caller } from '../setup';
|
|
4
4
|
import { expectUnauth } from '../helpers';
|
|
@@ -7,16 +7,19 @@ describe('Conversation Router', () => {
|
|
|
7
7
|
let dmConversation: any;
|
|
8
8
|
let groupConversation: any;
|
|
9
9
|
let user1Id: string;
|
|
10
|
+
let user2Id: string;
|
|
10
11
|
let user3Id: string;
|
|
11
12
|
let hideConversationCounter = 0;
|
|
12
13
|
|
|
13
14
|
beforeAll(async () => {
|
|
14
|
-
const [user1Profile, user3Profile] = await Promise.all([
|
|
15
|
+
const [user1Profile, user2Profile, user3Profile] = await Promise.all([
|
|
15
16
|
user1Caller.user.getProfile(),
|
|
17
|
+
user2Caller.user.getProfile(),
|
|
16
18
|
user3Caller.user.getProfile(),
|
|
17
19
|
]);
|
|
18
20
|
|
|
19
21
|
user1Id = user1Profile.id;
|
|
22
|
+
user2Id = user2Profile.id;
|
|
20
23
|
user3Id = user3Profile.id;
|
|
21
24
|
});
|
|
22
25
|
|
|
@@ -89,6 +92,122 @@ describe('Conversation Router', () => {
|
|
|
89
92
|
});
|
|
90
93
|
});
|
|
91
94
|
|
|
95
|
+
describe('searchMembers', () => {
|
|
96
|
+
let schoolId: string;
|
|
97
|
+
let previousUser2DisplayName: string | null = null;
|
|
98
|
+
let user2ProfileExisted = false;
|
|
99
|
+
|
|
100
|
+
beforeAll(async () => {
|
|
101
|
+
const existingProfile = await prisma.userProfile.findUnique({
|
|
102
|
+
where: { userId: user2Id },
|
|
103
|
+
select: { displayName: true },
|
|
104
|
+
});
|
|
105
|
+
user2ProfileExisted = Boolean(existingProfile);
|
|
106
|
+
previousUser2DisplayName = existingProfile?.displayName ?? null;
|
|
107
|
+
|
|
108
|
+
const school = await prisma.school.create({
|
|
109
|
+
data: {
|
|
110
|
+
name: 'Conversation Search School',
|
|
111
|
+
memberships: {
|
|
112
|
+
create: [
|
|
113
|
+
{ userId: user1Id, role: 'TEACHER', status: 'ACTIVE' },
|
|
114
|
+
{ userId: user2Id, role: 'STUDENT', status: 'ACTIVE' },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
schoolId = school.id;
|
|
120
|
+
|
|
121
|
+
await prisma.userProfile.upsert({
|
|
122
|
+
where: { userId: user2Id },
|
|
123
|
+
update: { displayName: 'Searchable Student' },
|
|
124
|
+
create: { userId: user2Id, displayName: 'Searchable Student' },
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('should search active institution members by username', async () => {
|
|
129
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
130
|
+
query: 'testuser2',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(results.map((user) => user.id)).toContain(user2Id);
|
|
134
|
+
expect(results.map((user) => user.id)).not.toContain(user1Id);
|
|
135
|
+
expect(results.map((user) => user.id)).not.toContain(user3Id);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should not search active institution members for one-character queries', async () => {
|
|
139
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
140
|
+
query: 't',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(results).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should search active institution members by display name', async () => {
|
|
147
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
148
|
+
query: 'searchable',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(results).toEqual(
|
|
152
|
+
expect.arrayContaining([
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
id: user2Id,
|
|
155
|
+
username: 'testuser2',
|
|
156
|
+
profile: expect.objectContaining({
|
|
157
|
+
displayName: 'Searchable Student',
|
|
158
|
+
}),
|
|
159
|
+
}),
|
|
160
|
+
]),
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should not expose users outside the requester institution', async () => {
|
|
165
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
166
|
+
query: 'testuser3',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(results).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('should not expose inactive members from the requester institution', async () => {
|
|
173
|
+
await prisma.schoolMembership.create({
|
|
174
|
+
data: {
|
|
175
|
+
schoolId,
|
|
176
|
+
userId: user3Id,
|
|
177
|
+
role: 'STUDENT',
|
|
178
|
+
status: 'INACTIVE',
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
183
|
+
query: 'testuser3',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(results).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('should return no institution search results for users without an active school', async () => {
|
|
190
|
+
const results = await user3Caller.conversation.searchMembers({
|
|
191
|
+
query: 'testuser1',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(results).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
afterAll(async () => {
|
|
198
|
+
if (user2ProfileExisted) {
|
|
199
|
+
await prisma.userProfile.update({
|
|
200
|
+
where: { userId: user2Id },
|
|
201
|
+
data: { displayName: previousUser2DisplayName },
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
await prisma.userProfile.deleteMany({ where: { userId: user2Id } });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await prisma.school.delete({ where: { id: schoolId } });
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
92
211
|
describe('get', () => {
|
|
93
212
|
test('should get conversation by ID', async () => {
|
|
94
213
|
const conversation = await user1Caller.conversation.get({
|