@studious-lms/server 1.12.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/.dockerignore +28 -0
  2. package/.env.example +5 -0
  3. package/.github/workflows/deploy.yml +123 -0
  4. package/Dockerfile +54 -0
  5. package/dist/index.js +5 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/config/cors.d.ts.map +1 -1
  8. package/dist/lib/config/cors.js +31 -11
  9. package/dist/lib/config/cors.js.map +1 -1
  10. package/dist/lib/config/env.d.ts +17 -0
  11. package/dist/lib/config/env.d.ts.map +1 -1
  12. package/dist/lib/config/env.js +8 -2
  13. package/dist/lib/config/env.js.map +1 -1
  14. package/dist/lib/googleCloudStorage.d.ts +10 -0
  15. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  16. package/dist/lib/googleCloudStorage.js +22 -2
  17. package/dist/lib/googleCloudStorage.js.map +1 -1
  18. package/dist/lib/prisma.d.ts +104 -5
  19. package/dist/lib/prisma.d.ts.map +1 -1
  20. package/dist/lib/prisma.js +33 -2
  21. package/dist/lib/prisma.js.map +1 -1
  22. package/dist/models/agenda.d.ts +20 -20
  23. package/dist/models/announcement.d.ts +177 -69
  24. package/dist/models/announcement.d.ts.map +1 -1
  25. package/dist/models/assignment.d.ts +551 -353
  26. package/dist/models/assignment.d.ts.map +1 -1
  27. package/dist/models/attendance.d.ts +197 -71
  28. package/dist/models/attendance.d.ts.map +1 -1
  29. package/dist/models/auth.d.ts +348 -114
  30. package/dist/models/auth.d.ts.map +1 -1
  31. package/dist/models/class.d.ts +324 -188
  32. package/dist/models/class.d.ts.map +1 -1
  33. package/dist/models/class.js +9 -3
  34. package/dist/models/class.js.map +1 -1
  35. package/dist/models/cloudStorage.d.ts +146 -56
  36. package/dist/models/cloudStorage.d.ts.map +1 -1
  37. package/dist/models/comment.d.ts +269 -110
  38. package/dist/models/comment.d.ts.map +1 -1
  39. package/dist/models/comment.js +3 -2
  40. package/dist/models/comment.js.map +1 -1
  41. package/dist/models/conversation.d.ts +206 -63
  42. package/dist/models/conversation.d.ts.map +1 -1
  43. package/dist/models/conversation.js +13 -2
  44. package/dist/models/conversation.js.map +1 -1
  45. package/dist/models/department.d.ts +196 -70
  46. package/dist/models/department.d.ts.map +1 -1
  47. package/dist/models/event.d.ts +287 -125
  48. package/dist/models/event.d.ts.map +1 -1
  49. package/dist/models/file.d.ts +389 -263
  50. package/dist/models/file.d.ts.map +1 -1
  51. package/dist/models/folder.d.ts +433 -181
  52. package/dist/models/folder.d.ts.map +1 -1
  53. package/dist/models/labChat.d.ts +209 -83
  54. package/dist/models/labChat.d.ts.map +1 -1
  55. package/dist/models/marketing.d.ts +80 -26
  56. package/dist/models/marketing.d.ts.map +1 -1
  57. package/dist/models/message.d.ts +95 -41
  58. package/dist/models/message.d.ts.map +1 -1
  59. package/dist/models/newtonChat.d.ts +83 -29
  60. package/dist/models/newtonChat.d.ts.map +1 -1
  61. package/dist/models/notification.d.ts +61 -25
  62. package/dist/models/notification.d.ts.map +1 -1
  63. package/dist/models/school.d.ts +349 -133
  64. package/dist/models/school.d.ts.map +1 -1
  65. package/dist/models/section.d.ts +216 -72
  66. package/dist/models/section.d.ts.map +1 -1
  67. package/dist/models/tag.d.ts +156 -48
  68. package/dist/models/tag.d.ts.map +1 -1
  69. package/dist/models/user.d.ts +82 -28
  70. package/dist/models/user.d.ts.map +1 -1
  71. package/dist/models/worksheet.d.ts +1023 -410
  72. package/dist/models/worksheet.d.ts.map +1 -1
  73. package/dist/models/worksheet.js +19 -7
  74. package/dist/models/worksheet.js.map +1 -1
  75. package/dist/pipelines/aiLabChat.d.ts +0 -8
  76. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  77. package/dist/pipelines/aiLabChat.js +50 -67
  78. package/dist/pipelines/aiLabChat.js.map +1 -1
  79. package/dist/pipelines/aiLabChatContract.d.ts +393 -388
  80. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  81. package/dist/pipelines/aiLabChatContract.js +63 -69
  82. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  83. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  84. package/dist/pipelines/aiNewtonChat.js +41 -41
  85. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  86. package/dist/pipelines/gradeWorksheet.d.ts +8 -8
  87. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  88. package/dist/pipelines/gradeWorksheet.js +59 -14
  89. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  90. package/dist/pipelines/labChatPrompt.d.ts +10 -8
  91. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  92. package/dist/pipelines/labChatPrompt.js +47 -67
  93. package/dist/pipelines/labChatPrompt.js.map +1 -1
  94. package/dist/pipelines/pdfToWorksheet.d.ts +2 -2
  95. package/dist/routers/_app.d.ts +2268 -1898
  96. package/dist/routers/_app.d.ts.map +1 -1
  97. package/dist/routers/_app.js +4 -2
  98. package/dist/routers/_app.js.map +1 -1
  99. package/dist/routers/admin.d.ts +13 -13
  100. package/dist/routers/agenda.d.ts +18 -18
  101. package/dist/routers/announcement.d.ts +32 -28
  102. package/dist/routers/announcement.d.ts.map +1 -1
  103. package/dist/routers/announcement.js +6 -5
  104. package/dist/routers/announcement.js.map +1 -1
  105. package/dist/routers/assignment.d.ts +335 -327
  106. package/dist/routers/assignment.d.ts.map +1 -1
  107. package/dist/routers/assignment.js +4 -4
  108. package/dist/routers/assignment.js.map +1 -1
  109. package/dist/routers/attendance.d.ts +10 -10
  110. package/dist/routers/auth.d.ts +2 -2
  111. package/dist/routers/class.d.ts +170 -147
  112. package/dist/routers/class.d.ts.map +1 -1
  113. package/dist/routers/comment.d.ts +40 -16
  114. package/dist/routers/comment.d.ts.map +1 -1
  115. package/dist/routers/comment.js +6 -3
  116. package/dist/routers/comment.js.map +1 -1
  117. package/dist/routers/conversation.d.ts +17 -17
  118. package/dist/routers/department.d.ts +34 -34
  119. package/dist/routers/discussion.d.ts +65 -53
  120. package/dist/routers/discussion.d.ts.map +1 -1
  121. package/dist/routers/discussion.js +6 -2
  122. package/dist/routers/discussion.js.map +1 -1
  123. package/dist/routers/embedding.d.ts +69 -0
  124. package/dist/routers/embedding.d.ts.map +1 -0
  125. package/dist/routers/embedding.js +143 -0
  126. package/dist/routers/embedding.js.map +1 -0
  127. package/dist/routers/event.d.ts +39 -39
  128. package/dist/routers/file.d.ts +12 -12
  129. package/dist/routers/folder.d.ts +38 -38
  130. package/dist/routers/labChat.d.ts +19 -19
  131. package/dist/routers/marketing.d.ts +2 -2
  132. package/dist/routers/message.d.ts +13 -13
  133. package/dist/routers/newtonChat.d.ts +4 -4
  134. package/dist/routers/notifications.d.ts +9 -9
  135. package/dist/routers/permissions.d.ts +14 -14
  136. package/dist/routers/school.d.ts +55 -55
  137. package/dist/routers/search.d.ts +22 -1
  138. package/dist/routers/search.d.ts.map +1 -1
  139. package/dist/routers/search.js +9 -3
  140. package/dist/routers/search.js.map +1 -1
  141. package/dist/routers/section.d.ts +4 -4
  142. package/dist/routers/studentProgress.d.ts +1 -1
  143. package/dist/routers/tag.d.ts +7 -7
  144. package/dist/routers/user.d.ts +10 -10
  145. package/dist/routers/worksheet.d.ts +97 -56
  146. package/dist/routers/worksheet.d.ts.map +1 -1
  147. package/dist/routers/worksheet.js +28 -2
  148. package/dist/routers/worksheet.js.map +1 -1
  149. package/dist/routes/aiStream.d.ts +2 -0
  150. package/dist/routes/aiStream.d.ts.map +1 -0
  151. package/dist/routes/aiStream.js +696 -0
  152. package/dist/routes/aiStream.js.map +1 -0
  153. package/dist/seedDatabase.d.ts +2 -2
  154. package/dist/seedDatabase.js +6 -6
  155. package/dist/seedDatabase.js.map +1 -1
  156. package/dist/services/adminInstitution.d.ts +8 -8
  157. package/dist/services/adminRunJob.d.ts +4 -4
  158. package/dist/services/agenda.d.ts +18 -18
  159. package/dist/services/aiChatStream.d.ts +68 -0
  160. package/dist/services/aiChatStream.d.ts.map +1 -0
  161. package/dist/services/aiChatStream.js +157 -0
  162. package/dist/services/aiChatStream.js.map +1 -0
  163. package/dist/services/aiModifierTools.d.ts +32 -0
  164. package/dist/services/aiModifierTools.d.ts.map +1 -0
  165. package/dist/services/aiModifierTools.js +165 -0
  166. package/dist/services/aiModifierTools.js.map +1 -0
  167. package/dist/services/aiProposalTools.d.ts +32 -0
  168. package/dist/services/aiProposalTools.d.ts.map +1 -0
  169. package/dist/services/aiProposalTools.js +207 -0
  170. package/dist/services/aiProposalTools.js.map +1 -0
  171. package/dist/services/aiStream.d.ts +22 -0
  172. package/dist/services/aiStream.d.ts.map +1 -0
  173. package/dist/services/aiStream.js +142 -0
  174. package/dist/services/aiStream.js.map +1 -0
  175. package/dist/services/aiTools.d.ts +16 -0
  176. package/dist/services/aiTools.d.ts.map +1 -0
  177. package/dist/services/aiTools.js +271 -0
  178. package/dist/services/aiTools.js.map +1 -0
  179. package/dist/services/announcement.d.ts +12 -12
  180. package/dist/services/announcement.d.ts.map +1 -1
  181. package/dist/services/announcement.js +7 -2
  182. package/dist/services/announcement.js.map +1 -1
  183. package/dist/services/assignment.d.ts +293 -293
  184. package/dist/services/assignment.d.ts.map +1 -1
  185. package/dist/services/assignment.js +36 -15
  186. package/dist/services/assignment.js.map +1 -1
  187. package/dist/services/attendance.d.ts +10 -10
  188. package/dist/services/auth.d.ts +2 -2
  189. package/dist/services/class.d.ts +161 -138
  190. package/dist/services/class.d.ts.map +1 -1
  191. package/dist/services/class.js +25 -4
  192. package/dist/services/class.js.map +1 -1
  193. package/dist/services/comment.d.ts +34 -16
  194. package/dist/services/comment.d.ts.map +1 -1
  195. package/dist/services/comment.js +23 -2
  196. package/dist/services/comment.js.map +1 -1
  197. package/dist/services/conversation.d.ts +17 -17
  198. package/dist/services/conversation.d.ts.map +1 -1
  199. package/dist/services/conversation.js +9 -7
  200. package/dist/services/conversation.js.map +1 -1
  201. package/dist/services/department.d.ts +33 -33
  202. package/dist/services/discussion.d.ts +58 -58
  203. package/dist/services/discussion.d.ts.map +1 -1
  204. package/dist/services/discussion.js +8 -8
  205. package/dist/services/discussion.js.map +1 -1
  206. package/dist/services/discussionSummary.d.ts +21 -0
  207. package/dist/services/discussionSummary.d.ts.map +1 -0
  208. package/dist/services/discussionSummary.js +179 -0
  209. package/dist/services/discussionSummary.js.map +1 -0
  210. package/dist/services/embedding.d.ts +67 -0
  211. package/dist/services/embedding.d.ts.map +1 -0
  212. package/dist/services/embedding.js +226 -0
  213. package/dist/services/embedding.js.map +1 -0
  214. package/dist/services/event.d.ts +37 -37
  215. package/dist/services/file.d.ts +24 -24
  216. package/dist/services/folder.d.ts +36 -36
  217. package/dist/services/labChat.d.ts +19 -19
  218. package/dist/services/marketing.d.ts +2 -2
  219. package/dist/services/message.d.ts +11 -11
  220. package/dist/services/message.d.ts.map +1 -1
  221. package/dist/services/message.js +2 -2
  222. package/dist/services/message.js.map +1 -1
  223. package/dist/services/newtonChat.d.ts +4 -4
  224. package/dist/services/notification.d.ts +9 -9
  225. package/dist/services/notification.d.ts.map +1 -1
  226. package/dist/services/permissions.d.ts +14 -14
  227. package/dist/services/school.d.ts +50 -50
  228. package/dist/services/search.d.ts +12 -0
  229. package/dist/services/search.d.ts.map +1 -1
  230. package/dist/services/search.js +120 -2
  231. package/dist/services/search.js.map +1 -1
  232. package/dist/services/section.d.ts +3 -3
  233. package/dist/services/tag.d.ts +4 -4
  234. package/dist/services/user.d.ts +4 -4
  235. package/dist/services/user.d.ts.map +1 -1
  236. package/dist/services/user.js +10 -15
  237. package/dist/services/user.js.map +1 -1
  238. package/dist/services/worksheet.d.ts +62 -47
  239. package/dist/services/worksheet.d.ts.map +1 -1
  240. package/dist/services/worksheet.js +11 -4
  241. package/dist/services/worksheet.js.map +1 -1
  242. package/dist/utils/streamBroadcast.d.ts +11 -0
  243. package/dist/utils/streamBroadcast.d.ts.map +1 -0
  244. package/dist/utils/streamBroadcast.js +32 -0
  245. package/dist/utils/streamBroadcast.js.map +1 -0
  246. package/dist/workers/connection.d.ts +15 -0
  247. package/dist/workers/connection.d.ts.map +1 -1
  248. package/dist/workers/connection.js +20 -2
  249. package/dist/workers/connection.js.map +1 -1
  250. package/dist/workers/index.js +4 -2
  251. package/dist/workers/index.js.map +1 -1
  252. package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -1
  253. package/dist/workers/processors/createRunJobWorker.js +31 -8
  254. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  255. package/dist/workers/processors/embedContent.d.ts +8 -0
  256. package/dist/workers/processors/embedContent.d.ts.map +1 -0
  257. package/dist/workers/processors/embedContent.js +76 -0
  258. package/dist/workers/processors/embedContent.js.map +1 -0
  259. package/dist/workers/queues.d.ts +6 -0
  260. package/dist/workers/queues.d.ts.map +1 -1
  261. package/dist/workers/queues.js +5 -3
  262. package/dist/workers/queues.js.map +1 -1
  263. package/package.json +5 -1
  264. package/prisma/migrations/20260609215135_init/migration.sql +1412 -0
  265. package/prisma/migrations/20260610130000_add_worksheet_tags/migration.sql +13 -0
  266. package/prisma/schema.prisma +39 -7
  267. package/src/index.ts +4 -0
  268. package/src/lib/config/cors.ts +32 -9
  269. package/src/lib/config/env.ts +6 -0
  270. package/src/lib/googleCloudStorage.ts +21 -0
  271. package/src/lib/prisma.ts +40 -1
  272. package/src/models/class.ts +10 -3
  273. package/src/models/comment.ts +1 -0
  274. package/src/models/conversation.ts +12 -0
  275. package/src/models/worksheet.ts +16 -7
  276. package/src/pipelines/aiLabChat.ts +54 -79
  277. package/src/pipelines/aiLabChatContract.ts +69 -69
  278. package/src/pipelines/aiNewtonChat.ts +46 -43
  279. package/src/pipelines/gradeWorksheet.ts +66 -15
  280. package/src/pipelines/labChatPrompt.ts +63 -88
  281. package/src/routers/_app.ts +2 -0
  282. package/src/routers/announcement.ts +4 -3
  283. package/src/routers/assignment.ts +2 -2
  284. package/src/routers/comment.ts +5 -0
  285. package/src/routers/discussion.ts +7 -0
  286. package/src/routers/embedding.ts +159 -0
  287. package/src/routers/search.ts +10 -1
  288. package/src/routers/worksheet.ts +29 -0
  289. package/src/routes/aiStream.ts +789 -0
  290. package/src/seedDatabase.ts +4 -4
  291. package/src/services/aiChatStream.ts +219 -0
  292. package/src/services/aiModifierTools.ts +188 -0
  293. package/src/services/aiProposalTools.ts +272 -0
  294. package/src/services/aiStream.ts +175 -0
  295. package/src/services/aiTools.ts +290 -0
  296. package/src/services/announcement.ts +7 -0
  297. package/src/services/assignment.ts +36 -14
  298. package/src/services/class.ts +30 -2
  299. package/src/services/comment.ts +22 -0
  300. package/src/services/conversation.ts +9 -4
  301. package/src/services/discussion.ts +9 -8
  302. package/src/services/discussionSummary.ts +226 -0
  303. package/src/services/embedding.ts +285 -0
  304. package/src/services/message.ts +2 -2
  305. package/src/services/search.ts +138 -0
  306. package/src/services/user.ts +9 -17
  307. package/src/services/worksheet.ts +10 -2
  308. package/src/utils/streamBroadcast.ts +48 -0
  309. package/src/workers/connection.ts +19 -0
  310. package/src/workers/index.ts +2 -0
  311. package/src/workers/processors/createRunJobWorker.ts +32 -6
  312. package/src/workers/processors/embedContent.ts +90 -0
  313. package/src/workers/queues.ts +5 -1
  314. package/tests/integration/routers/conversation.test.ts +9 -7
  315. package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
  316. package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
  317. package/prisma/migrations/20250807062924_init/migration.sql +0 -436
  318. package/prisma/migrations/20250919063704_init2/migration.sql +0 -24
  319. package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +0 -15
  320. package/prisma/migrations/20250922165147_add_chat_models/migration.sql +0 -68
  321. package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +0 -5
  322. package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +0 -20
  323. package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +0 -21
  324. package/prisma/migrations/20250925072732_add_lab_chat_system/migration.sql +0 -25
  325. package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +0 -57
  326. package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +0 -30
  327. package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +0 -35
  328. package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +0 -14
  329. package/prisma/migrations/20260415103500_add_session_type/migration.sql +0 -33
  330. package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +0 -19
  331. package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +0 -2
  332. package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +0 -60
  333. package/prisma/migrations/20260504071500_school_memberships/migration.sql +0 -81
  334. package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +0 -166
  335. package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +0 -184
  336. package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +0 -93
  337. package/prisma/migrations/20260510183000_add_discussions/migration.sql +0 -151
  338. /package/.github/workflows/{npm.yml → npm-publish.yml} +0 -0
@@ -0,0 +1,13 @@
1
+ -- CreateTable (implicit many-to-many join for ClassTag <-> Worksheet)
2
+ CREATE TABLE "_WorksheetToTag" (
3
+ "A" TEXT NOT NULL,
4
+ "B" TEXT NOT NULL,
5
+ CONSTRAINT "_WorksheetToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "DiscussionTag"("id") ON DELETE CASCADE ON UPDATE CASCADE,
6
+ CONSTRAINT "_WorksheetToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Worksheet"("id") ON DELETE CASCADE ON UPDATE CASCADE
7
+ );
8
+
9
+ -- CreateIndex
10
+ CREATE UNIQUE INDEX "_WorksheetToTag_AB_unique" ON "_WorksheetToTag"("A", "B");
11
+
12
+ -- CreateIndex
13
+ CREATE INDEX "_WorksheetToTag_B_index" ON "_WorksheetToTag"("B");
@@ -5,7 +5,8 @@
5
5
  // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6
6
 
7
7
  generator client {
8
- provider = "prisma-client-js"
8
+ provider = "prisma-client-js"
9
+ previewFeatures = ["postgresqlExtensions"]
9
10
  // output = "../generated/prisma"
10
11
  }
11
12
 
@@ -15,9 +16,10 @@ generator client {
15
16
  // }
16
17
 
17
18
  datasource db {
18
- provider = "postgresql"
19
- url = env("DATABASE_URL")
20
- directUrl = env("DIRECT_URL")
19
+ provider = "postgresql"
20
+ url = env("DATABASE_URL")
21
+ directUrl = env("DIRECT_URL")
22
+ extensions = [vector]
21
23
  }
22
24
 
23
25
  enum AssignmentType {
@@ -321,7 +323,8 @@ model Class {
321
323
  tags Tag[] @relation("ClassTags")
322
324
  permissions Permission[] @relation("ClassPermissions")
323
325
  discussions Discussion[]
324
- discussionTags DiscussionTag[]
326
+ classTags ClassTag[]
327
+ embeddings Embedding[]
325
328
 
326
329
  discussionsEnabled Boolean @default(true)
327
330
  studentsCanCreateDiscussions Boolean @default(true)
@@ -519,6 +522,7 @@ model Comment {
519
522
  parentCommentId String?
520
523
  replies Comment[] @relation("CommentReplies")
521
524
  reactions Reaction[]
525
+ isPinned Boolean @default(false)
522
526
  createdAt DateTime @default(now())
523
527
  modifiedAt DateTime? @updatedAt
524
528
  studentQuestionProgress StudentQuestionProgress? @relation("StudentQuestionProgressComments", fields: [studentQuestionProgressId], references: [id], onDelete: Cascade)
@@ -541,7 +545,7 @@ model Discussion {
541
545
  isLocked Boolean @default(false)
542
546
  allowReplies Boolean @default(true)
543
547
  attachments File[] @relation("DiscussionFiles")
544
- tags DiscussionTag[] @relation("DiscussionToTag")
548
+ tags ClassTag[] @relation("DiscussionToTag")
545
549
  replies DiscussionReply[]
546
550
  reactions Reaction[]
547
551
  reads DiscussionRead[]
@@ -574,15 +578,17 @@ model DiscussionReply {
574
578
  @@index([parentId])
575
579
  }
576
580
 
577
- model DiscussionTag {
581
+ model ClassTag {
578
582
  id String @id @default(uuid())
579
583
  class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
580
584
  classId String
581
585
  name String
582
586
  color String @default("#3B82F6")
583
587
  discussions Discussion[] @relation("DiscussionToTag")
588
+ worksheets Worksheet[] @relation("WorksheetToTag")
584
589
 
585
590
  @@unique([classId, name])
591
+ @@map("DiscussionTag")
586
592
  }
587
593
 
588
594
  model DiscussionRead {
@@ -860,6 +866,7 @@ model Worksheet {
860
866
  runJob RunJob? @relation("WorksheetRunJob", fields: [runJobId], references: [id], onDelete: SetNull)
861
867
 
862
868
  status JobStatus? @default(NOT_STARTED)
869
+ tags ClassTag[] @relation("WorksheetToTag")
863
870
 
864
871
  @@unique([classId, sourceUrl])
865
872
  }
@@ -961,6 +968,31 @@ enum RunJobType {
961
968
  AI_LAB_CHAT
962
969
  AI_NEWTON_CHAT
963
970
  STUDENT_PROGRESS_CHAT
971
+ EMBED_CONTENT
972
+ }
973
+
974
+ enum EmbeddingSourceType {
975
+ FILE
976
+ ASSIGNMENT
977
+ WORKSHEET
978
+ ANNOUNCEMENT
979
+ SECTION
980
+ }
981
+
982
+ model Embedding {
983
+ id String @id @default(uuid())
984
+ classId String
985
+ sourceType EmbeddingSourceType
986
+ sourceId String
987
+ content String
988
+ vector Unsupported("vector(1536)")
989
+ metadata Json? @default("{}")
990
+ createdAt DateTime @default(now())
991
+
992
+ class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
993
+
994
+ @@index([classId])
995
+ @@index([sourceType, sourceId])
964
996
  }
965
997
 
966
998
  enum RunJobStatus {
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ import { v4 as uuidv4 } from 'uuid';
27
27
 
28
28
  import "./instrument.js";
29
29
  import { openAIClient } from './utils/inference.js';
30
+ import { aiStreamRouter } from './routes/aiStream.js';
30
31
 
31
32
  const app = express();
32
33
 
@@ -313,6 +314,9 @@ io.engine.on('connection_error', (err: Error) => {
313
314
  // Setup socket handlers
314
315
  setupSocketHandlers(io);
315
316
 
317
+ // AI streaming routes
318
+ app.use('/api/ai', aiStreamRouter);
319
+
316
320
  // File serving endpoint for secure file access
317
321
  app.get('/api/files/:fileId', async (req, res) => {
318
322
  try {
@@ -5,18 +5,24 @@ type CorsEnv = {
5
5
  CORS_ALLOWED_ORIGIN_PATTERNS?: string;
6
6
  };
7
7
 
8
- const productionOrigins = [
8
+ // First-party Studious domains. These are always allowed, regardless of
9
+ // NODE_ENV, so local/dev servers can still serve the real frontends.
10
+ const studiousOrigins = [
9
11
  'https://www.studious.sh',
10
12
  'https://studious.sh',
11
13
  'https://dev.studious.sh',
12
- // @todo: remove in actual prod
13
- 'http://localhost:3000',
14
- 'http://localhost:3001',
15
- 'http://127.0.0.1:3000',
16
- 'http://127.0.0.1:3001',
14
+ 'https://superuser.studious.sh',
15
+ 'https://*.studious.sh',
16
+ 'https://www.studious.ac',
17
+ 'https://studious.ac',
18
+ 'https://dev.studious.ac',
19
+ 'https://superuser.studious.ac',
20
+ 'https://*.studious.ac',
17
21
  ];
18
22
 
19
- const nonProductionOrigins = [
23
+ // Local development frontends.
24
+ // @todo: gate these behind a non-production check before real prod.
25
+ const localhostOrigins = [
20
26
  'http://localhost:3000',
21
27
  'http://localhost:3001',
22
28
  'http://127.0.0.1:3000',
@@ -40,6 +46,17 @@ const parseList = (value?: string) =>
40
46
 
41
47
  const isDefined = <T>(value: T | undefined | null): value is T => Boolean(value);
42
48
 
49
+ const hasWildcard = (value: string) => value.includes('*');
50
+
51
+ // Turn a wildcard origin like `https://*.studious.ac` into an anchored regex.
52
+ // `*` matches one or more subdomain labels (e.g. `app` or `a.b`).
53
+ const wildcardOriginToRegExp = (value: string) => {
54
+ const escaped = value
55
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
56
+ .replace(/\\\*/g, '[a-z0-9-]+(?:\\.[a-z0-9-]+)*');
57
+ return new RegExp(`^${escaped}$`, 'i');
58
+ };
59
+
43
60
  const toCorsOrigin = (value: string) => {
44
61
  try {
45
62
  return new URL(value).origin;
@@ -79,14 +96,20 @@ export const isValidCorsOriginPatternList = (patterns?: string) => {
79
96
  };
80
97
 
81
98
  export const createCorsOriginMatcher = (env: CorsEnv) => {
82
- const allowedOrigins = Array.from(new Set([
83
- ...(env.NODE_ENV === 'production' ? productionOrigins : nonProductionOrigins),
99
+ const configuredOrigins = Array.from(new Set([
100
+ ...studiousOrigins,
101
+ ...localhostOrigins,
84
102
  env.NEXT_PUBLIC_APP_URL ? toCorsOrigin(env.NEXT_PUBLIC_APP_URL) : undefined,
85
103
  ...parseCorsOrigins(env.CORS_ALLOWED_ORIGINS),
86
104
  ].filter(isDefined)));
87
105
 
106
+ // Wildcard entries (e.g. `https://*.studious.ac`) can't be matched by exact
107
+ // string comparison, so they're compiled into regex patterns instead.
108
+ const allowedOrigins = configuredOrigins.filter((origin) => !hasWildcard(origin));
109
+
88
110
  const allowedOriginPatterns = [
89
111
  vercelAppOriginPattern,
112
+ ...configuredOrigins.filter(hasWildcard).map(wildcardOriginToRegExp),
90
113
  ...parseCorsOriginPatterns(env.CORS_ALLOWED_ORIGIN_PATTERNS),
91
114
  ];
92
115
 
@@ -35,6 +35,12 @@ const baseSchema = z.object({
35
35
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
36
36
  PORT: z.string().transform(Number).default('3001'),
37
37
  DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
38
+ /**
39
+ * Optional BullMQ key prefix. Defaults to `bull:<NODE_ENV>` (see
40
+ * `getBullPrefix`). Set explicitly to isolate queues when multiple
41
+ * databases share one Redis instance (e.g. several devs on one Upstash).
42
+ */
43
+ BULL_PREFIX: z.string().optional(),
38
44
  });
39
45
 
40
46
  // Production/development schema with all required vars
@@ -98,4 +98,25 @@ export async function copyFile(sourcePath: string, destPath: string): Promise<vo
98
98
  message: 'Failed to copy file in storage',
99
99
  });
100
100
  }
101
+ }
102
+
103
+ /**
104
+ * Returns the public URL for a GCS object.
105
+ * The object must be publicly readable (via ACL or bucket-level IAM).
106
+ */
107
+ export function getPublicUrl(filePath: string): string {
108
+ return `https://storage.googleapis.com/${env.GOOGLE_CLOUD_BUCKET_NAME}/${filePath}`;
109
+ }
110
+
111
+ /**
112
+ * Best-effort attempt to make a GCS object publicly readable.
113
+ * Silently ignores errors (e.g. uniform bucket-level access enabled).
114
+ */
115
+ export async function makeFilePublic(filePath: string): Promise<void> {
116
+ try {
117
+ await bucket.file(filePath).makePublic();
118
+ } catch {
119
+ // Bucket may use uniform access or already have a public IAM policy – either way, the
120
+ // public URL will work if the bucket allows it; nothing to do here.
121
+ }
101
122
  }
package/src/lib/prisma.ts CHANGED
@@ -30,6 +30,19 @@ const getDatabaseUrl = () => {
30
30
  }
31
31
  };
32
32
 
33
+ /**
34
+ * Resolve a profile-picture value stored in the DB to a browser-loadable URL.
35
+ * - `null` / empty → `null`
36
+ * - already an HTTP URL (e.g. dicebear) → returned as-is
37
+ * - raw GCS object path → converted to a public GCS URL
38
+ */
39
+ function resolveProfilePictureUrl(raw: string | null): string | null {
40
+ if (!raw) return null;
41
+ if (raw.startsWith('http')) return raw;
42
+ const bucket = env.GOOGLE_CLOUD_BUCKET_NAME;
43
+ return `https://storage.googleapis.com/${bucket}/${raw}`;
44
+ }
45
+
33
46
  const prismaClientSingleton = () => {
34
47
  return new PrismaClient({
35
48
  datasources: {
@@ -37,6 +50,23 @@ const prismaClientSingleton = () => {
37
50
  url: getDatabaseUrl(),
38
51
  },
39
52
  },
53
+ }).$extends({
54
+ result: {
55
+ userProfile: {
56
+ profilePicture: {
57
+ needs: { profilePicture: true },
58
+ compute(profile) {
59
+ return resolveProfilePictureUrl(profile.profilePicture);
60
+ },
61
+ },
62
+ profilePictureThumbnail: {
63
+ needs: { profilePictureThumbnail: true },
64
+ compute(profile) {
65
+ return resolveProfilePictureUrl(profile.profilePictureThumbnail);
66
+ },
67
+ },
68
+ },
69
+ },
40
70
  });
41
71
  };
42
72
 
@@ -53,4 +83,13 @@ if (env.NODE_ENV !== 'production') {
53
83
 
54
84
  process.on('beforeExit', async () => {
55
85
  await prisma.$disconnect();
56
- });
86
+ });
87
+
88
+ /** The concrete (extended) Prisma client type used throughout the app. */
89
+ export type AppPrismaClient = typeof prisma;
90
+
91
+ /** Transaction client type compatible with the extended Prisma client. */
92
+ export type AppTransactionClient = Omit<
93
+ AppPrismaClient,
94
+ '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'
95
+ >;
@@ -5,7 +5,8 @@
5
5
  */
6
6
 
7
7
  import { Assignment, Class, Folder, GradingBoundary, MarkScheme, Section, SessionType, Worksheet } from "@prisma/client";
8
- import type { PrismaClient, WorksheetQuestion } from "@prisma/client";
8
+ import type { WorksheetQuestion } from "@prisma/client";
9
+ import type { AppPrismaClient, AppTransactionClient } from "../lib/prisma.js";
9
10
 
10
11
  import { prisma } from "../lib/prisma.js";
11
12
  import { v4 as uuidv4 } from 'uuid';
@@ -18,7 +19,6 @@ import {
18
19
  const classIncludeForGetAll = {
19
20
  assignments: {
20
21
  where: {
21
- dueDate: { lte: new Date(new Date().setHours(23, 59, 59, 999)) },
22
22
  template: false,
23
23
  },
24
24
  select: {
@@ -26,6 +26,13 @@ const classIncludeForGetAll = {
26
26
  title: true,
27
27
  type: true,
28
28
  dueDate: true,
29
+ submissions: {
30
+ select: {
31
+ studentId: true,
32
+ submitted: true,
33
+ returned: true,
34
+ },
35
+ },
29
36
  },
30
37
  },
31
38
  students: {
@@ -471,7 +478,7 @@ async function recursivelyCopyFilesToGcs(folderId: string): Promise<Map<string,
471
478
  }
472
479
 
473
480
  async function recursivelyCreateFiles(
474
- tx: PrismaClient | Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends">,
481
+ tx: AppPrismaClient | AppTransactionClient,
475
482
  folderId: string,
476
483
  targetFolderId: string | undefined,
477
484
  pathMap: Map<string, string>
@@ -11,6 +11,7 @@ export const commentSelect = {
11
11
  assignmentId: true,
12
12
  submissionId: true,
13
13
  parentCommentId: true,
14
+ isPinned: true,
14
15
  createdAt: true,
15
16
  modifiedAt: true,
16
17
  author: {
@@ -256,6 +256,18 @@ export function hideConversationMember(userId: string, conversationId: string) {
256
256
  });
257
257
  }
258
258
 
259
+ /** Unhide a conversation for a single member. */
260
+ export function unhideConversationMember(userId: string, conversationId: string) {
261
+ return prisma.conversationMember.update({
262
+ where: {
263
+ userId_conversationId: { userId, conversationId },
264
+ },
265
+ data: {
266
+ hiddenAt: null,
267
+ },
268
+ });
269
+ }
270
+
259
271
  /** Remove a member from a conversation. */
260
272
  export function deleteConversationMember(userId: string, conversationId: string) {
261
273
  return prisma.conversationMember.delete({
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Worksheet model – worksheets, questions, submissions, student progress, grading.
3
3
  */
4
- import type { Prisma } from "@prisma/client";
5
4
  import { prisma } from "../lib/prisma.js";
5
+ import type { AppTransactionClient } from "../lib/prisma.js";
6
6
  import type { WorksheetQuestionType, JobStatus } from "@prisma/client";
7
7
  import {
8
8
  classMemberOrSchoolAdminWhere,
@@ -22,7 +22,7 @@ export function findWorksheetById(id: string) {
22
22
  return prisma.worksheet.findUnique({
23
23
  where: { id },
24
24
  include: {
25
- questions: { orderBy: { createdAt: "asc" as const } },
25
+ questions: { orderBy: { order: "asc" as const } },
26
26
  class: true,
27
27
  },
28
28
  });
@@ -43,8 +43,9 @@ export function findWorksheetByIdForClassMember(id: string, userId: string) {
43
43
  ...worksheetClassMemberWhere(userId),
44
44
  },
45
45
  include: {
46
- questions: { orderBy: { createdAt: "asc" as const } },
46
+ questions: { orderBy: { order: "asc" as const } },
47
47
  class: true,
48
+ tags: true,
48
49
  },
49
50
  });
50
51
  }
@@ -57,8 +58,9 @@ export function findWorksheetByIdForTeacher(id: string, userId: string) {
57
58
  ...worksheetTeacherWhere(userId),
58
59
  },
59
60
  include: {
60
- questions: { orderBy: { createdAt: "asc" as const } },
61
+ questions: { orderBy: { order: "asc" as const } },
61
62
  class: true,
63
+ tags: true,
62
64
  },
63
65
  });
64
66
  }
@@ -69,6 +71,7 @@ export function findWorksheetsByClassId(classId: string) {
69
71
  where: { classId },
70
72
  include: {
71
73
  questions: { select: { id: true } },
74
+ tags: true,
72
75
  },
73
76
  orderBy: { createdAt: "desc" },
74
77
  });
@@ -121,7 +124,7 @@ export function deleteWorksheet(id: string) {
121
124
  * Does not copy student responses or assignment links.
122
125
  */
123
126
  export async function duplicateWorksheetWithClient(
124
- tx: Prisma.TransactionClient,
127
+ tx: AppTransactionClient,
125
128
  sourceWorksheetId: string,
126
129
  targetClassId: string,
127
130
  name: string
@@ -273,7 +276,10 @@ export function findSubmissionByIdForWorksheetAccess(
273
276
  const worksheetResponseInclude = {
274
277
  responses: {
275
278
  include: {
276
- comments: { select: { id: true } },
279
+ comments: {
280
+ select: { id: true, isPinned: true },
281
+ orderBy: [{ isPinned: "desc" as const }, { createdAt: "asc" as const }],
282
+ },
277
283
  },
278
284
  },
279
285
  };
@@ -319,7 +325,10 @@ export function createWorksheetResponse(data: {
319
325
  include: {
320
326
  responses: {
321
327
  include: {
322
- comments: { select: { id: true } },
328
+ comments: {
329
+ select: { id: true, isPinned: true },
330
+ orderBy: [{ isPinned: "desc" as const }, { createdAt: "asc" as const }],
331
+ },
323
332
  },
324
333
  },
325
334
  },
@@ -7,7 +7,6 @@ import { prisma } from "../lib/prisma.js";
7
7
  import { pusher, teacherChannel } from "../lib/pusher.js";
8
8
  import type { Assignment, Class, File, Section, User } from "@prisma/client";
9
9
  import {
10
- inference,
11
10
  completePendingAIMessage,
12
11
  failPendingAIMessage,
13
12
  } from "../utils/inference.js";
@@ -16,8 +15,11 @@ import { createPdf } from "../lib/jsonConversion.js";
16
15
  import { v4 } from "uuid";
17
16
  import { bucket } from "../lib/googleCloudStorage.js";
18
17
  import { DocumentBlock } from "../lib/jsonStyles.js";
19
- import { type LabChatResponse, labChatResponseSchema } from "./aiLabChatContract.js";
20
- import { buildLabChatResponseMessages } from "./labChatPrompt.js";
18
+ import { broadcastTextChunks, broadcastStreamComplete } from "../utils/streamBroadcast.js";
19
+ import { buildLabChatAgentMessages } from "./labChatPrompt.js";
20
+ import { streamChatResponse } from "../services/aiChatStream.js";
21
+ import { aiTools } from "../services/aiTools.js";
22
+ import { labProposalTools, createLabProposals } from "../services/aiProposalTools.js";
21
23
 
22
24
  /** Cap model wait time; leave headroom after inference for PDF/GCS/DB within Vercel maxDuration (500s). */
23
25
  const LAB_CHAT_RESPONSE_TIMEOUT_MS = 400_000;
@@ -303,33 +305,6 @@ export const getBaseSystemPrompt = (
303
305
 
304
306
 
305
307
 
306
- /**
307
- * Generate labchat responses
308
- * Allow for the generation of the following:
309
- * - Assignment(s) either individual or bulk as an lesson / course plan.
310
- * - Worksheet(s) either individual or bulk as an lesson / course plan.
311
- * - Files (PDFs)
312
- * @param labChatId
313
- */
314
- // export const sendAiLabChatResponsePipeline = async (labChatId: string) => {
315
- // const message = await prisma?.message.create({
316
- // data: {
317
- // content: "GENERATING_CONTENT",
318
- // senderId: getAIUserId(),
319
- // conversationId: labChatId,
320
- // status: JobStatus.PENDING,
321
- // },
322
- // });
323
-
324
- // try {
325
-
326
- // inference(`
327
- // `)
328
- // }
329
-
330
- // };
331
-
332
-
333
308
  /**
334
309
  * Fill the AI introduction placeholder for a brand-new lab chat.
335
310
  *
@@ -348,6 +323,7 @@ export const generateAndSendLabIntroduction = async (
348
323
  "Hello teaching team! I'm your AI assistant for course material development. I'll help you create educational content - when you have a clear request, I'll produce it directly. I only ask questions when I need to clarify the topic or scope. What would you like to work on?";
349
324
 
350
325
  try {
326
+ await broadcastTextChunks(conversationId, aiMessageId, introMessage);
351
327
  await completePendingAIMessage(aiMessageId, introMessage);
352
328
  logger.info('AI Introduction sent', { labChatId, conversationId, aiMessageId });
353
329
  } catch (error) {
@@ -474,50 +450,51 @@ export const generateAndSendLabIntroduction = async (
474
450
  assignments: classData.assignments,
475
451
  });
476
452
 
477
- const messages = buildLabChatResponseMessages({
453
+ const { system, messages } = buildLabChatAgentMessages({
478
454
  context: fullLabChat.context,
479
455
  classContext,
480
456
  recentMessages: recentMessages.reverse(),
481
457
  isAIUser,
482
458
  });
483
-
484
-
485
- // const completion = await inferenceClient.chat.completions.create({
486
- // model: 'command-a-03-2025',
487
- // messages,
488
- // temperature: 0.7,
489
- // response_format: zodTextFormat(labChatResponseSchema, "lab_chat_response_format"),
490
- // });
491
-
492
- const response = await withTimeout(
493
- inference<LabChatResponse>(messages, labChatResponseSchema, {
494
- reason: 'lab_chat_response',
495
- metadata: {
496
- labChatId,
459
+
460
+ // Genuine agentic generation: the model streams prose token-by-token and
461
+ // calls real tools inline — read-only RAG lookups plus "propose_*" tools
462
+ // that record create-actions into `proposals`. `streamChatResponse`
463
+ // broadcasts the real text chunks and tool-call events as they happen.
464
+ const proposals = createLabProposals();
465
+ const tools = {
466
+ ...aiTools(fullLabChat.classId),
467
+ ...labProposalTools(proposals),
468
+ };
469
+
470
+ const { text: streamedText, parts: streamParts } = await withTimeout(
471
+ streamChatResponse({
497
472
  conversationId,
498
- classId: fullLabChat.classId,
499
473
  aiMessageId: emitOptions.aiMessageId,
500
- },
501
- }),
502
- LAB_CHAT_RESPONSE_TIMEOUT_MS,
503
- "Lab chat response generation",
504
- );
505
-
506
- if (!response) {
507
- throw new Error('No response generated from inference API');
508
- }
509
- // Parse the JSON response and generate PDF if docs are provided
474
+ system,
475
+ messages,
476
+ tools,
477
+ temperature: 0.7,
478
+ maxOutputTokens: 4096,
479
+ maxSteps: 8,
480
+ }),
481
+ LAB_CHAT_RESPONSE_TIMEOUT_MS,
482
+ "Lab chat response generation",
483
+ );
484
+
485
+ // Real text + tool-call events were already broadcast inline (in order)
486
+ // during the stream by `streamChatResponse`. Just close the stream.
487
+ await broadcastStreamComplete(conversationId, emitOptions.aiMessageId);
488
+
489
+ // Generate PDFs for any proposed documents and persist the message.
510
490
  try {
511
- const jsonData = response;
512
-
513
-
514
491
  const attachmentIds: string[] = [];
515
- // Generate PDFs if docs are provided
516
- if (jsonData.docs && Array.isArray(jsonData.docs)) {
492
+ // Generate PDFs if docs were proposed
493
+ if (proposals.docs.length > 0) {
517
494
 
518
495
 
519
- for (let i = 0; i < jsonData.docs.length; i++) {
520
- const doc = jsonData.docs[i];
496
+ for (let i = 0; i < proposals.docs.length; i++) {
497
+ const doc = proposals.docs[i];
521
498
  if (!doc.title || !doc.blocks || !Array.isArray(doc.blocks)) {
522
499
  logger.error(`Document ${i + 1} is missing title or blocks`);
523
500
  continue;
@@ -577,30 +554,28 @@ export const generateAndSendLabIntroduction = async (
577
554
  }
578
555
  }
579
556
 
580
- await completePendingAIMessage(emitOptions.aiMessageId, jsonData.text, {
557
+ await completePendingAIMessage(emitOptions.aiMessageId, streamedText, {
581
558
  attachments: {
582
559
  connect: attachmentIds.map(id => ({ id })),
583
560
  },
584
561
  meta: {
585
- assignmentsToCreate: jsonData.assignmentsToCreate?.map(assignment => ({
586
- ...assignment,
587
- id: v4(),
588
- })) || null,
589
- worksheetsToCreate: jsonData.worksheetsToCreate?.map(worksheet => ({
590
- ...worksheet,
591
- id: v4(),
592
- })) || null,
593
- sectionsToCreate: jsonData.sectionsToCreate?.map(section => ({
594
- ...section,
595
- id: v4(),
596
- })) || null,
562
+ assignmentsToCreate: proposals.assignments.length > 0
563
+ ? proposals.assignments.map(assignment => ({ ...assignment, id: v4() }))
564
+ : null,
565
+ worksheetsToCreate: proposals.worksheets.length > 0
566
+ ? proposals.worksheets.map(worksheet => ({ ...worksheet, id: v4() }))
567
+ : null,
568
+ sectionsToCreate: proposals.sections.length > 0
569
+ ? proposals.sections.map(section => ({ ...section, id: v4() }))
570
+ : null,
571
+ streamParts: streamParts.length > 0 ? streamParts : null,
597
572
  },
598
573
  });
599
- } catch (parseError) {
600
- logger.error('Failed to parse AI response or generate PDF:', { error: parseError, labChatId });
574
+ } catch (persistError) {
575
+ logger.error('Failed to persist AI response or generate PDF:', { error: persistError, labChatId });
601
576
  // Fallback: complete the placeholder with the raw text so the
602
577
  // teacher still sees something useful instead of FAILED.
603
- await completePendingAIMessage(emitOptions.aiMessageId, response.text);
578
+ await completePendingAIMessage(emitOptions.aiMessageId, streamedText);
604
579
  }
605
580
 
606
581
  try {