@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.
Files changed (241) 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 +7 -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 +368 -108
  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 +17 -4
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/assignment.js +51 -19
  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 +2 -0
  126. package/dist/routers/comment.d.ts.map +1 -1
  127. package/dist/routers/conversation.d.ts +2 -0
  128. package/dist/routers/conversation.d.ts.map +1 -1
  129. package/dist/routers/conversation.js +46 -31
  130. package/dist/routers/conversation.js.map +1 -1
  131. package/dist/routers/file.d.ts.map +1 -1
  132. package/dist/routers/file.js +30 -7
  133. package/dist/routers/file.js.map +1 -1
  134. package/dist/routers/labChat.d.ts +2 -0
  135. package/dist/routers/labChat.d.ts.map +1 -1
  136. package/dist/routers/labChat.js +5 -322
  137. package/dist/routers/labChat.js.map +1 -1
  138. package/dist/routers/marketing.d.ts +1 -1
  139. package/dist/routers/message.d.ts +1 -0
  140. package/dist/routers/message.d.ts.map +1 -1
  141. package/dist/routers/message.js +3 -2
  142. package/dist/routers/message.js.map +1 -1
  143. package/dist/routers/newtonChat.d.ts +55 -0
  144. package/dist/routers/newtonChat.d.ts.map +1 -0
  145. package/dist/routers/newtonChat.js +262 -0
  146. package/dist/routers/newtonChat.js.map +1 -0
  147. package/dist/routers/notifications.d.ts +4 -4
  148. package/dist/routers/section.d.ts +19 -4
  149. package/dist/routers/section.d.ts.map +1 -1
  150. package/dist/routers/section.js +26 -8
  151. package/dist/routers/section.js.map +1 -1
  152. package/dist/routers/user.d.ts.map +1 -1
  153. package/dist/routers/user.js +5 -4
  154. package/dist/routers/user.js.map +1 -1
  155. package/dist/routers/worksheet.d.ts +44 -41
  156. package/dist/routers/worksheet.d.ts.map +1 -1
  157. package/dist/routers/worksheet.js +25 -34
  158. package/dist/routers/worksheet.js.map +1 -1
  159. package/dist/seedDatabase.d.ts +1 -1
  160. package/dist/seedDatabase.js +275 -284
  161. package/dist/seedDatabase.js.map +1 -1
  162. package/dist/server/pipelines/aiLabChat.d.ts +21 -0
  163. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  164. package/dist/server/pipelines/aiLabChat.js +456 -0
  165. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  166. package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
  167. package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
  168. package/dist/server/pipelines/aiNewtonChat.js +280 -0
  169. package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
  170. package/dist/server/pipelines/gradeWorksheet.d.ts +15 -0
  171. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  172. package/dist/server/pipelines/gradeWorksheet.js +139 -0
  173. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  174. package/dist/trpc.d.ts.map +1 -1
  175. package/dist/trpc.js +2 -2
  176. package/dist/trpc.js.map +1 -1
  177. package/dist/utils/email.d.ts +9 -1
  178. package/dist/utils/email.d.ts.map +1 -1
  179. package/dist/utils/email.js +20 -5
  180. package/dist/utils/email.js.map +1 -1
  181. package/dist/utils/inference.d.ts +5 -0
  182. package/dist/utils/inference.d.ts.map +1 -1
  183. package/dist/utils/inference.js +71 -7
  184. package/dist/utils/inference.js.map +1 -1
  185. package/dist/utils/logger.d.ts.map +1 -1
  186. package/dist/utils/logger.js +3 -3
  187. package/dist/utils/logger.js.map +1 -1
  188. package/docker-compose.yml +14 -0
  189. package/package.json +13 -4
  190. package/prisma/schema.prisma +34 -5
  191. package/scripts/test-pre-push.ts +14 -0
  192. package/src/index.ts +98 -54
  193. package/src/instrument.ts +13 -6
  194. package/src/lib/config/env.ts +126 -0
  195. package/src/lib/fileUpload.ts +3 -2
  196. package/src/lib/googleCloudStorage.ts +6 -6
  197. package/src/lib/jsonConversion.ts +12 -14
  198. package/src/lib/prisma.ts +23 -2
  199. package/src/lib/pusher.ts +6 -5
  200. package/src/middleware/auth.ts +5 -3
  201. package/src/middleware/security.ts +80 -0
  202. package/src/routers/_app.ts +2 -0
  203. package/src/routers/agenda.ts +10 -7
  204. package/src/routers/announcement.ts +4 -2
  205. package/src/routers/assignment.ts +74 -41
  206. package/src/routers/attendance.ts +2 -2
  207. package/src/routers/auth.ts +143 -14
  208. package/src/routers/class.ts +52 -3
  209. package/src/routers/conversation.ts +49 -29
  210. package/src/routers/file.ts +29 -5
  211. package/src/routers/labChat.ts +3 -367
  212. package/src/routers/message.ts +1 -1
  213. package/src/routers/newtonChat.ts +299 -0
  214. package/src/routers/section.ts +26 -6
  215. package/src/routers/user.ts +3 -2
  216. package/src/routers/worksheet.ts +26 -38
  217. package/src/seedDatabase.ts +290 -283
  218. package/src/server/pipelines/aiLabChat.ts +507 -0
  219. package/src/server/pipelines/aiNewtonChat.ts +338 -0
  220. package/src/server/pipelines/gradeWorksheet.ts +151 -0
  221. package/src/trpc.ts +2 -0
  222. package/src/utils/email.ts +30 -3
  223. package/src/utils/inference.ts +85 -5
  224. package/src/utils/logger.ts +2 -1
  225. package/tests/announcement.test.ts +164 -0
  226. package/tests/assignment.test.ts +296 -0
  227. package/tests/attendance.test.ts +168 -0
  228. package/tests/auth.test.ts +33 -10
  229. package/tests/class.test.ts +34 -9
  230. package/tests/event.test.ts +228 -0
  231. package/tests/section.test.ts +216 -0
  232. package/tests/setup.ts +70 -16
  233. package/tests/user.test.ts +158 -0
  234. package/vitest.config.ts +26 -0
  235. package/API_SPECIFICATION.md +0 -1597
  236. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  237. package/CHAT_API_SPEC.md +0 -579
  238. package/LAB_CHAT_API_SPEC.md +0 -518
  239. package/dist/routers/school.d.ts +0 -208
  240. package/dist/routers/school.d.ts.map +0 -1
  241. package/dist/routers/school.js +0 -483
@@ -148,51 +148,69 @@ export const conversationRouter = createTRPCRouter({
148
148
 
149
149
  // For DMs, check if conversation already exists
150
150
  if (type === 'DM') {
151
- const existingDM = await prisma.conversation.findFirst({
151
+ // Get the target user's ID from their username
152
+ const targetUser = await prisma.user.findFirst({
153
+ where: { username: memberIds[0] },
154
+ select: { id: true, username: true },
155
+ });
156
+
157
+ if (!targetUser) {
158
+ throw new TRPCError({
159
+ code: 'BAD_REQUEST',
160
+ message: `User "${memberIds[0]}" not found`,
161
+ });
162
+ }
163
+
164
+ // Find all DM conversations where current user is a member
165
+ const existingDMs = await prisma.conversation.findMany({
152
166
  where: {
153
167
  type: 'DM',
154
168
  members: {
155
- every: {
156
- userId: {
157
- in: [userId, memberIds[0]],
158
- },
159
- },
160
- },
161
- AND: {
162
- members: {
163
- some: {
164
- userId,
165
- },
169
+ some: {
170
+ userId,
166
171
  },
167
172
  },
168
173
  },
169
174
  include: {
170
175
  members: {
171
- include: {
172
- user: {
173
- select: {
174
- id: true,
175
- username: true,
176
- profile: {
177
- select: {
178
- displayName: true,
179
- profilePicture: true,
180
- },
181
- },
182
- },
183
- },
176
+ select: {
177
+ userId: true,
184
178
  },
185
179
  },
186
180
  },
187
181
  });
188
182
 
183
+ // Check if any of these conversations has exactly 2 members (current user + target user)
184
+ const existingDM = existingDMs.find(conv => {
185
+ const memberUserIds = conv.members.map(m => m.userId);
186
+ return memberUserIds.length === 2 &&
187
+ memberUserIds.includes(userId) &&
188
+ memberUserIds.includes(targetUser.id);
189
+ });
190
+
189
191
  if (existingDM) {
190
- return existingDM;
192
+ // Conversation already exists, throw error with friendly message
193
+ throw new TRPCError({
194
+ code: 'BAD_REQUEST',
195
+ message: `A conversation with ${targetUser.username} already exists`,
196
+ });
191
197
  }
192
198
  }
193
199
 
194
200
  // Verify all members exist
195
- const members = await prisma.user.findMany({
201
+ const membersWithIds = await prisma.user.findMany({
202
+ where: {
203
+ id: {
204
+ in: memberIds,
205
+ },
206
+ },
207
+ select: {
208
+ id: true,
209
+ username: true,
210
+ },
211
+ });
212
+
213
+ const membersWithUsernames = await prisma.user.findMany({
196
214
  where: {
197
215
  username: {
198
216
  in: memberIds,
@@ -204,6 +222,8 @@ export const conversationRouter = createTRPCRouter({
204
222
  },
205
223
  });
206
224
 
225
+ const members = [...membersWithIds, ...membersWithUsernames];
226
+
207
227
  if (members.length !== memberIds.length) {
208
228
  throw new TRPCError({
209
229
  code: 'BAD_REQUEST',
@@ -222,8 +242,8 @@ export const conversationRouter = createTRPCRouter({
222
242
  userId,
223
243
  role: type === 'GROUP' ? 'ADMIN' : 'MEMBER',
224
244
  },
225
- ...memberIds.map((memberId) => ({
226
- userId: members.find((member) => member.username === memberId)!.id,
245
+ ...members.map((member) => ({
246
+ userId: member.id,
227
247
  role: 'MEMBER' as const,
228
248
  })),
229
249
  ],
@@ -73,6 +73,16 @@ export const fileRouter = createTRPCRouter({
73
73
  }
74
74
  }
75
75
  }
76
+ },
77
+ announcement: {
78
+ include: {
79
+ class: {
80
+ include: {
81
+ students: true,
82
+ teachers: true
83
+ }
84
+ }
85
+ }
76
86
  }
77
87
  }
78
88
  });
@@ -92,18 +102,30 @@ export const fileRouter = createTRPCRouter({
92
102
  // Check if user is a teacher of the class
93
103
  if (file.assignment?.class) {
94
104
  classId = file.assignment.class.id;
95
- hasAccess = file.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
105
+ const isTeacher = file.assignment.class.teachers.some(teacher => teacher.id === userId);
106
+ const isStudent = file.assignment.class.students.some(student => student.id === userId);
107
+ logger.info(`Assignment file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}`);
108
+ hasAccess = isTeacher || isStudent;
109
+ }
110
+
111
+ // Check if user has access to announcement files (teachers or students in the class)
112
+ if ((file as any).announcement?.class) {
113
+ classId = (file as any).announcement.class.id;
114
+ const isTeacher = (file as any).announcement.class.teachers.some((teacher: any) => teacher.id === userId);
115
+ const isStudent = (file as any).announcement.class.students.some((student: any) => student.id === userId);
116
+ logger.info(`Announcement file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}`);
117
+ hasAccess = hasAccess || isTeacher || isStudent;
96
118
  }
97
119
 
98
120
  if (file.submission?.assignment?.classId) {
99
121
  classId = file.submission.assignment.classId;
100
- hasAccess = file.submission?.studentId === userId || false;
122
+ hasAccess = hasAccess || file.submission?.studentId === userId || false;
101
123
  if (!hasAccess) hasAccess = file.submission.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
102
124
  }
103
125
 
104
126
  if (file.annotations?.assignment?.classId) {
105
127
  classId = file.annotations?.assignment.classId;
106
- hasAccess = file.annotations?.studentId === userId || false;
128
+ hasAccess = hasAccess || file.annotations?.studentId === userId || false;
107
129
  if (!hasAccess) hasAccess = file.annotations.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
108
130
  }
109
131
 
@@ -114,8 +136,10 @@ export const fileRouter = createTRPCRouter({
114
136
 
115
137
  // Check if file is in a folder and user has access to the class
116
138
  if (file.folder?.class) {
117
- hasAccess = hasAccess || file.folder.class.teachers.some(teacher => teacher.id === userId);
118
- hasAccess = hasAccess || file.folder.class.students.some(student => student.id === userId);
139
+ const isTeacher = file.folder.class.teachers.some(teacher => teacher.id === userId);
140
+ const isStudent = file.folder.class.students.some(student => student.id === userId);
141
+ hasAccess = hasAccess || isTeacher;
142
+ hasAccess = hasAccess || isStudent;
119
143
  }
120
144
 
121
145
  if (!hasAccess) {
@@ -3,17 +3,8 @@ import { createTRPCRouter, protectedProcedure } from '../trpc.js';
3
3
  import { prisma } from '../lib/prisma.js';
4
4
  import { pusher } from '../lib/pusher.js';
5
5
  import { TRPCError } from '@trpc/server';
6
- import {
7
- inferenceClient,
8
- sendAIMessage,
9
- type LabChatContext
10
- } from '../utils/inference.js';
11
- import { logger } from '../utils/logger.js';
12
6
  import { isAIUser } from '../utils/aiUser.js';
13
- import { bucket } from '../lib/googleCloudStorage.js';
14
- import { createPdf } from "../lib/jsonConversion.js"
15
- import OpenAI from 'openai';
16
- import { v4 as uuidv4 } from "uuid";
7
+ import { generateAndSendLabIntroduction, generateAndSendLabResponse } from '../server/pipelines/aiLabChat.js';
17
8
 
18
9
  export const labChatRouter = createTRPCRouter({
19
10
  create: protectedProcedure
@@ -164,10 +155,7 @@ export const labChatRouter = createTRPCRouter({
164
155
  });
165
156
 
166
157
  // Generate AI introduction message in parallel (don't await - fire and forget)
167
- generateAndSendLabIntroduction(result.id, result.conversationId, context, classWithTeachers.subject || 'Lab').catch(error => {
168
- logger.error('Failed to generate AI introduction:', { error, labChatId: result.id });
169
- });
170
-
158
+ generateAndSendLabIntroduction(result.id, result.conversationId, context, classWithTeachers.subject || 'Lab');
171
159
  // Broadcast lab chat creation to class members
172
160
  try {
173
161
  await pusher.trigger(`class-${classId}`, 'lab-chat-created', {
@@ -557,9 +545,7 @@ export const labChatRouter = createTRPCRouter({
557
545
  // Generate AI response in parallel (don't await - fire and forget)
558
546
  if (!isAIUser(userId)) {
559
547
  // Run AI response generation in background
560
- generateAndSendLabResponse(labChatId, content, labChat.conversationId).catch(error => {
561
- logger.error('Failed to generate AI response:', { error });
562
- });
548
+ generateAndSendLabResponse(labChatId, content, labChat.conversationId)
563
549
  }
564
550
 
565
551
  return {
@@ -645,353 +631,3 @@ export const labChatRouter = createTRPCRouter({
645
631
  }),
646
632
  });
647
633
 
648
- /**
649
- * Generate and send AI introduction for lab chat
650
- * Uses the stored context directly from database
651
- */
652
- async function generateAndSendLabIntroduction(
653
- labChatId: string,
654
- conversationId: string,
655
- contextString: string,
656
- subject: string
657
- ): Promise<void> {
658
- try {
659
- // Enhance the stored context with clarifying question instructions
660
- const enhancedSystemPrompt = `${contextString}
661
-
662
- IMPORTANT INSTRUCTIONS:
663
- - You are helping teachers create course materials
664
- - Use the context information provided above (subject, topic, difficulty, objectives, etc.) as your foundation
665
- - Only ask clarifying questions about details NOT already specified in the context
666
- - Focus your questions on format preferences, specific requirements, or missing details needed to create the content
667
- - Only output final course materials when you have sufficient details beyond what's in the context
668
- - Do not use markdown formatting in your responses - use plain text only
669
- - When creating content, make it clear and well-structured without markdown`;
670
-
671
- const completion = await inferenceClient.chat.completions.create({
672
- model: 'command-a-03-2025',
673
- messages: [
674
- { role: 'system', content: enhancedSystemPrompt },
675
- {
676
- role: 'user',
677
- content: 'Please introduce yourself to the teaching team. Explain that you will help create course materials by first asking clarifying questions based on the context provided, and only output final content when you have enough information.'
678
- },
679
- ],
680
- max_tokens: 300,
681
- temperature: 0.8,
682
- });
683
-
684
- const response = completion.choices[0]?.message?.content;
685
-
686
- if (!response) {
687
- throw new Error('No response generated from inference API');
688
- }
689
-
690
- // Send AI introduction using centralized sender
691
- await sendAIMessage(response, conversationId, {
692
- subject,
693
- });
694
-
695
- logger.info('AI Introduction sent', { labChatId, conversationId });
696
-
697
- } catch (error) {
698
- logger.error('Failed to generate AI introduction:', { error, labChatId });
699
-
700
- // Send fallback introduction
701
- try {
702
- const fallbackIntro = `Hello teaching team! I'm your AI assistant for course material development. I will help you create educational content by first asking clarifying questions based on the provided context, then outputting final materials when I have sufficient information. I won't use markdown formatting in my responses. What would you like to work on?`;
703
-
704
- await sendAIMessage(fallbackIntro, conversationId, {
705
- subject,
706
- });
707
-
708
- logger.info('Fallback AI introduction sent', { labChatId });
709
-
710
- } catch (fallbackError) {
711
- logger.error('Failed to send fallback AI introduction:', { error: fallbackError, labChatId });
712
- }
713
- }
714
- }
715
-
716
- /**
717
- * Generate and send AI response to teacher message
718
- * Uses the stored context directly from database
719
- */
720
- async function generateAndSendLabResponse(
721
- labChatId: string,
722
- teacherMessage: string,
723
- conversationId: string
724
- ): Promise<void> {
725
- try {
726
- // Get lab context from database
727
- const fullLabChat = await prisma.labChat.findUnique({
728
- where: { id: labChatId },
729
- include: {
730
- class: {
731
- select: {
732
- name: true,
733
- subject: true,
734
- },
735
- },
736
- },
737
- });
738
-
739
- if (!fullLabChat) {
740
- throw new Error('Lab chat not found');
741
- }
742
-
743
- // Get recent conversation history
744
- const recentMessages = await prisma.message.findMany({
745
- where: {
746
- conversationId,
747
- },
748
- include: {
749
- sender: {
750
- select: {
751
- id: true,
752
- username: true,
753
- profile: {
754
- select: {
755
- displayName: true,
756
- },
757
- },
758
- },
759
- },
760
- },
761
- orderBy: {
762
- createdAt: 'desc',
763
- },
764
- take: 10, // Last 10 messages for context
765
- });
766
-
767
- // Build conversation history as proper message objects
768
- // Enhance the stored context with clarifying question instructions
769
- const enhancedSystemPrompt = `${fullLabChat.context}
770
-
771
- IMPORTANT INSTRUCTIONS:
772
- - Use the context information provided above (subject, topic, difficulty, objectives, etc.) as your foundation
773
- - Based on the teacher's input and existing context, only ask clarifying questions about details NOT already specified
774
- - Focus questions on format preferences, specific requirements, quantity, or missing implementation details
775
- - Only output final course materials when you have sufficient details beyond what's in the context
776
- - Do not use markdown formatting in your responses - use plain text only
777
- - When you do create content, make it clear and well-structured without markdown
778
- - If the request is vague, ask 1-2 specific clarifying questions about missing details only
779
- - You are primarily a chatbot - only provide files when it is necessary
780
-
781
- RESPONSE FORMAT:
782
- - Always respond with JSON in this format: { "text": string, "docs": null | array }
783
- - "text": Your conversational response (questions, explanations, etc.) - use plain text, no markdown
784
- - "docs": null for regular conversation, or array of PDF document objects when creating course materials
785
-
786
- WHEN CREATING COURSE MATERIALS (docs field):
787
- - docs: [ { "title": string, "blocks": [ { "format": <int 0-12>, "content": string | string[], "metadata"?: { fontSize?: number, lineHeight?: number, paragraphSpacing?: number, indentWidth?: number, paddingX?: number, paddingY?: number, font?: 0|1|2|3|4|5, color?: "#RGB"|"#RRGGBB", background?: "#RGB"|"#RRGGBB", align?: "left"|"center"|"right" } } ] } ]
788
- - Each document in the array should have a "title" (used for filename) and "blocks" (content)
789
- - You can create multiple documents when it makes sense (e.g., separate worksheets, answer keys, different topics)
790
- - Use descriptive titles like "Biology_Cell_Structure_Worksheet" or "Chemistry_Lab_Instructions"
791
- - Format enum (integers): 0=HEADER_1, 1=HEADER_2, 2=HEADER_3, 3=HEADER_4, 4=HEADER_5, 5=HEADER_6, 6=PARAGRAPH, 7=BULLET, 8=NUMBERED, 9=TABLE, 10=IMAGE, 11=CODE_BLOCK, 12=QUOTE
792
- - Fonts enum: 0=TIMES_ROMAN, 1=COURIER, 2=HELVETICA, 3=HELVETICA_BOLD, 4=HELVETICA_ITALIC, 5=HELVETICA_BOLD_ITALIC
793
- - Colors must be hex strings: "#RGB" or "#RRGGBB".
794
- - Headings (0-5): content is a single string; you may set metadata.align.
795
- - Paragraphs (6) and Quotes (12): content is a single string.
796
- - Bullets (7) and Numbered (8): content is an array of strings (one item per list entry). DO NOT include bullet symbols (*) or numbers (1. 2. 3.) in the content - the format will automatically add these.
797
- - Code blocks (11): prefer content as an array of lines; preserve indentation via leading tabs/spaces. If using a single string, include \n between lines.
798
- - Table (9) and Image (10) are not supported by the renderer now; do not emit them.
799
- - Use metadata sparingly; omit fields you don't need. For code blocks you may set metadata.paddingX, paddingY, background, and font (1 for Courier).
800
- - Wrap text naturally; do not insert manual line breaks except where semantically required (lists, code).
801
- - The JSON must be valid and ready for PDF rendering by the server.`;
802
-
803
- const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
804
- { role: 'system', content: enhancedSystemPrompt },
805
- ];
806
-
807
- // Add recent conversation history
808
- recentMessages.reverse().forEach(msg => {
809
- const role = isAIUser(msg.senderId) ? 'assistant' : 'user';
810
- const senderName = msg.sender?.profile?.displayName || msg.sender?.username || 'Teacher';
811
- const content = isAIUser(msg.senderId) ? msg.content : `${senderName}: ${msg.content}`;
812
-
813
- messages.push({
814
- role: role as 'user' | 'assistant',
815
- content,
816
- });
817
- });
818
-
819
- // Add the new teacher message
820
- const senderName = 'Teacher'; // We could get this from the actual sender if needed
821
- messages.push({
822
- role: 'user',
823
- content: `${senderName}: ${teacherMessage}`,
824
- });
825
-
826
-
827
- const completion = await inferenceClient.chat.completions.create({
828
- model: 'command-a-03-2025',
829
- messages,
830
- temperature: 0.7,
831
- response_format: {
832
- type: "json_object",
833
- // @ts-expect-error
834
- schema: {
835
- type: "object",
836
- properties: {
837
- text: { type: "string" },
838
- docs: {
839
- type: "array",
840
- items: {
841
- type: "object",
842
- properties: {
843
- title: { type: "string" },
844
- blocks: {
845
- type: "array",
846
- items: {
847
- type: "object",
848
- properties: {
849
- format: { type: "integer", minimum: 0, maximum: 12 },
850
- content: {
851
- oneOf: [
852
- { type: "string" },
853
- { type: "array", items: { type: "string" } }
854
- ]
855
- },
856
- metadata: {
857
- type: "object",
858
- properties: {
859
- fontSize: { type: "number", minimum: 6 },
860
- lineHeight: { type: "number", minimum: 0.6 },
861
- paragraphSpacing: { type: "number", minimum: 0 },
862
- indentWidth: { type: "number", minimum: 0 },
863
- paddingX: { type: "number", minimum: 0 },
864
- paddingY: { type: "number", minimum: 0 },
865
- font: { type: "integer", minimum: 0, maximum: 5 },
866
- color: { type: "string", pattern: "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" },
867
- background: { type: "string", pattern: "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" },
868
- align: { type: "string", enum: ["left", "center", "right"] }
869
- },
870
- additionalProperties: false
871
- }
872
- },
873
- required: ["format", "content"],
874
- additionalProperties: false
875
- }
876
- }
877
- },
878
- required: ["title", "blocks"],
879
- additionalProperties: false
880
- }
881
- }
882
- },
883
- required: ["text"],
884
- additionalProperties: false
885
- }
886
- },
887
- });
888
-
889
- const response = completion.choices[0]?.message?.content;
890
-
891
- if (!response) {
892
- throw new Error('No response generated from inference API');
893
- }
894
-
895
- // Parse the JSON response and generate PDF if docs are provided
896
- try {
897
- const jsonData = JSON.parse(response);
898
-
899
-
900
- const attachmentIds: string[] = [];
901
- // Generate PDFs if docs are provided
902
- if (jsonData.docs && Array.isArray(jsonData.docs)) {
903
-
904
-
905
- console.log('Generating PDFs', { labChatId, docs: JSON.stringify(jsonData.docs, null, 2) });
906
- for (let i = 0; i < jsonData.docs.length; i++) {
907
- const doc = jsonData.docs[i];
908
- if (!doc.title || !doc.blocks || !Array.isArray(doc.blocks)) {
909
- logger.error(`Document ${i + 1} is missing title or blocks`);
910
- continue;
911
- }
912
-
913
-
914
- try {
915
- let pdfBytes = await createPdf(doc.blocks);
916
- if (pdfBytes) {
917
- // Sanitize filename - remove special characters and limit length
918
- const sanitizedTitle = doc.title
919
- .replace(/[^a-zA-Z0-9\s\-_]/g, '')
920
- .replace(/\s+/g, '_')
921
- .substring(0, 50);
922
-
923
- const filename = `${sanitizedTitle}_${uuidv4().substring(0, 8)}.pdf`;
924
- const filePath = `class/generated/${fullLabChat.classId}/${filename}`;
925
-
926
- logger.info(`PDF ${i + 1} generated successfully`, { labChatId, title: doc.title });
927
-
928
- // Upload directly to Google Cloud Storage
929
- const gcsFile = bucket.file(filePath);
930
- await gcsFile.save(Buffer.from(pdfBytes), {
931
- metadata: {
932
- contentType: 'application/pdf',
933
- }
934
- });
935
-
936
- logger.info(`PDF ${i + 1} uploaded successfully`, { labChatId, filename });
937
-
938
- const file = await prisma.file.create({
939
- data: {
940
- name: filename,
941
- path: filePath,
942
- type: 'application/pdf',
943
- size: pdfBytes.length,
944
- userId: fullLabChat.createdById,
945
- uploadStatus: 'COMPLETED',
946
- uploadedAt: new Date(),
947
- },
948
- });
949
- attachmentIds.push(file.id);
950
- } else {
951
- logger.error(`PDF ${i + 1} creation returned undefined/null`, { labChatId, title: doc.title });
952
- }
953
- } catch (pdfError) {
954
- logger.error(`PDF creation threw an error for document ${i + 1}:`, {
955
- error: pdfError instanceof Error ? {
956
- message: pdfError.message,
957
- stack: pdfError.stack,
958
- name: pdfError.name
959
- } : pdfError,
960
- labChatId,
961
- title: doc.title
962
- });
963
- }
964
- }
965
- }
966
-
967
- // Send the text response to the conversation
968
- await sendAIMessage(jsonData.text || response, conversationId, {
969
- attachments: {
970
- connect: attachmentIds.map(id => ({ id })),
971
- },
972
- subject: fullLabChat.class?.subject || 'Lab',
973
- });
974
- } catch (parseError) {
975
- logger.error('Failed to parse AI response or generate PDF:', { error: parseError, labChatId });
976
- // Fallback: send the raw response if parsing fails
977
- await sendAIMessage(response, conversationId, {
978
- subject: fullLabChat.class?.subject || 'Lab',
979
- });
980
- }
981
-
982
- logger.info('AI response sent', { labChatId, conversationId });
983
-
984
- } catch (error) {
985
- console.error('Full error object:', error);
986
- logger.error('Failed to generate AI response:', {
987
- error: error instanceof Error ? {
988
- message: error.message,
989
- stack: error.stack,
990
- name: error.name
991
- } : error,
992
- labChatId
993
- });
994
- throw error; // Re-throw to see the full error in the calling function
995
- }
996
- }
997
-
@@ -103,6 +103,7 @@ export const messageRouter = createTRPCRouter({
103
103
  name: attachment.name,
104
104
  type: attachment.type,
105
105
  })),
106
+ meta: message.meta as Record<string, any>,
106
107
  mentions: message.mentions.map((mention) => ({
107
108
  user: mention.user,
108
109
  })),
@@ -111,7 +112,6 @@ export const messageRouter = createTRPCRouter({
111
112
  nextCursor,
112
113
  };
113
114
  }),
114
-
115
115
  send: protectedProcedure
116
116
  .input(
117
117
  z.object({