@goscribe/server 1.3.4 → 1.5.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 (378) hide show
  1. package/.env.example +12 -0
  2. package/.vscode/settings.json +3 -0
  3. package/REFACTOR_NOTES.md +60 -0
  4. package/TESTING_PROMPT.md +225 -0
  5. package/dist/controllers/admin.controller.d.ts +715 -0
  6. package/dist/controllers/admin.controller.js +9 -0
  7. package/dist/controllers/annotations.controller.d.ts +439 -0
  8. package/dist/controllers/annotations.controller.js +9 -0
  9. package/dist/controllers/app-router.controller.d.ts +3011 -0
  10. package/dist/controllers/app-router.controller.js +38 -0
  11. package/dist/controllers/app-router.controller.test.d.ts +1 -0
  12. package/dist/controllers/app-router.controller.test.js +36 -0
  13. package/dist/controllers/auth.controller.d.ts +323 -0
  14. package/dist/controllers/auth.controller.js +9 -0
  15. package/dist/controllers/base.controller.d.ts +4 -0
  16. package/dist/controllers/base.controller.js +5 -0
  17. package/dist/controllers/chat.controller.d.ts +341 -0
  18. package/dist/controllers/chat.controller.js +9 -0
  19. package/dist/controllers/copilot.controller.d.ts +397 -0
  20. package/dist/controllers/copilot.controller.js +9 -0
  21. package/dist/controllers/flashcards.controller.d.ts +651 -0
  22. package/dist/controllers/flashcards.controller.js +9 -0
  23. package/dist/controllers/members.controller.d.ts +339 -0
  24. package/dist/controllers/members.controller.js +9 -0
  25. package/dist/controllers/notifications.controller.d.ts +199 -0
  26. package/dist/controllers/notifications.controller.js +9 -0
  27. package/dist/controllers/payment.controller.d.ts +181 -0
  28. package/dist/controllers/payment.controller.js +9 -0
  29. package/dist/controllers/podcast.controller.d.ts +575 -0
  30. package/dist/controllers/podcast.controller.js +9 -0
  31. package/dist/controllers/router-module.controller.d.ts +5 -0
  32. package/dist/controllers/router-module.controller.js +6 -0
  33. package/dist/controllers/studyguide.controller.d.ts +73 -0
  34. package/dist/controllers/studyguide.controller.js +9 -0
  35. package/dist/controllers/worksheets.controller.d.ts +829 -0
  36. package/dist/controllers/worksheets.controller.js +9 -0
  37. package/dist/controllers/workspace.controller.d.ts +1207 -0
  38. package/dist/controllers/workspace.controller.js +9 -0
  39. package/dist/lib/activity_human_description.test.js +16 -15
  40. package/dist/lib/activity_log_service.test.js +28 -23
  41. package/dist/lib/ai/config.d.ts +20 -0
  42. package/dist/lib/ai/config.js +31 -0
  43. package/dist/lib/ai/embedding-client.d.ts +8 -0
  44. package/dist/lib/ai/embedding-client.js +30 -0
  45. package/dist/lib/ai/index.d.ts +47 -0
  46. package/dist/lib/ai/index.js +28 -0
  47. package/dist/lib/ai/inference-backend/client.d.ts +28 -0
  48. package/dist/lib/ai/inference-backend/client.js +301 -0
  49. package/dist/lib/ai/inference-backend/mocks.d.ts +12 -0
  50. package/dist/lib/ai/inference-backend/mocks.js +133 -0
  51. package/dist/lib/ai/inference-backend/types.d.ts +44 -0
  52. package/dist/lib/ai/inference-backend/types.js +1 -0
  53. package/dist/lib/ai/json-parse.d.ts +2 -0
  54. package/dist/lib/ai/json-parse.js +34 -0
  55. package/dist/lib/ai/llm-client.d.ts +6 -0
  56. package/dist/lib/ai/llm-client.js +19 -0
  57. package/dist/lib/ai/mock.d.ts +2 -0
  58. package/dist/lib/ai/mock.js +10 -0
  59. package/dist/lib/ai/types.d.ts +9 -0
  60. package/dist/lib/ai/types.js +1 -0
  61. package/dist/lib/chunking.d.ts +19 -0
  62. package/dist/lib/chunking.js +47 -0
  63. package/dist/lib/curated-kb-seed.d.ts +12 -0
  64. package/dist/lib/curated-kb-seed.js +155 -0
  65. package/dist/lib/email.js +67 -108
  66. package/dist/lib/embeddings.d.ts +2 -0
  67. package/dist/lib/embeddings.js +1 -0
  68. package/dist/lib/ensure-curated-kb-catalog.d.ts +6 -0
  69. package/dist/lib/ensure-curated-kb-catalog.js +53 -0
  70. package/dist/lib/env.d.ts +1 -5
  71. package/dist/lib/env.js +2 -7
  72. package/dist/lib/inference.d.ts +1 -8
  73. package/dist/lib/inference.js +1 -19
  74. package/dist/lib/kb-meta.d.ts +8 -0
  75. package/dist/lib/kb-meta.js +77 -0
  76. package/dist/lib/note-text.d.ts +1 -0
  77. package/dist/lib/note-text.js +47 -0
  78. package/dist/lib/notification-service.test.js +37 -36
  79. package/dist/lib/pdf.d.ts +11 -0
  80. package/dist/lib/pdf.js +11 -0
  81. package/dist/lib/usage_service.d.ts +2 -1
  82. package/dist/lib/usage_service.js +30 -12
  83. package/dist/lib/worksheet-generation.js +4 -4
  84. package/dist/lib/worksheet-generation.test.js +32 -17
  85. package/dist/lib/workspace-kb.d.ts +5 -0
  86. package/dist/lib/workspace-kb.js +7 -0
  87. package/dist/models/controller-context.model.d.ts +8 -0
  88. package/dist/models/controller-context.model.js +1 -0
  89. package/dist/repositories/artifact.repository.d.ts +60 -0
  90. package/dist/repositories/artifact.repository.js +40 -0
  91. package/dist/repositories/base.repository.d.ts +14 -0
  92. package/dist/repositories/base.repository.js +14 -0
  93. package/dist/repositories/invitation.repository.d.ts +94 -0
  94. package/dist/repositories/invitation.repository.js +44 -0
  95. package/dist/repositories/notification.repository.d.ts +72 -0
  96. package/dist/repositories/notification.repository.js +44 -0
  97. package/dist/repositories/router-module.repository.d.ts +10 -0
  98. package/dist/repositories/router-module.repository.js +14 -0
  99. package/dist/repositories/user.repository.d.ts +74 -0
  100. package/dist/repositories/user.repository.js +37 -0
  101. package/dist/repositories/workspace-member.repository.d.ts +31 -0
  102. package/dist/repositories/workspace-member.repository.js +31 -0
  103. package/dist/repositories/workspace.repository.d.ts +97 -0
  104. package/dist/repositories/workspace.repository.js +79 -0
  105. package/dist/routers/_app.d.ts +528 -33
  106. package/dist/routers/_app.js +4 -0
  107. package/dist/routers/admin.d.ts +0 -4
  108. package/dist/routers/admin.js +21 -549
  109. package/dist/routers/annotations.js +12 -170
  110. package/dist/routers/artifactVersions.d.ts +65 -0
  111. package/dist/routers/artifactVersions.js +14 -0
  112. package/dist/routers/auth.d.ts +0 -6
  113. package/dist/routers/auth.js +36 -421
  114. package/dist/routers/chat.js +15 -229
  115. package/dist/routers/copilot.d.ts +14 -13
  116. package/dist/routers/copilot.js +13 -532
  117. package/dist/routers/flashcards.d.ts +5 -5
  118. package/dist/routers/flashcards.js +23 -349
  119. package/dist/routers/knowledgeBase.d.ts +421 -0
  120. package/dist/routers/knowledgeBase.js +118 -0
  121. package/dist/routers/members.d.ts +0 -41
  122. package/dist/routers/members.js +22 -710
  123. package/dist/routers/notes.d.ts +94 -0
  124. package/dist/routers/notes.js +37 -0
  125. package/dist/routers/notifications.js +7 -109
  126. package/dist/routers/payment.d.ts +3 -2
  127. package/dist/routers/payment.js +11 -393
  128. package/dist/routers/podcast.d.ts +1 -1
  129. package/dist/routers/podcast.js +11 -784
  130. package/dist/routers/studyguide.js +3 -129
  131. package/dist/routers/worksheets.d.ts +29 -14
  132. package/dist/routers/worksheets.js +49 -628
  133. package/dist/routers/workspace.d.ts +0 -4
  134. package/dist/routers/workspace.js +28 -922
  135. package/dist/scripts/purge-deleted-users.js +2 -2
  136. package/dist/server.js +10 -3
  137. package/dist/services/activity/activity-human-description.service.d.ts +13 -0
  138. package/dist/services/activity/activity-human-description.service.js +221 -0
  139. package/dist/services/activity/activity-human-description.service.test.d.ts +1 -0
  140. package/dist/services/activity/activity-human-description.service.test.js +16 -0
  141. package/dist/services/activity/activity-log.service.d.ts +87 -0
  142. package/dist/services/activity/activity-log.service.js +276 -0
  143. package/dist/services/activity/activity-log.service.test.d.ts +1 -0
  144. package/dist/services/activity/activity-log.service.test.js +27 -0
  145. package/dist/services/activity-human-description.service.d.ts +13 -0
  146. package/dist/services/activity-human-description.service.js +221 -0
  147. package/dist/services/activity-human-description.service.test.d.ts +1 -0
  148. package/dist/services/activity-human-description.service.test.js +16 -0
  149. package/dist/services/activity-log.service.d.ts +87 -0
  150. package/dist/services/activity-log.service.js +276 -0
  151. package/dist/services/activity-log.service.test.d.ts +1 -0
  152. package/dist/services/activity-log.service.test.js +27 -0
  153. package/dist/services/admin/admin.service.d.ts +270 -0
  154. package/dist/services/admin/admin.service.js +476 -0
  155. package/dist/services/admin.service.d.ts +270 -0
  156. package/dist/services/admin.service.js +476 -0
  157. package/dist/services/ai/ai-session.service.d.ts +5 -0
  158. package/dist/services/ai/ai-session.service.js +4 -0
  159. package/dist/services/ai-session.service.d.ts +60 -0
  160. package/dist/services/ai-session.service.js +561 -0
  161. package/dist/services/annotation.service.d.ts +177 -0
  162. package/dist/services/annotation.service.js +154 -0
  163. package/dist/services/artifact-notification.service.d.ts +14 -0
  164. package/dist/services/artifact-notification.service.js +20 -0
  165. package/dist/services/artifact-version.service.d.ts +38 -0
  166. package/dist/services/artifact-version.service.js +129 -0
  167. package/dist/services/artifacts/annotation.service.d.ts +177 -0
  168. package/dist/services/artifacts/annotation.service.js +154 -0
  169. package/dist/services/artifacts/artifact-version.service.d.ts +38 -0
  170. package/dist/services/artifacts/artifact-version.service.js +129 -0
  171. package/dist/services/artifacts/chat.service.d.ts +127 -0
  172. package/dist/services/artifacts/chat.service.js +182 -0
  173. package/dist/services/artifacts/study-guide.service.d.ts +18 -0
  174. package/dist/services/artifacts/study-guide.service.js +65 -0
  175. package/dist/services/auth/auth.service.d.ts +94 -0
  176. package/dist/services/auth/auth.service.js +368 -0
  177. package/dist/services/auth.service.d.ts +94 -0
  178. package/dist/services/auth.service.js +368 -0
  179. package/dist/services/base.service.d.ts +14 -0
  180. package/dist/services/base.service.js +14 -0
  181. package/dist/services/billing/payment.service.d.ts +55 -0
  182. package/dist/services/billing/payment.service.js +368 -0
  183. package/dist/services/billing/subscription.service.d.ts +37 -0
  184. package/dist/services/billing/subscription.service.js +654 -0
  185. package/dist/services/billing/usage.service.d.ts +27 -0
  186. package/dist/services/billing/usage.service.js +77 -0
  187. package/dist/services/chat.service.d.ts +127 -0
  188. package/dist/services/chat.service.js +182 -0
  189. package/dist/services/content/copilot.service.d.ts +113 -0
  190. package/dist/services/content/copilot.service.js +453 -0
  191. package/dist/services/content/flashcard-progress.service.d.ts +159 -0
  192. package/dist/services/content/flashcard-progress.service.js +432 -0
  193. package/dist/services/content/flashcard.service.d.ts +140 -0
  194. package/dist/services/content/flashcard.service.js +326 -0
  195. package/dist/services/content/media-analysis.service.d.ts +23 -0
  196. package/dist/services/content/media-analysis.service.js +404 -0
  197. package/dist/services/content/podcast.service.d.ts +267 -0
  198. package/dist/services/content/podcast.service.js +653 -0
  199. package/dist/services/content/worksheet-content.service.d.ts +37 -0
  200. package/dist/services/content/worksheet-content.service.js +84 -0
  201. package/dist/services/content/worksheet-content.service.test.d.ts +1 -0
  202. package/dist/services/content/worksheet-content.service.test.js +69 -0
  203. package/dist/services/content/worksheet-generation.service.d.ts +91 -0
  204. package/dist/services/content/worksheet-generation.service.js +95 -0
  205. package/dist/services/content/worksheet-generation.service.test.d.ts +1 -0
  206. package/dist/services/content/worksheet-generation.service.test.js +20 -0
  207. package/dist/services/content/worksheet.service.d.ts +347 -0
  208. package/dist/services/content/worksheet.service.js +599 -0
  209. package/dist/services/copilot.service.d.ts +116 -0
  210. package/dist/services/copilot.service.js +447 -0
  211. package/dist/services/flashcard-progress.service.d.ts +2 -2
  212. package/dist/services/flashcard-progress.service.js +3 -2
  213. package/dist/services/flashcard.service.d.ts +140 -0
  214. package/dist/services/flashcard.service.js +325 -0
  215. package/dist/services/invitation.service.d.ts +66 -0
  216. package/dist/services/invitation.service.js +348 -0
  217. package/dist/services/knowledge/knowledge-base.service.d.ts +316 -0
  218. package/dist/services/knowledge/knowledge-base.service.js +544 -0
  219. package/dist/services/knowledge-base.service.d.ts +316 -0
  220. package/dist/services/knowledge-base.service.js +536 -0
  221. package/dist/services/media-analysis.service.d.ts +23 -0
  222. package/dist/services/media-analysis.service.js +384 -0
  223. package/dist/services/member.service.d.ts +36 -0
  224. package/dist/services/member.service.js +193 -0
  225. package/dist/services/members/invitation.service.d.ts +66 -0
  226. package/dist/services/members/invitation.service.js +348 -0
  227. package/dist/services/members/member.service.d.ts +36 -0
  228. package/dist/services/members/member.service.js +193 -0
  229. package/dist/services/note.service.d.ts +55 -0
  230. package/dist/services/note.service.js +111 -0
  231. package/dist/services/notification.service.d.ts +214 -0
  232. package/dist/services/notification.service.js +550 -0
  233. package/dist/services/notification.service.test.d.ts +1 -0
  234. package/dist/services/notification.service.test.js +87 -0
  235. package/dist/services/notifications/notification.service.d.ts +214 -0
  236. package/dist/services/notifications/notification.service.js +550 -0
  237. package/dist/services/notifications/notification.service.test.d.ts +1 -0
  238. package/dist/services/notifications/notification.service.test.js +87 -0
  239. package/dist/services/payment.service.d.ts +55 -0
  240. package/dist/services/payment.service.js +368 -0
  241. package/dist/services/podcast.service.d.ts +267 -0
  242. package/dist/services/podcast.service.js +654 -0
  243. package/dist/services/router-module.service.d.ts +7 -0
  244. package/dist/services/router-module.service.js +10 -0
  245. package/dist/services/study-guide.service.d.ts +18 -0
  246. package/dist/services/study-guide.service.js +65 -0
  247. package/dist/services/subscription.service.d.ts +37 -0
  248. package/dist/services/subscription.service.js +654 -0
  249. package/dist/services/usage-limit-policy.service.d.ts +12 -0
  250. package/dist/services/usage-limit-policy.service.js +22 -0
  251. package/dist/services/usage-limit-policy.service.test.d.ts +1 -0
  252. package/dist/services/usage-limit-policy.service.test.js +46 -0
  253. package/dist/services/usage.service.d.ts +27 -0
  254. package/dist/services/usage.service.js +77 -0
  255. package/dist/services/worksheet-content.service.d.ts +42 -0
  256. package/dist/services/worksheet-content.service.js +84 -0
  257. package/dist/services/worksheet-content.service.test.d.ts +1 -0
  258. package/dist/services/worksheet-content.service.test.js +69 -0
  259. package/dist/services/worksheet-generation.service.d.ts +91 -0
  260. package/dist/services/worksheet-generation.service.js +95 -0
  261. package/dist/services/worksheet-generation.service.test.d.ts +1 -0
  262. package/dist/services/worksheet-generation.service.test.js +20 -0
  263. package/dist/services/worksheet.service.d.ts +385 -0
  264. package/dist/services/worksheet.service.js +596 -0
  265. package/dist/services/workspace/workspace-analytics.service.d.ts +24 -0
  266. package/dist/services/workspace/workspace-analytics.service.js +95 -0
  267. package/dist/services/workspace/workspace-kb.service.d.ts +40 -0
  268. package/dist/services/workspace/workspace-kb.service.js +184 -0
  269. package/dist/services/workspace/workspace.service.d.ts +307 -0
  270. package/dist/services/workspace/workspace.service.js +394 -0
  271. package/dist/services/workspace-analytics.service.d.ts +24 -0
  272. package/dist/services/workspace-analytics.service.js +95 -0
  273. package/dist/services/workspace-kb.service.d.ts +40 -0
  274. package/dist/services/workspace-kb.service.js +184 -0
  275. package/dist/services/workspace-progress.service.d.ts +27 -0
  276. package/dist/services/workspace-progress.service.js +56 -0
  277. package/dist/services/workspace-progress.service.test.d.ts +1 -0
  278. package/dist/services/workspace-progress.service.test.js +49 -0
  279. package/dist/services/workspace.service.d.ts +307 -0
  280. package/dist/services/workspace.service.js +390 -0
  281. package/dist/trpc.js +2 -2
  282. package/package.json +5 -6
  283. package/prisma/migrations/20260509000001_add_knowledge_base/migration.sql +99 -0
  284. package/prisma/migrations/20260509000002_curate_knowledge_base/migration.sql +52 -0
  285. package/prisma/migrations/20260522000000_add_notes/migration.sql +27 -0
  286. package/prisma/migrations/20260524000000_remove_notes/migration.sql +3 -0
  287. package/prisma/schema.prisma +150 -48
  288. package/prisma/seed.mjs +67 -0
  289. package/scripts/debug/README.md +4 -0
  290. package/src/README.md +63 -0
  291. package/src/lib/ai/config.ts +34 -0
  292. package/src/lib/ai/embedding-client.ts +47 -0
  293. package/src/lib/ai/index.ts +62 -0
  294. package/src/lib/ai/inference-backend/client.ts +479 -0
  295. package/src/lib/ai/inference-backend/mocks.ts +171 -0
  296. package/src/lib/ai/inference-backend/types.ts +50 -0
  297. package/src/lib/ai/json-parse.ts +35 -0
  298. package/src/lib/ai/llm-client.ts +31 -0
  299. package/src/lib/ai/mock.ts +12 -0
  300. package/src/lib/ai/types.ts +11 -0
  301. package/src/lib/chunking.ts +81 -0
  302. package/src/lib/curated-kb-seed.ts +164 -0
  303. package/src/lib/email.ts +77 -115
  304. package/src/lib/embeddings.ts +9 -0
  305. package/src/lib/ensure-curated-kb-catalog.ts +60 -0
  306. package/src/lib/env.ts +2 -7
  307. package/src/lib/inference.ts +1 -21
  308. package/src/lib/kb-meta.ts +81 -0
  309. package/src/lib/pdf.ts +23 -0
  310. package/src/lib/workspace-kb.ts +7 -0
  311. package/src/repositories/artifact.repository.ts +55 -0
  312. package/src/repositories/base.repository.ts +19 -0
  313. package/src/repositories/invitation.repository.ts +53 -0
  314. package/src/repositories/notification.repository.ts +53 -0
  315. package/src/repositories/user.repository.ts +44 -0
  316. package/src/repositories/workspace-member.repository.ts +38 -0
  317. package/src/repositories/workspace.repository.ts +89 -0
  318. package/src/routers/_app.ts +4 -0
  319. package/src/routers/admin.ts +124 -692
  320. package/src/routers/annotations.ts +25 -203
  321. package/src/routers/artifactVersions.ts +32 -0
  322. package/src/routers/auth.ts +81 -519
  323. package/src/routers/chat.ts +42 -245
  324. package/src/routers/copilot.ts +41 -666
  325. package/src/routers/flashcards.ts +108 -404
  326. package/src/routers/knowledgeBase.ts +216 -0
  327. package/src/routers/members.ts +60 -782
  328. package/src/routers/notifications.ts +15 -117
  329. package/src/routers/payment.ts +37 -446
  330. package/src/routers/podcast.ts +36 -898
  331. package/src/routers/studyguide.ts +5 -144
  332. package/src/routers/worksheets.ts +171 -735
  333. package/src/routers/workspace.ts +138 -1109
  334. package/src/scripts/purge-deleted-users.ts +2 -2
  335. package/src/server.ts +10 -3
  336. package/src/{lib/activity_human_description.test.ts → services/activity/activity-human-description.service.test.ts} +1 -1
  337. package/src/{lib/activity_log_service.test.ts → services/activity/activity-log.service.test.ts} +1 -1
  338. package/src/{lib/activity_log_service.ts → services/activity/activity-log.service.ts} +2 -2
  339. package/src/services/admin/admin.service.ts +612 -0
  340. package/src/services/ai/ai-session.service.ts +5 -0
  341. package/src/services/artifacts/annotation.service.ts +189 -0
  342. package/src/services/artifacts/artifact-version.service.ts +151 -0
  343. package/src/services/artifacts/chat.service.ts +197 -0
  344. package/src/services/artifacts/study-guide.service.ts +72 -0
  345. package/src/services/auth/auth.service.ts +473 -0
  346. package/src/services/base.service.ts +19 -0
  347. package/src/services/billing/payment.service.ts +436 -0
  348. package/src/{lib/subscription_service.ts → services/billing/subscription.service.ts} +5 -5
  349. package/src/{lib/usage_service.ts → services/billing/usage.service.ts} +32 -12
  350. package/src/services/content/copilot.service.ts +596 -0
  351. package/src/services/{flashcard-progress.service.ts → content/flashcard-progress.service.ts} +6 -3
  352. package/src/services/content/flashcard.service.ts +394 -0
  353. package/src/services/content/media-analysis.service.ts +556 -0
  354. package/src/services/content/podcast.service.ts +777 -0
  355. package/src/services/content/worksheet-content.service.test.ts +83 -0
  356. package/src/services/content/worksheet-content.service.ts +117 -0
  357. package/src/{lib/worksheet-generation.test.ts → services/content/worksheet-generation.service.test.ts} +1 -1
  358. package/src/services/content/worksheet.service.ts +751 -0
  359. package/src/services/knowledge/knowledge-base.service.ts +705 -0
  360. package/src/services/members/invitation.service.ts +427 -0
  361. package/src/services/members/member.service.ts +241 -0
  362. package/src/{lib/notification-service.test.ts → services/notifications/notification.service.test.ts} +2 -2
  363. package/src/{lib/notification-service.ts → services/notifications/notification.service.ts} +102 -1
  364. package/src/services/workspace/workspace-analytics.service.ts +107 -0
  365. package/src/services/workspace/workspace-kb.service.ts +273 -0
  366. package/src/services/workspace/workspace.service.ts +481 -0
  367. package/src/trpc.ts +2 -2
  368. package/src/lib/ai-session.ts +0 -704
  369. package/src/lib/workspace-access.ts +0 -13
  370. /package/{check-difficulty.cjs → scripts/debug/check-difficulty.cjs} +0 -0
  371. /package/{check-questions.cjs → scripts/debug/check-questions.cjs} +0 -0
  372. /package/{db-summary.cjs → scripts/debug/db-summary.cjs} +0 -0
  373. /package/{mcq-test.cjs → scripts/debug/mcq-test.cjs} +0 -0
  374. /package/{test-generate.js → scripts/debug/test-generate.js} +0 -0
  375. /package/{test-ratio.cjs → scripts/debug/test-ratio.cjs} +0 -0
  376. /package/{zod-test.cjs → scripts/debug/zod-test.cjs} +0 -0
  377. /package/src/{lib/activity_human_description.ts → services/activity/activity-human-description.service.ts} +0 -0
  378. /package/src/{lib/worksheet-generation.ts → services/content/worksheet-generation.service.ts} +0 -0
@@ -1,780 +1,216 @@
1
1
  import { z } from 'zod';
2
- import { TRPCError } from '@trpc/server';
3
- import { router, authedProcedure, verifiedProcedure, limitedProcedure } from '../trpc.js';
4
- import { aiSessionService } from '../lib/ai-session.js';
5
- import PusherService from '../lib/pusher.js';
6
- import { notifyArtifactFailed, notifyArtifactReady } from '../lib/notification-service.js';
7
- import { logger } from '../lib/logger.js';
8
- import { ArtifactType } from '../lib/constants.js';
9
- import { workspaceAccessFilter } from '../lib/workspace-access.js';
2
+ import { router, authedProcedure, limitedProcedure } from '../trpc.js';
3
+ import { WorksheetService } from '../services/content/worksheet.service.js';
10
4
  import {
11
- mergeWorksheetGenerationConfig,
12
- normalizeWorksheetProblemForDb,
13
- parsePresetConfig,
14
5
  worksheetModeSchema,
15
6
  worksheetPresetConfigPartialSchema,
16
- } from '../lib/worksheet-generation.js';
17
-
18
- const Difficulty = {
19
- EASY: 'EASY',
20
- MEDIUM: 'MEDIUM',
21
- HARD: 'HARD',
22
- } as const;
23
-
24
- const QuestionType = {
25
- MULTIPLE_CHOICE: 'MULTIPLE_CHOICE',
26
- TEXT: 'TEXT',
27
- NUMERIC: 'NUMERIC',
28
- TRUE_FALSE: 'TRUE_FALSE',
29
- MATCHING: 'MATCHING',
30
- FILL_IN_THE_BLANK: 'FILL_IN_THE_BLANK',
31
- } as const;
7
+ } from '../services/content/worksheet-generation.service.js';
8
+
9
+ const questionTypeEnum = z.enum([
10
+ 'MULTIPLE_CHOICE',
11
+ 'TEXT',
12
+ 'NUMERIC',
13
+ 'TRUE_FALSE',
14
+ 'MATCHING',
15
+ 'FILL_IN_THE_BLANK',
16
+ ]);
17
+ const difficultyEnum = z.enum(['EASY', 'MEDIUM', 'HARD']);
32
18
 
33
19
  export const worksheets = router({
34
- // List all worksheet artifacts for a workspace
35
20
  list: authedProcedure
36
21
  .input(z.object({ workspaceId: z.string() }))
37
- .query(async ({ ctx, input }) => {
38
- const worksheets = await ctx.db.artifact.findMany({
39
- where: { workspaceId: input.workspaceId, type: ArtifactType.WORKSHEET },
40
- include: {
41
- versions: {
42
- orderBy: { version: 'desc' },
43
- take: 1, // Get only the latest version
44
- },
45
- questions: true,
46
- },
47
- orderBy: { updatedAt: 'desc' },
48
- });
49
- if (!worksheets) throw new TRPCError({ code: 'NOT_FOUND' });
50
-
51
- // Merge per-user progress into question.meta for compatibility with UI
52
- const allQuestionIds = worksheets.flatMap(w => w.questions.map(q => q.id));
53
- if (allQuestionIds.length === 0) return worksheets;
54
-
55
- const progress = await ctx.db.worksheetQuestionProgress.findMany({
56
- where: { userId: ctx.session.user.id, worksheetQuestionId: { in: allQuestionIds } },
57
- });
58
- const progressByQuestionId = new Map(progress.map(p => [p.worksheetQuestionId, p]));
59
-
60
- const merged = worksheets.map(w => ({
61
- ...w,
62
- questions: w.questions.map(q => {
63
- const p = progressByQuestionId.get(q.id);
64
- if (!p) return q;
65
- const existingMeta = q.meta ? (typeof q.meta === 'object' ? q.meta : JSON.parse(q.meta as any)) : {} as any;
66
- const progressMeta = p.meta ? (typeof p.meta === 'object' ? p.meta : JSON.parse(p.meta as any)) : {} as any;
67
- return {
68
- ...q,
69
- meta: {
70
- ...existingMeta,
71
- completed: p.modified,
72
- userAnswer: p.userAnswer,
73
- completedAt: p.completedAt,
74
- userMarkScheme: progressMeta.userMarkScheme,
75
- },
76
- } as typeof q;
77
- }),
78
- }));
79
-
80
- return merged;
81
- }),
22
+ .query(({ ctx, input }) =>
23
+ new WorksheetService(ctx.db).list(ctx.session.user.id, input.workspaceId),
24
+ ),
82
25
 
83
26
  listPresets: authedProcedure
84
27
  .input(z.object({ workspaceId: z.string() }))
85
- .query(async ({ ctx, input }) => {
86
- const ws = await ctx.db.workspace.findFirst({
87
- where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
88
- });
89
- if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
90
-
91
- return ctx.db.worksheetPreset.findMany({
92
- where: {
93
- OR: [
94
- { isSystem: true },
95
- {
96
- userId: ctx.session.user.id,
97
- OR: [{ workspaceId: input.workspaceId }, { workspaceId: null }],
98
- },
99
- ],
100
- },
101
- orderBy: [{ isSystem: 'desc' }, { name: 'asc' }],
102
- });
103
- }),
28
+ .query(({ ctx, input }) =>
29
+ new WorksheetService(ctx.db).listPresets(ctx.session.user.id, input.workspaceId),
30
+ ),
104
31
 
105
32
  createPreset: authedProcedure
106
- .input(z.object({
107
- workspaceId: z.string().optional(),
108
- name: z.string().min(1).max(120),
109
- config: z.record(z.string(), z.unknown()),
110
- }))
111
- .mutation(async ({ ctx, input }) => {
112
- if (input.workspaceId) {
113
- const ws = await ctx.db.workspace.findFirst({
114
- where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
115
- });
116
- if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
117
- }
118
- const config = parsePresetConfig(input.config);
119
- return ctx.db.worksheetPreset.create({
120
- data: {
121
- userId: ctx.session.user.id,
122
- workspaceId: input.workspaceId ?? null,
123
- name: input.name,
124
- isSystem: false,
125
- config: config as object,
126
- },
127
- });
128
- }),
33
+ .input(
34
+ z.object({
35
+ workspaceId: z.string().optional(),
36
+ name: z.string().min(1).max(120),
37
+ config: z.record(z.string(), z.unknown()),
38
+ }),
39
+ )
40
+ .mutation(({ ctx, input }) =>
41
+ new WorksheetService(ctx.db).createPreset(ctx.session.user.id, input),
42
+ ),
129
43
 
130
44
  updatePreset: authedProcedure
131
- .input(z.object({
132
- id: z.string(),
133
- name: z.string().min(1).max(120).optional(),
134
- config: z.record(z.string(), z.unknown()).optional(),
135
- }).refine(d => d.name !== undefined || d.config !== undefined, { message: 'Provide name and/or config' }))
136
- .mutation(async ({ ctx, input }) => {
137
- const existing = await ctx.db.worksheetPreset.findFirst({
138
- where: { id: input.id, userId: ctx.session.user.id, isSystem: false },
139
- });
140
- if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found or read-only' });
141
-
142
- const data: { name?: string; config?: object } = {};
143
- if (input.name !== undefined) data.name = input.name;
144
- if (input.config !== undefined) data.config = parsePresetConfig(input.config) as object;
145
-
146
- return ctx.db.worksheetPreset.update({
147
- where: { id: input.id },
148
- data,
149
- });
150
- }),
45
+ .input(
46
+ z
47
+ .object({
48
+ id: z.string(),
49
+ name: z.string().min(1).max(120).optional(),
50
+ config: z.record(z.string(), z.unknown()).optional(),
51
+ })
52
+ .refine((d) => d.name !== undefined || d.config !== undefined, {
53
+ message: 'Provide name and/or config',
54
+ }),
55
+ )
56
+ .mutation(({ ctx, input }) =>
57
+ new WorksheetService(ctx.db).updatePreset(ctx.session.user.id, input),
58
+ ),
151
59
 
152
60
  deletePreset: authedProcedure
153
61
  .input(z.object({ id: z.string() }))
154
- .mutation(async ({ ctx, input }) => {
155
- const existing = await ctx.db.worksheetPreset.findFirst({
156
- where: { id: input.id, userId: ctx.session.user.id, isSystem: false },
157
- });
158
- if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found or read-only' });
159
-
160
- await ctx.db.worksheetPreset.delete({ where: { id: input.id } });
161
- return true;
162
- }),
62
+ .mutation(({ ctx, input }) =>
63
+ new WorksheetService(ctx.db).deletePreset(ctx.session.user.id, input.id),
64
+ ),
163
65
 
164
- // Create a worksheet set
165
66
  create: limitedProcedure
166
- .input(z.object({
167
- workspaceId: z.string(),
168
- title: z.string().min(1).max(120),
169
- description: z.string().optional(),
170
- difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
171
- estimatedTime: z.string().optional(),
172
- problems: z.array(z.object({
173
- question: z.string().min(1),
174
- answer: z.string().min(1),
175
- type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).default('TEXT'),
176
- options: z.array(z.string()).optional(),
177
- })).optional(),
178
- }))
179
- .mutation(async ({ ctx, input }) => {
180
- const workspace = await ctx.db.workspace.findFirst({
181
- where: { id: input.workspaceId, ownerId: ctx.session.user.id },
182
- });
183
- if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
184
-
185
- const { problems, ...worksheetData } = input;
67
+ .input(
68
+ z.object({
69
+ workspaceId: z.string(),
70
+ title: z.string().min(1).max(120),
71
+ description: z.string().optional(),
72
+ difficulty: difficultyEnum.optional(),
73
+ estimatedTime: z.string().optional(),
74
+ problems: z
75
+ .array(
76
+ z.object({
77
+ question: z.string().min(1),
78
+ answer: z.string().min(1),
79
+ type: questionTypeEnum.default('TEXT'),
80
+ options: z.array(z.string()).optional(),
81
+ }),
82
+ )
83
+ .optional(),
84
+ }),
85
+ )
86
+ .mutation(({ ctx, input }) =>
87
+ new WorksheetService(ctx.db).create(ctx.session.user.id, input as any),
88
+ ),
186
89
 
187
- return ctx.db.artifact.create({
188
- data: {
189
- workspaceId: input.workspaceId,
190
- type: ArtifactType.WORKSHEET,
191
- title: input.title,
192
- difficulty: input.difficulty as any,
193
- estimatedTime: input.estimatedTime,
194
- createdById: ctx.session.user.id,
195
- questions: problems ? {
196
- create: problems.map((problem, index) => ({
197
- prompt: problem.question,
198
- answer: problem.answer,
199
- type: problem.type as any,
200
- order: index,
201
- meta: problem.options ? { options: problem.options } : undefined,
202
- })),
203
- } : undefined,
204
- },
205
- include: {
206
- questions: true,
207
- },
208
- });
209
- }),
210
-
211
- // Get a worksheet with its questions
212
90
  get: authedProcedure
213
91
  .input(z.object({ worksheetId: z.string() }))
214
- .query(async ({ ctx, input }) => {
215
- const worksheet = await ctx.db.artifact.findFirst({
216
- where: {
217
- id: input.worksheetId,
218
- type: ArtifactType.WORKSHEET,
219
- workspace: workspaceAccessFilter(ctx.session.user.id),
220
- },
221
- include: { questions: true },
222
- orderBy: { updatedAt: 'desc' },
223
- });
224
- if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
225
-
226
- // Merge per-user progress into question.meta for compatibility with UI
227
- const questionIds = worksheet.questions.map(q => q.id);
228
- if (questionIds.length === 0) return worksheet;
229
- const progress = await ctx.db.worksheetQuestionProgress.findMany({
230
- where: { userId: ctx.session.user.id, worksheetQuestionId: { in: questionIds } },
231
- });
232
- const progressByQuestionId = new Map(progress.map(p => [p.worksheetQuestionId, p]));
92
+ .query(({ ctx, input }) =>
93
+ new WorksheetService(ctx.db).get(ctx.session.user.id, input.worksheetId),
94
+ ),
233
95
 
234
- const merged = {
235
- ...worksheet,
236
- questions: worksheet.questions.map(q => {
237
- const p = progressByQuestionId.get(q.id);
238
- if (!p) return q;
239
- const existingMeta = q.meta ? (typeof q.meta === 'object' ? q.meta : JSON.parse(q.meta as any)) : {} as any;
240
- const progressMeta = p.meta ? (typeof p.meta === 'object' ? p.meta : JSON.parse(p.meta as any)) : {} as any;
241
- return {
242
- ...q,
243
- meta: {
244
- ...existingMeta,
245
- completed: p.modified,
246
- userAnswer: p.userAnswer,
247
- completedAt: p.completedAt,
248
- userMarkScheme: progressMeta.userMarkScheme,
249
- },
250
- } as typeof q;
251
- }),
252
- };
253
-
254
- return merged;
255
- }),
256
-
257
- // Add a question to a worksheet
258
96
  createWorksheetQuestion: limitedProcedure
259
- .input(z.object({
260
- worksheetId: z.string(),
261
- prompt: z.string().min(1),
262
- answer: z.string().optional(),
263
- type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).optional(),
264
- difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
265
- order: z.number().int().optional(),
266
- meta: z.record(z.string(), z.unknown()).optional(),
267
- }))
268
- .mutation(async ({ ctx, input }) => {
269
- const worksheet = await ctx.db.artifact.findFirst({
270
- where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) },
271
- });
272
- if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
273
- return ctx.db.worksheetQuestion.create({
274
- data: {
275
- artifactId: input.worksheetId,
276
- prompt: input.prompt,
277
- answer: input.answer,
278
- type: (input.type ?? QuestionType.TEXT) as any,
279
- difficulty: (input.difficulty ?? Difficulty.MEDIUM) as any,
280
- order: input.order ?? 0,
281
- meta: input.meta as any,
282
- },
283
- });
284
- }),
97
+ .input(
98
+ z.object({
99
+ worksheetId: z.string(),
100
+ prompt: z.string().min(1),
101
+ answer: z.string().optional(),
102
+ type: questionTypeEnum.optional(),
103
+ difficulty: difficultyEnum.optional(),
104
+ order: z.number().int().optional(),
105
+ meta: z.record(z.string(), z.unknown()).optional(),
106
+ }),
107
+ )
108
+ .mutation(({ ctx, input }) =>
109
+ new WorksheetService(ctx.db).createWorksheetQuestion(ctx.session.user.id, input),
110
+ ),
285
111
 
286
- // Update a question
287
112
  updateWorksheetQuestion: authedProcedure
288
- .input(z.object({
289
- worksheetQuestionId: z.string(),
290
- prompt: z.string().optional(),
291
- answer: z.string().optional(),
292
- type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).optional(),
293
- difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
294
- order: z.number().int().optional(),
295
- meta: z.record(z.string(), z.unknown()).optional(),
296
- }))
297
- .mutation(async ({ ctx, input }) => {
298
- const q = await ctx.db.worksheetQuestion.findFirst({
299
- where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) } },
300
- });
301
- if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
302
- return ctx.db.worksheetQuestion.update({
303
- where: { id: input.worksheetQuestionId },
304
- data: {
305
- prompt: input.prompt ?? q.prompt,
306
- answer: input.answer ?? q.answer,
307
- type: (input.type ?? q.type) as any,
308
- difficulty: (input.difficulty ?? q.difficulty) as any,
309
- order: input.order ?? q.order,
310
- meta: (input.meta ?? q.meta) as any,
311
- },
312
- });
313
- }),
113
+ .input(
114
+ z.object({
115
+ worksheetQuestionId: z.string(),
116
+ prompt: z.string().optional(),
117
+ answer: z.string().optional(),
118
+ type: questionTypeEnum.optional(),
119
+ difficulty: difficultyEnum.optional(),
120
+ order: z.number().int().optional(),
121
+ meta: z.record(z.string(), z.unknown()).optional(),
122
+ }),
123
+ )
124
+ .mutation(({ ctx, input }) =>
125
+ new WorksheetService(ctx.db).updateWorksheetQuestion(ctx.session.user.id, input),
126
+ ),
314
127
 
315
- // Delete a question
316
128
  deleteWorksheetQuestion: authedProcedure
317
129
  .input(z.object({ worksheetQuestionId: z.string() }))
318
- .mutation(async ({ ctx, input }) => {
319
- const q = await ctx.db.worksheetQuestion.findFirst({
320
- where: { id: input.worksheetQuestionId, artifact: { workspace: workspaceAccessFilter(ctx.session.user.id) } },
321
- });
322
- if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
323
- await ctx.db.worksheetQuestion.delete({ where: { id: input.worksheetQuestionId } });
324
- return true;
325
- }),
130
+ .mutation(({ ctx, input }) =>
131
+ new WorksheetService(ctx.db).deleteWorksheetQuestion(
132
+ ctx.session.user.id,
133
+ input.worksheetQuestionId,
134
+ ),
135
+ ),
326
136
 
327
- // Update problem completion status
328
137
  updateProblemStatus: authedProcedure
329
- .input(z.object({
330
- problemId: z.string(),
331
- completed: z.boolean(),
332
- answer: z.string().optional(),
333
- correct: z.boolean().optional(),
334
- }))
335
- .mutation(async ({ ctx, input }) => {
336
- // Verify question ownership through worksheet
337
- const question = await ctx.db.worksheetQuestion.findFirst({
338
- where: {
339
- id: input.problemId,
340
- artifact: {
341
- type: ArtifactType.WORKSHEET,
342
- workspace: workspaceAccessFilter(ctx.session.user.id),
343
- },
344
- },
345
- });
346
- if (!question) throw new TRPCError({ code: 'NOT_FOUND' });
347
-
348
- // Upsert per-user progress row
349
- const progress = await ctx.db.worksheetQuestionProgress.upsert({
350
- where: {
351
- worksheetQuestionId_userId: {
352
- worksheetQuestionId: input.problemId,
353
- userId: ctx.session.user.id,
354
- },
355
- },
356
- create: {
357
- worksheetQuestionId: input.problemId,
358
- userId: ctx.session.user.id,
359
- modified: input.completed,
360
- userAnswer: input.answer,
361
- correct: input.correct,
362
- completedAt: input.completed ? new Date() : null,
363
- attempts: 1,
364
- },
365
- update: {
366
- modified: input.completed,
367
- userAnswer: input.answer,
368
- completedAt: input.completed ? new Date() : null,
369
- attempts: { increment: 1 },
370
- correct: input.correct,
371
- },
372
- });
138
+ .input(
139
+ z.object({
140
+ problemId: z.string(),
141
+ completed: z.boolean(),
142
+ answer: z.string().optional(),
143
+ correct: z.boolean().optional(),
144
+ }),
145
+ )
146
+ .mutation(({ ctx, input }) =>
147
+ new WorksheetService(ctx.db).updateProblemStatus(ctx.session.user.id, input),
148
+ ),
373
149
 
374
- return progress;
375
- }),
376
-
377
- // Get current user's progress for all questions in a worksheet
378
150
  getProgress: authedProcedure
379
151
  .input(z.object({ worksheetId: z.string() }))
380
- .query(async ({ ctx, input }) => {
381
- // Verify worksheet ownership
382
- const worksheet = await ctx.db.artifact.findFirst({
383
- where: {
384
- id: input.worksheetId,
385
- type: ArtifactType.WORKSHEET,
386
- workspace: workspaceAccessFilter(ctx.session.user.id),
387
- },
388
- });
389
- if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
390
-
391
- const questions = await ctx.db.worksheetQuestion.findMany({
392
- where: { artifactId: input.worksheetId },
393
- select: { id: true },
394
- });
395
- const questionIds = questions.map(q => q.id);
396
-
397
- const progress = await ctx.db.worksheetQuestionProgress.findMany({
398
- where: {
399
- userId: ctx.session.user.id,
400
- worksheetQuestionId: { in: questionIds },
401
- },
402
- });
403
-
404
- return progress;
405
- }),
152
+ .query(({ ctx, input }) =>
153
+ new WorksheetService(ctx.db).getProgress(ctx.session.user.id, input.worksheetId),
154
+ ),
406
155
 
407
- // Update a worksheet
408
156
  update: authedProcedure
409
- .input(z.object({
410
- id: z.string(),
411
- title: z.string().min(1).max(120).optional(),
412
- description: z.string().optional(),
413
- difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
414
- estimatedTime: z.string().optional(),
415
- problems: z.array(z.object({
416
- id: z.string().optional(),
417
- question: z.string().min(1),
418
- answer: z.string().min(1),
419
- type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).default('TEXT'),
420
- options: z.array(z.string()).optional(),
421
- })).optional(),
422
- }))
423
- .mutation(async ({ ctx, input }) => {
424
- const { id, problems, ...updateData } = input;
425
-
426
- // Verify worksheet ownership
427
- const existingWorksheet = await ctx.db.artifact.findFirst({
428
- where: {
429
- id,
430
- type: ArtifactType.WORKSHEET,
431
- workspace: workspaceAccessFilter(ctx.session.user.id),
432
- },
433
- });
434
- if (!existingWorksheet) throw new TRPCError({ code: 'NOT_FOUND' });
435
-
436
- // Handle questions update if provided
437
- if (problems) {
438
- // Delete existing questions and create new ones
439
- await ctx.db.worksheetQuestion.deleteMany({
440
- where: { artifactId: id },
441
- });
442
-
443
- await ctx.db.worksheetQuestion.createMany({
444
- data: problems.map((problem, index) => ({
445
- artifactId: id,
446
- prompt: problem.question,
447
- answer: problem.answer,
448
- type: problem.type as any,
449
- order: index,
450
- meta: problem.options ? { options: problem.options } : undefined,
451
- })),
452
- });
453
- }
157
+ .input(
158
+ z.object({
159
+ id: z.string(),
160
+ title: z.string().min(1).max(120).optional(),
161
+ description: z.string().optional(),
162
+ difficulty: difficultyEnum.optional(),
163
+ estimatedTime: z.string().optional(),
164
+ problems: z
165
+ .array(
166
+ z.object({
167
+ id: z.string().optional(),
168
+ question: z.string().min(1),
169
+ answer: z.string().min(1),
170
+ type: questionTypeEnum.default('TEXT'),
171
+ options: z.array(z.string()).optional(),
172
+ }),
173
+ )
174
+ .optional(),
175
+ }),
176
+ )
177
+ .mutation(({ ctx, input }) =>
178
+ new WorksheetService(ctx.db).update(ctx.session.user.id, input as any),
179
+ ),
454
180
 
455
- // Process update data
456
- const processedUpdateData = {
457
- ...updateData,
458
- difficulty: updateData.difficulty as any,
459
- };
460
-
461
- return ctx.db.artifact.update({
462
- where: { id },
463
- data: processedUpdateData,
464
- include: {
465
- questions: {
466
- orderBy: { order: 'asc' },
467
- },
468
- },
469
- });
470
- }),
471
-
472
- // Delete a worksheet set and its questions
473
181
  delete: authedProcedure
474
182
  .input(z.object({ id: z.string() }))
475
- .mutation(async ({ ctx, input }) => {
476
- const deleted = await ctx.db.artifact.deleteMany({
477
- where: { id: input.id, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) },
478
- });
479
- if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
480
- return true;
481
- }),
183
+ .mutation(({ ctx, input }) =>
184
+ new WorksheetService(ctx.db).delete(ctx.session.user.id, input.id),
185
+ ),
482
186
 
483
- // Generate a worksheet from a user prompt
484
187
  generateFromPrompt: limitedProcedure
485
- .input(z.object({
486
- workspaceId: z.string(),
487
- prompt: z.string().min(1),
488
- numQuestions: z.number().int().min(1).max(20).default(8),
489
- difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
490
- mode: worksheetModeSchema.optional(),
491
- presetId: z.string().optional(),
492
- configOverride: worksheetPresetConfigPartialSchema.optional(),
493
- title: z.string().optional(),
494
- estimatedTime: z.string().optional(),
495
- }))
496
- .mutation(async ({ ctx, input }) => {
497
- const workspace = await ctx.db.workspace.findFirst({
498
- where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
499
- });
500
- if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
501
-
502
- let presetRow: Awaited<ReturnType<typeof ctx.db.worksheetPreset.findFirst>> = null;
503
- if (input.presetId) {
504
- presetRow = await ctx.db.worksheetPreset.findFirst({
505
- where: {
506
- id: input.presetId,
507
- OR: [
508
- { isSystem: true },
509
- {
510
- userId: ctx.session.user.id,
511
- OR: [{ workspaceId: input.workspaceId }, { workspaceId: null }],
512
- },
513
- ],
514
- },
515
- });
516
- if (!presetRow) throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found' });
517
- }
518
-
519
- const presetConfig = presetRow?.config
520
- ? parsePresetConfig(presetRow.config)
521
- : undefined;
188
+ .input(
189
+ z.object({
190
+ workspaceId: z.string(),
191
+ prompt: z.string().min(1),
192
+ numQuestions: z.number().int().min(1).max(20).default(8),
193
+ difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
194
+ mode: worksheetModeSchema.optional(),
195
+ presetId: z.string().optional(),
196
+ configOverride: worksheetPresetConfigPartialSchema.optional(),
197
+ title: z.string().optional(),
198
+ estimatedTime: z.string().optional(),
199
+ }),
200
+ )
201
+ .mutation(({ ctx, input }) =>
202
+ new WorksheetService(ctx.db).generateFromPrompt(ctx.session.user.id, input),
203
+ ),
522
204
 
523
- const resolved = mergeWorksheetGenerationConfig(
524
- presetConfig,
525
- input.configOverride ?? undefined,
526
- {
527
- numQuestions: input.numQuestions,
528
- difficulty: input.difficulty,
529
- mode: input.mode,
530
- },
531
- );
532
-
533
- const difficultyUpper = resolved.difficulty.toUpperCase() as 'EASY' | 'MEDIUM' | 'HARD';
534
-
535
- console.log("[DEBUG TRPC PAYLOAD] input =", input);
536
- console.log("[DEBUG TRPC PAYLOAD] presetConfig =", presetConfig);
537
- console.log("[DEBUG TRPC PAYLOAD] legacy merged legacy values:", { numQuestions: input.numQuestions, difficulty: input.difficulty, mode: input.mode });
538
- console.log("[DEBUG TRPC PAYLOAD] RESOLVED =", resolved);
539
-
540
- const worksheetConfigSnapshot = {
541
- mode: resolved.mode,
542
- presetId: input.presetId ?? null,
543
- presetName: presetRow?.name ?? null,
544
- numQuestions: resolved.numQuestions,
545
- difficulty: resolved.difficulty,
546
- mcqRatio: resolved.mcqRatio ?? null,
547
- questionTypes: resolved.questionTypes ?? null,
548
- };
549
-
550
- await PusherService.emitWorksheetGenerationStart(input.workspaceId);
551
-
552
- const artifact = await ctx.db.artifact.create({
553
- data: {
554
- workspaceId: input.workspaceId,
555
- type: ArtifactType.WORKSHEET,
556
- title: input.title || (resolved.mode === 'quiz' ? `Quiz - ${new Date().toLocaleString()}` : `Worksheet - ${new Date().toLocaleString()}`),
557
- createdById: ctx.session.user.id,
558
- difficulty: difficultyUpper as any,
559
- estimatedTime: input.estimatedTime,
560
- generating: true,
561
- generatingMetadata: {
562
- quantity: resolved.numQuestions,
563
- difficulty: resolved.difficulty,
564
- mode: resolved.mode,
565
- presetId: input.presetId ?? null,
566
- },
567
- worksheetConfig: worksheetConfigSnapshot as object,
568
- },
569
- });
570
-
571
- await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: resolved.numQuestions });
572
- await PusherService.emitWorksheetNew(input.workspaceId, artifact);
573
-
574
- const userId = ctx.session.user.id;
575
- const workspaceId = input.workspaceId;
576
- const promptText = input.prompt;
577
- const estTime = input.estimatedTime;
578
-
579
- // Launch generation in the background to free up the connection pool immediately
580
- (async () => {
581
- try {
582
- const content = await aiSessionService.generateWorksheetQuestions(
583
- workspaceId,
584
- userId,
585
- resolved.numQuestions,
586
- difficultyUpper,
587
- {
588
- mode: resolved.mode,
589
- mcqRatio: resolved.mcqRatio,
590
- questionTypes: resolved.questionTypes,
591
- prompt: promptText,
592
- },
593
- );
594
-
595
- let problems: any[] = [];
596
- try {
597
- const worksheetData = JSON.parse(content);
598
- let actualWorksheetData: any = worksheetData;
599
- if (worksheetData.last_response) {
600
- try { actualWorksheetData = JSON.parse(worksheetData.last_response); } catch { /* noop */ }
601
- }
602
- problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
603
- if (!Array.isArray(problems)) problems = [];
604
- } catch (parseError) {
605
- logger.error('Failed to parse worksheet JSON', parseError);
606
- throw new Error('Failed to parse worksheet JSON');
607
- }
608
-
609
- const forceMcq = resolved.mode === 'quiz';
610
- const questionsToCreate = [];
611
- for (let i = 0; i < Math.min(problems.length, resolved.numQuestions); i++) {
612
- const problem = problems[i] && typeof problems[i] === 'object' ? problems[i] as Record<string, unknown> : {};
613
- const row = normalizeWorksheetProblemForDb(problem, i, difficultyUpper, forceMcq);
614
- const metaPayload: Record<string, unknown> = {};
615
- if (row.meta.options?.length) metaPayload.options = row.meta.options;
616
- if (row.meta.markScheme != null) metaPayload.markScheme = row.meta.markScheme;
617
-
618
- questionsToCreate.push({
619
- artifactId: artifact.id,
620
- prompt: row.prompt,
621
- answer: row.answer ?? '',
622
- difficulty: row.difficulty as any,
623
- order: row.order,
624
- type: row.type as any,
625
- meta: Object.keys(metaPayload).length ? (metaPayload as object) : undefined,
626
- });
627
- }
628
-
629
- if (questionsToCreate.length > 0) {
630
- await ctx.db.worksheetQuestion.createMany({
631
- data: questionsToCreate,
632
- });
633
- }
634
-
635
- let parsedTitle: string | undefined;
636
- let parsedDescription: string | undefined;
637
- let parsedEstimated: string | undefined;
638
- try {
639
- const worksheetData = JSON.parse(content);
640
- let actualWorksheetData: any = worksheetData;
641
- if (worksheetData.last_response) {
642
- try { actualWorksheetData = JSON.parse(worksheetData.last_response); } catch { /* noop */ }
643
- }
644
- parsedTitle = typeof actualWorksheetData.title === 'string' ? actualWorksheetData.title : undefined;
645
- parsedDescription = typeof actualWorksheetData.description === 'string' ? actualWorksheetData.description : undefined;
646
- parsedEstimated = typeof actualWorksheetData.estimatedTime === 'string' ? actualWorksheetData.estimatedTime : undefined;
647
- } catch { /* noop */ }
648
-
649
- await ctx.db.artifact.update({
650
- where: { id: artifact.id },
651
- data: {
652
- generating: false,
653
- title: parsedTitle || artifact.title,
654
- description: parsedDescription ?? undefined,
655
- estimatedTime: estTime || parsedEstimated || undefined,
656
- worksheetConfig: worksheetConfigSnapshot as object,
657
- },
658
- });
659
-
660
- const updatedWorksheet = await ctx.db.artifact.findFirst({
661
- where: { id: artifact.id },
662
- include: { questions: true }
663
- });
664
-
665
- await PusherService.emitWorksheetGenerationComplete(workspaceId, updatedWorksheet);
666
- await PusherService.emitWorksheetComplete(workspaceId, artifact);
667
-
668
- await notifyArtifactReady(ctx.db, {
669
- userId,
670
- workspaceId,
671
- artifactId: artifact.id,
672
- artifactType: ArtifactType.WORKSHEET,
673
- title: updatedWorksheet?.title ?? artifact.title,
674
- }).catch(() => {});
675
- } catch (error) {
676
- logger.error(`Background generation failed for artifact ${artifact.id}:`, error);
677
- await notifyArtifactFailed(ctx.db, {
678
- userId,
679
- workspaceId,
680
- artifactId: artifact.id,
681
- artifactType: ArtifactType.WORKSHEET,
682
- title: artifact.title,
683
- message: `Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`,
684
- }).catch(() => {});
685
-
686
- await ctx.db.artifact.deleteMany({
687
- where: { id: artifact.id },
688
- }).catch(() => { }); // Ignore delete errors if already gone
689
-
690
- await PusherService.emitError(
691
- workspaceId,
692
- `Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`,
693
- 'worksheet_generation'
694
- );
695
- }
696
- })();
697
-
698
- return { artifact };
699
- }),
700
205
  checkAnswer: authedProcedure
701
- .input(z.object({
702
- worksheetId: z.string(),
703
- questionId: z.string(),
704
- answer: z.string().min(1),
705
- }))
706
- .mutation(async ({ ctx, input }) => {
707
- const worksheet = await ctx.db.artifact.findFirst({ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) }, include: { workspace: true } });
708
- if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
709
- const question = await ctx.db.worksheetQuestion.findFirst({ where: { id: input.questionId, artifactId: input.worksheetId } });
710
- if (!question) throw new TRPCError({ code: 'NOT_FOUND' });
711
-
712
- // Parse question meta to get mark_scheme
713
- const questionMeta = question.meta ? (typeof question.meta === 'object' ? question.meta : JSON.parse(question.meta as any)) : {} as any;
714
- const markScheme = questionMeta.markScheme;
715
-
716
- let isCorrect = false;
717
- let userMarkScheme = null;
718
-
719
- // If mark scheme exists, use AI marking
720
- if (markScheme && markScheme.points && markScheme.points.length > 0) {
721
- try {
722
- userMarkScheme = await aiSessionService.checkWorksheetQuestions(
723
- worksheet.workspace.id,
724
- ctx.session.user.id,
725
- question.prompt,
726
- input.answer,
727
- markScheme
728
- );
729
-
730
- // Determine if correct by comparing achieved points vs total points
731
- const achievedTotal = userMarkScheme.points.reduce((sum: number, p: any) => sum + (p.achievedPoints || 0), 0);
732
- isCorrect = achievedTotal === markScheme.totalPoints;
733
-
734
- } catch (error) {
735
- logger.error('Failed to check answer with AI', error instanceof Error ? error.message : 'Unknown error');
736
- // Fallback to simple string comparison
737
- isCorrect = question.answer === input.answer;
738
- }
739
- } else {
740
- // Simple string comparison if no mark scheme
741
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'No mark scheme found for question' });
742
- }
743
-
744
-
745
-
746
- // @todo: figure out this wierd fix
747
- const progress = await ctx.db.worksheetQuestionProgress.upsert({
748
- where: {
749
- worksheetQuestionId_userId: {
750
- worksheetQuestionId: input.questionId,
751
- userId: ctx.session.user.id,
752
- },
753
- },
754
- create: {
755
- worksheetQuestionId: input.questionId,
756
- userId: ctx.session.user.id,
757
- modified: true,
758
- userAnswer: input.answer,
759
- correct: isCorrect,
760
- completedAt: new Date(),
761
- attempts: 1,
762
- meta: userMarkScheme ? { userMarkScheme: JSON.parse(JSON.stringify(userMarkScheme)) } : { userMarkScheme: null },
763
- },
764
- update: {
765
- modified: true,
766
- userAnswer: input.answer,
767
- correct: isCorrect,
768
- completedAt: new Date(),
769
- attempts: { increment: 1 },
770
- meta: userMarkScheme
771
- ? { userMarkScheme: JSON.parse(JSON.stringify(userMarkScheme)) }
772
- : { userMarkScheme: null },
773
- },
774
- });
775
-
776
- return { isCorrect, userMarkScheme, progress };
777
- }),
206
+ .input(
207
+ z.object({
208
+ worksheetId: z.string(),
209
+ questionId: z.string(),
210
+ answer: z.string().min(1),
211
+ }),
212
+ )
213
+ .mutation(({ ctx, input }) =>
214
+ new WorksheetService(ctx.db).checkAnswer(ctx.session.user.id, input),
215
+ ),
778
216
  });
779
-
780
-