@goscribe/server 1.3.4 → 1.6.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 (383) 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/context.d.ts +14 -1
  6. package/dist/context.js +23 -2
  7. package/dist/controllers/admin.controller.d.ts +715 -0
  8. package/dist/controllers/admin.controller.js +9 -0
  9. package/dist/controllers/annotations.controller.d.ts +439 -0
  10. package/dist/controllers/annotations.controller.js +9 -0
  11. package/dist/controllers/app-router.controller.d.ts +3011 -0
  12. package/dist/controllers/app-router.controller.js +38 -0
  13. package/dist/controllers/app-router.controller.test.d.ts +1 -0
  14. package/dist/controllers/app-router.controller.test.js +36 -0
  15. package/dist/controllers/auth.controller.d.ts +323 -0
  16. package/dist/controllers/auth.controller.js +9 -0
  17. package/dist/controllers/base.controller.d.ts +4 -0
  18. package/dist/controllers/base.controller.js +5 -0
  19. package/dist/controllers/chat.controller.d.ts +341 -0
  20. package/dist/controllers/chat.controller.js +9 -0
  21. package/dist/controllers/copilot.controller.d.ts +397 -0
  22. package/dist/controllers/copilot.controller.js +9 -0
  23. package/dist/controllers/flashcards.controller.d.ts +651 -0
  24. package/dist/controllers/flashcards.controller.js +9 -0
  25. package/dist/controllers/members.controller.d.ts +339 -0
  26. package/dist/controllers/members.controller.js +9 -0
  27. package/dist/controllers/notifications.controller.d.ts +199 -0
  28. package/dist/controllers/notifications.controller.js +9 -0
  29. package/dist/controllers/payment.controller.d.ts +181 -0
  30. package/dist/controllers/payment.controller.js +9 -0
  31. package/dist/controllers/podcast.controller.d.ts +575 -0
  32. package/dist/controllers/podcast.controller.js +9 -0
  33. package/dist/controllers/router-module.controller.d.ts +5 -0
  34. package/dist/controllers/router-module.controller.js +6 -0
  35. package/dist/controllers/studyguide.controller.d.ts +73 -0
  36. package/dist/controllers/studyguide.controller.js +9 -0
  37. package/dist/controllers/worksheets.controller.d.ts +829 -0
  38. package/dist/controllers/worksheets.controller.js +9 -0
  39. package/dist/controllers/workspace.controller.d.ts +1207 -0
  40. package/dist/controllers/workspace.controller.js +9 -0
  41. package/dist/lib/activity_human_description.test.js +16 -15
  42. package/dist/lib/activity_log_service.test.js +28 -23
  43. package/dist/lib/ai/config.d.ts +20 -0
  44. package/dist/lib/ai/config.js +31 -0
  45. package/dist/lib/ai/embedding-client.d.ts +8 -0
  46. package/dist/lib/ai/embedding-client.js +30 -0
  47. package/dist/lib/ai/index.d.ts +48 -0
  48. package/dist/lib/ai/index.js +29 -0
  49. package/dist/lib/ai/inference-backend/client.d.ts +28 -0
  50. package/dist/lib/ai/inference-backend/client.js +301 -0
  51. package/dist/lib/ai/inference-backend/mocks.d.ts +12 -0
  52. package/dist/lib/ai/inference-backend/mocks.js +133 -0
  53. package/dist/lib/ai/inference-backend/types.d.ts +44 -0
  54. package/dist/lib/ai/inference-backend/types.js +1 -0
  55. package/dist/lib/ai/json-parse.d.ts +2 -0
  56. package/dist/lib/ai/json-parse.js +34 -0
  57. package/dist/lib/ai/llm-client.d.ts +7 -0
  58. package/dist/lib/ai/llm-client.js +36 -0
  59. package/dist/lib/ai/mock.d.ts +2 -0
  60. package/dist/lib/ai/mock.js +10 -0
  61. package/dist/lib/ai/types.d.ts +9 -0
  62. package/dist/lib/ai/types.js +1 -0
  63. package/dist/lib/chunking.d.ts +19 -0
  64. package/dist/lib/chunking.js +47 -0
  65. package/dist/lib/curated-kb-seed.d.ts +12 -0
  66. package/dist/lib/curated-kb-seed.js +155 -0
  67. package/dist/lib/email.js +67 -108
  68. package/dist/lib/embeddings.d.ts +2 -0
  69. package/dist/lib/embeddings.js +1 -0
  70. package/dist/lib/ensure-curated-kb-catalog.d.ts +6 -0
  71. package/dist/lib/ensure-curated-kb-catalog.js +53 -0
  72. package/dist/lib/env.d.ts +1 -5
  73. package/dist/lib/env.js +2 -7
  74. package/dist/lib/inference.d.ts +1 -8
  75. package/dist/lib/inference.js +1 -19
  76. package/dist/lib/kb-meta.d.ts +8 -0
  77. package/dist/lib/kb-meta.js +77 -0
  78. package/dist/lib/note-text.d.ts +1 -0
  79. package/dist/lib/note-text.js +47 -0
  80. package/dist/lib/notification-service.test.js +37 -36
  81. package/dist/lib/pdf.d.ts +11 -0
  82. package/dist/lib/pdf.js +11 -0
  83. package/dist/lib/usage_service.d.ts +2 -1
  84. package/dist/lib/usage_service.js +30 -12
  85. package/dist/lib/worksheet-generation.js +4 -4
  86. package/dist/lib/worksheet-generation.test.js +32 -17
  87. package/dist/lib/workspace-kb.d.ts +5 -0
  88. package/dist/lib/workspace-kb.js +7 -0
  89. package/dist/models/controller-context.model.d.ts +8 -0
  90. package/dist/models/controller-context.model.js +1 -0
  91. package/dist/repositories/artifact.repository.d.ts +60 -0
  92. package/dist/repositories/artifact.repository.js +40 -0
  93. package/dist/repositories/base.repository.d.ts +14 -0
  94. package/dist/repositories/base.repository.js +14 -0
  95. package/dist/repositories/invitation.repository.d.ts +94 -0
  96. package/dist/repositories/invitation.repository.js +44 -0
  97. package/dist/repositories/notification.repository.d.ts +72 -0
  98. package/dist/repositories/notification.repository.js +44 -0
  99. package/dist/repositories/router-module.repository.d.ts +10 -0
  100. package/dist/repositories/router-module.repository.js +14 -0
  101. package/dist/repositories/user.repository.d.ts +74 -0
  102. package/dist/repositories/user.repository.js +37 -0
  103. package/dist/repositories/workspace-member.repository.d.ts +31 -0
  104. package/dist/repositories/workspace-member.repository.js +31 -0
  105. package/dist/repositories/workspace.repository.d.ts +97 -0
  106. package/dist/repositories/workspace.repository.js +79 -0
  107. package/dist/routers/_app.d.ts +566 -111
  108. package/dist/routers/_app.js +4 -0
  109. package/dist/routers/admin.d.ts +0 -4
  110. package/dist/routers/admin.js +21 -549
  111. package/dist/routers/annotations.js +12 -170
  112. package/dist/routers/artifactVersions.d.ts +65 -0
  113. package/dist/routers/artifactVersions.js +14 -0
  114. package/dist/routers/auth.d.ts +0 -6
  115. package/dist/routers/auth.js +37 -422
  116. package/dist/routers/chat.js +15 -229
  117. package/dist/routers/copilot.d.ts +14 -13
  118. package/dist/routers/copilot.js +13 -532
  119. package/dist/routers/flashcards.d.ts +17 -6
  120. package/dist/routers/flashcards.js +23 -349
  121. package/dist/routers/knowledgeBase.d.ts +421 -0
  122. package/dist/routers/knowledgeBase.js +118 -0
  123. package/dist/routers/members.d.ts +0 -41
  124. package/dist/routers/members.js +22 -710
  125. package/dist/routers/notes.d.ts +94 -0
  126. package/dist/routers/notes.js +37 -0
  127. package/dist/routers/notifications.js +7 -109
  128. package/dist/routers/payment.d.ts +2 -12
  129. package/dist/routers/payment.js +11 -393
  130. package/dist/routers/podcast.d.ts +1 -1
  131. package/dist/routers/podcast.js +11 -784
  132. package/dist/routers/studyguide.js +3 -129
  133. package/dist/routers/worksheets.d.ts +29 -14
  134. package/dist/routers/worksheets.js +49 -628
  135. package/dist/routers/workspace.d.ts +27 -71
  136. package/dist/routers/workspace.js +29 -922
  137. package/dist/scripts/purge-deleted-users.js +2 -2
  138. package/dist/server.js +10 -3
  139. package/dist/services/activity/activity-human-description.service.d.ts +13 -0
  140. package/dist/services/activity/activity-human-description.service.js +221 -0
  141. package/dist/services/activity/activity-human-description.service.test.d.ts +1 -0
  142. package/dist/services/activity/activity-human-description.service.test.js +16 -0
  143. package/dist/services/activity/activity-log.service.d.ts +87 -0
  144. package/dist/services/activity/activity-log.service.js +276 -0
  145. package/dist/services/activity/activity-log.service.test.d.ts +1 -0
  146. package/dist/services/activity/activity-log.service.test.js +27 -0
  147. package/dist/services/activity-human-description.service.d.ts +13 -0
  148. package/dist/services/activity-human-description.service.js +221 -0
  149. package/dist/services/activity-human-description.service.test.d.ts +1 -0
  150. package/dist/services/activity-human-description.service.test.js +16 -0
  151. package/dist/services/activity-log.service.d.ts +87 -0
  152. package/dist/services/activity-log.service.js +276 -0
  153. package/dist/services/activity-log.service.test.d.ts +1 -0
  154. package/dist/services/activity-log.service.test.js +27 -0
  155. package/dist/services/admin/admin.service.d.ts +270 -0
  156. package/dist/services/admin/admin.service.js +476 -0
  157. package/dist/services/admin.service.d.ts +270 -0
  158. package/dist/services/admin.service.js +476 -0
  159. package/dist/services/ai/ai-session.service.d.ts +5 -0
  160. package/dist/services/ai/ai-session.service.js +4 -0
  161. package/dist/services/ai-session.service.d.ts +60 -0
  162. package/dist/services/ai-session.service.js +561 -0
  163. package/dist/services/annotation.service.d.ts +177 -0
  164. package/dist/services/annotation.service.js +154 -0
  165. package/dist/services/artifact-notification.service.d.ts +14 -0
  166. package/dist/services/artifact-notification.service.js +20 -0
  167. package/dist/services/artifact-version.service.d.ts +38 -0
  168. package/dist/services/artifact-version.service.js +129 -0
  169. package/dist/services/artifacts/annotation.service.d.ts +177 -0
  170. package/dist/services/artifacts/annotation.service.js +154 -0
  171. package/dist/services/artifacts/artifact-version.service.d.ts +38 -0
  172. package/dist/services/artifacts/artifact-version.service.js +129 -0
  173. package/dist/services/artifacts/chat.service.d.ts +127 -0
  174. package/dist/services/artifacts/chat.service.js +182 -0
  175. package/dist/services/artifacts/study-guide.service.d.ts +18 -0
  176. package/dist/services/artifacts/study-guide.service.js +65 -0
  177. package/dist/services/auth/auth.service.d.ts +94 -0
  178. package/dist/services/auth/auth.service.js +368 -0
  179. package/dist/services/auth.service.d.ts +94 -0
  180. package/dist/services/auth.service.js +368 -0
  181. package/dist/services/base.service.d.ts +14 -0
  182. package/dist/services/base.service.js +14 -0
  183. package/dist/services/billing/payment.service.d.ts +44 -0
  184. package/dist/services/billing/payment.service.js +365 -0
  185. package/dist/services/billing/subscription.service.d.ts +37 -0
  186. package/dist/services/billing/subscription.service.js +654 -0
  187. package/dist/services/billing/usage.service.d.ts +47 -0
  188. package/dist/services/billing/usage.service.js +149 -0
  189. package/dist/services/chat.service.d.ts +127 -0
  190. package/dist/services/chat.service.js +182 -0
  191. package/dist/services/content/copilot.service.d.ts +113 -0
  192. package/dist/services/content/copilot.service.js +439 -0
  193. package/dist/services/content/flashcard-progress.service.d.ts +159 -0
  194. package/dist/services/content/flashcard-progress.service.js +432 -0
  195. package/dist/services/content/flashcard.service.d.ts +184 -0
  196. package/dist/services/content/flashcard.service.js +339 -0
  197. package/dist/services/content/media-analysis.service.d.ts +23 -0
  198. package/dist/services/content/media-analysis.service.js +404 -0
  199. package/dist/services/content/podcast.service.d.ts +267 -0
  200. package/dist/services/content/podcast.service.js +653 -0
  201. package/dist/services/content/worksheet-content.service.d.ts +37 -0
  202. package/dist/services/content/worksheet-content.service.js +84 -0
  203. package/dist/services/content/worksheet-content.service.test.d.ts +1 -0
  204. package/dist/services/content/worksheet-content.service.test.js +69 -0
  205. package/dist/services/content/worksheet-generation.service.d.ts +91 -0
  206. package/dist/services/content/worksheet-generation.service.js +95 -0
  207. package/dist/services/content/worksheet-generation.service.test.d.ts +1 -0
  208. package/dist/services/content/worksheet-generation.service.test.js +20 -0
  209. package/dist/services/content/worksheet.service.d.ts +347 -0
  210. package/dist/services/content/worksheet.service.js +599 -0
  211. package/dist/services/copilot.service.d.ts +116 -0
  212. package/dist/services/copilot.service.js +447 -0
  213. package/dist/services/flashcard-progress.service.d.ts +2 -2
  214. package/dist/services/flashcard-progress.service.js +3 -2
  215. package/dist/services/flashcard.service.d.ts +140 -0
  216. package/dist/services/flashcard.service.js +325 -0
  217. package/dist/services/invitation.service.d.ts +66 -0
  218. package/dist/services/invitation.service.js +348 -0
  219. package/dist/services/knowledge/knowledge-base.service.d.ts +316 -0
  220. package/dist/services/knowledge/knowledge-base.service.js +544 -0
  221. package/dist/services/knowledge-base.service.d.ts +316 -0
  222. package/dist/services/knowledge-base.service.js +536 -0
  223. package/dist/services/media-analysis.service.d.ts +23 -0
  224. package/dist/services/media-analysis.service.js +384 -0
  225. package/dist/services/member.service.d.ts +36 -0
  226. package/dist/services/member.service.js +193 -0
  227. package/dist/services/members/invitation.service.d.ts +66 -0
  228. package/dist/services/members/invitation.service.js +348 -0
  229. package/dist/services/members/member.service.d.ts +36 -0
  230. package/dist/services/members/member.service.js +193 -0
  231. package/dist/services/note.service.d.ts +55 -0
  232. package/dist/services/note.service.js +111 -0
  233. package/dist/services/notification.service.d.ts +214 -0
  234. package/dist/services/notification.service.js +550 -0
  235. package/dist/services/notification.service.test.d.ts +1 -0
  236. package/dist/services/notification.service.test.js +87 -0
  237. package/dist/services/notifications/notification.service.d.ts +214 -0
  238. package/dist/services/notifications/notification.service.js +550 -0
  239. package/dist/services/notifications/notification.service.test.d.ts +1 -0
  240. package/dist/services/notifications/notification.service.test.js +87 -0
  241. package/dist/services/payment.service.d.ts +55 -0
  242. package/dist/services/payment.service.js +368 -0
  243. package/dist/services/podcast.service.d.ts +267 -0
  244. package/dist/services/podcast.service.js +654 -0
  245. package/dist/services/router-module.service.d.ts +7 -0
  246. package/dist/services/router-module.service.js +10 -0
  247. package/dist/services/study-guide.service.d.ts +18 -0
  248. package/dist/services/study-guide.service.js +65 -0
  249. package/dist/services/subscription.service.d.ts +37 -0
  250. package/dist/services/subscription.service.js +654 -0
  251. package/dist/services/usage-limit-policy.service.d.ts +12 -0
  252. package/dist/services/usage-limit-policy.service.js +22 -0
  253. package/dist/services/usage-limit-policy.service.test.d.ts +1 -0
  254. package/dist/services/usage-limit-policy.service.test.js +46 -0
  255. package/dist/services/usage.service.d.ts +27 -0
  256. package/dist/services/usage.service.js +77 -0
  257. package/dist/services/worksheet-content.service.d.ts +42 -0
  258. package/dist/services/worksheet-content.service.js +84 -0
  259. package/dist/services/worksheet-content.service.test.d.ts +1 -0
  260. package/dist/services/worksheet-content.service.test.js +69 -0
  261. package/dist/services/worksheet-generation.service.d.ts +91 -0
  262. package/dist/services/worksheet-generation.service.js +95 -0
  263. package/dist/services/worksheet-generation.service.test.d.ts +1 -0
  264. package/dist/services/worksheet-generation.service.test.js +20 -0
  265. package/dist/services/worksheet.service.d.ts +385 -0
  266. package/dist/services/worksheet.service.js +596 -0
  267. package/dist/services/workspace/workspace-analytics.service.d.ts +24 -0
  268. package/dist/services/workspace/workspace-analytics.service.js +95 -0
  269. package/dist/services/workspace/workspace-kb.service.d.ts +40 -0
  270. package/dist/services/workspace/workspace-kb.service.js +184 -0
  271. package/dist/services/workspace/workspace.service.d.ts +263 -0
  272. package/dist/services/workspace/workspace.service.js +401 -0
  273. package/dist/services/workspace-analytics.service.d.ts +24 -0
  274. package/dist/services/workspace-analytics.service.js +95 -0
  275. package/dist/services/workspace-kb.service.d.ts +40 -0
  276. package/dist/services/workspace-kb.service.js +184 -0
  277. package/dist/services/workspace-progress.service.d.ts +27 -0
  278. package/dist/services/workspace-progress.service.js +56 -0
  279. package/dist/services/workspace-progress.service.test.d.ts +1 -0
  280. package/dist/services/workspace-progress.service.test.js +49 -0
  281. package/dist/services/workspace.service.d.ts +307 -0
  282. package/dist/services/workspace.service.js +390 -0
  283. package/dist/trpc.d.ts +12 -4
  284. package/dist/trpc.js +7 -13
  285. package/package.json +5 -6
  286. package/prisma/migrations/20260509000001_add_knowledge_base/migration.sql +99 -0
  287. package/prisma/migrations/20260509000002_curate_knowledge_base/migration.sql +52 -0
  288. package/prisma/migrations/20260522000000_add_notes/migration.sql +27 -0
  289. package/prisma/migrations/20260524000000_remove_notes/migration.sql +3 -0
  290. package/prisma/schema.prisma +150 -48
  291. package/prisma/seed.mjs +67 -0
  292. package/scripts/debug/README.md +4 -0
  293. package/src/README.md +63 -0
  294. package/src/context.ts +33 -3
  295. package/src/lib/ai/config.ts +34 -0
  296. package/src/lib/ai/embedding-client.ts +47 -0
  297. package/src/lib/ai/index.ts +65 -0
  298. package/src/lib/ai/inference-backend/client.ts +479 -0
  299. package/src/lib/ai/inference-backend/mocks.ts +171 -0
  300. package/src/lib/ai/inference-backend/types.ts +50 -0
  301. package/src/lib/ai/json-parse.ts +35 -0
  302. package/src/lib/ai/llm-client.ts +54 -0
  303. package/src/lib/ai/mock.ts +12 -0
  304. package/src/lib/ai/types.ts +11 -0
  305. package/src/lib/chunking.ts +81 -0
  306. package/src/lib/curated-kb-seed.ts +164 -0
  307. package/src/lib/email.ts +77 -115
  308. package/src/lib/embeddings.ts +9 -0
  309. package/src/lib/ensure-curated-kb-catalog.ts +60 -0
  310. package/src/lib/env.ts +2 -7
  311. package/src/lib/inference.ts +1 -21
  312. package/src/lib/kb-meta.ts +81 -0
  313. package/src/lib/pdf.ts +23 -0
  314. package/src/lib/workspace-kb.ts +7 -0
  315. package/src/repositories/artifact.repository.ts +55 -0
  316. package/src/repositories/base.repository.ts +19 -0
  317. package/src/repositories/invitation.repository.ts +53 -0
  318. package/src/repositories/notification.repository.ts +53 -0
  319. package/src/repositories/user.repository.ts +44 -0
  320. package/src/repositories/workspace-member.repository.ts +38 -0
  321. package/src/repositories/workspace.repository.ts +89 -0
  322. package/src/routers/_app.ts +4 -0
  323. package/src/routers/admin.ts +124 -692
  324. package/src/routers/annotations.ts +25 -203
  325. package/src/routers/artifactVersions.ts +32 -0
  326. package/src/routers/auth.ts +82 -520
  327. package/src/routers/chat.ts +42 -245
  328. package/src/routers/copilot.ts +41 -666
  329. package/src/routers/flashcards.ts +108 -404
  330. package/src/routers/knowledgeBase.ts +216 -0
  331. package/src/routers/members.ts +60 -782
  332. package/src/routers/notifications.ts +15 -117
  333. package/src/routers/payment.ts +37 -446
  334. package/src/routers/podcast.ts +36 -898
  335. package/src/routers/studyguide.ts +5 -144
  336. package/src/routers/worksheets.ts +171 -735
  337. package/src/routers/workspace.ts +141 -1108
  338. package/src/scripts/purge-deleted-users.ts +2 -2
  339. package/src/server.ts +10 -3
  340. package/src/{lib/activity_human_description.test.ts → services/activity/activity-human-description.service.test.ts} +1 -1
  341. package/src/{lib/activity_log_service.test.ts → services/activity/activity-log.service.test.ts} +1 -1
  342. package/src/{lib/activity_log_service.ts → services/activity/activity-log.service.ts} +2 -2
  343. package/src/services/admin/admin.service.ts +612 -0
  344. package/src/services/ai/ai-session.service.ts +5 -0
  345. package/src/services/artifacts/annotation.service.ts +189 -0
  346. package/src/services/artifacts/artifact-version.service.ts +151 -0
  347. package/src/services/artifacts/chat.service.ts +197 -0
  348. package/src/services/artifacts/study-guide.service.ts +72 -0
  349. package/src/services/auth/auth.service.ts +473 -0
  350. package/src/services/base.service.ts +19 -0
  351. package/src/services/billing/payment.service.ts +433 -0
  352. package/src/{lib/subscription_service.ts → services/billing/subscription.service.ts} +5 -5
  353. package/src/services/billing/usage.service.ts +207 -0
  354. package/src/services/content/copilot.service.ts +587 -0
  355. package/src/services/{flashcard-progress.service.ts → content/flashcard-progress.service.ts} +18 -12
  356. package/src/services/content/flashcard.service.ts +417 -0
  357. package/src/services/content/media-analysis.service.ts +561 -0
  358. package/src/services/content/podcast.service.ts +777 -0
  359. package/src/services/content/worksheet-content.service.test.ts +83 -0
  360. package/src/services/content/worksheet-content.service.ts +117 -0
  361. package/src/{lib/worksheet-generation.test.ts → services/content/worksheet-generation.service.test.ts} +3 -3
  362. package/src/services/content/worksheet.service.ts +751 -0
  363. package/src/services/knowledge/knowledge-base.service.ts +705 -0
  364. package/src/services/members/invitation.service.ts +427 -0
  365. package/src/services/members/member.service.ts +241 -0
  366. package/src/{lib/notification-service.test.ts → services/notifications/notification.service.test.ts} +2 -2
  367. package/src/{lib/notification-service.ts → services/notifications/notification.service.ts} +102 -1
  368. package/src/services/workspace/workspace-analytics.service.ts +107 -0
  369. package/src/services/workspace/workspace-kb.service.ts +273 -0
  370. package/src/services/workspace/workspace.service.ts +488 -0
  371. package/src/trpc.ts +7 -15
  372. package/src/lib/ai-session.ts +0 -704
  373. package/src/lib/usage_service.ts +0 -74
  374. package/src/lib/workspace-access.ts +0 -13
  375. /package/{check-difficulty.cjs → scripts/debug/check-difficulty.cjs} +0 -0
  376. /package/{check-questions.cjs → scripts/debug/check-questions.cjs} +0 -0
  377. /package/{db-summary.cjs → scripts/debug/db-summary.cjs} +0 -0
  378. /package/{mcq-test.cjs → scripts/debug/mcq-test.cjs} +0 -0
  379. /package/{test-generate.js → scripts/debug/test-generate.js} +0 -0
  380. /package/{test-ratio.cjs → scripts/debug/test-ratio.cjs} +0 -0
  381. /package/{zod-test.cjs → scripts/debug/zod-test.cjs} +0 -0
  382. /package/src/{lib/activity_human_description.ts → services/activity/activity-human-description.service.ts} +0 -0
  383. /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
-