@goscribe/server 1.2.0 → 1.3.1

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 (126) hide show
  1. package/check-difficulty.cjs +14 -0
  2. package/check-questions.cjs +14 -0
  3. package/db-summary.cjs +22 -0
  4. package/dist/context.d.ts +5 -1
  5. package/dist/lib/activity_human_description.d.ts +13 -0
  6. package/dist/lib/activity_human_description.js +221 -0
  7. package/dist/lib/activity_human_description.test.d.ts +1 -0
  8. package/dist/lib/activity_human_description.test.js +16 -0
  9. package/dist/lib/activity_log_service.d.ts +87 -0
  10. package/dist/lib/activity_log_service.js +276 -0
  11. package/dist/lib/activity_log_service.test.d.ts +1 -0
  12. package/dist/lib/activity_log_service.test.js +27 -0
  13. package/dist/lib/ai-session.d.ts +15 -2
  14. package/dist/lib/ai-session.js +147 -85
  15. package/dist/lib/constants.d.ts +13 -0
  16. package/dist/lib/constants.js +12 -0
  17. package/dist/lib/email.d.ts +11 -0
  18. package/dist/lib/email.js +193 -0
  19. package/dist/lib/env.d.ts +13 -0
  20. package/dist/lib/env.js +16 -0
  21. package/dist/lib/inference.d.ts +4 -1
  22. package/dist/lib/inference.js +3 -3
  23. package/dist/lib/logger.d.ts +4 -4
  24. package/dist/lib/logger.js +30 -8
  25. package/dist/lib/notification-service.d.ts +152 -0
  26. package/dist/lib/notification-service.js +473 -0
  27. package/dist/lib/notification-service.test.d.ts +1 -0
  28. package/dist/lib/notification-service.test.js +87 -0
  29. package/dist/lib/prisma.d.ts +2 -1
  30. package/dist/lib/prisma.js +5 -1
  31. package/dist/lib/pusher.d.ts +23 -0
  32. package/dist/lib/pusher.js +69 -5
  33. package/dist/lib/retry.d.ts +15 -0
  34. package/dist/lib/retry.js +37 -0
  35. package/dist/lib/storage.js +2 -2
  36. package/dist/lib/stripe.d.ts +9 -0
  37. package/dist/lib/stripe.js +36 -0
  38. package/dist/lib/subscription_service.d.ts +37 -0
  39. package/dist/lib/subscription_service.js +654 -0
  40. package/dist/lib/usage_service.d.ts +26 -0
  41. package/dist/lib/usage_service.js +59 -0
  42. package/dist/lib/worksheet-generation.d.ts +91 -0
  43. package/dist/lib/worksheet-generation.js +95 -0
  44. package/dist/lib/worksheet-generation.test.d.ts +1 -0
  45. package/dist/lib/worksheet-generation.test.js +20 -0
  46. package/dist/lib/workspace-access.d.ts +18 -0
  47. package/dist/lib/workspace-access.js +13 -0
  48. package/dist/routers/_app.d.ts +1349 -253
  49. package/dist/routers/_app.js +10 -0
  50. package/dist/routers/admin.d.ts +361 -0
  51. package/dist/routers/admin.js +633 -0
  52. package/dist/routers/annotations.d.ts +219 -0
  53. package/dist/routers/annotations.js +187 -0
  54. package/dist/routers/auth.d.ts +88 -7
  55. package/dist/routers/auth.js +339 -19
  56. package/dist/routers/chat.d.ts +6 -12
  57. package/dist/routers/copilot.d.ts +199 -0
  58. package/dist/routers/copilot.js +571 -0
  59. package/dist/routers/flashcards.d.ts +47 -81
  60. package/dist/routers/flashcards.js +143 -27
  61. package/dist/routers/members.d.ts +36 -7
  62. package/dist/routers/members.js +200 -19
  63. package/dist/routers/notifications.d.ts +99 -0
  64. package/dist/routers/notifications.js +127 -0
  65. package/dist/routers/payment.d.ts +89 -0
  66. package/dist/routers/payment.js +403 -0
  67. package/dist/routers/podcast.d.ts +8 -13
  68. package/dist/routers/podcast.js +54 -31
  69. package/dist/routers/studyguide.d.ts +1 -29
  70. package/dist/routers/studyguide.js +80 -71
  71. package/dist/routers/worksheets.d.ts +105 -38
  72. package/dist/routers/worksheets.js +258 -68
  73. package/dist/routers/workspace.d.ts +139 -60
  74. package/dist/routers/workspace.js +455 -315
  75. package/dist/scripts/purge-deleted-users.d.ts +1 -0
  76. package/dist/scripts/purge-deleted-users.js +149 -0
  77. package/dist/server.js +130 -10
  78. package/dist/services/flashcard-progress.service.d.ts +18 -66
  79. package/dist/services/flashcard-progress.service.js +51 -42
  80. package/dist/trpc.d.ts +20 -21
  81. package/dist/trpc.js +150 -1
  82. package/mcq-test.cjs +36 -0
  83. package/package.json +9 -2
  84. package/prisma/migrations/20260413143206_init/migration.sql +873 -0
  85. package/prisma/schema.prisma +471 -324
  86. package/src/context.ts +4 -1
  87. package/src/lib/activity_human_description.test.ts +28 -0
  88. package/src/lib/activity_human_description.ts +239 -0
  89. package/src/lib/activity_log_service.test.ts +37 -0
  90. package/src/lib/activity_log_service.ts +353 -0
  91. package/src/lib/ai-session.ts +79 -51
  92. package/src/lib/email.ts +213 -29
  93. package/src/lib/env.ts +23 -6
  94. package/src/lib/inference.ts +2 -2
  95. package/src/lib/notification-service.test.ts +106 -0
  96. package/src/lib/notification-service.ts +677 -0
  97. package/src/lib/prisma.ts +6 -1
  98. package/src/lib/pusher.ts +86 -2
  99. package/src/lib/stripe.ts +39 -0
  100. package/src/lib/subscription_service.ts +722 -0
  101. package/src/lib/usage_service.ts +74 -0
  102. package/src/lib/worksheet-generation.test.ts +31 -0
  103. package/src/lib/worksheet-generation.ts +139 -0
  104. package/src/routers/_app.ts +9 -0
  105. package/src/routers/admin.ts +710 -0
  106. package/src/routers/annotations.ts +41 -0
  107. package/src/routers/auth.ts +338 -28
  108. package/src/routers/copilot.ts +719 -0
  109. package/src/routers/flashcards.ts +201 -68
  110. package/src/routers/members.ts +280 -80
  111. package/src/routers/notifications.ts +142 -0
  112. package/src/routers/payment.ts +448 -0
  113. package/src/routers/podcast.ts +112 -83
  114. package/src/routers/studyguide.ts +12 -0
  115. package/src/routers/worksheets.ts +289 -66
  116. package/src/routers/workspace.ts +329 -122
  117. package/src/scripts/purge-deleted-users.ts +167 -0
  118. package/src/server.ts +137 -11
  119. package/src/services/flashcard-progress.service.ts +49 -37
  120. package/src/trpc.ts +184 -5
  121. package/test-generate.js +30 -0
  122. package/test-ratio.cjs +9 -0
  123. package/zod-test.cjs +22 -0
  124. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
  125. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
  126. package/prisma/seed.mjs +0 -135
@@ -1,11 +1,5 @@
1
1
  export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
2
- ctx: {
3
- db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
4
- session: any;
5
- req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
6
- res: import("express").Response<any, Record<string, any>>;
7
- cookies: Record<string, string | undefined>;
8
- };
2
+ ctx: import("../context.js").Context;
9
3
  meta: object;
10
4
  errorShape: {
11
5
  data: {
@@ -38,17 +32,18 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
38
32
  id: string;
39
33
  createdAt: Date;
40
34
  updatedAt: Date;
41
- title: string;
42
- description: string | null;
43
35
  workspaceId: string;
44
36
  type: import("@prisma/client").$Enums.ArtifactType;
37
+ title: string;
45
38
  isArchived: boolean;
46
- generating: boolean;
47
- generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
48
39
  difficulty: import("@prisma/client").$Enums.Difficulty | null;
49
40
  estimatedTime: string | null;
50
- imageObjectKey: string | null;
51
41
  createdById: string | null;
42
+ description: string | null;
43
+ generating: boolean;
44
+ generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
45
+ worksheetConfig: import("@prisma/client/runtime/library").JsonValue | null;
46
+ imageObjectKey: string | null;
52
47
  })[];
53
48
  meta: object;
54
49
  }>;
@@ -82,6 +77,7 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
82
77
  front: string;
83
78
  back: string;
84
79
  tags: string[];
80
+ acceptedAnswers: string[];
85
81
  })[];
86
82
  meta: object;
87
83
  }>;
@@ -97,6 +93,7 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
97
93
  workspaceId: string;
98
94
  front: string;
99
95
  back: string;
96
+ acceptedAnswers?: string[] | undefined;
100
97
  tags?: string[] | undefined;
101
98
  order?: number | undefined;
102
99
  };
@@ -108,6 +105,7 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
108
105
  front: string;
109
106
  back: string;
110
107
  tags: string[];
108
+ acceptedAnswers: string[];
111
109
  };
112
110
  meta: object;
113
111
  }>;
@@ -116,6 +114,7 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
116
114
  cardId: string;
117
115
  front?: string | undefined;
118
116
  back?: string | undefined;
117
+ acceptedAnswers?: string[] | undefined;
119
118
  tags?: string[] | undefined;
120
119
  order?: number | undefined;
121
120
  };
@@ -127,6 +126,20 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
127
126
  front: string;
128
127
  back: string;
129
128
  tags: string[];
129
+ acceptedAnswers: string[];
130
+ };
131
+ meta: object;
132
+ }>;
133
+ gradeTypedAnswer: import("@trpc/server").TRPCMutationProcedure<{
134
+ input: {
135
+ flashcardId: string;
136
+ userAnswer: string;
137
+ };
138
+ output: {
139
+ isCorrect: boolean;
140
+ confidence: number;
141
+ reason: string;
142
+ matchedAnswer: string | null;
130
143
  };
131
144
  meta: object;
132
145
  }>;
@@ -158,17 +171,18 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
158
171
  id: string;
159
172
  createdAt: Date;
160
173
  updatedAt: Date;
161
- title: string;
162
- description: string | null;
163
174
  workspaceId: string;
164
175
  type: import("@prisma/client").$Enums.ArtifactType;
176
+ title: string;
165
177
  isArchived: boolean;
166
- generating: boolean;
167
- generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
168
178
  difficulty: import("@prisma/client").$Enums.Difficulty | null;
169
179
  estimatedTime: string | null;
170
- imageObjectKey: string | null;
171
180
  createdById: string | null;
181
+ description: string | null;
182
+ generating: boolean;
183
+ generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
184
+ worksheetConfig: import("@prisma/client/runtime/library").JsonValue | null;
185
+ imageObjectKey: string | null;
172
186
  };
173
187
  createdCards: number;
174
188
  };
@@ -181,33 +195,7 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
181
195
  confidence?: "easy" | "medium" | "hard" | undefined;
182
196
  timeSpentMs?: number | undefined;
183
197
  };
184
- output: {
185
- flashcard: {
186
- id: string;
187
- createdAt: Date;
188
- artifactId: string;
189
- order: number;
190
- front: string;
191
- back: string;
192
- tags: string[];
193
- };
194
- } & {
195
- id: string;
196
- createdAt: Date;
197
- updatedAt: Date;
198
- userId: string;
199
- flashcardId: string;
200
- timesStudied: number;
201
- timesCorrect: number;
202
- timesIncorrect: number;
203
- timesIncorrectConsecutive: number;
204
- easeFactor: number;
205
- interval: number;
206
- repetitions: number;
207
- masteryLevel: number;
208
- lastStudiedAt: Date | null;
209
- nextReviewAt: Date | null;
210
- };
198
+ output: any;
211
199
  meta: object;
212
200
  }>;
213
201
  getSetProgress: import("@trpc/server").TRPCQueryProcedure<{
@@ -247,17 +235,18 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
247
235
  id: string;
248
236
  createdAt: Date;
249
237
  updatedAt: Date;
250
- title: string;
251
- description: string | null;
252
238
  workspaceId: string;
253
239
  type: import("@prisma/client").$Enums.ArtifactType;
240
+ title: string;
254
241
  isArchived: boolean;
255
- generating: boolean;
256
- generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
257
242
  difficulty: import("@prisma/client").$Enums.Difficulty | null;
258
243
  estimatedTime: string | null;
259
- imageObjectKey: string | null;
260
244
  createdById: string | null;
245
+ description: string | null;
246
+ generating: boolean;
247
+ generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
248
+ worksheetConfig: import("@prisma/client/runtime/library").JsonValue | null;
249
+ imageObjectKey: string | null;
261
250
  };
262
251
  } & {
263
252
  id: string;
@@ -267,22 +256,24 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
267
256
  front: string;
268
257
  back: string;
269
258
  tags: string[];
259
+ acceptedAnswers: string[];
270
260
  }) | {
271
261
  artifact: {
272
262
  id: string;
273
263
  createdAt: Date;
274
264
  updatedAt: Date;
275
- title: string;
276
- description: string | null;
277
265
  workspaceId: string;
278
266
  type: import("@prisma/client").$Enums.ArtifactType;
267
+ title: string;
279
268
  isArchived: boolean;
280
- generating: boolean;
281
- generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
282
269
  difficulty: import("@prisma/client").$Enums.Difficulty | null;
283
270
  estimatedTime: string | null;
284
- imageObjectKey: string | null;
285
271
  createdById: string | null;
272
+ description: string | null;
273
+ generating: boolean;
274
+ generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
275
+ worksheetConfig: import("@prisma/client/runtime/library").JsonValue | null;
276
+ imageObjectKey: string | null;
286
277
  };
287
278
  id: string;
288
279
  createdAt: Date;
@@ -291,6 +282,7 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
291
282
  front: string;
292
283
  back: string;
293
284
  tags: string[];
285
+ acceptedAnswers: string[];
294
286
  })[];
295
287
  meta: object;
296
288
  }>;
@@ -327,33 +319,7 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
327
319
  timeSpentMs?: number | undefined;
328
320
  }[];
329
321
  };
330
- output: ({
331
- flashcard: {
332
- id: string;
333
- createdAt: Date;
334
- artifactId: string;
335
- order: number;
336
- front: string;
337
- back: string;
338
- tags: string[];
339
- };
340
- } & {
341
- id: string;
342
- createdAt: Date;
343
- updatedAt: Date;
344
- userId: string;
345
- flashcardId: string;
346
- timesStudied: number;
347
- timesCorrect: number;
348
- timesIncorrect: number;
349
- timesIncorrectConsecutive: number;
350
- easeFactor: number;
351
- interval: number;
352
- repetitions: number;
353
- masteryLevel: number;
354
- lastStudiedAt: Date | null;
355
- nextReviewAt: Date | null;
356
- })[];
322
+ output: any[];
357
323
  meta: object;
358
324
  }>;
359
325
  }>>;
@@ -1,17 +1,43 @@
1
1
  import { z } from 'zod';
2
2
  import { TRPCError } from '@trpc/server';
3
- import { router, authedProcedure } from '../trpc.js';
3
+ import { router, authedProcedure, limitedProcedure } from '../trpc.js';
4
4
  import { aiSessionService } from '../lib/ai-session.js';
5
5
  import PusherService from '../lib/pusher.js';
6
+ import { notifyArtifactFailed, notifyArtifactReady } from '../lib/notification-service.js';
6
7
  import { createFlashcardProgressService } from '../services/flashcard-progress.service.js';
7
- // Prisma enum values mapped manually to avoid type import issues in ESM
8
- const ArtifactType = {
9
- STUDY_GUIDE: 'STUDY_GUIDE',
10
- FLASHCARD_SET: 'FLASHCARD_SET',
11
- WORKSHEET: 'WORKSHEET',
12
- MEETING_SUMMARY: 'MEETING_SUMMARY',
13
- PODCAST_EPISODE: 'PODCAST_EPISODE',
14
- };
8
+ import { ArtifactType } from '../lib/constants.js';
9
+ import { workspaceAccessFilter } from '../lib/workspace-access.js';
10
+ import inference from '../lib/inference.js';
11
+ const typedAnswerGradeSchema = z.object({
12
+ isCorrect: z.boolean(),
13
+ confidence: z.number().min(0).max(1),
14
+ reason: z.string().min(1),
15
+ matchedAnswer: z.string().nullable(),
16
+ });
17
+ function normalizeAcceptedAnswers(answers) {
18
+ if (!answers || answers.length === 0)
19
+ return [];
20
+ const seen = new Set();
21
+ const normalized = [];
22
+ for (const answer of answers) {
23
+ const trimmed = answer.trim();
24
+ if (!trimmed)
25
+ continue;
26
+ const key = trimmed.toLowerCase();
27
+ if (seen.has(key))
28
+ continue;
29
+ seen.add(key);
30
+ normalized.push(trimmed);
31
+ }
32
+ return normalized;
33
+ }
34
+ function extractFirstJsonObject(text) {
35
+ const start = text.indexOf('{');
36
+ const end = text.lastIndexOf('}');
37
+ if (start === -1 || end === -1 || end <= start)
38
+ return null;
39
+ return text.slice(start, end + 1);
40
+ }
15
41
  export const flashcards = router({
16
42
  listSets: authedProcedure
17
43
  .input(z.object({ workspaceId: z.string() }))
@@ -36,7 +62,7 @@ export const flashcards = router({
36
62
  .input(z.object({ workspaceId: z.string() }))
37
63
  .query(async ({ ctx, input }) => {
38
64
  const set = await ctx.db.artifact.findFirst({
39
- where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
65
+ where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace: workspaceAccessFilter(ctx.session.user.id) },
40
66
  include: {
41
67
  flashcards: {
42
68
  include: {
@@ -48,7 +74,7 @@ export const flashcards = router({
48
74
  }
49
75
  },
50
76
  },
51
- orderBy: { updatedAt: 'desc' },
77
+ orderBy: { createdAt: 'desc' },
52
78
  });
53
79
  if (!set)
54
80
  throw new TRPCError({ code: 'NOT_FOUND' });
@@ -58,23 +84,27 @@ export const flashcards = router({
58
84
  .input(z.object({ workspaceId: z.string() }))
59
85
  .query(async ({ ctx, input }) => {
60
86
  const artifact = await ctx.db.artifact.findFirst({
61
- where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } }, orderBy: { updatedAt: 'desc' },
87
+ where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace: workspaceAccessFilter(ctx.session.user.id) },
88
+ orderBy: { createdAt: 'desc' },
62
89
  });
63
90
  return artifact?.generating;
64
91
  }),
65
- createCard: authedProcedure
92
+ createCard: limitedProcedure
66
93
  .input(z.object({
67
94
  workspaceId: z.string(),
68
95
  front: z.string().min(1),
69
96
  back: z.string().min(1),
97
+ acceptedAnswers: z.array(z.string()).optional(),
70
98
  tags: z.array(z.string()).optional(),
71
99
  order: z.number().int().optional(),
72
100
  }))
73
101
  .mutation(async ({ ctx, input }) => {
74
102
  const set = await ctx.db.artifact.findFirst({
75
- where: { type: ArtifactType.FLASHCARD_SET, workspace: {
103
+ where: {
104
+ type: ArtifactType.FLASHCARD_SET, workspace: {
76
105
  id: input.workspaceId,
77
- } },
106
+ }
107
+ },
78
108
  include: {
79
109
  flashcards: true,
80
110
  },
@@ -87,6 +117,7 @@ export const flashcards = router({
87
117
  artifactId: set.id,
88
118
  front: input.front,
89
119
  back: input.back,
120
+ acceptedAnswers: normalizeAcceptedAnswers(input.acceptedAnswers),
90
121
  tags: input.tags ?? [],
91
122
  order: input.order ?? 0,
92
123
  },
@@ -97,12 +128,13 @@ export const flashcards = router({
97
128
  cardId: z.string(),
98
129
  front: z.string().optional(),
99
130
  back: z.string().optional(),
131
+ acceptedAnswers: z.array(z.string()).optional(),
100
132
  tags: z.array(z.string()).optional(),
101
133
  order: z.number().int().optional(),
102
134
  }))
103
135
  .mutation(async ({ ctx, input }) => {
104
136
  const card = await ctx.db.flashcard.findFirst({
105
- where: { id: input.cardId, artifact: { type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } } },
137
+ where: { id: input.cardId, artifact: { type: ArtifactType.FLASHCARD_SET, workspace: workspaceAccessFilter(ctx.session.user.id) } },
106
138
  });
107
139
  if (!card)
108
140
  throw new TRPCError({ code: 'NOT_FOUND' });
@@ -111,16 +143,81 @@ export const flashcards = router({
111
143
  data: {
112
144
  front: input.front ?? card.front,
113
145
  back: input.back ?? card.back,
146
+ acceptedAnswers: input.acceptedAnswers ? normalizeAcceptedAnswers(input.acceptedAnswers) : card.acceptedAnswers,
114
147
  tags: input.tags ?? card.tags,
115
148
  order: input.order ?? card.order,
116
149
  },
117
150
  });
118
151
  }),
152
+ gradeTypedAnswer: authedProcedure
153
+ .input(z.object({
154
+ flashcardId: z.string().cuid(),
155
+ userAnswer: z.string().min(1),
156
+ }))
157
+ .mutation(async ({ ctx, input }) => {
158
+ const flashcard = await ctx.db.flashcard.findFirst({
159
+ where: {
160
+ id: input.flashcardId,
161
+ artifact: {
162
+ type: ArtifactType.FLASHCARD_SET,
163
+ workspace: workspaceAccessFilter(ctx.session.user.id),
164
+ },
165
+ },
166
+ select: {
167
+ id: true,
168
+ front: true,
169
+ back: true,
170
+ acceptedAnswers: true,
171
+ },
172
+ });
173
+ if (!flashcard) {
174
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Flashcard not found' });
175
+ }
176
+ const acceptedAnswers = [
177
+ flashcard.back,
178
+ ...normalizeAcceptedAnswers(flashcard.acceptedAnswers),
179
+ ];
180
+ const prompt = [
181
+ 'Grade whether the student answer is semantically correct for the flashcard.',
182
+ '',
183
+ 'Return ONLY valid JSON with this exact shape:',
184
+ '{"isCorrect": boolean, "confidence": number, "reason": string, "matchedAnswer": string | null}',
185
+ '',
186
+ 'Rules:',
187
+ '- Accept synonyms, short paraphrases, and equivalent wording.',
188
+ '- Reject answers that are contradictory, unrelated, or materially incomplete.',
189
+ '- confidence must be from 0 to 1.',
190
+ '- reason must be under 160 characters.',
191
+ '- matchedAnswer must be the matched canonical/alias answer, or null if incorrect.',
192
+ '',
193
+ `Question: ${flashcard.front}`,
194
+ `Canonical answer: ${flashcard.back}`,
195
+ `Accepted aliases: ${JSON.stringify(acceptedAnswers)}`,
196
+ `Student answer: ${input.userAnswer}`,
197
+ ].join('\n');
198
+ try {
199
+ const response = await inference([{ role: 'user', content: prompt }]);
200
+ const content = response.choices?.[0]?.message?.content ?? '';
201
+ const jsonCandidate = extractFirstJsonObject(content);
202
+ if (!jsonCandidate) {
203
+ throw new Error('No JSON object found in grading response');
204
+ }
205
+ const parsed = JSON.parse(jsonCandidate);
206
+ return typedAnswerGradeSchema.parse(parsed);
207
+ }
208
+ catch (error) {
209
+ throw new TRPCError({
210
+ code: 'INTERNAL_SERVER_ERROR',
211
+ message: 'Failed to grade typed answer. Please retry.',
212
+ cause: error,
213
+ });
214
+ }
215
+ }),
119
216
  deleteCard: authedProcedure
120
217
  .input(z.object({ cardId: z.string() }))
121
218
  .mutation(async ({ ctx, input }) => {
122
219
  const card = await ctx.db.flashcard.findFirst({
123
- where: { id: input.cardId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
220
+ where: { id: input.cardId, artifact: { workspace: workspaceAccessFilter(ctx.session.user.id) } },
124
221
  });
125
222
  if (!card)
126
223
  throw new TRPCError({ code: 'NOT_FOUND' });
@@ -131,14 +228,14 @@ export const flashcards = router({
131
228
  .input(z.object({ setId: z.string().uuid() }))
132
229
  .mutation(async ({ ctx, input }) => {
133
230
  const deleted = await ctx.db.artifact.deleteMany({
134
- where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
231
+ where: { id: input.setId, type: ArtifactType.FLASHCARD_SET, workspace: workspaceAccessFilter(ctx.session.user.id) },
135
232
  });
136
233
  if (deleted.count === 0)
137
234
  throw new TRPCError({ code: 'NOT_FOUND' });
138
235
  return true;
139
236
  }),
140
237
  // Generate a flashcard set from a user prompt
141
- generateFromPrompt: authedProcedure
238
+ generateFromPrompt: limitedProcedure
142
239
  .input(z.object({
143
240
  workspaceId: z.string(),
144
241
  prompt: z.string().min(1),
@@ -168,10 +265,6 @@ export const flashcards = router({
168
265
  },
169
266
  });
170
267
  try {
171
- await ctx.db.artifact.update({
172
- where: { id: flashcardCurrent?.id },
173
- data: { generating: true, generatingMetadata: { quantity: input.numCards, difficulty: input.difficulty.toLowerCase() } },
174
- });
175
268
  await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { status: 'generating', numCards: input.numCards, difficulty: input.difficulty });
176
269
  const artifact = await ctx.db.artifact.create({
177
270
  data: {
@@ -179,6 +272,8 @@ export const flashcards = router({
179
272
  type: ArtifactType.FLASHCARD_SET,
180
273
  title: input.title || `Flashcards - ${new Date().toLocaleString()}`,
181
274
  createdById: ctx.session.user.id,
275
+ generating: true,
276
+ generatingMetadata: { quantity: input.numCards, difficulty: input.difficulty.toLowerCase() },
182
277
  flashcards: {
183
278
  create: flashcardCurrent?.flashcards.map((card) => ({
184
279
  front: card.front,
@@ -190,10 +285,11 @@ export const flashcards = router({
190
285
  const currentCards = flashcardCurrent?.flashcards.length || 0;
191
286
  const newCards = input.numCards - currentCards;
192
287
  // Generate
193
- const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, input.numCards, input.difficulty);
288
+ const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, input.numCards, input.difficulty, input.prompt);
194
289
  let createdCards = 0;
195
290
  try {
196
- const flashcardData = content;
291
+ const parsed = typeof content === 'string' ? JSON.parse(content) : content;
292
+ const flashcardData = Array.isArray(parsed) ? parsed : (parsed.flashcards || []);
197
293
  for (let i = 0; i < Math.min(flashcardData.length, input.numCards); i++) {
198
294
  const card = flashcardData[i];
199
295
  const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
@@ -210,7 +306,8 @@ export const flashcards = router({
210
306
  createdCards++;
211
307
  }
212
308
  }
213
- catch {
309
+ catch (error) {
310
+ console.error("Failed to parse flashcard JSON or create cards:", error);
214
311
  // Fallback to text parsing if JSON fails
215
312
  const lines = content.split('\n').filter(line => line.trim());
216
313
  for (let i = 0; i < Math.min(lines.length, input.numCards); i++) {
@@ -232,11 +329,30 @@ export const flashcards = router({
232
329
  }
233
330
  // Pusher complete
234
331
  await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
332
+ // Set generating to false on the artifact
333
+ await ctx.db.artifact.update({ where: { id: artifact.id }, data: { generating: false } });
334
+ await notifyArtifactReady(ctx.db, {
335
+ userId: ctx.session.user.id,
336
+ workspaceId: input.workspaceId,
337
+ artifactId: artifact.id,
338
+ artifactType: ArtifactType.FLASHCARD_SET,
339
+ title: artifact.title,
340
+ }).catch(() => { });
235
341
  return { artifact, createdCards };
236
342
  }
237
343
  catch (error) {
238
- await ctx.db.artifact.update({ where: { id: flashcardCurrent?.id }, data: { generating: false } });
344
+ if (flashcardCurrent?.id) {
345
+ await ctx.db.artifact.update({ where: { id: flashcardCurrent.id }, data: { generating: false } });
346
+ }
239
347
  await PusherService.emitError(input.workspaceId, `Failed to generate flashcards: ${error}`, 'flash_card_generation');
348
+ await notifyArtifactFailed(ctx.db, {
349
+ userId: ctx.session.user.id,
350
+ workspaceId: input.workspaceId,
351
+ artifactType: ArtifactType.FLASHCARD_SET,
352
+ message: error instanceof Error
353
+ ? error.message
354
+ : 'Flashcard generation failed.',
355
+ }).catch(() => { });
240
356
  throw error;
241
357
  }
242
358
  }),
@@ -10,13 +10,7 @@
10
10
  * - Get current user's role
11
11
  */
12
12
  export declare const members: import("@trpc/server").TRPCBuiltRouter<{
13
- ctx: {
14
- db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
15
- session: any;
16
- req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
17
- res: import("express").Response<any, Record<string, any>>;
18
- cookies: Record<string, string | undefined>;
19
- };
13
+ ctx: import("../context.js").Context;
20
14
  meta: object;
21
15
  errorShape: {
22
16
  data: {
@@ -109,6 +103,7 @@ export declare const members: import("@trpc/server").TRPCBuiltRouter<{
109
103
  token: string;
110
104
  };
111
105
  output: {
106
+ message?: string | undefined;
112
107
  workspaceId: string;
113
108
  workspaceTitle: string;
114
109
  role: string;
@@ -178,4 +173,38 @@ export declare const members: import("@trpc/server").TRPCBuiltRouter<{
178
173
  };
179
174
  meta: object;
180
175
  }>;
176
+ /**
177
+ * Resend a pending invitation (owner only)
178
+ */
179
+ resendInvitation: import("@trpc/server").TRPCMutationProcedure<{
180
+ input: {
181
+ invitationId: string;
182
+ };
183
+ output: {
184
+ invitationId: string;
185
+ message: string;
186
+ };
187
+ meta: object;
188
+ }>;
189
+ /**
190
+ * DEBUG ONLY: Get all invitations for a workspace
191
+ */
192
+ getAllInvitationsDebug: import("@trpc/server").TRPCQueryProcedure<{
193
+ input: {
194
+ workspaceId: string;
195
+ };
196
+ output: {
197
+ id: string;
198
+ email: string;
199
+ createdAt: Date;
200
+ updatedAt: Date;
201
+ workspaceId: string;
202
+ role: string;
203
+ token: string;
204
+ invitedById: string;
205
+ acceptedAt: Date | null;
206
+ expiresAt: Date;
207
+ }[];
208
+ meta: object;
209
+ }>;
181
210
  }>>;