@goscribe/server 1.6.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 (281) hide show
  1. package/dist/generated/prisma/client.d.ts +224 -0
  2. package/dist/generated/prisma/client.js +34 -0
  3. package/dist/generated/prisma/commonInputTypes.d.ts +941 -0
  4. package/dist/generated/prisma/commonInputTypes.js +10 -0
  5. package/dist/generated/prisma/enums.d.ts +67 -0
  6. package/dist/generated/prisma/enums.js +66 -0
  7. package/dist/generated/prisma/internal/class.d.ts +539 -0
  8. package/dist/generated/prisma/internal/class.js +49 -0
  9. package/dist/generated/prisma/internal/prismaNamespace.d.ts +3924 -0
  10. package/dist/generated/prisma/internal/prismaNamespace.js +557 -0
  11. package/dist/generated/prisma/models/ActivityLog.d.ts +1847 -0
  12. package/dist/generated/prisma/models/ActivityLog.js +1 -0
  13. package/dist/generated/prisma/models/Artifact.d.ts +2345 -0
  14. package/dist/generated/prisma/models/Artifact.js +1 -0
  15. package/dist/generated/prisma/models/ArtifactVersion.d.ts +1550 -0
  16. package/dist/generated/prisma/models/ArtifactVersion.js +1 -0
  17. package/dist/generated/prisma/models/Channel.d.ts +1257 -0
  18. package/dist/generated/prisma/models/Channel.js +1 -0
  19. package/dist/generated/prisma/models/Chat.d.ts +1339 -0
  20. package/dist/generated/prisma/models/Chat.js +1 -0
  21. package/dist/generated/prisma/models/CopilotConversation.d.ts +1450 -0
  22. package/dist/generated/prisma/models/CopilotConversation.js +1 -0
  23. package/dist/generated/prisma/models/CopilotMessage.d.ts +1179 -0
  24. package/dist/generated/prisma/models/CopilotMessage.js +1 -0
  25. package/dist/generated/prisma/models/FileAsset.d.ts +1832 -0
  26. package/dist/generated/prisma/models/FileAsset.js +1 -0
  27. package/dist/generated/prisma/models/Flashcard.d.ts +1460 -0
  28. package/dist/generated/prisma/models/Flashcard.js +1 -0
  29. package/dist/generated/prisma/models/FlashcardProgress.d.ts +1782 -0
  30. package/dist/generated/prisma/models/FlashcardProgress.js +1 -0
  31. package/dist/generated/prisma/models/Folder.d.ts +1685 -0
  32. package/dist/generated/prisma/models/Folder.js +1 -0
  33. package/dist/generated/prisma/models/IdempotencyRecord.d.ts +1319 -0
  34. package/dist/generated/prisma/models/IdempotencyRecord.js +1 -0
  35. package/dist/generated/prisma/models/Invoice.d.ts +1586 -0
  36. package/dist/generated/prisma/models/Invoice.js +1 -0
  37. package/dist/generated/prisma/models/KnowledgeBase.d.ts +1721 -0
  38. package/dist/generated/prisma/models/KnowledgeBase.js +1 -0
  39. package/dist/generated/prisma/models/KnowledgeBaseChunk.d.ts +1333 -0
  40. package/dist/generated/prisma/models/KnowledgeBaseChunk.js +1 -0
  41. package/dist/generated/prisma/models/KnowledgeBaseDocument.d.ts +1695 -0
  42. package/dist/generated/prisma/models/KnowledgeBaseDocument.js +1 -0
  43. package/dist/generated/prisma/models/Notification.d.ts +1992 -0
  44. package/dist/generated/prisma/models/Notification.js +1 -0
  45. package/dist/generated/prisma/models/PasswordResetToken.d.ts +1210 -0
  46. package/dist/generated/prisma/models/PasswordResetToken.js +1 -0
  47. package/dist/generated/prisma/models/Plan.d.ts +1431 -0
  48. package/dist/generated/prisma/models/Plan.js +1 -0
  49. package/dist/generated/prisma/models/PlanLimit.d.ts +1328 -0
  50. package/dist/generated/prisma/models/PlanLimit.js +1 -0
  51. package/dist/generated/prisma/models/PodcastSegment.d.ts +1564 -0
  52. package/dist/generated/prisma/models/PodcastSegment.js +1 -0
  53. package/dist/generated/prisma/models/ResourcePrice.d.ts +1008 -0
  54. package/dist/generated/prisma/models/ResourcePrice.js +1 -0
  55. package/dist/generated/prisma/models/Role.d.ts +1065 -0
  56. package/dist/generated/prisma/models/Role.js +1 -0
  57. package/dist/generated/prisma/models/Session.d.ts +1105 -0
  58. package/dist/generated/prisma/models/Session.js +1 -0
  59. package/dist/generated/prisma/models/StripeEvent.d.ts +1081 -0
  60. package/dist/generated/prisma/models/StripeEvent.js +1 -0
  61. package/dist/generated/prisma/models/StudyGuideComment.d.ts +1321 -0
  62. package/dist/generated/prisma/models/StudyGuideComment.js +1 -0
  63. package/dist/generated/prisma/models/StudyGuideHighlight.d.ts +1629 -0
  64. package/dist/generated/prisma/models/StudyGuideHighlight.js +1 -0
  65. package/dist/generated/prisma/models/Subscription.d.ts +1677 -0
  66. package/dist/generated/prisma/models/Subscription.js +1 -0
  67. package/dist/generated/prisma/models/User.d.ts +7559 -0
  68. package/dist/generated/prisma/models/User.js +1 -0
  69. package/dist/generated/prisma/models/UserCredit.d.ts +1249 -0
  70. package/dist/generated/prisma/models/UserCredit.js +1 -0
  71. package/dist/generated/prisma/models/VerificationToken.d.ts +946 -0
  72. package/dist/generated/prisma/models/VerificationToken.js +1 -0
  73. package/dist/generated/prisma/models/WorksheetPreset.d.ts +1433 -0
  74. package/dist/generated/prisma/models/WorksheetPreset.js +1 -0
  75. package/dist/generated/prisma/models/WorksheetQuestion.d.ts +1491 -0
  76. package/dist/generated/prisma/models/WorksheetQuestion.js +1 -0
  77. package/dist/generated/prisma/models/WorksheetQuestionProgress.d.ts +1620 -0
  78. package/dist/generated/prisma/models/WorksheetQuestionProgress.js +1 -0
  79. package/dist/generated/prisma/models/Workspace.d.ts +3620 -0
  80. package/dist/generated/prisma/models/Workspace.js +1 -0
  81. package/dist/generated/prisma/models/WorkspaceInvitation.d.ts +1490 -0
  82. package/dist/generated/prisma/models/WorkspaceInvitation.js +1 -0
  83. package/dist/generated/prisma/models/WorkspaceKnowledgeBase.d.ts +1410 -0
  84. package/dist/generated/prisma/models/WorkspaceKnowledgeBase.js +1 -0
  85. package/dist/generated/prisma/models/WorkspaceMember.d.ts +1326 -0
  86. package/dist/generated/prisma/models/WorkspaceMember.js +1 -0
  87. package/dist/generated/prisma/models.d.ts +39 -0
  88. package/dist/generated/prisma/models.js +1 -0
  89. package/dist/src/context.d.ts +27 -0
  90. package/dist/src/context.js +33 -0
  91. package/dist/src/index.d.ts +3 -0
  92. package/dist/src/index.js +1 -0
  93. package/dist/src/lib/ai/config.d.ts +20 -0
  94. package/dist/src/lib/ai/config.js +31 -0
  95. package/dist/src/lib/ai/embedding-client.d.ts +8 -0
  96. package/dist/src/lib/ai/embedding-client.js +30 -0
  97. package/dist/src/lib/ai/index.d.ts +48 -0
  98. package/dist/src/lib/ai/index.js +29 -0
  99. package/dist/src/lib/ai/inference-backend/client.d.ts +28 -0
  100. package/dist/src/lib/ai/inference-backend/client.js +301 -0
  101. package/dist/src/lib/ai/inference-backend/mocks.d.ts +12 -0
  102. package/dist/src/lib/ai/inference-backend/mocks.js +133 -0
  103. package/dist/src/lib/ai/inference-backend/types.d.ts +44 -0
  104. package/dist/src/lib/ai/inference-backend/types.js +1 -0
  105. package/dist/src/lib/ai/json-parse.d.ts +2 -0
  106. package/dist/src/lib/ai/json-parse.js +34 -0
  107. package/dist/src/lib/ai/llm-client.d.ts +7 -0
  108. package/dist/src/lib/ai/llm-client.js +36 -0
  109. package/dist/src/lib/ai/mock.d.ts +2 -0
  110. package/dist/src/lib/ai/mock.js +10 -0
  111. package/dist/src/lib/ai/types.d.ts +9 -0
  112. package/dist/src/lib/ai/types.js +1 -0
  113. package/dist/src/lib/auth.d.ts +36 -0
  114. package/dist/src/lib/auth.js +117 -0
  115. package/dist/src/lib/chunking.d.ts +19 -0
  116. package/dist/src/lib/chunking.js +47 -0
  117. package/dist/src/lib/constants.d.ts +13 -0
  118. package/dist/src/lib/constants.js +12 -0
  119. package/dist/src/lib/curated-kb-seed.d.ts +12 -0
  120. package/dist/src/lib/curated-kb-seed.js +155 -0
  121. package/dist/src/lib/email.d.ts +11 -0
  122. package/dist/src/lib/email.js +152 -0
  123. package/dist/src/lib/embeddings.d.ts +2 -0
  124. package/dist/src/lib/embeddings.js +1 -0
  125. package/dist/src/lib/ensure-curated-kb-catalog.d.ts +6 -0
  126. package/dist/src/lib/ensure-curated-kb-catalog.js +53 -0
  127. package/dist/src/lib/env.d.ts +41 -0
  128. package/dist/src/lib/env.js +57 -0
  129. package/dist/src/lib/errors.d.ts +33 -0
  130. package/dist/src/lib/errors.js +78 -0
  131. package/dist/src/lib/file.d.ts +0 -0
  132. package/dist/src/lib/file.js +1 -0
  133. package/dist/src/lib/inference.d.ts +1 -0
  134. package/dist/src/lib/inference.js +1 -0
  135. package/dist/src/lib/kb-meta.d.ts +8 -0
  136. package/dist/src/lib/kb-meta.js +77 -0
  137. package/dist/src/lib/logger.d.ts +62 -0
  138. package/dist/src/lib/logger.js +364 -0
  139. package/dist/src/lib/pdf.d.ts +11 -0
  140. package/dist/src/lib/pdf.js +11 -0
  141. package/dist/src/lib/prisma.d.ts +3 -0
  142. package/dist/src/lib/prisma.js +15 -0
  143. package/dist/src/lib/pusher.d.ts +38 -0
  144. package/dist/src/lib/pusher.js +170 -0
  145. package/dist/src/lib/retry.d.ts +15 -0
  146. package/dist/src/lib/retry.js +37 -0
  147. package/dist/src/lib/storage.d.ts +11 -0
  148. package/dist/src/lib/storage.js +71 -0
  149. package/dist/src/lib/stripe.d.ts +10 -0
  150. package/dist/src/lib/stripe.js +36 -0
  151. package/dist/src/lib/validation.d.ts +51 -0
  152. package/dist/src/lib/validation.js +64 -0
  153. package/dist/src/lib/workspace-kb.d.ts +5 -0
  154. package/dist/src/lib/workspace-kb.js +7 -0
  155. package/dist/src/repositories/artifact.repository.d.ts +64 -0
  156. package/dist/src/repositories/artifact.repository.js +40 -0
  157. package/dist/src/repositories/base.repository.d.ts +14 -0
  158. package/dist/src/repositories/base.repository.js +14 -0
  159. package/dist/src/repositories/invitation.repository.d.ts +104 -0
  160. package/dist/src/repositories/invitation.repository.js +44 -0
  161. package/dist/src/repositories/notification.repository.d.ts +76 -0
  162. package/dist/src/repositories/notification.repository.js +44 -0
  163. package/dist/src/repositories/user.repository.d.ts +84 -0
  164. package/dist/src/repositories/user.repository.js +37 -0
  165. package/dist/src/repositories/workspace-member.repository.d.ts +35 -0
  166. package/dist/src/repositories/workspace-member.repository.js +31 -0
  167. package/dist/src/repositories/workspace.repository.d.ts +101 -0
  168. package/dist/src/repositories/workspace.repository.js +79 -0
  169. package/dist/src/routers/_app.d.ts +3464 -0
  170. package/dist/src/routers/_app.js +36 -0
  171. package/dist/src/routers/admin.d.ts +358 -0
  172. package/dist/src/routers/admin.js +105 -0
  173. package/dist/src/routers/annotations.d.ts +219 -0
  174. package/dist/src/routers/annotations.js +29 -0
  175. package/dist/src/routers/artifactVersions.d.ts +65 -0
  176. package/dist/src/routers/artifactVersions.js +14 -0
  177. package/dist/src/routers/auth.d.ts +161 -0
  178. package/dist/src/routers/auth.js +97 -0
  179. package/dist/src/routers/chat.d.ts +170 -0
  180. package/dist/src/routers/chat.js +32 -0
  181. package/dist/src/routers/copilot.d.ts +200 -0
  182. package/dist/src/routers/copilot.js +52 -0
  183. package/dist/src/routers/flashcards.d.ts +336 -0
  184. package/dist/src/routers/flashcards.js +93 -0
  185. package/dist/src/routers/knowledgeBase.d.ts +421 -0
  186. package/dist/src/routers/knowledgeBase.js +118 -0
  187. package/dist/src/routers/members.d.ts +169 -0
  188. package/dist/src/routers/members.js +47 -0
  189. package/dist/src/routers/notifications.d.ts +99 -0
  190. package/dist/src/routers/notifications.js +25 -0
  191. package/dist/src/routers/payment.d.ts +80 -0
  192. package/dist/src/routers/payment.js +21 -0
  193. package/dist/src/routers/podcast.d.ts +287 -0
  194. package/dist/src/routers/podcast.js +34 -0
  195. package/dist/src/routers/studyguide.d.ts +36 -0
  196. package/dist/src/routers/studyguide.js +8 -0
  197. package/dist/src/routers/worksheets.d.ts +429 -0
  198. package/dist/src/routers/worksheets.js +139 -0
  199. package/dist/src/routers/workspace.d.ts +563 -0
  200. package/dist/src/routers/workspace.js +104 -0
  201. package/dist/src/scripts/purge-deleted-users.d.ts +1 -0
  202. package/dist/src/scripts/purge-deleted-users.js +148 -0
  203. package/dist/src/server.d.ts +1 -0
  204. package/dist/src/server.js +190 -0
  205. package/dist/src/services/activity/activity-human-description.service.d.ts +13 -0
  206. package/dist/src/services/activity/activity-human-description.service.js +221 -0
  207. package/dist/src/services/activity/activity-human-description.service.test.d.ts +1 -0
  208. package/dist/src/services/activity/activity-human-description.service.test.js +16 -0
  209. package/dist/src/services/activity/activity-log.service.d.ts +87 -0
  210. package/dist/src/services/activity/activity-log.service.js +276 -0
  211. package/dist/src/services/activity/activity-log.service.test.d.ts +1 -0
  212. package/dist/src/services/activity/activity-log.service.test.js +27 -0
  213. package/dist/src/services/admin/admin.service.d.ts +270 -0
  214. package/dist/src/services/admin/admin.service.js +476 -0
  215. package/dist/src/services/ai/ai-session.service.d.ts +5 -0
  216. package/dist/src/services/ai/ai-session.service.js +4 -0
  217. package/dist/src/services/artifacts/annotation.service.d.ts +177 -0
  218. package/dist/src/services/artifacts/annotation.service.js +154 -0
  219. package/dist/src/services/artifacts/artifact-version.service.d.ts +38 -0
  220. package/dist/src/services/artifacts/artifact-version.service.js +129 -0
  221. package/dist/src/services/artifacts/chat.service.d.ts +127 -0
  222. package/dist/src/services/artifacts/chat.service.js +182 -0
  223. package/dist/src/services/artifacts/study-guide.service.d.ts +18 -0
  224. package/dist/src/services/artifacts/study-guide.service.js +65 -0
  225. package/dist/src/services/auth/auth.service.d.ts +94 -0
  226. package/dist/src/services/auth/auth.service.js +368 -0
  227. package/dist/src/services/base.service.d.ts +14 -0
  228. package/dist/src/services/base.service.js +14 -0
  229. package/dist/src/services/billing/payment.service.d.ts +44 -0
  230. package/dist/src/services/billing/payment.service.js +365 -0
  231. package/dist/src/services/billing/subscription.service.d.ts +37 -0
  232. package/dist/src/services/billing/subscription.service.js +654 -0
  233. package/dist/src/services/billing/usage.service.d.ts +47 -0
  234. package/dist/src/services/billing/usage.service.js +149 -0
  235. package/dist/src/services/content/copilot.service.d.ts +113 -0
  236. package/dist/src/services/content/copilot.service.js +439 -0
  237. package/dist/src/services/content/flashcard-progress.service.d.ts +159 -0
  238. package/dist/src/services/content/flashcard-progress.service.js +432 -0
  239. package/dist/src/services/content/flashcard.service.d.ts +184 -0
  240. package/dist/src/services/content/flashcard.service.js +339 -0
  241. package/dist/src/services/content/media-analysis.service.d.ts +23 -0
  242. package/dist/src/services/content/media-analysis.service.js +404 -0
  243. package/dist/src/services/content/podcast.service.d.ts +267 -0
  244. package/dist/src/services/content/podcast.service.js +653 -0
  245. package/dist/src/services/content/worksheet-content.service.d.ts +37 -0
  246. package/dist/src/services/content/worksheet-content.service.js +84 -0
  247. package/dist/src/services/content/worksheet-content.service.test.d.ts +1 -0
  248. package/dist/src/services/content/worksheet-content.service.test.js +69 -0
  249. package/dist/src/services/content/worksheet-generation.service.d.ts +91 -0
  250. package/dist/src/services/content/worksheet-generation.service.js +95 -0
  251. package/dist/src/services/content/worksheet-generation.service.test.d.ts +1 -0
  252. package/dist/src/services/content/worksheet-generation.service.test.js +20 -0
  253. package/dist/src/services/content/worksheet.service.d.ts +347 -0
  254. package/dist/src/services/content/worksheet.service.js +599 -0
  255. package/dist/src/services/knowledge/knowledge-base.service.d.ts +316 -0
  256. package/dist/src/services/knowledge/knowledge-base.service.js +544 -0
  257. package/dist/src/services/members/invitation.service.d.ts +66 -0
  258. package/dist/src/services/members/invitation.service.js +348 -0
  259. package/dist/src/services/members/member.service.d.ts +36 -0
  260. package/dist/src/services/members/member.service.js +193 -0
  261. package/dist/src/services/notifications/notification.service.d.ts +214 -0
  262. package/dist/src/services/notifications/notification.service.js +550 -0
  263. package/dist/src/services/notifications/notification.service.test.d.ts +1 -0
  264. package/dist/src/services/notifications/notification.service.test.js +87 -0
  265. package/dist/src/services/workspace/workspace-analytics.service.d.ts +24 -0
  266. package/dist/src/services/workspace/workspace-analytics.service.js +95 -0
  267. package/dist/src/services/workspace/workspace-kb.service.d.ts +40 -0
  268. package/dist/src/services/workspace/workspace-kb.service.js +184 -0
  269. package/dist/src/services/workspace/workspace.service.d.ts +263 -0
  270. package/dist/src/services/workspace/workspace.service.js +401 -0
  271. package/dist/src/trpc.d.ts +60 -0
  272. package/dist/src/trpc.js +217 -0
  273. package/dist/src/types/index.d.ts +126 -0
  274. package/dist/src/types/index.js +1 -0
  275. package/package.json +8 -9
  276. package/prisma/schema.prisma +3 -4
  277. package/prisma/seed.mjs +5 -2
  278. package/prisma.config.ts +16 -0
  279. package/src/lib/prisma.ts +18 -9
  280. package/src/scripts/purge-deleted-users.ts +1 -3
  281. package/tsconfig.json +3 -0
@@ -0,0 +1,159 @@
1
+ import { Prisma, type PrismaClient } from '@prisma/client';
2
+ import { BaseService } from '../base.service.js';
3
+ /**
4
+ * SM-2 Spaced Repetition Algorithm
5
+ * https://www.supermemo.com/en/archives1990-2015/english/ol/sm2
6
+ */
7
+ export interface SM2Result {
8
+ easeFactor: number;
9
+ interval: number;
10
+ repetitions: number;
11
+ nextReviewAt: Date;
12
+ }
13
+ export declare class FlashcardProgressService extends BaseService {
14
+ constructor(db: PrismaClient);
15
+ /**
16
+ * Calculate next review using SM-2 algorithm with smart scheduling
17
+ * @param quality - 0-5 rating (0=complete blackout, 5=perfect response)
18
+ * @param easeFactor - Current ease factor (default 2.5)
19
+ * @param interval - Current interval in days (default 0)
20
+ * @param repetitions - Number of consecutive correct responses (default 0)
21
+ * @param consecutiveIncorrect - Number of consecutive failures (for smart scheduling)
22
+ * @param totalIncorrect - Total incorrect count (for context)
23
+ */
24
+ calculateSM2(quality: number, easeFactor?: number, interval?: number, repetitions?: number, consecutiveIncorrect?: number, totalIncorrect?: number): SM2Result;
25
+ /**
26
+ * Infer confidence level based on consecutive incorrect attempts
27
+ */
28
+ inferConfidence(isCorrect: boolean, consecutiveIncorrect: number, timesStudied: number): 'easy' | 'medium' | 'hard';
29
+ /**
30
+ * Convert confidence to SM-2 quality rating
31
+ */
32
+ confidenceToQuality(confidence: 'easy' | 'medium' | 'hard'): number;
33
+ /**
34
+ * Record flashcard study attempt
35
+ */
36
+ recordStudyAttempt(data: {
37
+ userId: string;
38
+ flashcardId: string;
39
+ isCorrect: boolean;
40
+ confidence?: 'easy' | 'medium' | 'hard';
41
+ timeSpentMs?: number;
42
+ }, retryCount?: number): Promise<any>;
43
+ /**
44
+ * Get user's progress on all flashcards in a set
45
+ */
46
+ getSetProgress(userId: string, artifactId: string): Promise<{
47
+ flashcardId: any;
48
+ front: any;
49
+ back: any;
50
+ progress: {
51
+ userId: string;
52
+ id: string;
53
+ createdAt: Date;
54
+ updatedAt: Date;
55
+ interval: number;
56
+ flashcardId: string;
57
+ timesStudied: number;
58
+ timesCorrect: number;
59
+ timesIncorrect: number;
60
+ timesIncorrectConsecutive: number;
61
+ easeFactor: number;
62
+ repetitions: number;
63
+ masteryLevel: number;
64
+ lastStudiedAt: Date | null;
65
+ nextReviewAt: Date | null;
66
+ } | null;
67
+ }[]>;
68
+ /**
69
+ * Get flashcards due for review, non-studied flashcards, and flashcards with low mastery
70
+ */
71
+ getDueFlashcards(userId: string, workspaceId: string): Promise<(({
72
+ artifact: {
73
+ id: string;
74
+ createdAt: Date;
75
+ updatedAt: Date;
76
+ title: string;
77
+ description: string | null;
78
+ createdById: string | null;
79
+ type: import("@prisma/client").ArtifactType;
80
+ workspaceId: string;
81
+ difficulty: import("@prisma/client").Difficulty | null;
82
+ estimatedTime: string | null;
83
+ isArchived: boolean;
84
+ generating: boolean;
85
+ generatingMetadata: import("@prisma/client/runtime/client").JsonValue | null;
86
+ worksheetConfig: import("@prisma/client/runtime/client").JsonValue | null;
87
+ imageObjectKey: string | null;
88
+ };
89
+ } & {
90
+ id: string;
91
+ createdAt: Date;
92
+ tags: string[];
93
+ artifactId: string;
94
+ order: number;
95
+ front: string;
96
+ back: string;
97
+ acceptedAnswers: string[];
98
+ }) | {
99
+ artifact: {
100
+ id: string;
101
+ createdAt: Date;
102
+ updatedAt: Date;
103
+ title: string;
104
+ description: string | null;
105
+ createdById: string | null;
106
+ type: import("@prisma/client").ArtifactType;
107
+ workspaceId: string;
108
+ difficulty: import("@prisma/client").Difficulty | null;
109
+ estimatedTime: string | null;
110
+ isArchived: boolean;
111
+ generating: boolean;
112
+ generatingMetadata: import("@prisma/client/runtime/client").JsonValue | null;
113
+ worksheetConfig: import("@prisma/client/runtime/client").JsonValue | null;
114
+ imageObjectKey: string | null;
115
+ };
116
+ id: string;
117
+ createdAt: Date;
118
+ tags: string[];
119
+ artifactId: string;
120
+ order: number;
121
+ front: string;
122
+ back: string;
123
+ acceptedAnswers: string[];
124
+ })[]>;
125
+ /**
126
+ * Get user statistics for a flashcard set
127
+ */
128
+ getSetStatistics(userId: string, artifactId: string): Promise<{
129
+ totalCards: number;
130
+ studiedCards: number;
131
+ unstudiedCards: number;
132
+ masteredCards: number;
133
+ dueForReview: number;
134
+ averageMastery: number;
135
+ successRate: number;
136
+ totalAttempts: number;
137
+ totalCorrect: number;
138
+ }>;
139
+ /**
140
+ * Reset progress for a flashcard
141
+ */
142
+ resetProgress(userId: string, flashcardId: string): Promise<Prisma.BatchPayload>;
143
+ /**
144
+ * Bulk record study session
145
+ */
146
+ recordStudySession(data: {
147
+ userId: string;
148
+ attempts: Array<{
149
+ flashcardId: string;
150
+ isCorrect: boolean;
151
+ confidence?: 'easy' | 'medium' | 'hard';
152
+ timeSpentMs?: number;
153
+ }>;
154
+ }): Promise<any[]>;
155
+ }
156
+ /**
157
+ * Factory function
158
+ */
159
+ export declare function createFlashcardProgressService(db: PrismaClient): FlashcardProgressService;
@@ -0,0 +1,432 @@
1
+ import { Prisma } from '@prisma/client';
2
+ import { NotFoundError } from '../../lib/errors.js';
3
+ import { BaseService } from '../base.service.js';
4
+ export class FlashcardProgressService extends BaseService {
5
+ constructor(db) {
6
+ super(db);
7
+ }
8
+ /**
9
+ * Calculate next review using SM-2 algorithm with smart scheduling
10
+ * @param quality - 0-5 rating (0=complete blackout, 5=perfect response)
11
+ * @param easeFactor - Current ease factor (default 2.5)
12
+ * @param interval - Current interval in days (default 0)
13
+ * @param repetitions - Number of consecutive correct responses (default 0)
14
+ * @param consecutiveIncorrect - Number of consecutive failures (for smart scheduling)
15
+ * @param totalIncorrect - Total incorrect count (for context)
16
+ */
17
+ calculateSM2(quality, easeFactor = 2.5, interval = 0, repetitions = 0, consecutiveIncorrect = 0, totalIncorrect = 0) {
18
+ // If quality < 3, determine if immediate review or short delay
19
+ if (quality < 3) {
20
+ // If no consecutive failures but has some overall failures, give short delay
21
+ const shouldDelayReview = consecutiveIncorrect === 0 && totalIncorrect > 0;
22
+ const nextReviewAt = new Date();
23
+ if (shouldDelayReview) {
24
+ // Give them a few hours to let it sink in
25
+ nextReviewAt.setHours(nextReviewAt.getHours() + 4);
26
+ }
27
+ // Otherwise immediate review (consecutiveIncorrect > 0 or first failure)
28
+ return {
29
+ easeFactor: Math.max(1.3, easeFactor - 0.2),
30
+ interval: 0,
31
+ repetitions: 0,
32
+ nextReviewAt,
33
+ };
34
+ }
35
+ // Calculate new ease factor
36
+ const newEaseFactor = Math.max(1.3, easeFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)));
37
+ // Calculate new interval based on performance history
38
+ let newInterval;
39
+ if (repetitions === 0) {
40
+ // First correct answer
41
+ if (consecutiveIncorrect >= 2 || totalIncorrect >= 5) {
42
+ // If they struggled a lot, start conservative
43
+ newInterval = 1; // 1 day
44
+ }
45
+ else if (totalIncorrect === 0) {
46
+ // Perfect card, never failed
47
+ newInterval = 3; // 3 days (skip ahead)
48
+ }
49
+ else {
50
+ // Normal case
51
+ newInterval = 1; // 1 day
52
+ }
53
+ }
54
+ else if (repetitions === 1) {
55
+ newInterval = 6; // 6 days
56
+ }
57
+ else {
58
+ newInterval = Math.ceil(interval * newEaseFactor);
59
+ }
60
+ // Calculate next review date
61
+ const nextReviewAt = new Date();
62
+ nextReviewAt.setDate(nextReviewAt.getDate() + newInterval);
63
+ return {
64
+ easeFactor: newEaseFactor,
65
+ interval: newInterval,
66
+ repetitions: repetitions + 1,
67
+ nextReviewAt,
68
+ };
69
+ }
70
+ /**
71
+ * Infer confidence level based on consecutive incorrect attempts
72
+ */
73
+ inferConfidence(isCorrect, consecutiveIncorrect, timesStudied) {
74
+ if (!isCorrect) {
75
+ // If they got it wrong, it's obviously hard
76
+ return 'hard';
77
+ }
78
+ // If they got it right but have high consecutive failures, it's still hard
79
+ if (consecutiveIncorrect >= 3) {
80
+ return 'hard';
81
+ }
82
+ if (consecutiveIncorrect >= 1) {
83
+ return 'medium';
84
+ }
85
+ // If first time or low failure history, check overall performance
86
+ if (timesStudied === 0 || timesStudied === 1) {
87
+ return 'medium'; // Default for first attempts
88
+ }
89
+ // If they've studied it multiple times with no recent failures, it's easy
90
+ return 'easy';
91
+ }
92
+ /**
93
+ * Convert confidence to SM-2 quality rating
94
+ */
95
+ confidenceToQuality(confidence) {
96
+ switch (confidence) {
97
+ case 'easy':
98
+ return 5; // Perfect response
99
+ case 'medium':
100
+ return 4; // Correct after hesitation
101
+ case 'hard':
102
+ return 3; // Correct with difficulty
103
+ default:
104
+ return 4;
105
+ }
106
+ }
107
+ /**
108
+ * Record flashcard study attempt
109
+ */
110
+ async recordStudyAttempt(data, retryCount = 0) {
111
+ const { userId, flashcardId, isCorrect, timeSpentMs } = data;
112
+ // Verify flashcard exists and user has access
113
+ const flashcard = await this.db.flashcard.findFirst({
114
+ where: {
115
+ id: flashcardId,
116
+ artifact: {
117
+ workspace: {
118
+ OR: [
119
+ { ownerId: userId },
120
+ { members: { some: { userId } } },
121
+ ],
122
+ },
123
+ },
124
+ },
125
+ });
126
+ if (!flashcard) {
127
+ throw new NotFoundError('Flashcard');
128
+ }
129
+ // Get existing progress
130
+ const existingProgress = await this.db.flashcardProgress.findUnique({
131
+ where: {
132
+ userId_flashcardId: {
133
+ userId,
134
+ flashcardId,
135
+ },
136
+ },
137
+ });
138
+ // Calculate new consecutive incorrect count
139
+ const newConsecutiveIncorrect = isCorrect
140
+ ? 0
141
+ : (existingProgress?.timesIncorrectConsecutive || 0) + 1;
142
+ // Auto-infer confidence based on performance
143
+ const inferredConfidence = this.inferConfidence(isCorrect, newConsecutiveIncorrect, existingProgress?.timesStudied || 0);
144
+ // Use provided confidence or inferred
145
+ const finalConfidence = data.confidence || inferredConfidence;
146
+ const quality = this.confidenceToQuality(finalConfidence);
147
+ // Calculate total incorrect after this attempt
148
+ const totalIncorrect = (existingProgress?.timesIncorrect || 0) + (isCorrect ? 0 : 1);
149
+ const sm2Result = this.calculateSM2(quality, existingProgress?.easeFactor, existingProgress?.interval, existingProgress?.repetitions, newConsecutiveIncorrect, totalIncorrect);
150
+ // Calculate mastery level (0-100)
151
+ const totalAttempts = (existingProgress?.timesStudied || 0) + 1;
152
+ const totalCorrect = (existingProgress?.timesCorrect || 0) + (isCorrect ? 1 : 0);
153
+ const successRate = totalCorrect / totalAttempts;
154
+ // Mastery considers success rate, repetitions, and consecutive failures
155
+ const consecutivePenalty = Math.min(newConsecutiveIncorrect * 10, 30); // Max 30% penalty
156
+ const masteryLevel = Math.min(100, Math.max(0, Math.round((successRate * 70) + // 70% weight on success rate
157
+ (Math.min(sm2Result.repetitions, 10) / 10) * 30 - // 30% weight on repetitions
158
+ consecutivePenalty // Penalty for consecutive failures
159
+ )));
160
+ try {
161
+ // Upsert progress
162
+ return await this.db.flashcardProgress.upsert({
163
+ where: {
164
+ userId_flashcardId: {
165
+ userId,
166
+ flashcardId,
167
+ },
168
+ },
169
+ update: {
170
+ timesStudied: { increment: 1 },
171
+ timesCorrect: isCorrect ? { increment: 1 } : undefined,
172
+ timesIncorrect: !isCorrect ? { increment: 1 } : undefined,
173
+ timesIncorrectConsecutive: newConsecutiveIncorrect,
174
+ easeFactor: sm2Result.easeFactor,
175
+ interval: sm2Result.interval,
176
+ repetitions: sm2Result.repetitions,
177
+ masteryLevel,
178
+ lastStudiedAt: new Date(),
179
+ nextReviewAt: sm2Result.nextReviewAt,
180
+ },
181
+ create: {
182
+ userId,
183
+ flashcardId,
184
+ timesStudied: 1,
185
+ timesCorrect: isCorrect ? 1 : 0,
186
+ timesIncorrect: isCorrect ? 0 : 1,
187
+ timesIncorrectConsecutive: newConsecutiveIncorrect,
188
+ easeFactor: sm2Result.easeFactor,
189
+ interval: sm2Result.interval,
190
+ repetitions: sm2Result.repetitions,
191
+ masteryLevel,
192
+ lastStudiedAt: new Date(),
193
+ nextReviewAt: sm2Result.nextReviewAt,
194
+ },
195
+ include: {
196
+ flashcard: true,
197
+ },
198
+ });
199
+ }
200
+ catch (error) {
201
+ // Handle rare race condition where parallel submissions try creating same row.
202
+ if (error instanceof Prisma.PrismaClientKnownRequestError &&
203
+ error.code === 'P2002' &&
204
+ retryCount < 1) {
205
+ return this.recordStudyAttempt(data, retryCount + 1);
206
+ }
207
+ throw error;
208
+ }
209
+ }
210
+ /**
211
+ * Get user's progress on all flashcards in a set
212
+ */
213
+ async getSetProgress(userId, artifactId) {
214
+ const flashcards = await this.db.flashcard.findMany({
215
+ where: { artifactId },
216
+ });
217
+ // Manually fetch progress for each flashcard
218
+ const flashcardsWithProgress = await Promise.all(flashcards.map(async (card) => {
219
+ const progress = await this.db.flashcardProgress.findUnique({
220
+ where: {
221
+ userId_flashcardId: {
222
+ userId,
223
+ flashcardId: card.id,
224
+ },
225
+ },
226
+ });
227
+ return {
228
+ flashcardId: card.id,
229
+ front: card.front,
230
+ back: card.back,
231
+ progress: progress || null,
232
+ };
233
+ }));
234
+ return flashcardsWithProgress;
235
+ }
236
+ /**
237
+ * Get flashcards due for review, non-studied flashcards, and flashcards with low mastery
238
+ */
239
+ async getDueFlashcards(userId, workspaceId) {
240
+ const now = new Date();
241
+ const LOW_MASTERY_THRESHOLD = 50; // Consider mastery < 50 as low
242
+ const flashcardSets = await this.db.artifact.findMany({
243
+ where: {
244
+ workspaceId,
245
+ type: 'FLASHCARD_SET',
246
+ },
247
+ include: { _count: { select: { flashcards: true } } },
248
+ orderBy: { createdAt: 'asc' },
249
+ });
250
+ if (flashcardSets.length === 0) {
251
+ return [];
252
+ }
253
+ const withCards = flashcardSets.filter((set) => set._count.flashcards > 0);
254
+ const candidates = withCards.length > 0 ? withCards : flashcardSets;
255
+ const primaryArtifact = candidates.reduce((best, current) => current._count.flashcards > best._count.flashcards ? current : best);
256
+ const allFlashcards = await this.db.flashcard.findMany({
257
+ where: {
258
+ artifactId: primaryArtifact.id,
259
+ },
260
+ include: {
261
+ artifact: true,
262
+ progress: {
263
+ where: {
264
+ userId,
265
+ },
266
+ },
267
+ },
268
+ });
269
+ // allFlashcards count logged for debugging if needed
270
+ const TAKE_NUMBER = (allFlashcards.length > 10) ? 10 : allFlashcards.length;
271
+ // Get progress records for flashcards that are due or have low mastery
272
+ const progressRecords = await this.db.flashcardProgress.findMany({
273
+ where: {
274
+ userId,
275
+ OR: [
276
+ {
277
+ nextReviewAt: {
278
+ lte: now,
279
+ },
280
+ },
281
+ {
282
+ masteryLevel: {
283
+ lt: LOW_MASTERY_THRESHOLD,
284
+ },
285
+ },
286
+ {
287
+ timesStudied: {
288
+ lt: 3,
289
+ },
290
+ }
291
+ ],
292
+ flashcard: {
293
+ artifactId: primaryArtifact.id,
294
+ },
295
+ },
296
+ include: {
297
+ flashcard: {
298
+ include: {
299
+ artifact: true,
300
+ },
301
+ },
302
+ },
303
+ take: TAKE_NUMBER,
304
+ });
305
+ // Due card selection: TAKE_NUMBER=${TAKE_NUMBER}, existing=${progressRecords.length}
306
+ // Get flashcard IDs that already have progress records
307
+ const flashcardIdsWithProgress = new Set(progressRecords.map((progress) => progress.flashcard.id));
308
+ // Find flashcards without progress records (non-studied) to pad the results
309
+ const nonStudiedFlashcards = allFlashcards
310
+ .filter((flashcard) => !flashcardIdsWithProgress.has(flashcard.id))
311
+ .slice(0, TAKE_NUMBER - progressRecords.length);
312
+ // Create progress-like structures for non-studied flashcards
313
+ const progressRecordsPadding = nonStudiedFlashcards.map((flashcard) => {
314
+ const { progress, ...flashcardWithoutProgress } = flashcard;
315
+ return {
316
+ id: `temp-${flashcard.id}`,
317
+ userId,
318
+ flashcardId: flashcard.id,
319
+ timesStudied: 0,
320
+ timesCorrect: 0,
321
+ timesIncorrect: 0,
322
+ timesIncorrectConsecutive: 0,
323
+ easeFactor: 2.5,
324
+ interval: 0,
325
+ repetitions: 0,
326
+ masteryLevel: 0,
327
+ lastStudiedAt: null,
328
+ nextReviewAt: null,
329
+ flashcard: flashcardWithoutProgress,
330
+ };
331
+ });
332
+ // Combined: ${progressRecords.length} due + ${progressRecordsPadding.length} padding cards
333
+ const selectedCards = [...progressRecords, ...progressRecordsPadding];
334
+ // Build result array: include progress records and non-studied flashcards
335
+ const results = [];
336
+ // Add flashcards with progress (due or low mastery)
337
+ for (const progress of progressRecords) {
338
+ results.push(progress);
339
+ }
340
+ // Sort by priority: due first (by nextReviewAt), then low mastery, then non-studied
341
+ // @todo: make an actual algorithm. research
342
+ // results.sort((a, b) => {
343
+ // // Due flashcards first (nextReviewAt <= now)
344
+ // const aIsDue = a.nextReviewAt && a.nextReviewAt <= now;
345
+ // const bIsDue = b.nextReviewAt && b.nextReviewAt <= now;
346
+ // // if (aIsDue && !bIsDue) return -1;
347
+ // // if (!aIsDue && bIsDue) return 1;
348
+ // // Among due flashcards, sort by nextReviewAt
349
+ // if (aIsDue && bIsDue && a.nextReviewAt && b.nextReviewAt) {
350
+ // return a.nextReviewAt.getTime() - b.nextReviewAt.getTime();
351
+ // }
352
+ // // Then low mastery (lower mastery first)
353
+ // if (a.masteryLevel !== b.masteryLevel) {
354
+ // return a.masteryLevel - b.masteryLevel;
355
+ // }
356
+ // // Finally, non-studied (timesStudied === 0)
357
+ // if (a.timesStudied === 0 && b.timesStudied !== 0) return -1;
358
+ // if (a.timesStudied !== 0 && b.timesStudied === 0) return 1;
359
+ // return 0;
360
+ // });
361
+ return selectedCards.map((progress) => progress.flashcard);
362
+ }
363
+ /**
364
+ * Get user statistics for a flashcard set
365
+ */
366
+ async getSetStatistics(userId, artifactId) {
367
+ const progress = await this.db.flashcardProgress.findMany({
368
+ where: {
369
+ userId,
370
+ flashcard: {
371
+ artifactId,
372
+ },
373
+ },
374
+ });
375
+ const totalCards = await this.db.flashcard.count({
376
+ where: { artifactId },
377
+ });
378
+ const studiedCards = progress.length;
379
+ const masteredCards = progress.filter((p) => p.masteryLevel >= 80).length;
380
+ const dueForReview = progress.filter((p) => p.nextReviewAt && p.nextReviewAt <= new Date()).length;
381
+ const averageMastery = progress.length > 0
382
+ ? progress.reduce((sum, p) => sum + p.masteryLevel, 0) / progress.length
383
+ : 0;
384
+ const totalCorrect = progress.reduce((sum, p) => sum + p.timesCorrect, 0);
385
+ const totalAttempts = progress.reduce((sum, p) => sum + p.timesStudied, 0);
386
+ const successRate = totalAttempts > 0 ? (totalCorrect / totalAttempts) * 100 : 0;
387
+ return {
388
+ totalCards,
389
+ studiedCards,
390
+ unstudiedCards: totalCards - studiedCards,
391
+ masteredCards,
392
+ dueForReview,
393
+ averageMastery: Math.round(averageMastery),
394
+ successRate: Math.round(successRate),
395
+ totalAttempts,
396
+ totalCorrect,
397
+ };
398
+ }
399
+ /**
400
+ * Reset progress for a flashcard
401
+ */
402
+ async resetProgress(userId, flashcardId) {
403
+ return this.db.flashcardProgress.deleteMany({
404
+ where: {
405
+ userId,
406
+ flashcardId,
407
+ },
408
+ });
409
+ }
410
+ /**
411
+ * Bulk record study session
412
+ */
413
+ async recordStudySession(data) {
414
+ const { userId, attempts } = data;
415
+ // Process attempts sequentially
416
+ const results = [];
417
+ for (const attempt of attempts) {
418
+ const result = await this.recordStudyAttempt({
419
+ userId,
420
+ ...attempt,
421
+ });
422
+ results.push(result);
423
+ }
424
+ return results;
425
+ }
426
+ }
427
+ /**
428
+ * Factory function
429
+ */
430
+ export function createFlashcardProgressService(db) {
431
+ return new FlashcardProgressService(db);
432
+ }