@studious-lms/server 1.2.44 → 1.2.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/.env.example +45 -0
  2. package/.env.test.example +37 -0
  3. package/README.md +34 -7
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/clover.xml +12110 -0
  7. package/coverage/coverage-final.json +44 -0
  8. package/coverage/favicon.png +0 -0
  9. package/coverage/index.html +221 -0
  10. package/coverage/prettify.css +1 -0
  11. package/coverage/prettify.js +2 -0
  12. package/coverage/server/index.html +116 -0
  13. package/coverage/server/src/exportType.ts.html +109 -0
  14. package/coverage/server/src/index.html +161 -0
  15. package/coverage/server/src/index.ts.html +1702 -0
  16. package/coverage/server/src/instrument.ts.html +130 -0
  17. package/coverage/server/src/lib/config/env.ts.html +448 -0
  18. package/coverage/server/src/lib/config/index.html +116 -0
  19. package/coverage/server/src/lib/fileUpload.ts.html +1138 -0
  20. package/coverage/server/src/lib/googleCloudStorage.ts.html +334 -0
  21. package/coverage/server/src/lib/index.html +206 -0
  22. package/coverage/server/src/lib/jsonConversion.ts.html +2323 -0
  23. package/coverage/server/src/lib/jsonStyles.ts.html +193 -0
  24. package/coverage/server/src/lib/notificationHandler.ts.html +193 -0
  25. package/coverage/server/src/lib/pusher.ts.html +121 -0
  26. package/coverage/server/src/lib/thumbnailGenerator.ts.html +592 -0
  27. package/coverage/server/src/middleware/auth.ts.html +646 -0
  28. package/coverage/server/src/middleware/index.html +146 -0
  29. package/coverage/server/src/middleware/logging.ts.html +244 -0
  30. package/coverage/server/src/middleware/security.ts.html +271 -0
  31. package/coverage/server/src/routers/_app.ts.html +232 -0
  32. package/coverage/server/src/routers/agenda.ts.html +319 -0
  33. package/coverage/server/src/routers/announcement.ts.html +3481 -0
  34. package/coverage/server/src/routers/assignment.ts.html +7633 -0
  35. package/coverage/server/src/routers/attendance.ts.html +1030 -0
  36. package/coverage/server/src/routers/auth.ts.html +1081 -0
  37. package/coverage/server/src/routers/class.ts.html +3535 -0
  38. package/coverage/server/src/routers/comment.ts.html +991 -0
  39. package/coverage/server/src/routers/conversation.ts.html +982 -0
  40. package/coverage/server/src/routers/event.ts.html +1609 -0
  41. package/coverage/server/src/routers/file.ts.html +1144 -0
  42. package/coverage/server/src/routers/folder.ts.html +2797 -0
  43. package/coverage/server/src/routers/index.html +386 -0
  44. package/coverage/server/src/routers/labChat.ts.html +3073 -0
  45. package/coverage/server/src/routers/marketing.ts.html +340 -0
  46. package/coverage/server/src/routers/message.ts.html +1912 -0
  47. package/coverage/server/src/routers/notifications.ts.html +364 -0
  48. package/coverage/server/src/routers/section.ts.html +1120 -0
  49. package/coverage/server/src/routers/user.ts.html +862 -0
  50. package/coverage/server/src/routers/worksheet.ts.html +1729 -0
  51. package/coverage/server/src/trpc.ts.html +397 -0
  52. package/coverage/server/src/types/index.html +116 -0
  53. package/coverage/server/src/types/trpc.ts.html +127 -0
  54. package/coverage/server/src/utils/aiUser.ts.html +280 -0
  55. package/coverage/server/src/utils/email.ts.html +121 -0
  56. package/coverage/server/src/utils/generateInviteCode.ts.html +106 -0
  57. package/coverage/server/src/utils/index.html +206 -0
  58. package/coverage/server/src/utils/inference.ts.html +709 -0
  59. package/coverage/server/src/utils/logger.ts.html +664 -0
  60. package/coverage/server/src/utils/prismaErrorHandler.ts.html +907 -0
  61. package/coverage/server/src/utils/prismaWrapper.ts.html +355 -0
  62. package/coverage/server/vitest.config.ts.html +196 -0
  63. package/coverage/sort-arrow-sprite.png +0 -0
  64. package/coverage/sorter.js +210 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +83 -52
  67. package/dist/index.js.map +1 -1
  68. package/dist/instrument.js +15 -8
  69. package/dist/instrument.js.map +1 -1
  70. package/dist/lib/config/env.d.ts +169 -0
  71. package/dist/lib/config/env.d.ts.map +1 -0
  72. package/dist/lib/config/env.js +115 -0
  73. package/dist/lib/config/env.js.map +1 -0
  74. package/dist/lib/fileUpload.d.ts.map +1 -1
  75. package/dist/lib/fileUpload.js +5 -4
  76. package/dist/lib/fileUpload.js.map +1 -1
  77. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  78. package/dist/lib/googleCloudStorage.js +7 -8
  79. package/dist/lib/googleCloudStorage.js.map +1 -1
  80. package/dist/lib/jsonConversion.d.ts.map +1 -1
  81. package/dist/lib/jsonConversion.js +14 -16
  82. package/dist/lib/jsonConversion.js.map +1 -1
  83. package/dist/lib/notificationHandler.d.ts +2 -2
  84. package/dist/lib/prisma.d.ts +2 -2
  85. package/dist/lib/prisma.d.ts.map +1 -1
  86. package/dist/lib/prisma.js +22 -3
  87. package/dist/lib/prisma.js.map +1 -1
  88. package/dist/lib/pusher.d.ts.map +1 -1
  89. package/dist/lib/pusher.js +8 -7
  90. package/dist/lib/pusher.js.map +1 -1
  91. package/dist/middleware/auth.d.ts.map +1 -1
  92. package/dist/middleware/auth.js +6 -5
  93. package/dist/middleware/auth.js.map +1 -1
  94. package/dist/middleware/security.d.ts +5 -0
  95. package/dist/middleware/security.d.ts.map +1 -0
  96. package/dist/middleware/security.js +77 -0
  97. package/dist/middleware/security.js.map +1 -0
  98. package/dist/routers/_app.d.ts +304 -98
  99. package/dist/routers/_app.d.ts.map +1 -1
  100. package/dist/routers/_app.js +4 -2
  101. package/dist/routers/_app.js.map +1 -1
  102. package/dist/routers/agenda.d.ts.map +1 -1
  103. package/dist/routers/agenda.js +12 -9
  104. package/dist/routers/agenda.js.map +1 -1
  105. package/dist/routers/announcement.d.ts +8 -0
  106. package/dist/routers/announcement.d.ts.map +1 -1
  107. package/dist/routers/announcement.js +6 -4
  108. package/dist/routers/announcement.js.map +1 -1
  109. package/dist/routers/assignment.d.ts +7 -4
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/assignment.js +35 -18
  112. package/dist/routers/assignment.js.map +1 -1
  113. package/dist/routers/attendance.d.ts +1 -0
  114. package/dist/routers/attendance.d.ts.map +1 -1
  115. package/dist/routers/attendance.js +4 -4
  116. package/dist/routers/attendance.js.map +1 -1
  117. package/dist/routers/auth.d.ts +20 -0
  118. package/dist/routers/auth.d.ts.map +1 -1
  119. package/dist/routers/auth.js +132 -15
  120. package/dist/routers/auth.js.map +1 -1
  121. package/dist/routers/class.d.ts +10 -0
  122. package/dist/routers/class.d.ts.map +1 -1
  123. package/dist/routers/class.js +49 -5
  124. package/dist/routers/class.js.map +1 -1
  125. package/dist/routers/comment.d.ts +7 -0
  126. package/dist/routers/comment.d.ts.map +1 -1
  127. package/dist/routers/comment.js +9 -2
  128. package/dist/routers/comment.js.map +1 -1
  129. package/dist/routers/conversation.d.ts +1 -0
  130. package/dist/routers/conversation.d.ts.map +1 -1
  131. package/dist/routers/conversation.js +46 -31
  132. package/dist/routers/conversation.js.map +1 -1
  133. package/dist/routers/file.d.ts.map +1 -1
  134. package/dist/routers/file.js +30 -7
  135. package/dist/routers/file.js.map +1 -1
  136. package/dist/routers/labChat.d.ts +1 -0
  137. package/dist/routers/labChat.d.ts.map +1 -1
  138. package/dist/routers/labChat.js +2 -3
  139. package/dist/routers/labChat.js.map +1 -1
  140. package/dist/routers/marketing.d.ts +1 -1
  141. package/dist/routers/newtonChat.d.ts +55 -0
  142. package/dist/routers/newtonChat.d.ts.map +1 -0
  143. package/dist/routers/newtonChat.js +438 -0
  144. package/dist/routers/newtonChat.js.map +1 -0
  145. package/dist/routers/notifications.d.ts +4 -4
  146. package/dist/routers/section.d.ts +9 -4
  147. package/dist/routers/section.d.ts.map +1 -1
  148. package/dist/routers/section.js +8 -8
  149. package/dist/routers/section.js.map +1 -1
  150. package/dist/routers/user.d.ts.map +1 -1
  151. package/dist/routers/user.js +5 -4
  152. package/dist/routers/user.js.map +1 -1
  153. package/dist/routers/worksheet.d.ts +30 -36
  154. package/dist/routers/worksheet.d.ts.map +1 -1
  155. package/dist/routers/worksheet.js +11 -33
  156. package/dist/routers/worksheet.js.map +1 -1
  157. package/dist/seedDatabase.d.ts +1 -1
  158. package/dist/seedDatabase.js +275 -284
  159. package/dist/seedDatabase.js.map +1 -1
  160. package/dist/server/pipelines/aiLabChat.d.ts +10 -0
  161. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  162. package/dist/server/pipelines/aiLabChat.js +83 -0
  163. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  164. package/dist/server/pipelines/gradeWorksheet.d.ts +2 -0
  165. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  166. package/dist/server/pipelines/gradeWorksheet.js +138 -0
  167. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  168. package/dist/trpc.d.ts.map +1 -1
  169. package/dist/trpc.js +2 -2
  170. package/dist/trpc.js.map +1 -1
  171. package/dist/utils/email.d.ts +9 -1
  172. package/dist/utils/email.d.ts.map +1 -1
  173. package/dist/utils/email.js +20 -5
  174. package/dist/utils/email.js.map +1 -1
  175. package/dist/utils/inference.d.ts +3 -0
  176. package/dist/utils/inference.d.ts.map +1 -1
  177. package/dist/utils/inference.js +41 -7
  178. package/dist/utils/inference.js.map +1 -1
  179. package/dist/utils/logger.d.ts.map +1 -1
  180. package/dist/utils/logger.js +3 -3
  181. package/dist/utils/logger.js.map +1 -1
  182. package/docker-compose.yml +14 -0
  183. package/package.json +13 -4
  184. package/prisma/schema.prisma +32 -5
  185. package/scripts/test-pre-push.ts +14 -0
  186. package/src/index.ts +98 -54
  187. package/src/instrument.ts +13 -6
  188. package/src/lib/config/env.ts +126 -0
  189. package/src/lib/fileUpload.ts +3 -2
  190. package/src/lib/googleCloudStorage.ts +6 -6
  191. package/src/lib/jsonConversion.ts +12 -14
  192. package/src/lib/prisma.ts +23 -2
  193. package/src/lib/pusher.ts +6 -5
  194. package/src/middleware/auth.ts +4 -3
  195. package/src/middleware/security.ts +80 -0
  196. package/src/routers/_app.ts +2 -0
  197. package/src/routers/agenda.ts +10 -7
  198. package/src/routers/announcement.ts +4 -2
  199. package/src/routers/assignment.ts +58 -40
  200. package/src/routers/attendance.ts +2 -2
  201. package/src/routers/auth.ts +143 -14
  202. package/src/routers/class.ts +52 -3
  203. package/src/routers/comment.ts +7 -0
  204. package/src/routers/conversation.ts +49 -29
  205. package/src/routers/file.ts +29 -5
  206. package/src/routers/labChat.ts +0 -1
  207. package/src/routers/newtonChat.ts +520 -0
  208. package/src/routers/section.ts +6 -6
  209. package/src/routers/user.ts +3 -2
  210. package/src/routers/worksheet.ts +9 -37
  211. package/src/seedDatabase.ts +290 -283
  212. package/src/server/pipelines/aiLabChat.ts +92 -0
  213. package/src/server/pipelines/gradeWorksheet.ts +152 -0
  214. package/src/trpc.ts +2 -0
  215. package/src/utils/email.ts +30 -3
  216. package/src/utils/inference.ts +50 -5
  217. package/src/utils/logger.ts +2 -1
  218. package/tests/announcement.test.ts +164 -0
  219. package/tests/assignment.test.ts +296 -0
  220. package/tests/attendance.test.ts +168 -0
  221. package/tests/auth.test.ts +33 -10
  222. package/tests/class.test.ts +34 -9
  223. package/tests/event.test.ts +228 -0
  224. package/tests/section.test.ts +216 -0
  225. package/tests/setup.ts +70 -16
  226. package/tests/user.test.ts +158 -0
  227. package/vitest.config.ts +26 -0
  228. package/API_SPECIFICATION.md +0 -1597
  229. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  230. package/CHAT_API_SPEC.md +0 -579
  231. package/LAB_CHAT_API_SPEC.md +0 -518
  232. package/dist/routers/school.d.ts +0 -208
  233. package/dist/routers/school.d.ts.map +0 -1
  234. package/dist/routers/school.js +0 -483
@@ -0,0 +1,520 @@
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 {
7
+ inferenceClient,
8
+ openAIClient,
9
+ sendAIMessage,
10
+ } from '../utils/inference.js';
11
+ import { logger } from '../utils/logger.js';
12
+ import { isAIUser } from '../utils/aiUser.js';
13
+
14
+ export const newtonChatRouter = createTRPCRouter({
15
+ getTutorConversation: protectedProcedure
16
+ .input(
17
+ z.object({
18
+ assignmentId: z.string(),
19
+ classId: z.string(),
20
+ })
21
+ )
22
+ .query(async ({ input, ctx }) => {
23
+ const userId = ctx.user!.id;
24
+ const { assignmentId, classId } = input;
25
+
26
+ // Verify user is a student in the class
27
+ const classMembership = await prisma.class.findFirst({
28
+ where: {
29
+ id: classId,
30
+ students: {
31
+ some: {
32
+ id: userId,
33
+ },
34
+ },
35
+ },
36
+ });
37
+
38
+ if (!classMembership) {
39
+ throw new TRPCError({
40
+ code: 'FORBIDDEN',
41
+ message: 'Not a student in this class',
42
+ });
43
+ }
44
+
45
+ // Find or create submission for this student and assignment
46
+ const submission = await prisma.submission.findFirst({
47
+ where: {
48
+ assignmentId,
49
+ studentId: userId,
50
+ },
51
+ });
52
+
53
+ if (!submission) {
54
+ throw new TRPCError({
55
+ code: 'NOT_FOUND',
56
+ message: 'Submission not found. Please create a submission first.',
57
+ });
58
+ }
59
+
60
+ // Find the latest NewtonChat for this submission, or create a new one
61
+ const result = await prisma.$transaction(async (tx) => {
62
+ // Get the latest NewtonChat for this submission
63
+ const existingNewtonChat = await tx.newtonChat.findFirst({
64
+ where: {
65
+ submissionId: submission.id,
66
+ },
67
+ include: {
68
+ conversation: {
69
+ include: {
70
+ members: {
71
+ where: {
72
+ userId,
73
+ },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ orderBy: {
79
+ createdAt: 'desc',
80
+ },
81
+ });
82
+
83
+ // If exists and user is already a member, return it
84
+ if (existingNewtonChat && existingNewtonChat.conversation.members.length > 0) {
85
+ return existingNewtonChat;
86
+ }
87
+
88
+ // If exists but user is not a member, add them
89
+ if (existingNewtonChat) {
90
+ await tx.conversationMember.create({
91
+ data: {
92
+ userId,
93
+ conversationId: existingNewtonChat.conversationId,
94
+ role: 'MEMBER',
95
+ },
96
+ });
97
+
98
+ return existingNewtonChat;
99
+ }
100
+
101
+ // Create new NewtonChat with associated conversation
102
+ const conversation = await tx.conversation.create({
103
+ data: {
104
+ type: 'DM',
105
+ name: 'Session with Newton Tutor',
106
+ displayInChat: false, // Newton chats don't show in regular chat list
107
+ },
108
+ });
109
+
110
+ // Add student to the conversation
111
+ await tx.conversationMember.create({
112
+ data: {
113
+ userId,
114
+ conversationId: conversation.id,
115
+ role: 'MEMBER',
116
+ },
117
+ });
118
+
119
+ // Create the NewtonChat
120
+ const newtonChat = await tx.newtonChat.create({
121
+ data: {
122
+ submissionId: submission.id,
123
+ conversationId: conversation.id,
124
+ title: 'Session with Newton Tutor',
125
+ },
126
+ });
127
+
128
+ return newtonChat;
129
+ });
130
+
131
+ // Generate AI introduction message in parallel (don't await - fire and forget)
132
+ generateAndSendNewtonIntroduction(
133
+ result.id,
134
+ result.conversationId,
135
+ submission.id
136
+ ).catch(error => {
137
+ logger.error('Failed to generate AI introduction:', { error, newtonChatId: result.id });
138
+ });
139
+
140
+ return {
141
+ conversationId: result.conversationId,
142
+ newtonChatId: result.id,
143
+ };
144
+ }),
145
+
146
+ postToNewtonChat: protectedProcedure
147
+ .input(
148
+ z.object({
149
+ newtonChatId: z.string(),
150
+ content: z.string().min(1).max(4000),
151
+ mentionedUserIds: z.array(z.string()).optional(),
152
+ })
153
+ )
154
+ .mutation(async ({ input, ctx }) => {
155
+ const userId = ctx.user!.id;
156
+ const { newtonChatId, content, mentionedUserIds = [] } = input;
157
+
158
+ // Get newton chat and verify user is a member
159
+ const newtonChat = await prisma.newtonChat.findFirst({
160
+ where: {
161
+ id: newtonChatId,
162
+ conversation: {
163
+ members: {
164
+ some: {
165
+ userId,
166
+ },
167
+ },
168
+ },
169
+ },
170
+ include: {
171
+ conversation: {
172
+ select: {
173
+ id: true,
174
+ },
175
+ },
176
+ submission: {
177
+ include: {
178
+ assignment: {
179
+ select: {
180
+ id: true,
181
+ title: true,
182
+ instructions: true,
183
+ class: {
184
+ select: {
185
+ subject: true,
186
+ },
187
+ },
188
+ },
189
+ },
190
+ },
191
+ },
192
+ },
193
+ });
194
+
195
+ if (!newtonChat) {
196
+ throw new TRPCError({
197
+ code: 'FORBIDDEN',
198
+ message: 'Newton chat not found or access denied',
199
+ });
200
+ }
201
+
202
+ // Verify mentioned users are members of the conversation
203
+ if (mentionedUserIds.length > 0) {
204
+ const mentionedMemberships = await prisma.conversationMember.findMany({
205
+ where: {
206
+ conversationId: newtonChat.conversationId,
207
+ userId: { in: mentionedUserIds },
208
+ },
209
+ });
210
+
211
+ if (mentionedMemberships.length !== mentionedUserIds.length) {
212
+ throw new TRPCError({
213
+ code: 'BAD_REQUEST',
214
+ message: 'Some mentioned users are not members of this conversation',
215
+ });
216
+ }
217
+ }
218
+
219
+ // Create message and mentions
220
+ const result = await prisma.$transaction(async (tx) => {
221
+ const message = await tx.message.create({
222
+ data: {
223
+ content,
224
+ senderId: userId,
225
+ conversationId: newtonChat.conversationId,
226
+ },
227
+ include: {
228
+ sender: {
229
+ select: {
230
+ id: true,
231
+ username: true,
232
+ profile: {
233
+ select: {
234
+ displayName: true,
235
+ profilePicture: true,
236
+ },
237
+ },
238
+ },
239
+ },
240
+ },
241
+ });
242
+
243
+ // Create mentions
244
+ if (mentionedUserIds.length > 0) {
245
+ await tx.mention.createMany({
246
+ data: mentionedUserIds.map((mentionedUserId) => ({
247
+ messageId: message.id,
248
+ userId: mentionedUserId,
249
+ })),
250
+ });
251
+ }
252
+
253
+ // Update newton chat timestamp
254
+ await tx.newtonChat.update({
255
+ where: { id: newtonChatId },
256
+ data: { updatedAt: new Date() },
257
+ });
258
+
259
+ return message;
260
+ });
261
+
262
+ // Broadcast to Pusher channel (same format as regular chat)
263
+ try {
264
+ await pusher.trigger(`conversation-${newtonChat.conversationId}`, 'new-message', {
265
+ id: result.id,
266
+ content: result.content,
267
+ senderId: result.senderId,
268
+ conversationId: result.conversationId,
269
+ createdAt: result.createdAt,
270
+ sender: result.sender,
271
+ mentionedUserIds,
272
+ });
273
+ } catch (error) {
274
+ console.error('Failed to broadcast newton chat message:', error);
275
+ // Don't fail the request if Pusher fails
276
+ }
277
+
278
+ // Generate AI response in parallel (don't await - fire and forget)
279
+ if (!isAIUser(userId)) {
280
+ // Run AI response generation in background
281
+ generateAndSendNewtonResponse(
282
+ newtonChatId,
283
+ content,
284
+ newtonChat.conversationId,
285
+ newtonChat.submission
286
+ ).catch(error => {
287
+ logger.error('Failed to generate AI response:', { error });
288
+ });
289
+ }
290
+
291
+ return {
292
+ id: result.id,
293
+ content: result.content,
294
+ senderId: result.senderId,
295
+ conversationId: result.conversationId,
296
+ createdAt: result.createdAt,
297
+ sender: result.sender,
298
+ mentionedUserIds,
299
+ };
300
+ }),
301
+ });
302
+
303
+ /**
304
+ * Generate and send AI introduction for Newton chat
305
+ */
306
+ async function generateAndSendNewtonIntroduction(
307
+ newtonChatId: string,
308
+ conversationId: string,
309
+ submissionId: string
310
+ ): Promise<void> {
311
+ try {
312
+ // Get submission details for context
313
+ const submission = await prisma.submission.findUnique({
314
+ where: { id: submissionId },
315
+ include: {
316
+ assignment: {
317
+ select: {
318
+ title: true,
319
+ instructions: true,
320
+ class: {
321
+ select: {
322
+ subject: true,
323
+ name: true,
324
+ },
325
+ },
326
+ },
327
+ },
328
+ attachments: {
329
+ select: {
330
+ id: true,
331
+ name: true,
332
+ type: true,
333
+ },
334
+ },
335
+ },
336
+ });
337
+
338
+ if (!submission) {
339
+ throw new Error('Submission not found');
340
+ }
341
+
342
+ const systemPrompt = `You are Newton, an AI tutor helping a student with their assignment submission.
343
+
344
+ Assignment: ${submission.assignment.title}
345
+ Subject: ${submission.assignment.class.subject}
346
+ Instructions: ${submission.assignment.instructions || 'No specific instructions provided'}
347
+
348
+ Your role:
349
+ - Help the student understand concepts related to their assignment
350
+ - Provide guidance and explanations without giving away direct answers
351
+ - Encourage learning and critical thinking
352
+ - Be supportive and encouraging
353
+ - Use clear, educational language appropriate for the subject
354
+
355
+ Do not use markdown formatting in your responses - use plain text only.`;
356
+
357
+ const completion = await inferenceClient.chat.completions.create({
358
+ model: 'command-a-03-2025',
359
+ messages: [
360
+ { role: 'system', content: systemPrompt },
361
+ {
362
+ role: 'user',
363
+ content: 'Please introduce yourself to the student. Explain that you are Newton, their AI tutor, and you are here to help them with their assignment. Ask them what they would like help with.'
364
+ },
365
+ ],
366
+ max_tokens: 300,
367
+ temperature: 0.8,
368
+ });
369
+
370
+ const response = completion.choices[0]?.message?.content;
371
+
372
+ if (!response) {
373
+ throw new Error('No response generated from inference API');
374
+ }
375
+
376
+ // Send AI introduction using centralized sender
377
+ await sendAIMessage(response, conversationId, {
378
+ subject: submission.assignment.class.subject || 'Assignment',
379
+ });
380
+
381
+ logger.info('AI Introduction sent', { newtonChatId, conversationId });
382
+
383
+ } catch (error) {
384
+ logger.error('Failed to generate AI introduction:', { error, newtonChatId });
385
+
386
+ // Send fallback introduction
387
+ try {
388
+ const fallbackIntro = `Hello! I'm Newton, your AI tutor. I'm here to help you with your assignment. I can answer questions, explain concepts, and guide you through your work. What would you like help with today?`;
389
+
390
+ await sendAIMessage(fallbackIntro, conversationId, {
391
+ subject: 'Assignment',
392
+ });
393
+
394
+ logger.info('Fallback AI introduction sent', { newtonChatId });
395
+
396
+ } catch (fallbackError) {
397
+ logger.error('Failed to send fallback AI introduction:', { error: fallbackError, newtonChatId });
398
+ }
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Generate and send AI response to student message
404
+ */
405
+ async function generateAndSendNewtonResponse(
406
+ newtonChatId: string,
407
+ studentMessage: string,
408
+ conversationId: string,
409
+ submission: {
410
+ id: string;
411
+ assignment: {
412
+ id: string;
413
+ title: string;
414
+ instructions: string | null;
415
+ class: {
416
+ subject: string | null;
417
+ };
418
+ };
419
+ }
420
+ ): Promise<void> {
421
+ try {
422
+ // Get recent conversation history
423
+ const recentMessages = await prisma.message.findMany({
424
+ where: {
425
+ conversationId,
426
+ },
427
+ include: {
428
+ sender: {
429
+ select: {
430
+ id: true,
431
+ username: true,
432
+ profile: {
433
+ select: {
434
+ displayName: true,
435
+ },
436
+ },
437
+ },
438
+ },
439
+ },
440
+ orderBy: {
441
+ createdAt: 'desc',
442
+ },
443
+ take: 10, // Last 10 messages for context
444
+ });
445
+
446
+ const systemPrompt = `You are Newton, an AI tutor helping a student with their assignment submission.
447
+
448
+ Assignment: ${submission.assignment.title}
449
+ Subject: ${submission.assignment.class.subject || 'General'}
450
+ Instructions: ${submission.assignment.instructions || 'No specific instructions provided'}
451
+
452
+ Your role:
453
+ - Help the student understand concepts related to their assignment
454
+ - Provide guidance and explanations without giving away direct answers
455
+ - Encourage learning and critical thinking
456
+ - Be supportive and encouraging
457
+ - Use clear, educational language appropriate for the subject
458
+ - If the student asks for direct answers, guide them to think through the problem instead
459
+ - Break down complex concepts into simpler parts
460
+ - Use examples and analogies when helpful
461
+
462
+ IMPORTANT:
463
+ - Do not use markdown formatting in your responses - use plain text only
464
+ - Keep responses conversational and educational
465
+ - Focus on helping the student learn, not just completing the assignment`;
466
+
467
+ const messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }> = [
468
+ { role: 'system', content: systemPrompt },
469
+ ];
470
+
471
+ // Add recent conversation history
472
+ recentMessages.reverse().forEach(msg => {
473
+ const role = isAIUser(msg.senderId) ? 'assistant' : 'user';
474
+ const senderName = msg.sender?.profile?.displayName || msg.sender?.username || 'Student';
475
+ const content = isAIUser(msg.senderId) ? msg.content : `${senderName}: ${msg.content}`;
476
+
477
+ messages.push({
478
+ role: role as 'user' | 'assistant',
479
+ content,
480
+ });
481
+ });
482
+
483
+ // Add the new student message
484
+ messages.push({
485
+ role: 'user',
486
+ content: `Student: ${studentMessage}`,
487
+ });
488
+
489
+ const completion = await openAIClient.chat.completions.create({
490
+ model: 'gpt-5-nano',
491
+ messages,
492
+ temperature: 0.7,
493
+ });
494
+
495
+ const response = completion.choices[0]?.message?.content;
496
+
497
+ if (!response) {
498
+ throw new Error('No response generated from inference API');
499
+ }
500
+
501
+ // Send the text response to the conversation
502
+ await sendAIMessage(response, conversationId, {
503
+ subject: submission.assignment.class.subject || 'Assignment',
504
+ });
505
+
506
+ logger.info('AI response sent', { newtonChatId, conversationId });
507
+
508
+ } catch (error) {
509
+ logger.error('Failed to generate AI response:', {
510
+ error: error instanceof Error ? {
511
+ message: error.message,
512
+ stack: error.stack,
513
+ name: error.name
514
+ } : error,
515
+ newtonChatId
516
+ });
517
+ }
518
+ }
519
+
520
+
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { createTRPCRouter, protectedProcedure } from "../trpc.js";
2
+ import { createTRPCRouter, protectedProcedure, protectedTeacherProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
5
 
@@ -22,7 +22,7 @@ const deleteSectionSchema = z.object({
22
22
  });
23
23
 
24
24
  export const sectionRouter = createTRPCRouter({
25
- create: protectedProcedure
25
+ create: protectedTeacherProcedure
26
26
  .input(createSectionSchema)
27
27
  .mutation(async ({ ctx, input }) => {
28
28
  if (!ctx.user) {
@@ -97,7 +97,7 @@ export const sectionRouter = createTRPCRouter({
97
97
  return section;
98
98
  }),
99
99
 
100
- reorder: protectedProcedure
100
+ reorder: protectedTeacherProcedure
101
101
  .input(z.object({
102
102
  classId: z.string(),
103
103
  movedId: z.string(), // Section ID
@@ -195,7 +195,7 @@ export const sectionRouter = createTRPCRouter({
195
195
  return result;
196
196
  }),
197
197
 
198
- update: protectedProcedure
198
+ update: protectedTeacherProcedure
199
199
  .input(updateSectionSchema)
200
200
  .mutation(async ({ ctx, input }) => {
201
201
  if (!ctx.user) {
@@ -237,7 +237,7 @@ export const sectionRouter = createTRPCRouter({
237
237
  return section;
238
238
  }),
239
239
 
240
- reOrder: protectedProcedure
240
+ reOrder: protectedTeacherProcedure
241
241
  .input(z.object({
242
242
  id: z.string(),
243
243
  classId: z.string(),
@@ -308,7 +308,7 @@ export const sectionRouter = createTRPCRouter({
308
308
  return { id: input.id };
309
309
  }),
310
310
 
311
- delete: protectedProcedure
311
+ delete: protectedTeacherProcedure
312
312
  .input(deleteSectionSchema)
313
313
  .mutation(async ({ ctx, input }) => {
314
314
  if (!ctx.user) {
@@ -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 = process.env.BACKEND_URL || 'http://localhost:3001';
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 = process.env.BACKEND_URL || 'http://localhost:3001';
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', {
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
2
2
  import { createTRPCRouter, 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 = {
@@ -372,6 +372,7 @@ export const worksheetRouter = createTRPCRouter({
372
372
  data: { response,
373
373
  ...(isMarkableByAlgo && { isCorrect: response === correctAnswer }),
374
374
  ...(isMarkableByAlgo && { points: response === correctAnswer ? marksAwardedIfCorrect : 0 }),
375
+ status: GenerationStatus.NOT_STARTED,
375
376
  },
376
377
  });
377
378
  } else {
@@ -398,52 +399,23 @@ export const worksheetRouter = createTRPCRouter({
398
399
 
399
400
  return updatedWorksheetResponse;
400
401
  }),
401
- submitWorksheet: protectedProcedure
402
+ cancelAutoGrading: protectedProcedure
402
403
  .input(z.object({
403
404
  worksheetResponseId: z.string(),
405
+ questionId: z.string(),
404
406
  }))
405
407
  .mutation(async ({ ctx, input }) => {
406
- const { worksheetResponseId } = input;
407
-
408
- const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
409
- where: { id: worksheetResponseId },
410
- include: {
411
- worksheet: {
412
- include: {
413
- questions: true,
414
- },
415
- },
416
- responses: true,
417
- },
418
- });
408
+ const { worksheetResponseId, questionId } = input;
419
409
 
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 },
410
+ const updatedQuestion = await prisma.studentQuestionProgress.update({
411
+ where: { id: questionId, studentWorksheetResponseId: worksheetResponseId },
431
412
  data: {
432
- submitted: true,
433
- submittedAt: new Date(),
434
- },
435
- include: {
436
- responses: true,
413
+ status: GenerationStatus.CANCELLED,
437
414
  },
438
415
  });
439
416
 
440
- // TODO: Implement AI grading here
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;
417
+ return updatedQuestion;
445
418
  }),
446
-
447
419
  // Grade a student's answer
448
420
  gradeAnswer: protectedProcedure
449
421
  .input(z.object({