@goscribe/server 1.0.11 → 1.1.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 (83) hide show
  1. package/ANALYSIS_PROGRESS_SPEC.md +463 -0
  2. package/PROGRESS_QUICK_REFERENCE.md +239 -0
  3. package/dist/lib/ai-session.d.ts +20 -9
  4. package/dist/lib/ai-session.js +316 -80
  5. package/dist/lib/auth.d.ts +35 -2
  6. package/dist/lib/auth.js +88 -15
  7. package/dist/lib/env.d.ts +32 -0
  8. package/dist/lib/env.js +46 -0
  9. package/dist/lib/errors.d.ts +33 -0
  10. package/dist/lib/errors.js +78 -0
  11. package/dist/lib/inference.d.ts +4 -1
  12. package/dist/lib/inference.js +9 -11
  13. package/dist/lib/logger.d.ts +62 -0
  14. package/dist/lib/logger.js +342 -0
  15. package/dist/lib/podcast-prompts.d.ts +43 -0
  16. package/dist/lib/podcast-prompts.js +135 -0
  17. package/dist/lib/pusher.d.ts +1 -0
  18. package/dist/lib/pusher.js +14 -2
  19. package/dist/lib/storage.d.ts +3 -3
  20. package/dist/lib/storage.js +51 -47
  21. package/dist/lib/validation.d.ts +51 -0
  22. package/dist/lib/validation.js +64 -0
  23. package/dist/routers/_app.d.ts +697 -111
  24. package/dist/routers/_app.js +5 -0
  25. package/dist/routers/auth.d.ts +11 -1
  26. package/dist/routers/chat.d.ts +11 -1
  27. package/dist/routers/flashcards.d.ts +205 -6
  28. package/dist/routers/flashcards.js +144 -66
  29. package/dist/routers/members.d.ts +165 -0
  30. package/dist/routers/members.js +531 -0
  31. package/dist/routers/podcast.d.ts +78 -63
  32. package/dist/routers/podcast.js +330 -393
  33. package/dist/routers/studyguide.d.ts +11 -1
  34. package/dist/routers/worksheets.d.ts +124 -13
  35. package/dist/routers/worksheets.js +123 -50
  36. package/dist/routers/workspace.d.ts +213 -26
  37. package/dist/routers/workspace.js +303 -181
  38. package/dist/server.js +12 -4
  39. package/dist/services/flashcard-progress.service.d.ts +183 -0
  40. package/dist/services/flashcard-progress.service.js +383 -0
  41. package/dist/services/flashcard.service.d.ts +183 -0
  42. package/dist/services/flashcard.service.js +224 -0
  43. package/dist/services/podcast-segment-reorder.d.ts +0 -0
  44. package/dist/services/podcast-segment-reorder.js +107 -0
  45. package/dist/services/podcast.service.d.ts +0 -0
  46. package/dist/services/podcast.service.js +326 -0
  47. package/dist/services/worksheet.service.d.ts +0 -0
  48. package/dist/services/worksheet.service.js +295 -0
  49. package/dist/trpc.d.ts +13 -2
  50. package/dist/trpc.js +55 -6
  51. package/dist/types/index.d.ts +126 -0
  52. package/dist/types/index.js +1 -0
  53. package/package.json +3 -2
  54. package/prisma/schema.prisma +142 -4
  55. package/src/lib/ai-session.ts +356 -85
  56. package/src/lib/auth.ts +113 -19
  57. package/src/lib/env.ts +59 -0
  58. package/src/lib/errors.ts +92 -0
  59. package/src/lib/inference.ts +11 -11
  60. package/src/lib/logger.ts +405 -0
  61. package/src/lib/pusher.ts +15 -3
  62. package/src/lib/storage.ts +56 -51
  63. package/src/lib/validation.ts +75 -0
  64. package/src/routers/_app.ts +5 -0
  65. package/src/routers/chat.ts +2 -23
  66. package/src/routers/flashcards.ts +108 -24
  67. package/src/routers/members.ts +586 -0
  68. package/src/routers/podcast.ts +385 -420
  69. package/src/routers/worksheets.ts +118 -36
  70. package/src/routers/workspace.ts +356 -195
  71. package/src/server.ts +13 -4
  72. package/src/services/flashcard-progress.service.ts +541 -0
  73. package/src/trpc.ts +59 -6
  74. package/src/types/index.ts +165 -0
  75. package/AUTH_FRONTEND_SPEC.md +0 -21
  76. package/CHAT_FRONTEND_SPEC.md +0 -474
  77. package/DATABASE_SETUP.md +0 -165
  78. package/MEETINGSUMMARY_FRONTEND_SPEC.md +0 -28
  79. package/PODCAST_FRONTEND_SPEC.md +0 -595
  80. package/STUDYGUIDE_FRONTEND_SPEC.md +0 -18
  81. package/WORKSHEETS_FRONTEND_SPEC.md +0 -26
  82. package/WORKSPACE_FRONTEND_SPEC.md +0 -47
  83. package/test-ai-integration.js +0 -134
@@ -6,6 +6,7 @@ import { worksheets } from './worksheets.js';
6
6
  import { studyguide } from './studyguide.js';
7
7
  import { podcast } from './podcast.js';
8
8
  import { chat } from './chat.js';
9
+ import { members } from './members.js';
9
10
  export const appRouter = router({
10
11
  auth,
11
12
  workspace,
@@ -14,4 +15,8 @@ export const appRouter = router({
14
15
  studyguide,
15
16
  podcast,
16
17
  chat,
18
+ // Public member endpoints (for invitation acceptance)
19
+ member: router({
20
+ acceptInvite: members.acceptInvite,
21
+ }),
17
22
  });
@@ -7,7 +7,17 @@ export declare const auth: import("@trpc/server").TRPCBuiltRouter<{
7
7
  cookies: Record<string, string | undefined>;
8
8
  };
9
9
  meta: object;
10
- errorShape: import("@trpc/server").TRPCDefaultErrorShape;
10
+ errorShape: {
11
+ data: {
12
+ zodError: string | null;
13
+ code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
14
+ httpStatus: number;
15
+ path?: string;
16
+ stack?: string;
17
+ };
18
+ message: string;
19
+ code: import("@trpc/server").TRPC_ERROR_CODE_NUMBER;
20
+ };
11
21
  transformer: true;
12
22
  }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
13
23
  signup: import("@trpc/server").TRPCMutationProcedure<{
@@ -7,7 +7,17 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
7
7
  cookies: Record<string, string | undefined>;
8
8
  };
9
9
  meta: object;
10
- errorShape: import("@trpc/server").TRPCDefaultErrorShape;
10
+ errorShape: {
11
+ data: {
12
+ zodError: string | null;
13
+ code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
14
+ httpStatus: number;
15
+ path?: string;
16
+ stack?: string;
17
+ };
18
+ message: string;
19
+ code: import("@trpc/server").TRPC_ERROR_CODE_NUMBER;
20
+ };
11
21
  transformer: true;
12
22
  }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
13
23
  getChannels: import("@trpc/server").TRPCQueryProcedure<{
@@ -7,7 +7,17 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
7
7
  cookies: Record<string, string | undefined>;
8
8
  };
9
9
  meta: object;
10
- errorShape: import("@trpc/server").TRPCDefaultErrorShape;
10
+ errorShape: {
11
+ data: {
12
+ zodError: string | null;
13
+ code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
14
+ httpStatus: number;
15
+ path?: string;
16
+ stack?: string;
17
+ };
18
+ message: string;
19
+ code: import("@trpc/server").TRPC_ERROR_CODE_NUMBER;
20
+ };
11
21
  transformer: true;
12
22
  }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
13
23
  listSets: import("@trpc/server").TRPCQueryProcedure<{
@@ -33,8 +43,11 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
33
43
  workspaceId: string;
34
44
  type: import("@prisma/client").$Enums.ArtifactType;
35
45
  isArchived: boolean;
46
+ generating: boolean;
47
+ generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
36
48
  difficulty: import("@prisma/client").$Enums.Difficulty | null;
37
49
  estimatedTime: string | null;
50
+ imageObjectKey: string | null;
38
51
  createdById: string | null;
39
52
  })[];
40
53
  meta: object;
@@ -43,15 +56,40 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
43
56
  input: {
44
57
  workspaceId: string;
45
58
  };
46
- output: {
59
+ output: ({
60
+ progress: {
61
+ id: string;
62
+ createdAt: Date;
63
+ updatedAt: Date;
64
+ userId: string;
65
+ flashcardId: string;
66
+ timesStudied: number;
67
+ timesCorrect: number;
68
+ timesIncorrect: number;
69
+ timesIncorrectConsecutive: number;
70
+ easeFactor: number;
71
+ interval: number;
72
+ repetitions: number;
73
+ masteryLevel: number;
74
+ lastStudiedAt: Date | null;
75
+ nextReviewAt: Date | null;
76
+ }[];
77
+ } & {
47
78
  id: string;
48
79
  createdAt: Date;
49
80
  artifactId: string;
81
+ order: number;
50
82
  front: string;
51
83
  back: string;
52
84
  tags: string[];
53
- order: number;
54
- }[];
85
+ })[];
86
+ meta: object;
87
+ }>;
88
+ isGenerating: import("@trpc/server").TRPCQueryProcedure<{
89
+ input: {
90
+ workspaceId: string;
91
+ };
92
+ output: boolean | undefined;
55
93
  meta: object;
56
94
  }>;
57
95
  createCard: import("@trpc/server").TRPCMutationProcedure<{
@@ -66,10 +104,10 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
66
104
  id: string;
67
105
  createdAt: Date;
68
106
  artifactId: string;
107
+ order: number;
69
108
  front: string;
70
109
  back: string;
71
110
  tags: string[];
72
- order: number;
73
111
  };
74
112
  meta: object;
75
113
  }>;
@@ -85,10 +123,10 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
85
123
  id: string;
86
124
  createdAt: Date;
87
125
  artifactId: string;
126
+ order: number;
88
127
  front: string;
89
128
  back: string;
90
129
  tags: string[];
91
- order: number;
92
130
  };
93
131
  meta: object;
94
132
  }>;
@@ -125,12 +163,173 @@ export declare const flashcards: import("@trpc/server").TRPCBuiltRouter<{
125
163
  workspaceId: string;
126
164
  type: import("@prisma/client").$Enums.ArtifactType;
127
165
  isArchived: boolean;
166
+ generating: boolean;
167
+ generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
128
168
  difficulty: import("@prisma/client").$Enums.Difficulty | null;
129
169
  estimatedTime: string | null;
170
+ imageObjectKey: string | null;
130
171
  createdById: string | null;
131
172
  };
132
173
  createdCards: number;
133
174
  };
134
175
  meta: object;
135
176
  }>;
177
+ recordStudyAttempt: import("@trpc/server").TRPCMutationProcedure<{
178
+ input: {
179
+ flashcardId: string;
180
+ isCorrect: boolean;
181
+ confidence?: "easy" | "medium" | "hard" | undefined;
182
+ timeSpentMs?: number | undefined;
183
+ };
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
+ };
211
+ meta: object;
212
+ }>;
213
+ getSetProgress: import("@trpc/server").TRPCQueryProcedure<{
214
+ input: {
215
+ artifactId: string;
216
+ };
217
+ output: {
218
+ flashcardId: any;
219
+ front: any;
220
+ back: any;
221
+ progress: {
222
+ id: string;
223
+ createdAt: Date;
224
+ updatedAt: Date;
225
+ userId: string;
226
+ flashcardId: string;
227
+ timesStudied: number;
228
+ timesCorrect: number;
229
+ timesIncorrect: number;
230
+ timesIncorrectConsecutive: number;
231
+ easeFactor: number;
232
+ interval: number;
233
+ repetitions: number;
234
+ masteryLevel: number;
235
+ lastStudiedAt: Date | null;
236
+ nextReviewAt: Date | null;
237
+ } | null;
238
+ }[];
239
+ meta: object;
240
+ }>;
241
+ getDueFlashcards: import("@trpc/server").TRPCQueryProcedure<{
242
+ input: {
243
+ workspaceId: string;
244
+ };
245
+ output: ({
246
+ artifact: {
247
+ id: string;
248
+ createdAt: Date;
249
+ updatedAt: Date;
250
+ title: string;
251
+ description: string | null;
252
+ workspaceId: string;
253
+ type: import("@prisma/client").$Enums.ArtifactType;
254
+ isArchived: boolean;
255
+ generating: boolean;
256
+ generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
257
+ difficulty: import("@prisma/client").$Enums.Difficulty | null;
258
+ estimatedTime: string | null;
259
+ imageObjectKey: string | null;
260
+ createdById: string | null;
261
+ };
262
+ } & {
263
+ id: string;
264
+ createdAt: Date;
265
+ artifactId: string;
266
+ order: number;
267
+ front: string;
268
+ back: string;
269
+ tags: string[];
270
+ })[];
271
+ meta: object;
272
+ }>;
273
+ getSetStatistics: import("@trpc/server").TRPCQueryProcedure<{
274
+ input: {
275
+ artifactId: string;
276
+ };
277
+ output: {
278
+ totalCards: number;
279
+ studiedCards: number;
280
+ unstudiedCards: number;
281
+ masteredCards: number;
282
+ dueForReview: number;
283
+ averageMastery: number;
284
+ successRate: number;
285
+ totalAttempts: number;
286
+ totalCorrect: number;
287
+ };
288
+ meta: object;
289
+ }>;
290
+ resetProgress: import("@trpc/server").TRPCMutationProcedure<{
291
+ input: {
292
+ flashcardId: string;
293
+ };
294
+ output: import("@prisma/client").Prisma.BatchPayload;
295
+ meta: object;
296
+ }>;
297
+ recordStudySession: import("@trpc/server").TRPCMutationProcedure<{
298
+ input: {
299
+ attempts: {
300
+ flashcardId: string;
301
+ isCorrect: boolean;
302
+ confidence?: "easy" | "medium" | "hard" | undefined;
303
+ timeSpentMs?: number | undefined;
304
+ }[];
305
+ };
306
+ output: ({
307
+ flashcard: {
308
+ id: string;
309
+ createdAt: Date;
310
+ artifactId: string;
311
+ order: number;
312
+ front: string;
313
+ back: string;
314
+ tags: string[];
315
+ };
316
+ } & {
317
+ id: string;
318
+ createdAt: Date;
319
+ updatedAt: Date;
320
+ userId: string;
321
+ flashcardId: string;
322
+ timesStudied: number;
323
+ timesCorrect: number;
324
+ timesIncorrect: number;
325
+ timesIncorrectConsecutive: number;
326
+ easeFactor: number;
327
+ interval: number;
328
+ repetitions: number;
329
+ masteryLevel: number;
330
+ lastStudiedAt: Date | null;
331
+ nextReviewAt: Date | null;
332
+ })[];
333
+ meta: object;
334
+ }>;
136
335
  }>>;
@@ -3,6 +3,7 @@ import { TRPCError } from '@trpc/server';
3
3
  import { router, authedProcedure } from '../trpc.js';
4
4
  import { aiSessionService } from '../lib/ai-session.js';
5
5
  import PusherService from '../lib/pusher.js';
6
+ import { createFlashcardProgressService } from '../services/flashcard-progress.service.js';
6
7
  // Prisma enum values mapped manually to avoid type import issues in ESM
7
8
  const ArtifactType = {
8
9
  STUDY_GUIDE: 'STUDY_GUIDE',
@@ -37,7 +38,15 @@ export const flashcards = router({
37
38
  const set = await ctx.db.artifact.findFirst({
38
39
  where: { workspaceId: input.workspaceId, type: ArtifactType.FLASHCARD_SET, workspace: { ownerId: ctx.session.user.id } },
39
40
  include: {
40
- flashcards: true,
41
+ flashcards: {
42
+ include: {
43
+ progress: {
44
+ where: {
45
+ userId: ctx.session.user.id,
46
+ },
47
+ },
48
+ }
49
+ },
41
50
  },
42
51
  orderBy: { updatedAt: 'desc' },
43
52
  });
@@ -45,6 +54,14 @@ export const flashcards = router({
45
54
  throw new TRPCError({ code: 'NOT_FOUND' });
46
55
  return set.flashcards;
47
56
  }),
57
+ isGenerating: authedProcedure
58
+ .input(z.object({ workspaceId: z.string() }))
59
+ .query(async ({ ctx, input }) => {
60
+ 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' },
62
+ });
63
+ return artifact?.generating;
64
+ }),
48
65
  createCard: authedProcedure
49
66
  .input(z.object({
50
67
  workspaceId: z.string(),
@@ -137,25 +154,30 @@ export const flashcards = router({
137
154
  });
138
155
  if (!workspace)
139
156
  throw new TRPCError({ code: 'NOT_FOUND' });
140
- // Pusher start
141
- await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_load_start', { source: 'prompt' });
142
157
  const flashcardCurrent = await ctx.db.artifact.findFirst({
143
158
  where: {
144
159
  workspaceId: input.workspaceId,
145
160
  type: ArtifactType.FLASHCARD_SET,
146
161
  },
147
162
  select: {
163
+ id: true,
148
164
  flashcards: true,
149
165
  },
150
166
  orderBy: {
151
167
  updatedAt: 'desc',
152
168
  },
153
169
  });
154
- const formattedPreviousCards = flashcardCurrent?.flashcards.map((card) => ({
155
- front: card.front,
156
- back: card.back,
157
- }));
158
- const partialPrompt = `
170
+ 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
+ await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { status: 'generating', numCards: input.numCards, difficulty: input.difficulty });
176
+ const formattedPreviousCards = flashcardCurrent?.flashcards.map((card) => ({
177
+ front: card.front,
178
+ back: card.back,
179
+ }));
180
+ const partialPrompt = `
159
181
  This is the users previous flashcards, avoid repeating any existing cards.
160
182
  Please generate ${input.numCards} new cards,
161
183
  Of a ${input.difficulty} difficulty,
@@ -165,63 +187,36 @@ export const flashcards = router({
165
187
 
166
188
  The user has also left you this prompt: ${input.prompt}
167
189
  `;
168
- // Init AI session and seed with prompt as instruction
169
- const session = await aiSessionService.initSession(input.workspaceId);
170
- await aiSessionService.setInstruction(session.id, partialPrompt);
171
- await aiSessionService.startLLMSession(session.id);
172
- const currentCards = flashcardCurrent?.flashcards.length || 0;
173
- const newCards = input.numCards - currentCards;
174
- // Generate
175
- await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { status: 'generating', numCards: input.numCards, difficulty: input.difficulty });
176
- const content = await aiSessionService.generateFlashcardQuestions(session.id, input.numCards, input.difficulty);
177
- // Previous cards
178
- // Create artifact
179
- const artifact = await ctx.db.artifact.create({
180
- data: {
181
- workspaceId: input.workspaceId,
182
- type: ArtifactType.FLASHCARD_SET,
183
- title: input.title || `Flashcards - ${new Date().toLocaleString()}`,
184
- createdById: ctx.session.user.id,
185
- flashcards: {
186
- create: flashcardCurrent?.flashcards.map((card) => ({
187
- front: card.front,
188
- back: card.back,
189
- })),
190
- },
191
- },
192
- });
193
- // Parse and create cards
194
- let createdCards = 0;
195
- try {
196
- const flashcardData = JSON.parse(content);
197
- for (let i = 0; i < Math.min(flashcardData.length, input.numCards); i++) {
198
- const card = flashcardData[i];
199
- const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
200
- const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
201
- await ctx.db.flashcard.create({
202
- data: {
203
- artifactId: artifact.id,
204
- front,
205
- back,
206
- order: i,
207
- tags: input.tags ?? ['ai-generated', input.difficulty],
190
+ const artifact = await ctx.db.artifact.create({
191
+ data: {
192
+ workspaceId: input.workspaceId,
193
+ type: ArtifactType.FLASHCARD_SET,
194
+ title: input.title || `Flashcards - ${new Date().toLocaleString()}`,
195
+ createdById: ctx.session.user.id,
196
+ flashcards: {
197
+ create: flashcardCurrent?.flashcards.map((card) => ({
198
+ front: card.front,
199
+ back: card.back,
200
+ })),
208
201
  },
209
- });
210
- createdCards++;
211
- }
212
- }
213
- catch {
214
- // Fallback to text parsing if JSON fails
215
- const lines = content.split('\n').filter(line => line.trim());
216
- for (let i = 0; i < Math.min(lines.length, input.numCards); i++) {
217
- const line = lines[i];
218
- if (line.includes(' - ')) {
219
- const [front, back] = line.split(' - ');
202
+ },
203
+ });
204
+ const currentCards = flashcardCurrent?.flashcards.length || 0;
205
+ const newCards = input.numCards - currentCards;
206
+ // Generate
207
+ const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, input.numCards, input.difficulty);
208
+ let createdCards = 0;
209
+ try {
210
+ const flashcardData = content;
211
+ for (let i = 0; i < Math.min(flashcardData.length, input.numCards); i++) {
212
+ const card = flashcardData[i];
213
+ const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
214
+ const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
220
215
  await ctx.db.flashcard.create({
221
216
  data: {
222
217
  artifactId: artifact.id,
223
- front: front.trim(),
224
- back: back.trim(),
218
+ front,
219
+ back,
225
220
  order: i,
226
221
  tags: input.tags ?? ['ai-generated', input.difficulty],
227
222
  },
@@ -229,11 +224,94 @@ export const flashcards = router({
229
224
  createdCards++;
230
225
  }
231
226
  }
227
+ catch {
228
+ // Fallback to text parsing if JSON fails
229
+ const lines = content.split('\n').filter(line => line.trim());
230
+ for (let i = 0; i < Math.min(lines.length, input.numCards); i++) {
231
+ const line = lines[i];
232
+ if (line.includes(' - ')) {
233
+ const [front, back] = line.split(' - ');
234
+ await ctx.db.flashcard.create({
235
+ data: {
236
+ artifactId: artifact.id,
237
+ front: front.trim(),
238
+ back: back.trim(),
239
+ order: i,
240
+ tags: input.tags ?? ['ai-generated', input.difficulty],
241
+ },
242
+ });
243
+ createdCards++;
244
+ }
245
+ }
246
+ }
247
+ // Pusher complete
248
+ await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
249
+ return { artifact, createdCards };
250
+ }
251
+ catch (error) {
252
+ await ctx.db.artifact.update({ where: { id: flashcardCurrent?.id }, data: { generating: false } });
253
+ await PusherService.emitError(input.workspaceId, `Failed to generate flashcards: ${error}`, 'flash_card_generation');
254
+ throw error;
232
255
  }
233
- // Pusher complete
234
- await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
235
- // Cleanup AI session (best-effort)
236
- aiSessionService.deleteSession(session.id);
237
- return { artifact, createdCards };
256
+ }),
257
+ // Record study attempt
258
+ recordStudyAttempt: authedProcedure
259
+ .input(z.object({
260
+ flashcardId: z.string().cuid(),
261
+ isCorrect: z.boolean(),
262
+ confidence: z.enum(['easy', 'medium', 'hard']).optional(),
263
+ timeSpentMs: z.number().optional(),
264
+ }))
265
+ .mutation(async ({ ctx, input }) => {
266
+ const service = createFlashcardProgressService(ctx.db);
267
+ return service.recordStudyAttempt({
268
+ userId: ctx.userId,
269
+ ...input,
270
+ });
271
+ }),
272
+ // Get progress for a flashcard set
273
+ getSetProgress: authedProcedure
274
+ .input(z.object({ artifactId: z.string().cuid() }))
275
+ .query(async ({ ctx, input }) => {
276
+ const service = createFlashcardProgressService(ctx.db);
277
+ return service.getSetProgress(ctx.userId, input.artifactId);
278
+ }),
279
+ // Get flashcards due for review
280
+ getDueFlashcards: authedProcedure
281
+ .input(z.object({ workspaceId: z.string() }))
282
+ .query(async ({ ctx, input }) => {
283
+ const service = createFlashcardProgressService(ctx.db);
284
+ return service.getDueFlashcards(ctx.userId, input.workspaceId);
285
+ }),
286
+ // Get statistics for a flashcard set
287
+ getSetStatistics: authedProcedure
288
+ .input(z.object({ artifactId: z.string().cuid() }))
289
+ .query(async ({ ctx, input }) => {
290
+ const service = createFlashcardProgressService(ctx.db);
291
+ return service.getSetStatistics(ctx.userId, input.artifactId);
292
+ }),
293
+ // Reset progress for a flashcard
294
+ resetProgress: authedProcedure
295
+ .input(z.object({ flashcardId: z.string().cuid() }))
296
+ .mutation(async ({ ctx, input }) => {
297
+ const service = createFlashcardProgressService(ctx.db);
298
+ return service.resetProgress(ctx.userId, input.flashcardId);
299
+ }),
300
+ // Bulk record study session
301
+ recordStudySession: authedProcedure
302
+ .input(z.object({
303
+ attempts: z.array(z.object({
304
+ flashcardId: z.string().cuid(),
305
+ isCorrect: z.boolean(),
306
+ confidence: z.enum(['easy', 'medium', 'hard']).optional(),
307
+ timeSpentMs: z.number().optional(),
308
+ })),
309
+ }))
310
+ .mutation(async ({ ctx, input }) => {
311
+ const service = createFlashcardProgressService(ctx.db);
312
+ return service.recordStudySession({
313
+ userId: ctx.userId,
314
+ ...input,
315
+ });
238
316
  }),
239
317
  });