@studious-lms/server 1.2.45 → 1.2.47
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/.env.example +45 -0
- package/.env.test.example +37 -0
- package/README.md +34 -7
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +12110 -0
- package/coverage/coverage-final.json +44 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +221 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/server/index.html +116 -0
- package/coverage/server/src/exportType.ts.html +109 -0
- package/coverage/server/src/index.html +161 -0
- package/coverage/server/src/index.ts.html +1702 -0
- package/coverage/server/src/instrument.ts.html +130 -0
- package/coverage/server/src/lib/config/env.ts.html +448 -0
- package/coverage/server/src/lib/config/index.html +116 -0
- package/coverage/server/src/lib/fileUpload.ts.html +1138 -0
- package/coverage/server/src/lib/googleCloudStorage.ts.html +334 -0
- package/coverage/server/src/lib/index.html +206 -0
- package/coverage/server/src/lib/jsonConversion.ts.html +2323 -0
- package/coverage/server/src/lib/jsonStyles.ts.html +193 -0
- package/coverage/server/src/lib/notificationHandler.ts.html +193 -0
- package/coverage/server/src/lib/pusher.ts.html +121 -0
- package/coverage/server/src/lib/thumbnailGenerator.ts.html +592 -0
- package/coverage/server/src/middleware/auth.ts.html +646 -0
- package/coverage/server/src/middleware/index.html +146 -0
- package/coverage/server/src/middleware/logging.ts.html +244 -0
- package/coverage/server/src/middleware/security.ts.html +271 -0
- package/coverage/server/src/routers/_app.ts.html +232 -0
- package/coverage/server/src/routers/agenda.ts.html +319 -0
- package/coverage/server/src/routers/announcement.ts.html +3481 -0
- package/coverage/server/src/routers/assignment.ts.html +7633 -0
- package/coverage/server/src/routers/attendance.ts.html +1030 -0
- package/coverage/server/src/routers/auth.ts.html +1081 -0
- package/coverage/server/src/routers/class.ts.html +3535 -0
- package/coverage/server/src/routers/comment.ts.html +991 -0
- package/coverage/server/src/routers/conversation.ts.html +982 -0
- package/coverage/server/src/routers/event.ts.html +1609 -0
- package/coverage/server/src/routers/file.ts.html +1144 -0
- package/coverage/server/src/routers/folder.ts.html +2797 -0
- package/coverage/server/src/routers/index.html +386 -0
- package/coverage/server/src/routers/labChat.ts.html +3073 -0
- package/coverage/server/src/routers/marketing.ts.html +340 -0
- package/coverage/server/src/routers/message.ts.html +1912 -0
- package/coverage/server/src/routers/notifications.ts.html +364 -0
- package/coverage/server/src/routers/section.ts.html +1120 -0
- package/coverage/server/src/routers/user.ts.html +862 -0
- package/coverage/server/src/routers/worksheet.ts.html +1729 -0
- package/coverage/server/src/trpc.ts.html +397 -0
- package/coverage/server/src/types/index.html +116 -0
- package/coverage/server/src/types/trpc.ts.html +127 -0
- package/coverage/server/src/utils/aiUser.ts.html +280 -0
- package/coverage/server/src/utils/email.ts.html +121 -0
- package/coverage/server/src/utils/generateInviteCode.ts.html +106 -0
- package/coverage/server/src/utils/index.html +206 -0
- package/coverage/server/src/utils/inference.ts.html +709 -0
- package/coverage/server/src/utils/logger.ts.html +664 -0
- package/coverage/server/src/utils/prismaErrorHandler.ts.html +907 -0
- package/coverage/server/src/utils/prismaWrapper.ts.html +355 -0
- package/coverage/server/vitest.config.ts.html +196 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +83 -52
- package/dist/index.js.map +1 -1
- package/dist/instrument.js +15 -8
- package/dist/instrument.js.map +1 -1
- package/dist/lib/config/env.d.ts +169 -0
- package/dist/lib/config/env.d.ts.map +1 -0
- package/dist/lib/config/env.js +115 -0
- package/dist/lib/config/env.js.map +1 -0
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +5 -4
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +7 -8
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/jsonConversion.d.ts.map +1 -1
- package/dist/lib/jsonConversion.js +14 -16
- package/dist/lib/jsonConversion.js.map +1 -1
- package/dist/lib/notificationHandler.d.ts +2 -2
- package/dist/lib/prisma.d.ts +2 -2
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +22 -3
- package/dist/lib/prisma.js.map +1 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +8 -7
- package/dist/lib/pusher.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +7 -5
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/security.d.ts +5 -0
- package/dist/middleware/security.d.ts.map +1 -0
- package/dist/middleware/security.js +77 -0
- package/dist/middleware/security.js.map +1 -0
- package/dist/routers/_app.d.ts +368 -108
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +12 -9
- package/dist/routers/agenda.js.map +1 -1
- package/dist/routers/announcement.d.ts +8 -0
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +6 -4
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +17 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +51 -19
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +1 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +4 -4
- package/dist/routers/attendance.js.map +1 -1
- package/dist/routers/auth.d.ts +20 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +132 -15
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +10 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +49 -5
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/comment.d.ts +2 -0
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/conversation.d.ts +2 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +46 -31
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +30 -7
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/labChat.d.ts +2 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +5 -322
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/marketing.d.ts +1 -1
- package/dist/routers/message.d.ts +1 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +3 -2
- package/dist/routers/message.js.map +1 -1
- package/dist/routers/newtonChat.d.ts +55 -0
- package/dist/routers/newtonChat.d.ts.map +1 -0
- package/dist/routers/newtonChat.js +262 -0
- package/dist/routers/newtonChat.js.map +1 -0
- package/dist/routers/notifications.d.ts +4 -4
- package/dist/routers/section.d.ts +19 -4
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +26 -8
- package/dist/routers/section.js.map +1 -1
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +5 -4
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +44 -41
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +25 -34
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/seedDatabase.d.ts +1 -1
- package/dist/seedDatabase.js +275 -284
- package/dist/seedDatabase.js.map +1 -1
- package/dist/server/pipelines/aiLabChat.d.ts +21 -0
- package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiLabChat.js +456 -0
- package/dist/server/pipelines/aiLabChat.js.map +1 -0
- package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
- package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiNewtonChat.js +280 -0
- package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts +15 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.js +139 -0
- package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +2 -2
- package/dist/trpc.js.map +1 -1
- package/dist/utils/email.d.ts +9 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +20 -5
- package/dist/utils/email.js.map +1 -1
- package/dist/utils/inference.d.ts +5 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +71 -7
- package/dist/utils/inference.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +3 -3
- package/dist/utils/logger.js.map +1 -1
- package/docker-compose.yml +14 -0
- package/package.json +13 -4
- package/prisma/schema.prisma +34 -5
- package/scripts/test-pre-push.ts +14 -0
- package/src/index.ts +98 -54
- package/src/instrument.ts +13 -6
- package/src/lib/config/env.ts +126 -0
- package/src/lib/fileUpload.ts +3 -2
- package/src/lib/googleCloudStorage.ts +6 -6
- package/src/lib/jsonConversion.ts +12 -14
- package/src/lib/prisma.ts +23 -2
- package/src/lib/pusher.ts +6 -5
- package/src/middleware/auth.ts +5 -3
- package/src/middleware/security.ts +80 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/agenda.ts +10 -7
- package/src/routers/announcement.ts +4 -2
- package/src/routers/assignment.ts +74 -41
- package/src/routers/attendance.ts +2 -2
- package/src/routers/auth.ts +143 -14
- package/src/routers/class.ts +52 -3
- package/src/routers/conversation.ts +49 -29
- package/src/routers/file.ts +29 -5
- package/src/routers/labChat.ts +3 -367
- package/src/routers/message.ts +1 -1
- package/src/routers/newtonChat.ts +299 -0
- package/src/routers/section.ts +26 -6
- package/src/routers/user.ts +3 -2
- package/src/routers/worksheet.ts +26 -38
- package/src/seedDatabase.ts +290 -283
- package/src/server/pipelines/aiLabChat.ts +507 -0
- package/src/server/pipelines/aiNewtonChat.ts +338 -0
- package/src/server/pipelines/gradeWorksheet.ts +151 -0
- package/src/trpc.ts +2 -0
- package/src/utils/email.ts +30 -3
- package/src/utils/inference.ts +85 -5
- package/src/utils/logger.ts +2 -1
- package/tests/announcement.test.ts +164 -0
- package/tests/assignment.test.ts +296 -0
- package/tests/attendance.test.ts +168 -0
- package/tests/auth.test.ts +33 -10
- package/tests/class.test.ts +34 -9
- package/tests/event.test.ts +228 -0
- package/tests/section.test.ts +216 -0
- package/tests/setup.ts +70 -16
- package/tests/user.test.ts +158 -0
- package/vitest.config.ts +26 -0
- package/API_SPECIFICATION.md +0 -1597
- package/BASE64_REMOVAL_SUMMARY.md +0 -164
- package/CHAT_API_SPEC.md +0 -579
- package/LAB_CHAT_API_SPEC.md +0 -518
- package/dist/routers/school.d.ts +0 -208
- package/dist/routers/school.d.ts.map +0 -1
- package/dist/routers/school.js +0 -483
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createTRPCRouter, protectedProcedure } from '../trpc.js';
|
|
3
|
+
import { prisma } from '../lib/prisma.js';
|
|
4
|
+
import { pusher } from '../lib/pusher.js';
|
|
5
|
+
import { TRPCError } from '@trpc/server';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
import { isAIUser } from '../utils/aiUser.js';
|
|
8
|
+
import { generateAndSendNewtonIntroduction, generateAndSendNewtonResponse } from '../server/pipelines/aiNewtonChat.js';
|
|
9
|
+
|
|
10
|
+
export const newtonChatRouter = createTRPCRouter({
|
|
11
|
+
getTutorConversation: protectedProcedure
|
|
12
|
+
.input(
|
|
13
|
+
z.object({
|
|
14
|
+
assignmentId: z.string(),
|
|
15
|
+
classId: z.string(),
|
|
16
|
+
})
|
|
17
|
+
)
|
|
18
|
+
.query(async ({ input, ctx }) => {
|
|
19
|
+
const userId = ctx.user!.id;
|
|
20
|
+
const { assignmentId, classId } = input;
|
|
21
|
+
|
|
22
|
+
// Verify user is a student in the class
|
|
23
|
+
const classMembership = await prisma.class.findFirst({
|
|
24
|
+
where: {
|
|
25
|
+
id: classId,
|
|
26
|
+
students: {
|
|
27
|
+
some: {
|
|
28
|
+
id: userId,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!classMembership) {
|
|
35
|
+
throw new TRPCError({
|
|
36
|
+
code: 'FORBIDDEN',
|
|
37
|
+
message: 'Not a student in this class',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Find or create submission for this student and assignment
|
|
42
|
+
const submission = await prisma.submission.findFirst({
|
|
43
|
+
where: {
|
|
44
|
+
assignmentId,
|
|
45
|
+
studentId: userId,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!submission) {
|
|
50
|
+
throw new TRPCError({
|
|
51
|
+
code: 'NOT_FOUND',
|
|
52
|
+
message: 'Submission not found. Please create a submission first.',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Find the latest NewtonChat for this submission, or create a new one
|
|
57
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
58
|
+
// Get the latest NewtonChat for this submission
|
|
59
|
+
const existingNewtonChat = await tx.newtonChat.findFirst({
|
|
60
|
+
where: {
|
|
61
|
+
submissionId: submission.id,
|
|
62
|
+
},
|
|
63
|
+
include: {
|
|
64
|
+
conversation: {
|
|
65
|
+
include: {
|
|
66
|
+
members: {
|
|
67
|
+
where: {
|
|
68
|
+
userId,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
orderBy: {
|
|
75
|
+
createdAt: 'desc',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// If exists and user is already a member, return it
|
|
80
|
+
if (existingNewtonChat && existingNewtonChat.conversation.members.length > 0) {
|
|
81
|
+
return existingNewtonChat;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// If exists but user is not a member, add them
|
|
85
|
+
if (existingNewtonChat) {
|
|
86
|
+
await tx.conversationMember.create({
|
|
87
|
+
data: {
|
|
88
|
+
userId,
|
|
89
|
+
conversationId: existingNewtonChat.conversationId,
|
|
90
|
+
role: 'MEMBER',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return existingNewtonChat;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create new NewtonChat with associated conversation
|
|
98
|
+
const conversation = await tx.conversation.create({
|
|
99
|
+
data: {
|
|
100
|
+
type: 'DM',
|
|
101
|
+
name: 'Session with Newton Tutor',
|
|
102
|
+
displayInChat: false, // Newton chats don't show in regular chat list
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Add student to the conversation
|
|
107
|
+
await tx.conversationMember.create({
|
|
108
|
+
data: {
|
|
109
|
+
userId,
|
|
110
|
+
conversationId: conversation.id,
|
|
111
|
+
role: 'MEMBER',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Create the NewtonChat
|
|
116
|
+
const newtonChat = await tx.newtonChat.create({
|
|
117
|
+
data: {
|
|
118
|
+
submissionId: submission.id,
|
|
119
|
+
conversationId: conversation.id,
|
|
120
|
+
title: 'Session with Newton Tutor',
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return newtonChat;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Generate AI introduction message in parallel (don't await - fire and forget)
|
|
128
|
+
generateAndSendNewtonIntroduction(
|
|
129
|
+
result.id,
|
|
130
|
+
result.conversationId,
|
|
131
|
+
submission.id
|
|
132
|
+
).catch(error => {
|
|
133
|
+
logger.error('Failed to generate AI introduction:', { error, newtonChatId: result.id });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
conversationId: result.conversationId,
|
|
138
|
+
newtonChatId: result.id,
|
|
139
|
+
};
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
postToNewtonChat: protectedProcedure
|
|
143
|
+
.input(
|
|
144
|
+
z.object({
|
|
145
|
+
newtonChatId: z.string(),
|
|
146
|
+
content: z.string().min(1).max(4000),
|
|
147
|
+
mentionedUserIds: z.array(z.string()).optional(),
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
.mutation(async ({ input, ctx }) => {
|
|
151
|
+
const userId = ctx.user!.id;
|
|
152
|
+
const { newtonChatId, content, mentionedUserIds = [] } = input;
|
|
153
|
+
|
|
154
|
+
// Get newton chat and verify user is a member
|
|
155
|
+
const newtonChat = await prisma.newtonChat.findFirst({
|
|
156
|
+
where: {
|
|
157
|
+
id: newtonChatId,
|
|
158
|
+
conversation: {
|
|
159
|
+
members: {
|
|
160
|
+
some: {
|
|
161
|
+
userId,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
include: {
|
|
167
|
+
conversation: {
|
|
168
|
+
select: {
|
|
169
|
+
id: true,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
submission: {
|
|
173
|
+
include: {
|
|
174
|
+
assignment: {
|
|
175
|
+
select: {
|
|
176
|
+
id: true,
|
|
177
|
+
title: true,
|
|
178
|
+
instructions: true,
|
|
179
|
+
class: {
|
|
180
|
+
select: {
|
|
181
|
+
subject: true,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!newtonChat) {
|
|
192
|
+
throw new TRPCError({
|
|
193
|
+
code: 'FORBIDDEN',
|
|
194
|
+
message: 'Newton chat not found or access denied',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Verify mentioned users are members of the conversation
|
|
199
|
+
if (mentionedUserIds.length > 0) {
|
|
200
|
+
const mentionedMemberships = await prisma.conversationMember.findMany({
|
|
201
|
+
where: {
|
|
202
|
+
conversationId: newtonChat.conversationId,
|
|
203
|
+
userId: { in: mentionedUserIds },
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (mentionedMemberships.length !== mentionedUserIds.length) {
|
|
208
|
+
throw new TRPCError({
|
|
209
|
+
code: 'BAD_REQUEST',
|
|
210
|
+
message: 'Some mentioned users are not members of this conversation',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Create message and mentions
|
|
216
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
217
|
+
const message = await tx.message.create({
|
|
218
|
+
data: {
|
|
219
|
+
content,
|
|
220
|
+
senderId: userId,
|
|
221
|
+
conversationId: newtonChat.conversationId,
|
|
222
|
+
},
|
|
223
|
+
include: {
|
|
224
|
+
sender: {
|
|
225
|
+
select: {
|
|
226
|
+
id: true,
|
|
227
|
+
username: true,
|
|
228
|
+
profile: {
|
|
229
|
+
select: {
|
|
230
|
+
displayName: true,
|
|
231
|
+
profilePicture: true,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Create mentions
|
|
240
|
+
if (mentionedUserIds.length > 0) {
|
|
241
|
+
await tx.mention.createMany({
|
|
242
|
+
data: mentionedUserIds.map((mentionedUserId) => ({
|
|
243
|
+
messageId: message.id,
|
|
244
|
+
userId: mentionedUserId,
|
|
245
|
+
})),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Update newton chat timestamp
|
|
250
|
+
await tx.newtonChat.update({
|
|
251
|
+
where: { id: newtonChatId },
|
|
252
|
+
data: { updatedAt: new Date() },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return message;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Broadcast to Pusher channel (same format as regular chat)
|
|
259
|
+
try {
|
|
260
|
+
await pusher.trigger(`conversation-${newtonChat.conversationId}`, 'new-message', {
|
|
261
|
+
id: result.id,
|
|
262
|
+
content: result.content,
|
|
263
|
+
senderId: result.senderId,
|
|
264
|
+
conversationId: result.conversationId,
|
|
265
|
+
createdAt: result.createdAt,
|
|
266
|
+
sender: result.sender,
|
|
267
|
+
mentionedUserIds,
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error('Failed to broadcast newton chat message:', error);
|
|
271
|
+
// Don't fail the request if Pusher fails
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Generate AI response in parallel (don't await - fire and forget)
|
|
275
|
+
if (!isAIUser(userId)) {
|
|
276
|
+
// Run AI response generation in background
|
|
277
|
+
generateAndSendNewtonResponse(
|
|
278
|
+
newtonChatId,
|
|
279
|
+
content,
|
|
280
|
+
newtonChat.conversationId,
|
|
281
|
+
newtonChat.submission
|
|
282
|
+
).catch(error => {
|
|
283
|
+
logger.error('Failed to generate AI response:', { error });
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
id: result.id,
|
|
289
|
+
content: result.content,
|
|
290
|
+
senderId: result.senderId,
|
|
291
|
+
conversationId: result.conversationId,
|
|
292
|
+
createdAt: result.createdAt,
|
|
293
|
+
sender: result.sender,
|
|
294
|
+
mentionedUserIds,
|
|
295
|
+
};
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
|
package/src/routers/section.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
2
|
+
import { createTRPCRouter, protectedClassMemberProcedure, protectedProcedure, protectedTeacherProcedure } from "../trpc.js";
|
|
3
3
|
import { TRPCError } from "@trpc/server";
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
5
|
|
|
6
6
|
const createSectionSchema = z.object({
|
|
7
7
|
classId: z.string(),
|
|
8
|
+
id: z.string().optional(),
|
|
8
9
|
name: z.string(),
|
|
9
10
|
color: z.string().optional(),
|
|
10
11
|
});
|
|
@@ -22,7 +23,25 @@ const deleteSectionSchema = z.object({
|
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
export const sectionRouter = createTRPCRouter({
|
|
25
|
-
|
|
26
|
+
exists: protectedClassMemberProcedure
|
|
27
|
+
.input(z.object({
|
|
28
|
+
id: z.string(),
|
|
29
|
+
}))
|
|
30
|
+
.query(async ({ ctx, input }) => {
|
|
31
|
+
if (!ctx.user) {
|
|
32
|
+
throw new TRPCError({
|
|
33
|
+
code: "UNAUTHORIZED",
|
|
34
|
+
message: "User must be authenticated",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const section = await prisma.section.findUnique({
|
|
39
|
+
where: { id: input.id },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return section ? true : false;
|
|
43
|
+
}),
|
|
44
|
+
create: protectedTeacherProcedure
|
|
26
45
|
.input(createSectionSchema)
|
|
27
46
|
.mutation(async ({ ctx, input }) => {
|
|
28
47
|
if (!ctx.user) {
|
|
@@ -53,6 +72,7 @@ export const sectionRouter = createTRPCRouter({
|
|
|
53
72
|
|
|
54
73
|
const section = await prisma.section.create({
|
|
55
74
|
data: {
|
|
75
|
+
...(input.id && { id: input.id }),
|
|
56
76
|
name: input.name,
|
|
57
77
|
order: 0,
|
|
58
78
|
class: {
|
|
@@ -97,7 +117,7 @@ export const sectionRouter = createTRPCRouter({
|
|
|
97
117
|
return section;
|
|
98
118
|
}),
|
|
99
119
|
|
|
100
|
-
reorder:
|
|
120
|
+
reorder: protectedTeacherProcedure
|
|
101
121
|
.input(z.object({
|
|
102
122
|
classId: z.string(),
|
|
103
123
|
movedId: z.string(), // Section ID
|
|
@@ -195,7 +215,7 @@ export const sectionRouter = createTRPCRouter({
|
|
|
195
215
|
return result;
|
|
196
216
|
}),
|
|
197
217
|
|
|
198
|
-
update:
|
|
218
|
+
update: protectedTeacherProcedure
|
|
199
219
|
.input(updateSectionSchema)
|
|
200
220
|
.mutation(async ({ ctx, input }) => {
|
|
201
221
|
if (!ctx.user) {
|
|
@@ -237,7 +257,7 @@ export const sectionRouter = createTRPCRouter({
|
|
|
237
257
|
return section;
|
|
238
258
|
}),
|
|
239
259
|
|
|
240
|
-
reOrder:
|
|
260
|
+
reOrder: protectedTeacherProcedure
|
|
241
261
|
.input(z.object({
|
|
242
262
|
id: z.string(),
|
|
243
263
|
classId: z.string(),
|
|
@@ -308,7 +328,7 @@ export const sectionRouter = createTRPCRouter({
|
|
|
308
328
|
return { id: input.id };
|
|
309
329
|
}),
|
|
310
330
|
|
|
311
|
-
delete:
|
|
331
|
+
delete: protectedTeacherProcedure
|
|
312
332
|
.input(deleteSectionSchema)
|
|
313
333
|
.mutation(async ({ ctx, input }) => {
|
|
314
334
|
if (!ctx.user) {
|
package/src/routers/user.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createDirectUploadFiles, type DirectUploadFile } from "../lib/fileUploa
|
|
|
6
6
|
import { getSignedUrl } from "../lib/googleCloudStorage.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
8
|
import { bucket } from "../lib/googleCloudStorage.js";
|
|
9
|
+
import { env } from "../lib/config/env.js";
|
|
9
10
|
|
|
10
11
|
// Helper function to convert file path to backend proxy URL
|
|
11
12
|
function getFileUrl(filePath: string | null): string | null {
|
|
@@ -17,7 +18,7 @@ function getFileUrl(filePath: string | null): string | null {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
// Convert GCS path to full backend proxy URL
|
|
20
|
-
const backendUrl =
|
|
21
|
+
const backendUrl = env.BACKEND_URL || 'http://localhost:3001';
|
|
21
22
|
return `${backendUrl}/api/files/${encodeURIComponent(filePath)}`;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -229,7 +230,7 @@ export const userRouter = createTRPCRouter({
|
|
|
229
230
|
const filePath = `users/${ctx.user.id}/profile/${uniqueFilename}`;
|
|
230
231
|
|
|
231
232
|
// Generate backend proxy upload URL instead of direct GCS signed URL
|
|
232
|
-
const backendUrl =
|
|
233
|
+
const backendUrl = env.BACKEND_URL || 'http://localhost:3001';
|
|
233
234
|
const uploadUrl = `${backendUrl}/api/upload/${encodeURIComponent(filePath)}`;
|
|
234
235
|
|
|
235
236
|
logger.info('Generated upload URL', {
|
package/src/routers/worksheet.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TRPCError } from "@trpc/server";
|
|
2
|
-
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
2
|
+
import { createTRPCRouter, protectedClassMemberProcedure, protectedProcedure } from "../trpc.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
|
-
import { WorksheetQuestionType } from "@prisma/client";
|
|
5
|
+
import { GenerationStatus, WorksheetQuestionType } from "@prisma/client";
|
|
6
6
|
import { commentSelect } from "./comment.js";
|
|
7
7
|
|
|
8
8
|
type MCQOptions = {
|
|
@@ -38,6 +38,22 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
38
38
|
return worksheet;
|
|
39
39
|
}),
|
|
40
40
|
|
|
41
|
+
exists: protectedClassMemberProcedure
|
|
42
|
+
.input(z.object({
|
|
43
|
+
id: z.string(),
|
|
44
|
+
}))
|
|
45
|
+
.query(async ({ ctx, input }) => {
|
|
46
|
+
if (!ctx.user) {
|
|
47
|
+
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User must be authenticated' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const worksheet = await prisma.worksheet.findUnique({
|
|
51
|
+
where: { id: input.id },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return worksheet ? true : false;
|
|
55
|
+
}),
|
|
56
|
+
|
|
41
57
|
// List all worksheets for a class
|
|
42
58
|
listWorksheets: protectedProcedure
|
|
43
59
|
.input(z.object({
|
|
@@ -372,6 +388,7 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
372
388
|
data: { response,
|
|
373
389
|
...(isMarkableByAlgo && { isCorrect: response === correctAnswer }),
|
|
374
390
|
...(isMarkableByAlgo && { points: response === correctAnswer ? marksAwardedIfCorrect : 0 }),
|
|
391
|
+
status: GenerationStatus.NOT_STARTED,
|
|
375
392
|
},
|
|
376
393
|
});
|
|
377
394
|
} else {
|
|
@@ -398,52 +415,23 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
398
415
|
|
|
399
416
|
return updatedWorksheetResponse;
|
|
400
417
|
}),
|
|
401
|
-
|
|
418
|
+
cancelAutoGrading: protectedProcedure
|
|
402
419
|
.input(z.object({
|
|
403
420
|
worksheetResponseId: z.string(),
|
|
421
|
+
questionId: z.string(),
|
|
404
422
|
}))
|
|
405
423
|
.mutation(async ({ ctx, input }) => {
|
|
406
|
-
const { worksheetResponseId } = input;
|
|
424
|
+
const { worksheetResponseId, questionId } = input;
|
|
407
425
|
|
|
408
|
-
const
|
|
409
|
-
where: { id: worksheetResponseId },
|
|
410
|
-
include: {
|
|
411
|
-
worksheet: {
|
|
412
|
-
include: {
|
|
413
|
-
questions: true,
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
responses: true,
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
if (!worksheetResponse) {
|
|
421
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (worksheetResponse.submitted) {
|
|
425
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Worksheet already submitted' });
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Mark worksheet as submitted
|
|
429
|
-
const submittedWorksheet = await prisma.studentWorksheetResponse.update({
|
|
430
|
-
where: { id: worksheetResponseId },
|
|
426
|
+
const updatedQuestion = await prisma.studentQuestionProgress.update({
|
|
427
|
+
where: { id: questionId, studentWorksheetResponseId: worksheetResponseId },
|
|
431
428
|
data: {
|
|
432
|
-
|
|
433
|
-
submittedAt: new Date(),
|
|
434
|
-
},
|
|
435
|
-
include: {
|
|
436
|
-
responses: true,
|
|
429
|
+
status: GenerationStatus.CANCELLED,
|
|
437
430
|
},
|
|
438
431
|
});
|
|
439
432
|
|
|
440
|
-
|
|
441
|
-
// For now, we'll just mark all answers as pending review
|
|
442
|
-
// You could integrate with an AI service to auto-grade certain question types
|
|
443
|
-
|
|
444
|
-
return submittedWorksheet;
|
|
433
|
+
return updatedQuestion;
|
|
445
434
|
}),
|
|
446
|
-
|
|
447
435
|
// Grade a student's answer
|
|
448
436
|
gradeAnswer: protectedProcedure
|
|
449
437
|
.input(z.object({
|