@goscribe/server 1.5.0 → 1.7.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 (321) hide show
  1. package/dist/context.d.ts +14 -1
  2. package/dist/context.js +23 -2
  3. package/dist/generated/prisma/client.d.ts +224 -0
  4. package/dist/generated/prisma/client.js +34 -0
  5. package/dist/generated/prisma/commonInputTypes.d.ts +941 -0
  6. package/dist/generated/prisma/commonInputTypes.js +10 -0
  7. package/dist/generated/prisma/enums.d.ts +67 -0
  8. package/dist/generated/prisma/enums.js +66 -0
  9. package/dist/generated/prisma/internal/class.d.ts +539 -0
  10. package/dist/generated/prisma/internal/class.js +49 -0
  11. package/dist/generated/prisma/internal/prismaNamespace.d.ts +3924 -0
  12. package/dist/generated/prisma/internal/prismaNamespace.js +557 -0
  13. package/dist/generated/prisma/models/ActivityLog.d.ts +1847 -0
  14. package/dist/generated/prisma/models/ActivityLog.js +1 -0
  15. package/dist/generated/prisma/models/Artifact.d.ts +2345 -0
  16. package/dist/generated/prisma/models/Artifact.js +1 -0
  17. package/dist/generated/prisma/models/ArtifactVersion.d.ts +1550 -0
  18. package/dist/generated/prisma/models/ArtifactVersion.js +1 -0
  19. package/dist/generated/prisma/models/Channel.d.ts +1257 -0
  20. package/dist/generated/prisma/models/Channel.js +1 -0
  21. package/dist/generated/prisma/models/Chat.d.ts +1339 -0
  22. package/dist/generated/prisma/models/Chat.js +1 -0
  23. package/dist/generated/prisma/models/CopilotConversation.d.ts +1450 -0
  24. package/dist/generated/prisma/models/CopilotConversation.js +1 -0
  25. package/dist/generated/prisma/models/CopilotMessage.d.ts +1179 -0
  26. package/dist/generated/prisma/models/CopilotMessage.js +1 -0
  27. package/dist/generated/prisma/models/FileAsset.d.ts +1832 -0
  28. package/dist/generated/prisma/models/FileAsset.js +1 -0
  29. package/dist/generated/prisma/models/Flashcard.d.ts +1460 -0
  30. package/dist/generated/prisma/models/Flashcard.js +1 -0
  31. package/dist/generated/prisma/models/FlashcardProgress.d.ts +1782 -0
  32. package/dist/generated/prisma/models/FlashcardProgress.js +1 -0
  33. package/dist/generated/prisma/models/Folder.d.ts +1685 -0
  34. package/dist/generated/prisma/models/Folder.js +1 -0
  35. package/dist/generated/prisma/models/IdempotencyRecord.d.ts +1319 -0
  36. package/dist/generated/prisma/models/IdempotencyRecord.js +1 -0
  37. package/dist/generated/prisma/models/Invoice.d.ts +1586 -0
  38. package/dist/generated/prisma/models/Invoice.js +1 -0
  39. package/dist/generated/prisma/models/KnowledgeBase.d.ts +1721 -0
  40. package/dist/generated/prisma/models/KnowledgeBase.js +1 -0
  41. package/dist/generated/prisma/models/KnowledgeBaseChunk.d.ts +1333 -0
  42. package/dist/generated/prisma/models/KnowledgeBaseChunk.js +1 -0
  43. package/dist/generated/prisma/models/KnowledgeBaseDocument.d.ts +1695 -0
  44. package/dist/generated/prisma/models/KnowledgeBaseDocument.js +1 -0
  45. package/dist/generated/prisma/models/Notification.d.ts +1992 -0
  46. package/dist/generated/prisma/models/Notification.js +1 -0
  47. package/dist/generated/prisma/models/PasswordResetToken.d.ts +1210 -0
  48. package/dist/generated/prisma/models/PasswordResetToken.js +1 -0
  49. package/dist/generated/prisma/models/Plan.d.ts +1431 -0
  50. package/dist/generated/prisma/models/Plan.js +1 -0
  51. package/dist/generated/prisma/models/PlanLimit.d.ts +1328 -0
  52. package/dist/generated/prisma/models/PlanLimit.js +1 -0
  53. package/dist/generated/prisma/models/PodcastSegment.d.ts +1564 -0
  54. package/dist/generated/prisma/models/PodcastSegment.js +1 -0
  55. package/dist/generated/prisma/models/ResourcePrice.d.ts +1008 -0
  56. package/dist/generated/prisma/models/ResourcePrice.js +1 -0
  57. package/dist/generated/prisma/models/Role.d.ts +1065 -0
  58. package/dist/generated/prisma/models/Role.js +1 -0
  59. package/dist/generated/prisma/models/Session.d.ts +1105 -0
  60. package/dist/generated/prisma/models/Session.js +1 -0
  61. package/dist/generated/prisma/models/StripeEvent.d.ts +1081 -0
  62. package/dist/generated/prisma/models/StripeEvent.js +1 -0
  63. package/dist/generated/prisma/models/StudyGuideComment.d.ts +1321 -0
  64. package/dist/generated/prisma/models/StudyGuideComment.js +1 -0
  65. package/dist/generated/prisma/models/StudyGuideHighlight.d.ts +1629 -0
  66. package/dist/generated/prisma/models/StudyGuideHighlight.js +1 -0
  67. package/dist/generated/prisma/models/Subscription.d.ts +1677 -0
  68. package/dist/generated/prisma/models/Subscription.js +1 -0
  69. package/dist/generated/prisma/models/User.d.ts +7559 -0
  70. package/dist/generated/prisma/models/User.js +1 -0
  71. package/dist/generated/prisma/models/UserCredit.d.ts +1249 -0
  72. package/dist/generated/prisma/models/UserCredit.js +1 -0
  73. package/dist/generated/prisma/models/VerificationToken.d.ts +946 -0
  74. package/dist/generated/prisma/models/VerificationToken.js +1 -0
  75. package/dist/generated/prisma/models/WorksheetPreset.d.ts +1433 -0
  76. package/dist/generated/prisma/models/WorksheetPreset.js +1 -0
  77. package/dist/generated/prisma/models/WorksheetQuestion.d.ts +1491 -0
  78. package/dist/generated/prisma/models/WorksheetQuestion.js +1 -0
  79. package/dist/generated/prisma/models/WorksheetQuestionProgress.d.ts +1620 -0
  80. package/dist/generated/prisma/models/WorksheetQuestionProgress.js +1 -0
  81. package/dist/generated/prisma/models/Workspace.d.ts +3620 -0
  82. package/dist/generated/prisma/models/Workspace.js +1 -0
  83. package/dist/generated/prisma/models/WorkspaceInvitation.d.ts +1490 -0
  84. package/dist/generated/prisma/models/WorkspaceInvitation.js +1 -0
  85. package/dist/generated/prisma/models/WorkspaceKnowledgeBase.d.ts +1410 -0
  86. package/dist/generated/prisma/models/WorkspaceKnowledgeBase.js +1 -0
  87. package/dist/generated/prisma/models/WorkspaceMember.d.ts +1326 -0
  88. package/dist/generated/prisma/models/WorkspaceMember.js +1 -0
  89. package/dist/generated/prisma/models.d.ts +39 -0
  90. package/dist/generated/prisma/models.js +1 -0
  91. package/dist/lib/ai/index.d.ts +3 -2
  92. package/dist/lib/ai/index.js +3 -2
  93. package/dist/lib/ai/llm-client.d.ts +1 -0
  94. package/dist/lib/ai/llm-client.js +17 -0
  95. package/dist/routers/_app.d.ts +40 -80
  96. package/dist/routers/auth.js +1 -1
  97. package/dist/routers/flashcards.d.ts +12 -1
  98. package/dist/routers/payment.d.ts +1 -12
  99. package/dist/routers/workspace.d.ts +27 -67
  100. package/dist/routers/workspace.js +1 -0
  101. package/dist/services/billing/payment.service.d.ts +1 -12
  102. package/dist/services/billing/payment.service.js +3 -6
  103. package/dist/services/billing/usage.service.d.ts +30 -10
  104. package/dist/services/billing/usage.service.js +87 -15
  105. package/dist/services/content/copilot.service.js +15 -29
  106. package/dist/services/content/flashcard-progress.service.js +9 -9
  107. package/dist/services/content/flashcard.service.d.ts +45 -1
  108. package/dist/services/content/flashcard.service.js +81 -68
  109. package/dist/services/content/media-analysis.service.js +27 -27
  110. package/dist/services/content/worksheet-generation.service.test.js +2 -2
  111. package/dist/services/workspace/workspace.service.d.ts +23 -67
  112. package/dist/services/workspace/workspace.service.js +69 -62
  113. package/dist/src/context.d.ts +27 -0
  114. package/dist/src/context.js +33 -0
  115. package/dist/src/index.d.ts +3 -0
  116. package/dist/src/index.js +1 -0
  117. package/dist/src/lib/ai/config.d.ts +20 -0
  118. package/dist/src/lib/ai/config.js +31 -0
  119. package/dist/src/lib/ai/embedding-client.d.ts +8 -0
  120. package/dist/src/lib/ai/embedding-client.js +30 -0
  121. package/dist/src/lib/ai/index.d.ts +48 -0
  122. package/dist/src/lib/ai/index.js +29 -0
  123. package/dist/src/lib/ai/inference-backend/client.d.ts +28 -0
  124. package/dist/src/lib/ai/inference-backend/client.js +301 -0
  125. package/dist/src/lib/ai/inference-backend/mocks.d.ts +12 -0
  126. package/dist/src/lib/ai/inference-backend/mocks.js +133 -0
  127. package/dist/src/lib/ai/inference-backend/types.d.ts +44 -0
  128. package/dist/src/lib/ai/inference-backend/types.js +1 -0
  129. package/dist/src/lib/ai/json-parse.d.ts +2 -0
  130. package/dist/src/lib/ai/json-parse.js +34 -0
  131. package/dist/src/lib/ai/llm-client.d.ts +7 -0
  132. package/dist/src/lib/ai/llm-client.js +36 -0
  133. package/dist/src/lib/ai/mock.d.ts +2 -0
  134. package/dist/src/lib/ai/mock.js +10 -0
  135. package/dist/src/lib/ai/types.d.ts +9 -0
  136. package/dist/src/lib/ai/types.js +1 -0
  137. package/dist/src/lib/auth.d.ts +36 -0
  138. package/dist/src/lib/auth.js +117 -0
  139. package/dist/src/lib/chunking.d.ts +19 -0
  140. package/dist/src/lib/chunking.js +47 -0
  141. package/dist/src/lib/constants.d.ts +13 -0
  142. package/dist/src/lib/constants.js +12 -0
  143. package/dist/src/lib/curated-kb-seed.d.ts +12 -0
  144. package/dist/src/lib/curated-kb-seed.js +155 -0
  145. package/dist/src/lib/email.d.ts +11 -0
  146. package/dist/src/lib/email.js +152 -0
  147. package/dist/src/lib/embeddings.d.ts +2 -0
  148. package/dist/src/lib/embeddings.js +1 -0
  149. package/dist/src/lib/ensure-curated-kb-catalog.d.ts +6 -0
  150. package/dist/src/lib/ensure-curated-kb-catalog.js +53 -0
  151. package/dist/src/lib/env.d.ts +41 -0
  152. package/dist/src/lib/env.js +57 -0
  153. package/dist/src/lib/errors.d.ts +33 -0
  154. package/dist/src/lib/errors.js +78 -0
  155. package/dist/src/lib/file.d.ts +0 -0
  156. package/dist/src/lib/file.js +1 -0
  157. package/dist/src/lib/inference.d.ts +1 -0
  158. package/dist/src/lib/inference.js +1 -0
  159. package/dist/src/lib/kb-meta.d.ts +8 -0
  160. package/dist/src/lib/kb-meta.js +77 -0
  161. package/dist/src/lib/logger.d.ts +62 -0
  162. package/dist/src/lib/logger.js +364 -0
  163. package/dist/src/lib/pdf.d.ts +11 -0
  164. package/dist/src/lib/pdf.js +11 -0
  165. package/dist/src/lib/prisma.d.ts +3 -0
  166. package/dist/src/lib/prisma.js +15 -0
  167. package/dist/src/lib/pusher.d.ts +38 -0
  168. package/dist/src/lib/pusher.js +170 -0
  169. package/dist/src/lib/retry.d.ts +15 -0
  170. package/dist/src/lib/retry.js +37 -0
  171. package/dist/src/lib/storage.d.ts +11 -0
  172. package/dist/src/lib/storage.js +71 -0
  173. package/dist/src/lib/stripe.d.ts +10 -0
  174. package/dist/src/lib/stripe.js +36 -0
  175. package/dist/src/lib/validation.d.ts +51 -0
  176. package/dist/src/lib/validation.js +64 -0
  177. package/dist/src/lib/workspace-kb.d.ts +5 -0
  178. package/dist/src/lib/workspace-kb.js +7 -0
  179. package/dist/src/repositories/artifact.repository.d.ts +64 -0
  180. package/dist/src/repositories/artifact.repository.js +40 -0
  181. package/dist/src/repositories/base.repository.d.ts +14 -0
  182. package/dist/src/repositories/base.repository.js +14 -0
  183. package/dist/src/repositories/invitation.repository.d.ts +104 -0
  184. package/dist/src/repositories/invitation.repository.js +44 -0
  185. package/dist/src/repositories/notification.repository.d.ts +76 -0
  186. package/dist/src/repositories/notification.repository.js +44 -0
  187. package/dist/src/repositories/user.repository.d.ts +84 -0
  188. package/dist/src/repositories/user.repository.js +37 -0
  189. package/dist/src/repositories/workspace-member.repository.d.ts +35 -0
  190. package/dist/src/repositories/workspace-member.repository.js +31 -0
  191. package/dist/src/repositories/workspace.repository.d.ts +101 -0
  192. package/dist/src/repositories/workspace.repository.js +79 -0
  193. package/dist/src/routers/_app.d.ts +3464 -0
  194. package/dist/src/routers/_app.js +36 -0
  195. package/dist/src/routers/admin.d.ts +358 -0
  196. package/dist/src/routers/admin.js +105 -0
  197. package/dist/src/routers/annotations.d.ts +219 -0
  198. package/dist/src/routers/annotations.js +29 -0
  199. package/dist/src/routers/artifactVersions.d.ts +65 -0
  200. package/dist/src/routers/artifactVersions.js +14 -0
  201. package/dist/src/routers/auth.d.ts +161 -0
  202. package/dist/src/routers/auth.js +97 -0
  203. package/dist/src/routers/chat.d.ts +170 -0
  204. package/dist/src/routers/chat.js +32 -0
  205. package/dist/src/routers/copilot.d.ts +200 -0
  206. package/dist/src/routers/copilot.js +52 -0
  207. package/dist/src/routers/flashcards.d.ts +336 -0
  208. package/dist/src/routers/flashcards.js +93 -0
  209. package/dist/src/routers/knowledgeBase.d.ts +421 -0
  210. package/dist/src/routers/knowledgeBase.js +118 -0
  211. package/dist/src/routers/members.d.ts +169 -0
  212. package/dist/src/routers/members.js +47 -0
  213. package/dist/src/routers/notifications.d.ts +99 -0
  214. package/dist/src/routers/notifications.js +25 -0
  215. package/dist/src/routers/payment.d.ts +80 -0
  216. package/dist/src/routers/payment.js +21 -0
  217. package/dist/src/routers/podcast.d.ts +287 -0
  218. package/dist/src/routers/podcast.js +34 -0
  219. package/dist/src/routers/studyguide.d.ts +36 -0
  220. package/dist/src/routers/studyguide.js +8 -0
  221. package/dist/src/routers/worksheets.d.ts +429 -0
  222. package/dist/src/routers/worksheets.js +139 -0
  223. package/dist/src/routers/workspace.d.ts +563 -0
  224. package/dist/src/routers/workspace.js +104 -0
  225. package/dist/src/scripts/purge-deleted-users.d.ts +1 -0
  226. package/dist/src/scripts/purge-deleted-users.js +148 -0
  227. package/dist/src/server.d.ts +1 -0
  228. package/dist/src/server.js +190 -0
  229. package/dist/src/services/activity/activity-human-description.service.d.ts +13 -0
  230. package/dist/src/services/activity/activity-human-description.service.js +221 -0
  231. package/dist/src/services/activity/activity-human-description.service.test.d.ts +1 -0
  232. package/dist/src/services/activity/activity-human-description.service.test.js +16 -0
  233. package/dist/src/services/activity/activity-log.service.d.ts +87 -0
  234. package/dist/src/services/activity/activity-log.service.js +276 -0
  235. package/dist/src/services/activity/activity-log.service.test.d.ts +1 -0
  236. package/dist/src/services/activity/activity-log.service.test.js +27 -0
  237. package/dist/src/services/admin/admin.service.d.ts +270 -0
  238. package/dist/src/services/admin/admin.service.js +476 -0
  239. package/dist/src/services/ai/ai-session.service.d.ts +5 -0
  240. package/dist/src/services/ai/ai-session.service.js +4 -0
  241. package/dist/src/services/artifacts/annotation.service.d.ts +177 -0
  242. package/dist/src/services/artifacts/annotation.service.js +154 -0
  243. package/dist/src/services/artifacts/artifact-version.service.d.ts +38 -0
  244. package/dist/src/services/artifacts/artifact-version.service.js +129 -0
  245. package/dist/src/services/artifacts/chat.service.d.ts +127 -0
  246. package/dist/src/services/artifacts/chat.service.js +182 -0
  247. package/dist/src/services/artifacts/study-guide.service.d.ts +18 -0
  248. package/dist/src/services/artifacts/study-guide.service.js +65 -0
  249. package/dist/src/services/auth/auth.service.d.ts +94 -0
  250. package/dist/src/services/auth/auth.service.js +368 -0
  251. package/dist/src/services/base.service.d.ts +14 -0
  252. package/dist/src/services/base.service.js +14 -0
  253. package/dist/src/services/billing/payment.service.d.ts +44 -0
  254. package/dist/src/services/billing/payment.service.js +365 -0
  255. package/dist/src/services/billing/subscription.service.d.ts +37 -0
  256. package/dist/src/services/billing/subscription.service.js +654 -0
  257. package/dist/src/services/billing/usage.service.d.ts +47 -0
  258. package/dist/src/services/billing/usage.service.js +149 -0
  259. package/dist/src/services/content/copilot.service.d.ts +113 -0
  260. package/dist/src/services/content/copilot.service.js +439 -0
  261. package/dist/src/services/content/flashcard-progress.service.d.ts +159 -0
  262. package/dist/src/services/content/flashcard-progress.service.js +432 -0
  263. package/dist/src/services/content/flashcard.service.d.ts +184 -0
  264. package/dist/src/services/content/flashcard.service.js +339 -0
  265. package/dist/src/services/content/media-analysis.service.d.ts +23 -0
  266. package/dist/src/services/content/media-analysis.service.js +404 -0
  267. package/dist/src/services/content/podcast.service.d.ts +267 -0
  268. package/dist/src/services/content/podcast.service.js +653 -0
  269. package/dist/src/services/content/worksheet-content.service.d.ts +37 -0
  270. package/dist/src/services/content/worksheet-content.service.js +84 -0
  271. package/dist/src/services/content/worksheet-content.service.test.d.ts +1 -0
  272. package/dist/src/services/content/worksheet-content.service.test.js +69 -0
  273. package/dist/src/services/content/worksheet-generation.service.d.ts +91 -0
  274. package/dist/src/services/content/worksheet-generation.service.js +95 -0
  275. package/dist/src/services/content/worksheet-generation.service.test.d.ts +1 -0
  276. package/dist/src/services/content/worksheet-generation.service.test.js +20 -0
  277. package/dist/src/services/content/worksheet.service.d.ts +347 -0
  278. package/dist/src/services/content/worksheet.service.js +599 -0
  279. package/dist/src/services/knowledge/knowledge-base.service.d.ts +316 -0
  280. package/dist/src/services/knowledge/knowledge-base.service.js +544 -0
  281. package/dist/src/services/members/invitation.service.d.ts +66 -0
  282. package/dist/src/services/members/invitation.service.js +348 -0
  283. package/dist/src/services/members/member.service.d.ts +36 -0
  284. package/dist/src/services/members/member.service.js +193 -0
  285. package/dist/src/services/notifications/notification.service.d.ts +214 -0
  286. package/dist/src/services/notifications/notification.service.js +550 -0
  287. package/dist/src/services/notifications/notification.service.test.d.ts +1 -0
  288. package/dist/src/services/notifications/notification.service.test.js +87 -0
  289. package/dist/src/services/workspace/workspace-analytics.service.d.ts +24 -0
  290. package/dist/src/services/workspace/workspace-analytics.service.js +95 -0
  291. package/dist/src/services/workspace/workspace-kb.service.d.ts +40 -0
  292. package/dist/src/services/workspace/workspace-kb.service.js +184 -0
  293. package/dist/src/services/workspace/workspace.service.d.ts +263 -0
  294. package/dist/src/services/workspace/workspace.service.js +401 -0
  295. package/dist/src/trpc.d.ts +60 -0
  296. package/dist/src/trpc.js +217 -0
  297. package/dist/src/types/index.d.ts +126 -0
  298. package/dist/src/types/index.js +1 -0
  299. package/dist/trpc.d.ts +12 -4
  300. package/dist/trpc.js +5 -11
  301. package/package.json +8 -9
  302. package/prisma/schema.prisma +3 -4
  303. package/prisma/seed.mjs +5 -2
  304. package/prisma.config.ts +16 -0
  305. package/src/context.ts +33 -3
  306. package/src/lib/ai/index.ts +3 -0
  307. package/src/lib/ai/llm-client.ts +23 -0
  308. package/src/lib/prisma.ts +18 -9
  309. package/src/routers/auth.ts +1 -1
  310. package/src/routers/workspace.ts +4 -0
  311. package/src/scripts/purge-deleted-users.ts +1 -3
  312. package/src/services/billing/payment.service.ts +3 -6
  313. package/src/services/billing/usage.service.ts +190 -77
  314. package/src/services/content/copilot.service.ts +23 -32
  315. package/src/services/content/flashcard-progress.service.ts +12 -9
  316. package/src/services/content/flashcard.service.ts +89 -66
  317. package/src/services/content/media-analysis.service.ts +34 -29
  318. package/src/services/content/worksheet-generation.service.test.ts +2 -2
  319. package/src/services/workspace/workspace.service.ts +73 -66
  320. package/src/trpc.ts +5 -13
  321. package/tsconfig.json +3 -0
@@ -0,0 +1,149 @@
1
+ import { prisma } from '../../lib/prisma.js';
2
+ import { workspaceAccessWhere } from '../../repositories/workspace.repository.js';
3
+ const FALLBACK_PLAN_LIMITS = {
4
+ id: 'fallback-free',
5
+ planId: 'fallback-free',
6
+ maxStorageBytes: BigInt(1024 * 1024 * 1024), // 1GB
7
+ maxWorksheets: 3,
8
+ maxFlashcards: 20,
9
+ maxPodcasts: 0,
10
+ maxStudyGuides: 1,
11
+ };
12
+ const CACHE_TTL_MS = 30000;
13
+ const usageCache = new Map();
14
+ const limitsCache = new Map();
15
+ const accountSummaryCache = new Map();
16
+ function readCache(map, userId) {
17
+ const entry = map.get(userId);
18
+ if (!entry || entry.expiresAt <= Date.now()) {
19
+ map.delete(userId);
20
+ return null;
21
+ }
22
+ return entry.value;
23
+ }
24
+ function writeCache(map, userId, value) {
25
+ map.set(userId, { value, expiresAt: Date.now() + CACHE_TTL_MS });
26
+ }
27
+ /** Bust cached usage/limit reads after creates, deletes, or billing changes. */
28
+ export function invalidateUserBillingCache(userId) {
29
+ usageCache.delete(userId);
30
+ limitsCache.delete(userId);
31
+ accountSummaryCache.delete(userId);
32
+ }
33
+ function workspaceAccessFilter(userId) {
34
+ return workspaceAccessWhere(userId);
35
+ }
36
+ /**
37
+ * Counts all resources consumed by a user across the platform.
38
+ */
39
+ export async function getUserUsage(userId) {
40
+ const cached = readCache(usageCache, userId);
41
+ if (cached)
42
+ return cached;
43
+ const [flashcards, worksheets, studyGuides, podcasts, storageResult] = await Promise.all([
44
+ prisma.artifact.count({ where: { createdById: userId, type: 'FLASHCARD_SET' } }),
45
+ prisma.artifact.count({ where: { createdById: userId, type: 'WORKSHEET' } }),
46
+ prisma.artifact.count({ where: { createdById: userId, type: 'STUDY_GUIDE' } }),
47
+ prisma.artifact.count({ where: { createdById: userId, type: 'PODCAST_EPISODE' } }),
48
+ prisma.fileAsset.aggregate({
49
+ _sum: { size: true },
50
+ where: { userId },
51
+ }),
52
+ ]);
53
+ const usage = {
54
+ flashcards,
55
+ worksheets,
56
+ studyGuides,
57
+ podcasts,
58
+ storageBytes: Number(storageResult._sum.size || 0),
59
+ };
60
+ writeCache(usageCache, userId, usage);
61
+ return usage;
62
+ }
63
+ /**
64
+ * Retrieves the specific plan limits for a user based on their active subscription
65
+ * PLUS any extra credits purchased.
66
+ */
67
+ export async function getUserPlanLimits(userId) {
68
+ const cached = readCache(limitsCache, userId);
69
+ if (cached)
70
+ return cached;
71
+ const [activeSub, freePlan, userCredits] = await Promise.all([
72
+ prisma.subscription.findFirst({
73
+ where: { userId, status: 'active' },
74
+ include: { plan: { include: { limit: true } } },
75
+ orderBy: { createdAt: 'desc' },
76
+ }),
77
+ prisma.plan.findFirst({
78
+ where: {
79
+ active: true,
80
+ price: 0,
81
+ limit: { isNot: null },
82
+ },
83
+ include: { limit: true },
84
+ orderBy: { createdAt: 'asc' },
85
+ }),
86
+ prisma.userCredit.groupBy({
87
+ by: ['resourceType'],
88
+ where: { userId },
89
+ _sum: { amount: true },
90
+ }),
91
+ ]);
92
+ const baseLimit = activeSub?.plan?.limit ?? freePlan?.limit;
93
+ const base = baseLimit ?? FALLBACK_PLAN_LIMITS;
94
+ const getCreditSum = (type) => userCredits.find((c) => c.resourceType === type)?._sum.amount || 0;
95
+ const limits = {
96
+ id: base.id,
97
+ planId: base.planId,
98
+ maxStorageBytes: BigInt(Number(base.maxStorageBytes) + getCreditSum('STORAGE') * 1024 * 1024),
99
+ maxWorksheets: base.maxWorksheets + getCreditSum('WORKSHEET'),
100
+ maxFlashcards: base.maxFlashcards + getCreditSum('FLASHCARD_SET'),
101
+ maxPodcasts: base.maxPodcasts + getCreditSum('PODCAST_EPISODE'),
102
+ maxStudyGuides: base.maxStudyGuides + getCreditSum('STUDY_GUIDE'),
103
+ createdAt: baseLimit?.createdAt || new Date(),
104
+ updatedAt: baseLimit?.updatedAt || new Date(),
105
+ isFallbackPlan: !baseLimit,
106
+ };
107
+ writeCache(limitsCache, userId, limits);
108
+ return limits;
109
+ }
110
+ /**
111
+ * Sidebar/dashboard summary: stats + usage + limits in one parallel DB round-trip batch.
112
+ */
113
+ export async function getAccountSummary(userId) {
114
+ const cached = readCache(accountSummaryCache, userId);
115
+ if (cached)
116
+ return cached;
117
+ const workspaceWhere = workspaceAccessFilter(userId);
118
+ const [workspaceMeta, folderCount, usage, limits, storageUsed] = await Promise.all([
119
+ prisma.workspace.aggregate({
120
+ where: workspaceWhere,
121
+ _count: { _all: true },
122
+ _max: { updatedAt: true },
123
+ }),
124
+ prisma.folder.count({ where: { ownerId: userId } }),
125
+ getUserUsage(userId),
126
+ getUserPlanLimits(userId),
127
+ prisma.fileAsset.aggregate({
128
+ where: {
129
+ userId,
130
+ workspace: workspaceWhere,
131
+ },
132
+ _sum: { size: true },
133
+ }),
134
+ ]);
135
+ const summary = {
136
+ stats: {
137
+ workspaces: workspaceMeta._count._all,
138
+ folders: folderCount,
139
+ lastUpdated: workspaceMeta._max.updatedAt,
140
+ spaceUsed: Number(storageUsed._sum.size ?? 0),
141
+ spaceTotal: Number(limits.maxStorageBytes),
142
+ },
143
+ usage,
144
+ limits,
145
+ hasActivePlan: !limits.isFallbackPlan,
146
+ };
147
+ writeCache(accountSummaryCache, userId, summary);
148
+ return summary;
149
+ }
@@ -0,0 +1,113 @@
1
+ import type { PrismaClient } from '@prisma/client';
2
+ import { z } from 'zod';
3
+ import { BaseService } from '../base.service.js';
4
+ export declare const copilotArtifactType: z.ZodEnum<{
5
+ flashcards: "flashcards";
6
+ worksheet: "worksheet";
7
+ "study-guide": "study-guide";
8
+ }>;
9
+ export declare const copilotContextSchema: z.ZodObject<{
10
+ workspaceId: z.ZodString;
11
+ artifactId: z.ZodString;
12
+ artifactType: z.ZodEnum<{
13
+ flashcards: "flashcards";
14
+ worksheet: "worksheet";
15
+ "study-guide": "study-guide";
16
+ }>;
17
+ documentContent: z.ZodString;
18
+ documentPlainText: z.ZodOptional<z.ZodString>;
19
+ selectedText: z.ZodOptional<z.ZodString>;
20
+ viewportText: z.ZodOptional<z.ZodString>;
21
+ cursorPosition: z.ZodOptional<z.ZodObject<{
22
+ start: z.ZodNumber;
23
+ end: z.ZodNumber;
24
+ }, z.core.$strip>>;
25
+ metadata: z.ZodOptional<z.ZodObject<{
26
+ flashcardId: z.ZodOptional<z.ZodString>;
27
+ questionId: z.ZodOptional<z.ZodString>;
28
+ documentId: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$strip>>;
30
+ }, z.core.$strip>;
31
+ export type CopilotContextInput = z.infer<typeof copilotContextSchema>;
32
+ export declare class CopilotService extends BaseService {
33
+ constructor(db: PrismaClient);
34
+ private assertWorkspaceAccess;
35
+ private assertConversationAccess;
36
+ private loadHistory;
37
+ private persistConversationExchange;
38
+ listConversations(userId: string, workspaceId: string): Promise<{
39
+ id: string;
40
+ title: string;
41
+ updatedAt: Date;
42
+ preview: string;
43
+ }[]>;
44
+ getConversation(userId: string, workspaceId: string, conversationId: string): Promise<{
45
+ id: string;
46
+ title: string;
47
+ messages: {
48
+ id: string;
49
+ role: string;
50
+ content: string;
51
+ createdAt: Date;
52
+ }[];
53
+ }>;
54
+ createConversation(userId: string, input: {
55
+ workspaceId: string;
56
+ title?: string;
57
+ }): Promise<{
58
+ id: string;
59
+ title: string;
60
+ updatedAt: Date;
61
+ }>;
62
+ deleteConversation(userId: string, workspaceId: string, conversationId: string): Promise<{
63
+ success: boolean;
64
+ }>;
65
+ ask(userId: string, input: {
66
+ context: CopilotContextInput;
67
+ message: string;
68
+ conversationId?: string;
69
+ }): Promise<{
70
+ answer: string;
71
+ highlights: {
72
+ text: string;
73
+ label?: string;
74
+ }[];
75
+ }>;
76
+ explainSelection(userId: string, input: {
77
+ context: CopilotContextInput;
78
+ message?: string;
79
+ conversationId?: string;
80
+ }): Promise<{
81
+ answer: string;
82
+ }>;
83
+ suggestHighlights(userId: string, input: {
84
+ context: CopilotContextInput;
85
+ message?: string;
86
+ conversationId?: string;
87
+ }): Promise<{
88
+ answer: string;
89
+ highlights: {
90
+ text: string;
91
+ label?: string;
92
+ }[];
93
+ }>;
94
+ generateFlashcards(userId: string, input: {
95
+ context: CopilotContextInput;
96
+ message?: string;
97
+ numCards: number;
98
+ conversationId?: string;
99
+ }): Promise<{
100
+ answer: string;
101
+ artifactId: string;
102
+ flashcards: {
103
+ id: string;
104
+ createdAt: Date;
105
+ tags: string[];
106
+ artifactId: string;
107
+ order: number;
108
+ front: string;
109
+ back: string;
110
+ acceptedAnswers: string[];
111
+ }[];
112
+ }>;
113
+ }
@@ -0,0 +1,439 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { z } from 'zod';
3
+ import { BaseService } from '../base.service.js';
4
+ import { ai } from '../../lib/ai/index.js';
5
+ import { workspaceKbService } from '../workspace/workspace-kb.service.js';
6
+ import { sanitizeString } from '../../lib/validation.js';
7
+ import { workspaceAccessWhere } from '../../repositories/workspace.repository.js';
8
+ import PusherService from '../../lib/pusher.js';
9
+ import { FlashcardService } from './flashcard.service.js';
10
+ export const copilotArtifactType = z.enum([
11
+ 'study-guide',
12
+ 'worksheet',
13
+ 'flashcards',
14
+ ]);
15
+ export const copilotContextSchema = z.object({
16
+ workspaceId: z.string().min(1),
17
+ artifactId: z.string().min(1),
18
+ artifactType: copilotArtifactType,
19
+ documentContent: z.string().min(1),
20
+ documentPlainText: z.string().optional(),
21
+ selectedText: z.string().optional(),
22
+ viewportText: z.string().optional(),
23
+ cursorPosition: z
24
+ .object({
25
+ start: z.number().int().min(0),
26
+ end: z.number().int().min(0),
27
+ })
28
+ .optional(),
29
+ metadata: z
30
+ .object({
31
+ flashcardId: z.string().optional(),
32
+ questionId: z.string().optional(),
33
+ documentId: z.string().optional(),
34
+ })
35
+ .optional(),
36
+ });
37
+ const highlightInstructionSchema = z.object({
38
+ text: z.string().min(1),
39
+ label: z.string().optional(),
40
+ });
41
+ const flashcardSchema = z.object({
42
+ front: z.string().min(1),
43
+ back: z.string().min(1),
44
+ });
45
+ const RATE_LIMIT_WINDOW_MS = 60000;
46
+ const RATE_LIMIT_MAX_REQUESTS = 20;
47
+ const requestBuckets = new Map();
48
+ function enforceRateLimit(userId, workspaceId) {
49
+ const now = Date.now();
50
+ const key = `${userId}:${workspaceId}`;
51
+ const existing = requestBuckets.get(key) ?? [];
52
+ const recent = existing.filter((timestamp) => now - timestamp < RATE_LIMIT_WINDOW_MS);
53
+ if (recent.length >= RATE_LIMIT_MAX_REQUESTS) {
54
+ throw new TRPCError({
55
+ code: 'TOO_MANY_REQUESTS',
56
+ message: 'Too many copilot requests. Please wait a moment.',
57
+ });
58
+ }
59
+ recent.push(now);
60
+ requestBuckets.set(key, recent);
61
+ }
62
+ function contextWindow(context) {
63
+ return sanitizeString(context.documentContent, 5000000);
64
+ }
65
+ function extractJson(content) {
66
+ return ai.parse.extractJson(content);
67
+ }
68
+ function getAnswerFromParsedJSON(parsed) {
69
+ if (!parsed)
70
+ return null;
71
+ const potentialKeys = [
72
+ 'answer',
73
+ 'summary',
74
+ 'explanation',
75
+ 'content',
76
+ 'explanationText',
77
+ 'response',
78
+ 'result',
79
+ ];
80
+ for (const key of potentialKeys) {
81
+ if (typeof parsed[key] === 'string' && parsed[key].length > 0) {
82
+ return parsed[key];
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+ function normalizeHighlightInstructions(raw, anchorDocument) {
88
+ const parsed = z.array(highlightInstructionSchema).safeParse(raw);
89
+ if (!parsed.success)
90
+ return [];
91
+ const seen = new Set();
92
+ const results = [];
93
+ for (const item of parsed.data) {
94
+ const text = sanitizeString(item.text, 800).trim();
95
+ if (text.length < 2)
96
+ continue;
97
+ if (!anchorDocument.includes(text))
98
+ continue;
99
+ const key = text.toLowerCase();
100
+ if (seen.has(key))
101
+ continue;
102
+ seen.add(key);
103
+ results.push({
104
+ text,
105
+ label: item.label ? sanitizeString(item.label, 120) : undefined,
106
+ });
107
+ }
108
+ return results;
109
+ }
110
+ async function callCopilotModel(params) {
111
+ const documentSnippet = contextWindow(params.context);
112
+ const selectedText = params.context.selectedText
113
+ ? sanitizeString(params.context.selectedText, 2000)
114
+ : '';
115
+ const viewportText = params.context.viewportText
116
+ ? sanitizeString(params.context.viewportText, 4000)
117
+ : '';
118
+ const message = sanitizeString(params.message, 4000);
119
+ const ragContext = await workspaceKbService.retrieveContext(params.context.workspaceId, message, 6);
120
+ const systemPrompt = [
121
+ 'You are an AI study assistant for the Scribe learning platform.',
122
+ 'Be concise, correct, and safe. Do not claim edits were applied unless asked and confirmed by the user.',
123
+ 'Return valid JSON only.',
124
+ '',
125
+ 'OUTPUT FORMAT:',
126
+ '{',
127
+ ' "answer": "markdown response for the user",',
128
+ ' "highlights": [{ "text": "exact quote from PLAIN_DOCUMENT", "label": "optional" }],',
129
+ ' "flashcards": [{ "front": "question", "back": "answer" }]',
130
+ '}',
131
+ 'Include only relevant keys for the task.',
132
+ 'For highlights: each "text" must be copied verbatim from PLAIN_DOCUMENT. Prefer unique phrases of about 3–40 words, not isolated common words. Do not use numeric start/end offsets.',
133
+ '',
134
+ `MODE: ${params.mode}`,
135
+ `ARTIFACT_TYPE: ${params.context.artifactType}`,
136
+ ].join('\n');
137
+ const plainSnippet = params.context.documentPlainText
138
+ ? sanitizeString(params.context.documentPlainText, 12000)
139
+ : '';
140
+ const contextPrompt = [
141
+ 'DOCUMENT:',
142
+ documentSnippet,
143
+ '',
144
+ 'PLAIN_DOCUMENT:',
145
+ plainSnippet || documentSnippet.slice(0, 12000),
146
+ '',
147
+ 'WORKSPACE_KNOWLEDGE:',
148
+ ragContext || 'None',
149
+ '',
150
+ 'SELECTION:',
151
+ selectedText || 'None',
152
+ '',
153
+ 'VIEWPORT_TEXT:',
154
+ viewportText || 'None',
155
+ ].join('\n');
156
+ const messages = [
157
+ { role: 'system', content: systemPrompt },
158
+ { role: 'user', content: contextPrompt },
159
+ ];
160
+ if (params.history && params.history.length > 0) {
161
+ params.history.forEach((msg) => {
162
+ messages.push({
163
+ role: msg.role === 'user' ? 'user' : 'assistant',
164
+ content: msg.content,
165
+ });
166
+ });
167
+ }
168
+ messages.push({ role: 'user', content: message });
169
+ const response = await ai.llm.complete(messages);
170
+ const content = response.choices?.[0]?.message?.content ?? '';
171
+ const parsed = extractJson(content);
172
+ return { rawContent: content, parsed };
173
+ }
174
+ export class CopilotService extends BaseService {
175
+ constructor(db) {
176
+ super(db);
177
+ }
178
+ async assertWorkspaceAccess(userId, workspaceId) {
179
+ const workspace = await this.db.workspace.findFirst({
180
+ where: { id: workspaceId, ...workspaceAccessWhere(userId) },
181
+ select: { id: true },
182
+ });
183
+ if (!workspace) {
184
+ throw new TRPCError({
185
+ code: 'FORBIDDEN',
186
+ message: 'You do not have access to this workspace',
187
+ });
188
+ }
189
+ }
190
+ async assertConversationAccess(userId, workspaceId, conversationId) {
191
+ const conversation = await this.db.copilotConversation.findFirst({
192
+ where: { id: conversationId, workspaceId, userId },
193
+ select: { id: true },
194
+ });
195
+ if (!conversation) {
196
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Conversation not found' });
197
+ }
198
+ }
199
+ async loadHistory(conversationId) {
200
+ if (!conversationId)
201
+ return [];
202
+ const messages = await this.db.copilotMessage.findMany({
203
+ where: { conversationId },
204
+ orderBy: { createdAt: 'asc' },
205
+ take: 50,
206
+ });
207
+ return messages.map((m) => ({
208
+ role: m.role.toLowerCase(),
209
+ content: m.content,
210
+ }));
211
+ }
212
+ async persistConversationExchange(params) {
213
+ if (!params.conversationId)
214
+ return;
215
+ await this.assertConversationAccess(params.userId, params.workspaceId, params.conversationId);
216
+ const generatedTitle = sanitizeString(params.userMessage.replace(/\s+/g, ' ').trim(), 80);
217
+ const conversation = await this.db.copilotConversation.findUnique({
218
+ where: { id: params.conversationId },
219
+ select: { title: true },
220
+ });
221
+ const shouldUpdateTitle = !!conversation &&
222
+ conversation.title.trim().toLowerCase() === 'new chat' &&
223
+ generatedTitle.length > 0;
224
+ await this.db.copilotMessage.createMany({
225
+ data: [
226
+ {
227
+ conversationId: params.conversationId,
228
+ role: 'USER',
229
+ content: sanitizeString(params.userMessage, 8000),
230
+ },
231
+ {
232
+ conversationId: params.conversationId,
233
+ role: 'ASSISTANT',
234
+ content: sanitizeString(params.assistantMessage, 20000),
235
+ },
236
+ ],
237
+ });
238
+ await this.db.copilotConversation.update({
239
+ where: { id: params.conversationId },
240
+ data: {
241
+ updatedAt: new Date(),
242
+ ...(shouldUpdateTitle ? { title: generatedTitle } : {}),
243
+ },
244
+ });
245
+ }
246
+ async listConversations(userId, workspaceId) {
247
+ await this.assertWorkspaceAccess(userId, workspaceId);
248
+ const conversations = await this.db.copilotConversation.findMany({
249
+ where: { workspaceId, userId },
250
+ include: {
251
+ messages: { orderBy: { createdAt: 'desc' }, take: 1 },
252
+ },
253
+ orderBy: { updatedAt: 'desc' },
254
+ });
255
+ return conversations.map((conversation) => ({
256
+ id: conversation.id,
257
+ title: conversation.title,
258
+ updatedAt: conversation.updatedAt,
259
+ preview: conversation.messages[0]?.content ?? '',
260
+ }));
261
+ }
262
+ async getConversation(userId, workspaceId, conversationId) {
263
+ await this.assertWorkspaceAccess(userId, workspaceId);
264
+ await this.assertConversationAccess(userId, workspaceId, conversationId);
265
+ const conversation = await this.db.copilotConversation.findUnique({
266
+ where: { id: conversationId },
267
+ include: { messages: { orderBy: { createdAt: 'asc' } } },
268
+ });
269
+ if (!conversation) {
270
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Conversation not found' });
271
+ }
272
+ return {
273
+ id: conversation.id,
274
+ title: conversation.title,
275
+ messages: conversation.messages.map((message) => ({
276
+ id: message.id,
277
+ role: message.role === 'USER' ? 'user' : 'assistant',
278
+ content: message.content,
279
+ createdAt: message.createdAt,
280
+ })),
281
+ };
282
+ }
283
+ async createConversation(userId, input) {
284
+ await this.assertWorkspaceAccess(userId, input.workspaceId);
285
+ const conversation = await this.db.copilotConversation.create({
286
+ data: {
287
+ workspaceId: input.workspaceId,
288
+ userId,
289
+ title: sanitizeString(input.title || 'New Chat', 120),
290
+ },
291
+ });
292
+ return {
293
+ id: conversation.id,
294
+ title: conversation.title,
295
+ updatedAt: conversation.updatedAt,
296
+ };
297
+ }
298
+ async deleteConversation(userId, workspaceId, conversationId) {
299
+ await this.assertWorkspaceAccess(userId, workspaceId);
300
+ await this.assertConversationAccess(userId, workspaceId, conversationId);
301
+ await this.db.copilotConversation.delete({ where: { id: conversationId } });
302
+ return { success: true };
303
+ }
304
+ async ask(userId, input) {
305
+ enforceRateLimit(userId, input.context.workspaceId);
306
+ await this.assertWorkspaceAccess(userId, input.context.workspaceId);
307
+ const history = await this.loadHistory(input.conversationId);
308
+ const model = await callCopilotModel({
309
+ mode: 'ask',
310
+ context: input.context,
311
+ message: input.message,
312
+ history,
313
+ });
314
+ const answer = getAnswerFromParsedJSON(model.parsed) ||
315
+ model.rawContent ||
316
+ 'I could not generate a response.';
317
+ const safeHighlights = normalizeHighlightInstructions(model.parsed?.highlights, input.context.documentPlainText ?? input.context.documentContent);
318
+ await this.persistConversationExchange({
319
+ userId,
320
+ workspaceId: input.context.workspaceId,
321
+ conversationId: input.conversationId,
322
+ userMessage: input.message,
323
+ assistantMessage: answer,
324
+ });
325
+ return { answer, highlights: safeHighlights };
326
+ }
327
+ async explainSelection(userId, input) {
328
+ enforceRateLimit(userId, input.context.workspaceId);
329
+ await this.assertWorkspaceAccess(userId, input.context.workspaceId);
330
+ const question = input.message && input.message.trim().length > 0
331
+ ? input.message
332
+ : 'Explain the selected text in simple study-friendly terms and include one practical example.';
333
+ const history = await this.loadHistory(input.conversationId);
334
+ const model = await callCopilotModel({
335
+ mode: 'explainSelection',
336
+ context: input.context,
337
+ message: question,
338
+ history,
339
+ });
340
+ const answer = getAnswerFromParsedJSON(model.parsed) ||
341
+ model.rawContent ||
342
+ 'I could not explain this selection.';
343
+ await this.persistConversationExchange({
344
+ userId,
345
+ workspaceId: input.context.workspaceId,
346
+ conversationId: input.conversationId,
347
+ userMessage: question,
348
+ assistantMessage: answer,
349
+ });
350
+ return { answer };
351
+ }
352
+ async suggestHighlights(userId, input) {
353
+ enforceRateLimit(userId, input.context.workspaceId);
354
+ await this.assertWorkspaceAccess(userId, input.context.workspaceId);
355
+ await PusherService.emitTaskComplete(input.context.workspaceId, 'copilot:thinking', {
356
+ status: 'started',
357
+ });
358
+ const instruction = input.message && input.message.trim().length > 0
359
+ ? input.message
360
+ : 'Suggest key highlights. Return exact phrases copied from PLAIN_DOCUMENT (about 3–40 words each).';
361
+ const history = await this.loadHistory(input.conversationId);
362
+ const model = await callCopilotModel({
363
+ mode: 'suggestHighlights',
364
+ context: input.context,
365
+ message: instruction,
366
+ history,
367
+ });
368
+ const safeHighlights = normalizeHighlightInstructions(model.parsed?.highlights, input.context.documentPlainText ?? input.context.documentContent);
369
+ await PusherService.emitTaskComplete(input.context.workspaceId, 'copilot:highlight_suggestions', { highlights: safeHighlights });
370
+ const answer = getAnswerFromParsedJSON(model.parsed) || 'Here are suggested highlights.';
371
+ await this.persistConversationExchange({
372
+ userId,
373
+ workspaceId: input.context.workspaceId,
374
+ conversationId: input.conversationId,
375
+ userMessage: instruction,
376
+ assistantMessage: answer,
377
+ });
378
+ return { answer, highlights: safeHighlights };
379
+ }
380
+ async generateFlashcards(userId, input) {
381
+ enforceRateLimit(userId, input.context.workspaceId);
382
+ await this.assertWorkspaceAccess(userId, input.context.workspaceId);
383
+ await PusherService.emitTaskComplete(input.context.workspaceId, 'copilot:thinking', {
384
+ status: 'started',
385
+ });
386
+ const instruction = input.message && input.message.trim().length > 0
387
+ ? input.message
388
+ : `Generate ${input.numCards} concise study flashcards from this context.`;
389
+ const history = await this.loadHistory(input.conversationId);
390
+ const model = await callCopilotModel({
391
+ mode: 'generateFlashcards',
392
+ context: input.context,
393
+ message: instruction,
394
+ history,
395
+ });
396
+ const parsedFlashcards = z.array(flashcardSchema).safeParse(model.parsed?.flashcards);
397
+ const cards = (parsedFlashcards.success ? parsedFlashcards.data : []).slice(0, input.numCards);
398
+ if (cards.length === 0) {
399
+ throw new TRPCError({
400
+ code: 'BAD_REQUEST',
401
+ message: 'Copilot did not return valid flashcards.',
402
+ });
403
+ }
404
+ const flashcardService = new FlashcardService(this.db);
405
+ const primarySet = await flashcardService.ensurePrimarySet(userId, input.context.workspaceId);
406
+ const orderOffset = primarySet.flashcards.reduce((max, card) => Math.max(max, card.order), -1);
407
+ await this.db.flashcard.createMany({
408
+ data: cards.map((card, index) => ({
409
+ artifactId: primarySet.id,
410
+ front: sanitizeString(card.front, 200),
411
+ back: sanitizeString(card.back, 500),
412
+ order: orderOffset + 1 + index,
413
+ tags: ['copilot-generated'],
414
+ })),
415
+ });
416
+ const artifact = await this.db.artifact.findUniqueOrThrow({
417
+ where: { id: primarySet.id },
418
+ include: { flashcards: true },
419
+ });
420
+ await PusherService.emitTaskComplete(input.context.workspaceId, 'copilot:response', {
421
+ status: 'completed',
422
+ flashcardSetId: artifact.id,
423
+ });
424
+ const answer = getAnswerFromParsedJSON(model.parsed) ||
425
+ `Generated ${artifact.flashcards.length} flashcards.`;
426
+ await this.persistConversationExchange({
427
+ userId,
428
+ workspaceId: input.context.workspaceId,
429
+ conversationId: input.conversationId,
430
+ userMessage: instruction,
431
+ assistantMessage: answer,
432
+ });
433
+ return {
434
+ answer,
435
+ artifactId: artifact.id,
436
+ flashcards: artifact.flashcards,
437
+ };
438
+ }
439
+ }