@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,153 +1,16 @@
1
1
  import { z } from 'zod';
2
- import { TRPCError } from '@trpc/server';
3
2
  import { router, authedProcedure, verifiedProcedure } from '../trpc.js';
4
- import { supabaseClient } from '../lib/storage.js';
5
- import { ArtifactType } from '../lib/constants.js';
6
- import { aiSessionService } from '../lib/ai-session.js';
7
- import PusherService from '../lib/pusher.js';
8
3
  import { members } from './members.js';
9
- import { logger } from '../lib/logger.js';
10
- import { getUserStorageLimit } from '../lib/subscription_service.js';
11
- import { getUserUsage, getUserPlanLimits } from '../lib/usage_service.js';
12
- import { notifyArtifactFailed, notifyArtifactReady, notifyWorkspaceDeleted, } from '../lib/notification-service.js';
13
- // Helper function to update and emit analysis progress
14
- async function updateAnalysisProgress(db, workspaceId, progress) {
15
- await db.workspace.update({
16
- where: { id: workspaceId },
17
- data: { analysisProgress: progress }
18
- });
19
- await PusherService.emitAnalysisProgress(workspaceId, progress);
20
- }
21
- const PIPELINE_STEPS = ['fileUpload', 'fileAnalysis', 'studyGuide', 'flashcards'];
22
- function buildProgressSteps(currentStep, currentStatus, config, overrides) {
23
- const stepIndex = PIPELINE_STEPS.indexOf(currentStep);
24
- const steps = {};
25
- for (let i = 0; i < PIPELINE_STEPS.length; i++) {
26
- const step = PIPELINE_STEPS[i];
27
- let status;
28
- if (overrides?.[step]) {
29
- status = overrides[step];
30
- }
31
- else if (i < stepIndex) {
32
- status = 'completed';
33
- }
34
- else if (i === stepIndex) {
35
- status = currentStatus;
36
- }
37
- else {
38
- // Future steps: check if they're configured
39
- if (step === 'studyGuide' && !config.generateStudyGuide) {
40
- status = 'skipped';
41
- }
42
- else if (step === 'flashcards' && !config.generateFlashcards) {
43
- status = 'skipped';
44
- }
45
- else {
46
- status = 'pending';
47
- }
48
- }
49
- steps[step] = { order: i + 1, status };
50
- }
51
- return steps;
52
- }
53
- function buildProgress(status, filename, fileType, currentStep, currentStepStatus, config, extra) {
54
- return {
55
- status,
56
- filename,
57
- fileType,
58
- startedAt: new Date().toISOString(),
59
- steps: buildProgressSteps(currentStep, currentStepStatus, config, extra),
60
- ...extra,
61
- };
62
- }
63
- // Helper function to calculate search relevance score
64
- function calculateRelevance(query, ...texts) {
65
- const queryLower = query.toLowerCase();
66
- let score = 0;
67
- for (const text of texts) {
68
- if (!text)
69
- continue;
70
- const textLower = text.toLowerCase();
71
- // Exact match gets highest score
72
- if (textLower.includes(queryLower)) {
73
- score += 10;
74
- }
75
- // Word boundary matches get good score
76
- const words = queryLower.split(/\s+/);
77
- for (const word of words) {
78
- if (word.length > 2 && textLower.includes(word)) {
79
- score += 5;
80
- }
81
- }
82
- // Partial matches get lower score
83
- const queryChars = queryLower.split('');
84
- let consecutiveMatches = 0;
85
- for (const char of queryChars) {
86
- if (textLower.includes(char)) {
87
- consecutiveMatches++;
88
- }
89
- else {
90
- consecutiveMatches = 0;
91
- }
92
- }
93
- score += consecutiveMatches * 0.1;
94
- }
95
- return score;
96
- }
4
+ import { WorkspaceService } from '../services/workspace/workspace.service.js';
5
+ import { WorkspaceAnalyticsService } from '../services/workspace/workspace-analytics.service.js';
6
+ import { MediaAnalysisService } from '../services/content/media-analysis.service.js';
97
7
  export const workspace = router({
98
- // List current user's workspaces
99
8
  list: authedProcedure
100
9
  .input(z.object({
101
10
  parentId: z.string().optional(),
102
11
  }))
103
- .query(async ({ ctx, input }) => {
104
- const workspaces = await ctx.db.workspace.findMany({
105
- where: {
106
- ownerId: ctx.session.user.id,
107
- folderId: input.parentId ?? null,
108
- },
109
- orderBy: { updatedAt: 'desc' },
110
- });
111
- const folders = await ctx.db.folder.findMany({
112
- where: {
113
- ownerId: ctx.session.user.id,
114
- parentId: input.parentId ?? null,
115
- },
116
- });
117
- return { workspaces, folders };
118
- }),
119
- /**
120
- * Fetches the entire directory tree for the user.
121
- * Includes Folders, Workspaces (files), and Uploads (sub-files).
122
- */
123
- getTree: authedProcedure
124
- .query(async ({ ctx }) => {
125
- const userId = ctx.session.user.id;
126
- // 1. Fetch all folders
127
- const allFolders = await ctx.db.folder.findMany({
128
- where: { ownerId: userId },
129
- orderBy: { updatedAt: 'desc' },
130
- });
131
- // 2. Fetch all workspaces
132
- const allWorkspaces = await ctx.db.workspace.findMany({
133
- where: { ownerId: userId },
134
- include: {
135
- uploads: {
136
- select: {
137
- id: true,
138
- name: true,
139
- mimeType: true,
140
- createdAt: true,
141
- }
142
- }
143
- },
144
- orderBy: { updatedAt: 'desc' },
145
- });
146
- return {
147
- folders: allFolders,
148
- workspaces: allWorkspaces,
149
- };
150
- }),
12
+ .query(({ ctx, input }) => new WorkspaceService(ctx.db).list(ctx.session.user.id, input.parentId ?? null)),
13
+ getTree: authedProcedure.query(({ ctx }) => new WorkspaceService(ctx.db).getTree(ctx.session.user.id)),
151
14
  create: authedProcedure
152
15
  .input(z.object({
153
16
  name: z.string().min(1).max(100),
@@ -155,215 +18,30 @@ export const workspace = router({
155
18
  parentId: z.string().optional(),
156
19
  markerColor: z.string().nullable().optional(),
157
20
  }))
158
- .mutation(async ({ ctx, input }) => {
159
- const ws = await ctx.db.workspace.create({
160
- data: {
161
- title: input.name,
162
- description: input.description,
163
- ownerId: ctx.session.user.id,
164
- folderId: input.parentId ?? null,
165
- ...(input.markerColor !== undefined ? { markerColor: input.markerColor } : {}),
166
- artifacts: {
167
- create: {
168
- type: ArtifactType.FLASHCARD_SET,
169
- title: "New Flashcard Set",
170
- },
171
- createMany: {
172
- data: [
173
- { type: ArtifactType.WORKSHEET, title: "Worksheet 1" },
174
- { type: ArtifactType.WORKSHEET, title: "Worksheet 2" },
175
- ],
176
- },
177
- },
178
- },
179
- });
180
- await aiSessionService.initSession(ws.id, ctx.session.user.id).catch((err) => {
181
- logger.error('Failed to init AI session on workspace creation:', err);
182
- });
183
- await PusherService.emitLibraryUpdate(ctx.session.user.id);
184
- return ws;
185
- }),
21
+ .mutation(({ ctx, input }) => new WorkspaceService(ctx.db).create(ctx.session.user.id, input)),
186
22
  createFolder: authedProcedure
187
23
  .input(z.object({
188
24
  name: z.string().min(1).max(100),
189
25
  color: z.string().optional(),
190
26
  parentId: z.string().optional(),
191
27
  }))
192
- .mutation(async ({ ctx, input }) => {
193
- const folder = await ctx.db.folder.create({
194
- data: {
195
- name: input.name,
196
- ownerId: ctx.session.user.id,
197
- color: input.color ?? '#9D00FF',
198
- parentId: input.parentId ?? null,
199
- },
200
- });
201
- await PusherService.emitLibraryUpdate(ctx.session.user.id);
202
- return folder;
203
- }),
28
+ .mutation(({ ctx, input }) => new WorkspaceService(ctx.db).createFolder(ctx.session.user.id, input)),
204
29
  updateFolder: authedProcedure
205
30
  .input(z.object({
206
31
  id: z.string(),
207
32
  name: z.string().min(1).max(100).optional(),
208
33
  markerColor: z.string().nullable().optional(),
209
34
  }))
210
- .mutation(async ({ ctx, input }) => {
211
- const folder = await ctx.db.folder.update({
212
- where: { id: input.id },
213
- data: {
214
- name: input.name,
215
- markerColor: input.markerColor
216
- }
217
- });
218
- await PusherService.emitLibraryUpdate(ctx.session.user.id);
219
- return folder;
220
- }),
35
+ .mutation(({ ctx, input }) => new WorkspaceService(ctx.db).updateFolder(ctx.session.user.id, input)),
221
36
  deleteFolder: authedProcedure
222
- .input(z.object({
223
- id: z.string(),
224
- }))
225
- .mutation(async ({ ctx, input }) => {
226
- const folder = await ctx.db.folder.delete({ where: { id: input.id } });
227
- await PusherService.emitLibraryUpdate(ctx.session.user.id);
228
- return folder;
229
- }),
37
+ .input(z.object({ id: z.string() }))
38
+ .mutation(({ ctx, input }) => new WorkspaceService(ctx.db).deleteFolder(ctx.session.user.id, input.id)),
230
39
  get: authedProcedure
231
- .input(z.object({
232
- id: z.string(),
233
- }))
234
- .query(async ({ ctx, input }) => {
235
- const ws = await ctx.db.workspace.findFirst({
236
- where: { id: input.id, ownerId: ctx.session.user.id },
237
- include: {
238
- artifacts: true,
239
- folder: true,
240
- uploads: true,
241
- },
242
- });
243
- if (!ws)
244
- throw new TRPCError({ code: 'NOT_FOUND' });
245
- return ws;
246
- }),
247
- getStats: authedProcedure
248
- .query(async ({ ctx }) => {
249
- const workspaces = await ctx.db.workspace.findMany({
250
- where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
251
- });
252
- const folders = await ctx.db.folder.findMany({
253
- where: { OR: [{ ownerId: ctx.session.user.id }] },
254
- });
255
- const lastUpdated = await ctx.db.workspace.findFirst({
256
- where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
257
- orderBy: { updatedAt: 'desc' },
258
- });
259
- const spaceLeft = await ctx.db.fileAsset.aggregate({
260
- where: { workspaceId: { in: workspaces.map((ws) => ws.id) }, userId: ctx.session.user.id },
261
- _sum: { size: true },
262
- });
263
- const storageLimit = await getUserStorageLimit(ctx.session.user.id);
264
- return {
265
- workspaces: workspaces.length,
266
- folders: folders.length,
267
- lastUpdated: lastUpdated?.updatedAt,
268
- spaceUsed: spaceLeft._sum?.size ?? 0,
269
- spaceTotal: storageLimit,
270
- };
271
- }),
272
- // Study analytics: streaks, flashcard mastery, worksheet accuracy
273
- getStudyAnalytics: authedProcedure
274
- .query(async ({ ctx }) => {
275
- const userId = ctx.session.user.id;
276
- // Gather all study activity dates
277
- const flashcardProgress = await ctx.db.flashcardProgress.findMany({
278
- where: { userId },
279
- select: { lastStudiedAt: true },
280
- });
281
- const worksheetProgress = await ctx.db.worksheetQuestionProgress.findMany({
282
- where: { userId },
283
- select: { updatedAt: true, completedAt: true },
284
- });
285
- // Build a set of unique study days (YYYY-MM-DD)
286
- const studyDays = new Set();
287
- for (const fp of flashcardProgress) {
288
- if (fp.lastStudiedAt) {
289
- studyDays.add(fp.lastStudiedAt.toISOString().split('T')[0]);
290
- }
291
- }
292
- for (const wp of worksheetProgress) {
293
- if (wp.completedAt) {
294
- studyDays.add(wp.completedAt.toISOString().split('T')[0]);
295
- }
296
- else {
297
- studyDays.add(wp.updatedAt.toISOString().split('T')[0]);
298
- }
299
- }
300
- // Calculate streak (consecutive days ending today or yesterday)
301
- const sortedDays = [...studyDays].sort().reverse();
302
- let streak = 0;
303
- if (sortedDays.length > 0) {
304
- const today = new Date();
305
- today.setHours(0, 0, 0, 0);
306
- const yesterday = new Date(today);
307
- yesterday.setDate(yesterday.getDate() - 1);
308
- const todayStr = today.toISOString().split('T')[0];
309
- const yesterdayStr = yesterday.toISOString().split('T')[0];
310
- // Streak only counts if the most recent study day is today or yesterday
311
- if (sortedDays[0] === todayStr || sortedDays[0] === yesterdayStr) {
312
- streak = 1;
313
- for (let i = 1; i < sortedDays.length; i++) {
314
- const current = new Date(sortedDays[i - 1]);
315
- const prev = new Date(sortedDays[i]);
316
- const diffDays = (current.getTime() - prev.getTime()) / (1000 * 60 * 60 * 24);
317
- if (diffDays === 1) {
318
- streak++;
319
- }
320
- else {
321
- break;
322
- }
323
- }
324
- }
325
- }
326
- // Weekly activity (last 7 days)
327
- const weeklyActivity = [];
328
- const today = new Date();
329
- today.setHours(0, 0, 0, 0);
330
- for (let i = 6; i >= 0; i--) {
331
- const d = new Date(today);
332
- d.setDate(d.getDate() - i);
333
- const dayStr = d.toISOString().split('T')[0];
334
- weeklyActivity.push(studyDays.has(dayStr));
335
- }
336
- // Flashcard stats
337
- const totalCards = await ctx.db.flashcardProgress.count({ where: { userId } });
338
- const masteredCards = await ctx.db.flashcardProgress.count({
339
- where: { userId, masteryLevel: { gte: 80 } },
340
- });
341
- const dueCards = await ctx.db.flashcardProgress.count({
342
- where: { userId, nextReviewAt: { lte: new Date() } },
343
- });
344
- // Worksheet stats
345
- const completedQuestions = await ctx.db.worksheetQuestionProgress.count({
346
- where: { userId, completedAt: { not: null } },
347
- });
348
- const correctQuestions = await ctx.db.worksheetQuestionProgress.count({
349
- where: { userId, correct: true },
350
- });
351
- return {
352
- streak,
353
- totalStudyDays: studyDays.size,
354
- weeklyActivity,
355
- flashcards: {
356
- total: totalCards,
357
- mastered: masteredCards,
358
- dueForReview: dueCards,
359
- },
360
- worksheets: {
361
- completed: completedQuestions,
362
- correct: correctQuestions,
363
- accuracy: completedQuestions > 0 ? Math.round((correctQuestions / completedQuestions) * 100) : 0,
364
- },
365
- };
366
- }),
40
+ .input(z.object({ id: z.string() }))
41
+ .query(({ ctx, input }) => new WorkspaceService(ctx.db).get(ctx.session.user.id, input.id)),
42
+ getStats: authedProcedure.query(({ ctx }) => new WorkspaceService(ctx.db).getStats(ctx.session.user.id)),
43
+ getAccountSummary: authedProcedure.query(({ ctx }) => new WorkspaceService(ctx.db).getAccountSummary(ctx.session.user.id)),
44
+ getStudyAnalytics: authedProcedure.query(({ ctx }) => new WorkspaceAnalyticsService(ctx.db).getStudyAnalytics(ctx.session.user.id)),
367
45
  update: authedProcedure
368
46
  .input(z.object({
369
47
  id: z.string(),
@@ -372,202 +50,32 @@ export const workspace = router({
372
50
  markerColor: z.string().nullable().optional(),
373
51
  icon: z.string().optional(),
374
52
  }))
375
- .mutation(async ({ ctx, input }) => {
376
- const existed = await ctx.db.workspace.findFirst({
377
- where: { id: input.id, ownerId: ctx.session.user.id },
378
- });
379
- if (!existed)
380
- throw new TRPCError({ code: 'NOT_FOUND' });
381
- const updated = await ctx.db.workspace.update({
382
- where: { id: input.id },
383
- data: {
384
- title: input.name ?? existed.title,
385
- description: input.description,
386
- // Preserve explicit null ("None color") instead of falling back.
387
- markerColor: input.markerColor !== undefined ? input.markerColor : existed.markerColor,
388
- icon: input.icon ?? existed.icon,
389
- },
390
- });
391
- await PusherService.emitLibraryUpdate(ctx.session.user.id);
392
- return updated;
393
- }),
53
+ .mutation(({ ctx, input }) => new WorkspaceService(ctx.db).update(ctx.session.user.id, input)),
394
54
  delete: authedProcedure
395
- .input(z.object({
396
- id: z.string(),
397
- }))
398
- .mutation(async ({ ctx, input }) => {
399
- const workspaceToDelete = await ctx.db.workspace.findFirst({
400
- where: { id: input.id, ownerId: ctx.session.user.id },
401
- select: {
402
- id: true,
403
- title: true,
404
- ownerId: true,
405
- members: {
406
- select: { userId: true },
407
- },
408
- },
409
- });
410
- if (!workspaceToDelete)
411
- throw new TRPCError({ code: 'NOT_FOUND' });
412
- const actor = await ctx.db.user.findUnique({
413
- where: { id: ctx.session.user.id },
414
- select: { name: true, email: true },
415
- });
416
- const actorName = actor?.name || actor?.email || 'A user';
417
- await notifyWorkspaceDeleted(ctx.db, {
418
- recipientUserIds: workspaceToDelete.members.map((m) => m.userId),
419
- actorUserId: ctx.session.user.id,
420
- actorName,
421
- workspaceId: workspaceToDelete.id,
422
- workspaceTitle: workspaceToDelete.title,
423
- });
424
- const deleted = await ctx.db.workspace.deleteMany({
425
- where: { id: input.id, ownerId: ctx.session.user.id },
426
- });
427
- if (deleted.count === 0)
428
- throw new TRPCError({ code: 'NOT_FOUND' });
429
- await PusherService.emitLibraryUpdate(ctx.session.user.id);
430
- return true;
431
- }),
55
+ .input(z.object({ id: z.string() }))
56
+ .mutation(({ ctx, input }) => new WorkspaceService(ctx.db).delete(ctx.session.user.id, input.id)),
432
57
  getFolderInformation: authedProcedure
433
- .input(z.object({
434
- id: z.string(),
435
- }))
436
- .query(async ({ ctx, input }) => {
437
- const folder = await ctx.db.folder.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
438
- // find all of its parents
439
- if (!folder)
440
- throw new TRPCError({ code: 'NOT_FOUND' });
441
- const parents = [];
442
- let current = folder;
443
- while (current.parentId) {
444
- const parent = await ctx.db.folder.findFirst({ where: { id: current.parentId, ownerId: ctx.session.user.id } });
445
- if (!parent)
446
- break;
447
- parents.push(parent);
448
- current = parent;
449
- }
450
- return { folder, parents };
451
- }),
58
+ .input(z.object({ id: z.string() }))
59
+ .query(({ ctx, input }) => new WorkspaceService(ctx.db).getFolderInformation(ctx.session.user.id, input.id)),
452
60
  getSharedWith: authedProcedure
453
- .input(z.object({
454
- id: z.string(),
455
- }))
456
- .query(async ({ ctx, input }) => {
457
- const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
458
- if (!user || !user.email)
459
- throw new TRPCError({ code: 'NOT_FOUND' });
460
- const sharedWith = await ctx.db.workspace.findMany({ where: { members: { some: { userId: ctx.session.user.id } } } });
461
- const invitations = await ctx.db.workspaceInvitation.findMany({
462
- where: { email: user.email, acceptedAt: null }, include: {
463
- workspace: true,
464
- }
465
- });
466
- return { shared: sharedWith, invitations };
467
- }),
61
+ .input(z.object({ id: z.string() }))
62
+ .query(({ ctx, input }) => new WorkspaceService(ctx.db).getSharedWith(ctx.session.user.id, input.id)),
468
63
  uploadFiles: authedProcedure
469
64
  .input(z.object({
470
65
  id: z.string(),
471
66
  files: z.array(z.object({
472
67
  filename: z.string().min(1).max(255),
473
68
  contentType: z.string().min(1).max(100),
474
- size: z.number().min(1), // size in bytes
69
+ size: z.number().min(1),
475
70
  })),
476
71
  }))
477
- .mutation(async ({ ctx, input }) => {
478
- // ensure workspace belongs to user
479
- const ws = await ctx.db.workspace.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
480
- if (!ws)
481
- throw new TRPCError({ code: 'NOT_FOUND' });
482
- // Check storage limit
483
- const workspaces = await ctx.db.workspace.findMany({
484
- where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
485
- });
486
- const spaceUsed = await ctx.db.fileAsset.aggregate({
487
- where: { workspaceId: { in: workspaces.map((w) => w.id) }, userId: ctx.session.user.id },
488
- _sum: { size: true },
489
- });
490
- const storageLimit = await getUserStorageLimit(ctx.session.user.id);
491
- const totalSize = input.files.reduce((acc, file) => acc + file.size, 0);
492
- if ((spaceUsed._sum?.size ?? 0) + totalSize > storageLimit) {
493
- logger.warn(`Storage limit exceeded for user ${ctx.session.user.id}. Used: ${spaceUsed._sum?.size}, Tried to upload: ${totalSize}, Limit: ${storageLimit}`);
494
- throw new TRPCError({
495
- code: 'FORBIDDEN',
496
- message: `Storage limit exceeded. Maximum allowed storage is ${(storageLimit / (1024 * 1024 * 1024)).toFixed(1)}GB.`
497
- });
498
- }
499
- const results = [];
500
- for (const file of input.files) {
501
- // 1. Insert into DB
502
- const record = await ctx.db.fileAsset.create({
503
- data: {
504
- userId: ctx.session.user.id,
505
- name: file.filename,
506
- mimeType: file.contentType,
507
- size: file.size,
508
- workspaceId: input.id,
509
- },
510
- });
511
- // 2. Generate signed URL for direct upload
512
- const objectKey = `${ctx.session.user.id}/${record.id}-${file.filename}`;
513
- const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
514
- .from('media')
515
- .createSignedUploadUrl(objectKey); // 5 minutes
516
- if (signedUrlError) {
517
- throw new TRPCError({
518
- code: 'INTERNAL_SERVER_ERROR',
519
- message: `Failed to upload file`
520
- });
521
- }
522
- // 3. Update record with bucket info
523
- await ctx.db.fileAsset.update({
524
- where: { id: record.id },
525
- data: {
526
- bucket: 'media',
527
- objectKey: objectKey,
528
- },
529
- });
530
- results.push({
531
- fileId: record.id,
532
- uploadUrl: signedUrlData.signedUrl,
533
- });
534
- }
535
- return results;
536
- }),
72
+ .mutation(({ ctx, input }) => new WorkspaceService(ctx.db).uploadFiles(ctx.session.user.id, input)),
537
73
  deleteFiles: authedProcedure
538
74
  .input(z.object({
539
75
  fileId: z.array(z.string()),
540
76
  id: z.string(),
541
77
  }))
542
- .mutation(async ({ ctx, input }) => {
543
- // ensure files are in the user's workspace
544
- const files = await ctx.db.fileAsset.findMany({
545
- where: {
546
- id: { in: input.fileId },
547
- workspaceId: input.id,
548
- userId: ctx.session.user.id,
549
- },
550
- });
551
- // Delete from Supabase Storage (best-effort)
552
- for (const file of files) {
553
- if (file.bucket && file.objectKey) {
554
- supabaseClient.storage
555
- .from(file.bucket)
556
- .remove([file.objectKey])
557
- .catch((err) => {
558
- logger.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
559
- });
560
- }
561
- }
562
- await ctx.db.fileAsset.deleteMany({
563
- where: {
564
- id: { in: input.fileId },
565
- workspaceId: input.id,
566
- userId: ctx.session.user.id,
567
- },
568
- });
569
- return true;
570
- }),
78
+ .mutation(({ ctx, input }) => new WorkspaceService(ctx.db).deleteFiles(ctx.session.user.id, input)),
571
79
  getFileUploadUrl: authedProcedure
572
80
  .input(z.object({
573
81
  workspaceId: z.string(),
@@ -575,423 +83,22 @@ export const workspace = router({
575
83
  contentType: z.string(),
576
84
  size: z.number(),
577
85
  }))
578
- .query(async ({ ctx, input }) => {
579
- // Check storage limit
580
- const workspaces = await ctx.db.workspace.findMany({
581
- where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
582
- });
583
- const spaceUsed = await ctx.db.fileAsset.aggregate({
584
- where: { workspaceId: { in: workspaces.map((w) => w.id) }, userId: ctx.session.user.id },
585
- _sum: { size: true },
586
- });
587
- const storageLimit = await getUserStorageLimit(ctx.session.user.id);
588
- if ((spaceUsed._sum?.size ?? 0) + input.size > storageLimit) {
589
- logger.warn(`Storage limit exceeded for user ${ctx.session.user.id}. Used: ${spaceUsed._sum?.size}, Tried to upload: ${input.size}, Limit: ${storageLimit}`);
590
- throw new TRPCError({
591
- code: 'FORBIDDEN',
592
- message: `Storage limit exceeded. Maximum allowed storage is ${(storageLimit / (1024 * 1024 * 1024)).toFixed(1)}GB.`
593
- });
594
- }
595
- const objectKey = `workspace_${ctx.session.user.id}/${input.workspaceId}-file_${input.filename}`;
596
- const fileAsset = await ctx.db.fileAsset.create({
597
- data: {
598
- workspaceId: input.workspaceId,
599
- name: input.filename,
600
- mimeType: input.contentType,
601
- size: input.size,
602
- userId: ctx.session.user.id,
603
- bucket: 'media',
604
- objectKey: objectKey,
605
- },
606
- });
607
- const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
608
- .from('media')
609
- .createSignedUploadUrl(objectKey, { upsert: true });
610
- if (signedUrlError) {
611
- logger.error('Signed upload URL error:', signedUrlError);
612
- throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to create upload URL: ${signedUrlError.message}` });
613
- }
614
- await ctx.db.workspace.update({
615
- where: { id: input.workspaceId },
616
- data: { needsAnalysis: true },
617
- });
618
- return {
619
- fileId: fileAsset.id,
620
- uploadUrl: signedUrlData.signedUrl,
621
- };
622
- }),
86
+ .query(({ ctx, input }) => new WorkspaceService(ctx.db).getFileUploadUrl(ctx.session.user.id, input)),
623
87
  uploadAndAnalyzeMedia: verifiedProcedure
624
88
  .input(z.object({
625
89
  workspaceId: z.string(),
626
- files: z.array(z.object({
627
- id: z.string(),
628
- })),
90
+ files: z.array(z.object({ id: z.string() })),
629
91
  generateStudyGuide: z.boolean().default(true),
630
92
  generateFlashcards: z.boolean().default(true),
631
93
  generateWorksheet: z.boolean().default(true),
632
94
  }))
633
- .mutation(async ({ ctx, input }) => {
634
- // Verify workspace ownership
635
- const workspace = await ctx.db.workspace.findFirst({
636
- where: { id: input.workspaceId, ownerId: ctx.session.user.id }
637
- });
638
- if (!workspace) {
639
- logger.error('Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
640
- throw new TRPCError({ code: 'NOT_FOUND' });
641
- }
642
- // Check if analysis is already in progress
643
- if (workspace.fileBeingAnalyzed) {
644
- throw new TRPCError({
645
- code: 'CONFLICT',
646
- message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
647
- });
648
- }
649
- // Fetch files from database
650
- const files = await ctx.db.fileAsset.findMany({
651
- where: {
652
- id: { in: input.files.map(file => file.id) },
653
- workspaceId: input.workspaceId,
654
- userId: ctx.session.user.id,
655
- },
656
- });
657
- if (files.length === 0) {
658
- throw new TRPCError({
659
- code: 'NOT_FOUND',
660
- message: 'No files found with the provided IDs'
661
- });
662
- }
663
- // Validate all files have bucket and objectKey
664
- for (const file of files) {
665
- if (!file.bucket || !file.objectKey) {
666
- throw new TRPCError({
667
- code: 'BAD_REQUEST',
668
- message: `File ${file.id} does not have bucket or objectKey set`
669
- });
670
- }
671
- }
672
- // Use the first file for progress tracking and artifact naming
673
- const primaryFile = files[0];
674
- const fileType = primaryFile.mimeType.startsWith('image/') ? 'image' : 'pdf';
675
- try {
676
- // Set analysis in progress flag
677
- await ctx.db.workspace.update({
678
- where: { id: input.workspaceId },
679
- data: { fileBeingAnalyzed: true },
680
- });
681
- const genConfig = { generateStudyGuide: input.generateStudyGuide, generateFlashcards: input.generateFlashcards };
682
- PusherService.emitAnalysisProgress(input.workspaceId, buildProgress('starting', primaryFile.name, fileType, 'fileUpload', 'pending', genConfig));
683
- try {
684
- await updateAnalysisProgress(ctx.db, input.workspaceId, buildProgress('starting', primaryFile.name, fileType, 'fileUpload', 'pending', genConfig));
685
- }
686
- catch (error) {
687
- logger.error('Failed to update analysis progress:', error);
688
- await ctx.db.workspace.update({
689
- where: { id: input.workspaceId },
690
- data: { fileBeingAnalyzed: false },
691
- });
692
- await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
693
- throw error;
694
- }
695
- await updateAnalysisProgress(ctx.db, input.workspaceId, buildProgress('uploading', primaryFile.name, fileType, 'fileUpload', 'in_progress', genConfig));
696
- // Process all files using the new process_file endpoint
697
- for (const file of files) {
698
- // TypeScript: We already validated bucket and objectKey exist above
699
- if (!file.bucket || !file.objectKey) {
700
- continue; // Skip if somehow missing (shouldn't happen due to validation above)
701
- }
702
- const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
703
- .from(file.bucket)
704
- .createSignedUrl(file.objectKey, 24 * 60 * 60); // 24 hours expiry
705
- if (signedUrlError) {
706
- await ctx.db.workspace.update({
707
- where: { id: input.workspaceId },
708
- data: { fileBeingAnalyzed: false },
709
- });
710
- throw new TRPCError({
711
- code: 'INTERNAL_SERVER_ERROR',
712
- message: `Failed to upload file`
713
- });
714
- }
715
- const fileUrl = signedUrlData.signedUrl;
716
- const currentFileType = file.mimeType.startsWith('image/') ? 'image' : 'pdf';
717
- // Use maxPages for large PDFs (>50 pages) to limit processing
718
- const maxPages = currentFileType === 'pdf' && file.size && file.size > 50 ? 50 : undefined;
719
- const processResult = await aiSessionService.processFile(input.workspaceId, ctx.session.user.id, fileUrl, currentFileType, maxPages);
720
- if (processResult.status === 'error') {
721
- logger.error(`Failed to process file ${file.name}:`, processResult.error);
722
- // Continue processing other files even if one fails
723
- // Optionally, you could throw an error or mark this file as failed
724
- }
725
- else {
726
- logger.info(`Successfully processed file ${file.name}: ${processResult.pageCount} pages`);
727
- // Store the comprehensive description in aiTranscription field
728
- await ctx.db.fileAsset.update({
729
- where: { id: file.id },
730
- data: {
731
- aiTranscription: {
732
- comprehensiveDescription: processResult.comprehensiveDescription,
733
- textContent: processResult.textContent,
734
- imageDescriptions: processResult.imageDescriptions,
735
- },
736
- }
737
- });
738
- }
739
- }
740
- await updateAnalysisProgress(ctx.db, input.workspaceId, buildProgress('analyzing', primaryFile.name, fileType, 'fileAnalysis', 'in_progress', genConfig));
741
- try {
742
- // Analyze all files - use PDF analysis if any file is a PDF, otherwise use image analysis
743
- // const hasPDF = files.some(f => !f.mimeType.startsWith('image/'));
744
- // if (hasPDF) {
745
- // await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id, file.id);
746
- // } else {
747
- // // If all files are images, analyze them
748
- // for (const file of files) {
749
- // await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id, file.id);
750
- // }
751
- // }
752
- await updateAnalysisProgress(ctx.db, input.workspaceId, buildProgress('generating_artifacts', primaryFile.name, fileType, 'studyGuide', 'pending', genConfig));
753
- }
754
- catch (error) {
755
- logger.error('Failed to analyze files:', error);
756
- await updateAnalysisProgress(ctx.db, input.workspaceId, buildProgress('error', primaryFile.name, fileType, 'fileAnalysis', 'error', genConfig, {
757
- error: `Failed to analyze ${fileType}: ${error}`,
758
- studyGuide: 'skipped', flashcards: 'skipped',
759
- }));
760
- await ctx.db.workspace.update({
761
- where: { id: input.workspaceId },
762
- data: { fileBeingAnalyzed: false },
763
- });
764
- throw error;
765
- }
766
- const results = {
767
- filename: primaryFile.name,
768
- artifacts: {
769
- studyGuide: null,
770
- flashcards: null,
771
- worksheet: null,
772
- }
773
- };
774
- // Ensure AI session is initialized before generating artifacts
775
- try {
776
- await aiSessionService.initSession(input.workspaceId, ctx.session.user.id);
777
- }
778
- catch (initError) {
779
- logger.error('Failed to init AI session (continuing with workspace context):', initError);
780
- }
781
- // Fetch current usage and limits to enforce plan restrictions for auto-generation
782
- const [usage, limits] = await Promise.all([
783
- getUserUsage(ctx.session.user.id),
784
- getUserPlanLimits(ctx.session.user.id)
785
- ]);
786
- // Generate artifacts - each step is isolated so failures don't block subsequent steps
787
- if (input.generateStudyGuide) {
788
- // Enforcement: Skip if limit reached
789
- if (limits && usage.studyGuides >= limits.maxStudyGuides) {
790
- await updateAnalysisProgress(ctx.db, input.workspaceId, buildProgress('skipped', primaryFile.name, fileType, 'studyGuide', 'skipped', genConfig));
791
- await PusherService.emitError(input.workspaceId, 'Study guide skipped: Limit reached.', 'study_guide');
792
- await notifyArtifactFailed(ctx.db, {
793
- userId: ctx.session.user.id,
794
- workspaceId: input.workspaceId,
795
- artifactType: ArtifactType.STUDY_GUIDE,
796
- message: 'Study guide was skipped because your plan limit was reached.',
797
- }).catch(() => { });
798
- }
799
- else {
800
- try {
801
- await updateAnalysisProgress(ctx.db, input.workspaceId, buildProgress('generating_study_guide', primaryFile.name, fileType, 'studyGuide', 'in_progress', genConfig));
802
- const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
803
- let artifact = await ctx.db.artifact.findFirst({
804
- where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
805
- });
806
- if (!artifact) {
807
- artifact = await ctx.db.artifact.create({
808
- data: {
809
- workspaceId: input.workspaceId,
810
- type: ArtifactType.STUDY_GUIDE,
811
- title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
812
- createdById: ctx.session.user.id,
813
- },
814
- });
815
- }
816
- const lastVersion = await ctx.db.artifactVersion.findFirst({
817
- where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
818
- orderBy: { version: 'desc' },
819
- });
820
- await ctx.db.artifactVersion.create({
821
- data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
822
- });
823
- results.artifacts.studyGuide = artifact;
824
- await PusherService.emitStudyGuideComplete(input.workspaceId, artifact);
825
- await notifyArtifactReady(ctx.db, {
826
- userId: ctx.session.user.id,
827
- workspaceId: input.workspaceId,
828
- artifactId: artifact.id,
829
- artifactType: ArtifactType.STUDY_GUIDE,
830
- title: artifact.title,
831
- }).catch(() => { });
832
- }
833
- catch (sgError) {
834
- logger.error('Study guide generation failed after retries:', sgError);
835
- await PusherService.emitError(input.workspaceId, 'Study guide generation failed. Please try regenerating later.', 'study_guide');
836
- await notifyArtifactFailed(ctx.db, {
837
- userId: ctx.session.user.id,
838
- workspaceId: input.workspaceId,
839
- artifactType: ArtifactType.STUDY_GUIDE,
840
- message: 'Study guide generation failed. Please try regenerating later.',
841
- }).catch(() => { });
842
- // Continue to flashcards - don't abort the whole pipeline
843
- }
844
- }
845
- }
846
- if (input.generateFlashcards) {
847
- // Enforcement: Skip if limit reached
848
- if (limits && usage.flashcards >= limits.maxFlashcards) {
849
- await updateAnalysisProgress(ctx.db, input.workspaceId, buildProgress('skipped', primaryFile.name, fileType, 'flashcards', 'skipped', genConfig));
850
- await PusherService.emitError(input.workspaceId, 'Flashcards skipped: Limit reached.', 'flashcards');
851
- await notifyArtifactFailed(ctx.db, {
852
- userId: ctx.session.user.id,
853
- workspaceId: input.workspaceId,
854
- artifactType: ArtifactType.FLASHCARD_SET,
855
- message: 'Flashcards were skipped because your plan limit was reached.',
856
- }).catch(() => { });
857
- }
858
- else {
859
- try {
860
- const sgStatus = input.generateStudyGuide ? (results.artifacts.studyGuide ? 'completed' : 'error') : 'skipped';
861
- await updateAnalysisProgress(ctx.db, input.workspaceId, buildProgress('generating_flashcards', primaryFile.name, fileType, 'flashcards', 'in_progress', genConfig, { studyGuide: sgStatus }));
862
- const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
863
- const artifact = await ctx.db.artifact.create({
864
- data: {
865
- workspaceId: input.workspaceId,
866
- type: ArtifactType.FLASHCARD_SET,
867
- title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
868
- createdById: ctx.session.user.id,
869
- },
870
- });
871
- // Parse JSON flashcard content
872
- try {
873
- const parsed = typeof content === 'string' ? JSON.parse(content) : content;
874
- const flashcardData = Array.isArray(parsed) ? parsed : (parsed.flashcards || []);
875
- for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
876
- const card = flashcardData[i];
877
- const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
878
- const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
879
- await ctx.db.flashcard.create({
880
- data: {
881
- artifactId: artifact.id,
882
- front: front,
883
- back: back,
884
- order: i,
885
- tags: ['ai-generated', 'medium'],
886
- },
887
- });
888
- }
889
- }
890
- catch (parseError) {
891
- console.error("Failed to parse flashcard JSON or create cards in workspace router:", parseError);
892
- // Fallback to text parsing if JSON fails
893
- const lines = content.split('\n').filter((line) => line.trim());
894
- for (let i = 0; i < Math.min(lines.length, 10); i++) {
895
- const line = lines[i];
896
- if (line.includes(' - ')) {
897
- const [front, back] = line.split(' - ');
898
- await ctx.db.flashcard.create({
899
- data: {
900
- artifactId: artifact.id,
901
- front: front.trim(),
902
- back: back.trim(),
903
- order: i,
904
- tags: ['ai-generated', 'medium'],
905
- },
906
- });
907
- }
908
- }
909
- }
910
- results.artifacts.flashcards = artifact;
911
- await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
912
- await notifyArtifactReady(ctx.db, {
913
- userId: ctx.session.user.id,
914
- workspaceId: input.workspaceId,
915
- artifactId: artifact.id,
916
- artifactType: ArtifactType.FLASHCARD_SET,
917
- title: artifact.title,
918
- }).catch(() => { });
919
- }
920
- catch (fcError) {
921
- logger.error('Flashcard generation failed after retries:', fcError);
922
- await PusherService.emitError(input.workspaceId, 'Flashcard generation failed. Please try regenerating later.', 'flashcards');
923
- await notifyArtifactFailed(ctx.db, {
924
- userId: ctx.session.user.id,
925
- workspaceId: input.workspaceId,
926
- artifactType: ArtifactType.FLASHCARD_SET,
927
- message: 'Flashcard generation failed. Please try regenerating later.',
928
- }).catch(() => { });
929
- }
930
- }
931
- }
932
- await ctx.db.workspace.update({
933
- where: { id: input.workspaceId },
934
- data: { fileBeingAnalyzed: false },
935
- });
936
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
937
- ...buildProgress('completed', primaryFile.name, fileType, 'flashcards', 'completed', genConfig),
938
- completedAt: new Date().toISOString(),
939
- });
940
- return results;
941
- }
942
- catch (error) {
943
- logger.error('Failed to update analysis progress:', error);
944
- await ctx.db.workspace.update({
945
- where: { id: input.workspaceId },
946
- data: { fileBeingAnalyzed: false },
947
- });
948
- await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
949
- throw error;
950
- }
951
- }),
95
+ .mutation(({ ctx, input }) => new MediaAnalysisService(ctx.db).uploadAndAnalyzeMedia(input, ctx.session.user.id)),
952
96
  search: authedProcedure
953
97
  .input(z.object({
954
98
  query: z.string(),
955
99
  color: z.string().optional(),
956
100
  limit: z.number().min(1).max(100).default(20),
957
101
  }))
958
- .query(async ({ ctx, input }) => {
959
- const { query, color } = input;
960
- // 1. Search Workspaces
961
- const workspaces = await ctx.db.workspace.findMany({
962
- where: {
963
- ownerId: ctx.session.user.id,
964
- markerColor: color || undefined,
965
- ...(query ? {
966
- OR: [
967
- { title: { contains: query, mode: 'insensitive' } },
968
- { description: { contains: query, mode: 'insensitive' } },
969
- ],
970
- } : {}),
971
- },
972
- orderBy: { updatedAt: 'desc' },
973
- take: input.limit,
974
- });
975
- // 2. Search Folders
976
- const folders = await ctx.db.folder.findMany({
977
- where: {
978
- ownerId: ctx.session.user.id,
979
- markerColor: color || undefined,
980
- ...(query ? {
981
- name: { contains: query, mode: 'insensitive' },
982
- } : {}),
983
- },
984
- orderBy: { updatedAt: 'desc' },
985
- take: input.limit,
986
- });
987
- // Combined results with type discriminator
988
- const results = [
989
- ...workspaces.map((w) => ({ ...w, type: 'workspace' })),
990
- ...folders.map((f) => ({ ...f, type: 'folder', title: f.name })), // normalize name to title
991
- ].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
992
- .slice(0, input.limit);
993
- return results;
994
- }),
995
- // Members sub-router
102
+ .query(({ ctx, input }) => new WorkspaceService(ctx.db).search(ctx.session.user.id, input)),
996
103
  members,
997
104
  });