@goscribe/server 1.3.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 (79) hide show
  1. package/dist/context.d.ts +5 -1
  2. package/dist/lib/activity_human_description.d.ts +13 -0
  3. package/dist/lib/activity_human_description.js +221 -0
  4. package/dist/lib/activity_human_description.test.d.ts +1 -0
  5. package/dist/lib/activity_human_description.test.js +16 -0
  6. package/dist/lib/activity_log_service.d.ts +87 -0
  7. package/dist/lib/activity_log_service.js +276 -0
  8. package/dist/lib/activity_log_service.test.d.ts +1 -0
  9. package/dist/lib/activity_log_service.test.js +27 -0
  10. package/dist/lib/ai-session.d.ts +15 -2
  11. package/dist/lib/ai-session.js +147 -85
  12. package/dist/lib/constants.d.ts +13 -0
  13. package/dist/lib/constants.js +12 -0
  14. package/dist/lib/email.d.ts +11 -0
  15. package/dist/lib/email.js +193 -0
  16. package/dist/lib/env.d.ts +13 -0
  17. package/dist/lib/env.js +16 -0
  18. package/dist/lib/inference.d.ts +4 -1
  19. package/dist/lib/inference.js +3 -3
  20. package/dist/lib/logger.d.ts +4 -4
  21. package/dist/lib/logger.js +30 -8
  22. package/dist/lib/notification-service.d.ts +152 -0
  23. package/dist/lib/notification-service.js +473 -0
  24. package/dist/lib/notification-service.test.d.ts +1 -0
  25. package/dist/lib/notification-service.test.js +87 -0
  26. package/dist/lib/prisma.d.ts +2 -1
  27. package/dist/lib/prisma.js +5 -1
  28. package/dist/lib/pusher.d.ts +23 -0
  29. package/dist/lib/pusher.js +69 -5
  30. package/dist/lib/retry.d.ts +15 -0
  31. package/dist/lib/retry.js +37 -0
  32. package/dist/lib/storage.js +2 -2
  33. package/dist/lib/stripe.d.ts +9 -0
  34. package/dist/lib/stripe.js +36 -0
  35. package/dist/lib/subscription_service.d.ts +37 -0
  36. package/dist/lib/subscription_service.js +654 -0
  37. package/dist/lib/usage_service.d.ts +26 -0
  38. package/dist/lib/usage_service.js +59 -0
  39. package/dist/lib/worksheet-generation.d.ts +91 -0
  40. package/dist/lib/worksheet-generation.js +95 -0
  41. package/dist/lib/worksheet-generation.test.d.ts +1 -0
  42. package/dist/lib/worksheet-generation.test.js +20 -0
  43. package/dist/lib/workspace-access.d.ts +18 -0
  44. package/dist/lib/workspace-access.js +13 -0
  45. package/dist/routers/_app.d.ts +1349 -253
  46. package/dist/routers/_app.js +10 -0
  47. package/dist/routers/admin.d.ts +361 -0
  48. package/dist/routers/admin.js +633 -0
  49. package/dist/routers/annotations.d.ts +219 -0
  50. package/dist/routers/annotations.js +187 -0
  51. package/dist/routers/auth.d.ts +88 -7
  52. package/dist/routers/auth.js +339 -19
  53. package/dist/routers/chat.d.ts +6 -12
  54. package/dist/routers/copilot.d.ts +199 -0
  55. package/dist/routers/copilot.js +571 -0
  56. package/dist/routers/flashcards.d.ts +47 -81
  57. package/dist/routers/flashcards.js +143 -27
  58. package/dist/routers/members.d.ts +36 -7
  59. package/dist/routers/members.js +200 -19
  60. package/dist/routers/notifications.d.ts +99 -0
  61. package/dist/routers/notifications.js +127 -0
  62. package/dist/routers/payment.d.ts +89 -0
  63. package/dist/routers/payment.js +403 -0
  64. package/dist/routers/podcast.d.ts +8 -13
  65. package/dist/routers/podcast.js +54 -31
  66. package/dist/routers/studyguide.d.ts +1 -29
  67. package/dist/routers/studyguide.js +80 -71
  68. package/dist/routers/worksheets.d.ts +105 -38
  69. package/dist/routers/worksheets.js +258 -68
  70. package/dist/routers/workspace.d.ts +139 -60
  71. package/dist/routers/workspace.js +455 -315
  72. package/dist/scripts/purge-deleted-users.d.ts +1 -0
  73. package/dist/scripts/purge-deleted-users.js +149 -0
  74. package/dist/server.js +130 -10
  75. package/dist/services/flashcard-progress.service.d.ts +18 -66
  76. package/dist/services/flashcard-progress.service.js +51 -42
  77. package/dist/trpc.d.ts +20 -21
  78. package/dist/trpc.js +150 -1
  79. package/package.json +1 -1
@@ -1,13 +1,13 @@
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 { logger } from '../lib/logger.js';
7
- // Avoid importing Prisma enums directly; mirror values as string literals
8
- const ArtifactType = {
9
- WORKSHEET: 'WORKSHEET',
10
- };
8
+ import { ArtifactType } from '../lib/constants.js';
9
+ import { workspaceAccessFilter } from '../lib/workspace-access.js';
10
+ import { mergeWorksheetGenerationConfig, normalizeWorksheetProblemForDb, parsePresetConfig, worksheetModeSchema, worksheetPresetConfigPartialSchema, } from '../lib/worksheet-generation.js';
11
11
  const Difficulty = {
12
12
  EASY: 'EASY',
13
13
  MEDIUM: 'MEDIUM',
@@ -69,8 +69,87 @@ export const worksheets = router({
69
69
  }));
70
70
  return merged;
71
71
  }),
72
+ listPresets: authedProcedure
73
+ .input(z.object({ workspaceId: z.string() }))
74
+ .query(async ({ ctx, input }) => {
75
+ const ws = await ctx.db.workspace.findFirst({
76
+ where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
77
+ });
78
+ if (!ws)
79
+ throw new TRPCError({ code: 'NOT_FOUND' });
80
+ return ctx.db.worksheetPreset.findMany({
81
+ where: {
82
+ OR: [
83
+ { isSystem: true },
84
+ {
85
+ userId: ctx.session.user.id,
86
+ OR: [{ workspaceId: input.workspaceId }, { workspaceId: null }],
87
+ },
88
+ ],
89
+ },
90
+ orderBy: [{ isSystem: 'desc' }, { name: 'asc' }],
91
+ });
92
+ }),
93
+ createPreset: authedProcedure
94
+ .input(z.object({
95
+ workspaceId: z.string().optional(),
96
+ name: z.string().min(1).max(120),
97
+ config: z.record(z.string(), z.unknown()),
98
+ }))
99
+ .mutation(async ({ ctx, input }) => {
100
+ if (input.workspaceId) {
101
+ const ws = await ctx.db.workspace.findFirst({
102
+ where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
103
+ });
104
+ if (!ws)
105
+ throw new TRPCError({ code: 'NOT_FOUND' });
106
+ }
107
+ const config = parsePresetConfig(input.config);
108
+ return ctx.db.worksheetPreset.create({
109
+ data: {
110
+ userId: ctx.session.user.id,
111
+ workspaceId: input.workspaceId ?? null,
112
+ name: input.name,
113
+ isSystem: false,
114
+ config: config,
115
+ },
116
+ });
117
+ }),
118
+ updatePreset: authedProcedure
119
+ .input(z.object({
120
+ id: z.string(),
121
+ name: z.string().min(1).max(120).optional(),
122
+ config: z.record(z.string(), z.unknown()).optional(),
123
+ }).refine(d => d.name !== undefined || d.config !== undefined, { message: 'Provide name and/or config' }))
124
+ .mutation(async ({ ctx, input }) => {
125
+ const existing = await ctx.db.worksheetPreset.findFirst({
126
+ where: { id: input.id, userId: ctx.session.user.id, isSystem: false },
127
+ });
128
+ if (!existing)
129
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found or read-only' });
130
+ const data = {};
131
+ if (input.name !== undefined)
132
+ data.name = input.name;
133
+ if (input.config !== undefined)
134
+ data.config = parsePresetConfig(input.config);
135
+ return ctx.db.worksheetPreset.update({
136
+ where: { id: input.id },
137
+ data,
138
+ });
139
+ }),
140
+ deletePreset: authedProcedure
141
+ .input(z.object({ id: z.string() }))
142
+ .mutation(async ({ ctx, input }) => {
143
+ const existing = await ctx.db.worksheetPreset.findFirst({
144
+ where: { id: input.id, userId: ctx.session.user.id, isSystem: false },
145
+ });
146
+ if (!existing)
147
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found or read-only' });
148
+ await ctx.db.worksheetPreset.delete({ where: { id: input.id } });
149
+ return true;
150
+ }),
72
151
  // Create a worksheet set
73
- create: authedProcedure
152
+ create: limitedProcedure
74
153
  .input(z.object({
75
154
  workspaceId: z.string(),
76
155
  title: z.string().min(1).max(120),
@@ -122,7 +201,7 @@ export const worksheets = router({
122
201
  where: {
123
202
  id: input.worksheetId,
124
203
  type: ArtifactType.WORKSHEET,
125
- workspace: { ownerId: ctx.session.user.id },
204
+ workspace: workspaceAccessFilter(ctx.session.user.id),
126
205
  },
127
206
  include: { questions: true },
128
207
  orderBy: { updatedAt: 'desc' },
@@ -160,7 +239,7 @@ export const worksheets = router({
160
239
  return merged;
161
240
  }),
162
241
  // Add a question to a worksheet
163
- createWorksheetQuestion: authedProcedure
242
+ createWorksheetQuestion: limitedProcedure
164
243
  .input(z.object({
165
244
  worksheetId: z.string(),
166
245
  prompt: z.string().min(1),
@@ -172,7 +251,7 @@ export const worksheets = router({
172
251
  }))
173
252
  .mutation(async ({ ctx, input }) => {
174
253
  const worksheet = await ctx.db.artifact.findFirst({
175
- where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
254
+ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) },
176
255
  });
177
256
  if (!worksheet)
178
257
  throw new TRPCError({ code: 'NOT_FOUND' });
@@ -201,7 +280,7 @@ export const worksheets = router({
201
280
  }))
202
281
  .mutation(async ({ ctx, input }) => {
203
282
  const q = await ctx.db.worksheetQuestion.findFirst({
204
- where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } } },
283
+ where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) } },
205
284
  });
206
285
  if (!q)
207
286
  throw new TRPCError({ code: 'NOT_FOUND' });
@@ -222,7 +301,7 @@ export const worksheets = router({
222
301
  .input(z.object({ worksheetQuestionId: z.string() }))
223
302
  .mutation(async ({ ctx, input }) => {
224
303
  const q = await ctx.db.worksheetQuestion.findFirst({
225
- where: { id: input.worksheetQuestionId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
304
+ where: { id: input.worksheetQuestionId, artifact: { workspace: workspaceAccessFilter(ctx.session.user.id) } },
226
305
  });
227
306
  if (!q)
228
307
  throw new TRPCError({ code: 'NOT_FOUND' });
@@ -244,7 +323,7 @@ export const worksheets = router({
244
323
  id: input.problemId,
245
324
  artifact: {
246
325
  type: ArtifactType.WORKSHEET,
247
- workspace: { ownerId: ctx.session.user.id },
326
+ workspace: workspaceAccessFilter(ctx.session.user.id),
248
327
  },
249
328
  },
250
329
  });
@@ -286,7 +365,7 @@ export const worksheets = router({
286
365
  where: {
287
366
  id: input.worksheetId,
288
367
  type: ArtifactType.WORKSHEET,
289
- workspace: { ownerId: ctx.session.user.id },
368
+ workspace: workspaceAccessFilter(ctx.session.user.id),
290
369
  },
291
370
  });
292
371
  if (!worksheet)
@@ -327,7 +406,7 @@ export const worksheets = router({
327
406
  where: {
328
407
  id,
329
408
  type: ArtifactType.WORKSHEET,
330
- workspace: { ownerId: ctx.session.user.id },
409
+ workspace: workspaceAccessFilter(ctx.session.user.id),
331
410
  },
332
411
  });
333
412
  if (!existingWorksheet)
@@ -369,93 +448,204 @@ export const worksheets = router({
369
448
  .input(z.object({ id: z.string() }))
370
449
  .mutation(async ({ ctx, input }) => {
371
450
  const deleted = await ctx.db.artifact.deleteMany({
372
- where: { id: input.id, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
451
+ where: { id: input.id, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) },
373
452
  });
374
453
  if (deleted.count === 0)
375
454
  throw new TRPCError({ code: 'NOT_FOUND' });
376
455
  return true;
377
456
  }),
378
457
  // Generate a worksheet from a user prompt
379
- generateFromPrompt: authedProcedure
458
+ generateFromPrompt: limitedProcedure
380
459
  .input(z.object({
381
460
  workspaceId: z.string(),
382
461
  prompt: z.string().min(1),
383
462
  numQuestions: z.number().int().min(1).max(20).default(8),
384
463
  difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
464
+ mode: worksheetModeSchema.optional(),
465
+ presetId: z.string().optional(),
466
+ configOverride: worksheetPresetConfigPartialSchema.optional(),
385
467
  title: z.string().optional(),
386
468
  estimatedTime: z.string().optional(),
387
469
  }))
388
470
  .mutation(async ({ ctx, input }) => {
389
- const workspace = await ctx.db.workspace.findFirst({ where: { id: input.workspaceId, ownerId: ctx.session.user.id } });
471
+ const workspace = await ctx.db.workspace.findFirst({
472
+ where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
473
+ });
390
474
  if (!workspace)
391
475
  throw new TRPCError({ code: 'NOT_FOUND' });
476
+ let presetRow = null;
477
+ if (input.presetId) {
478
+ presetRow = await ctx.db.worksheetPreset.findFirst({
479
+ where: {
480
+ id: input.presetId,
481
+ OR: [
482
+ { isSystem: true },
483
+ {
484
+ userId: ctx.session.user.id,
485
+ OR: [{ workspaceId: input.workspaceId }, { workspaceId: null }],
486
+ },
487
+ ],
488
+ },
489
+ });
490
+ if (!presetRow)
491
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found' });
492
+ }
493
+ const presetConfig = presetRow?.config
494
+ ? parsePresetConfig(presetRow.config)
495
+ : undefined;
496
+ const resolved = mergeWorksheetGenerationConfig(presetConfig, input.configOverride ?? undefined, {
497
+ numQuestions: input.numQuestions,
498
+ difficulty: input.difficulty,
499
+ mode: input.mode,
500
+ });
501
+ const difficultyUpper = resolved.difficulty.toUpperCase();
502
+ console.log("[DEBUG TRPC PAYLOAD] input =", input);
503
+ console.log("[DEBUG TRPC PAYLOAD] presetConfig =", presetConfig);
504
+ console.log("[DEBUG TRPC PAYLOAD] legacy merged legacy values:", { numQuestions: input.numQuestions, difficulty: input.difficulty, mode: input.mode });
505
+ console.log("[DEBUG TRPC PAYLOAD] RESOLVED =", resolved);
506
+ const worksheetConfigSnapshot = {
507
+ mode: resolved.mode,
508
+ presetId: input.presetId ?? null,
509
+ presetName: presetRow?.name ?? null,
510
+ numQuestions: resolved.numQuestions,
511
+ difficulty: resolved.difficulty,
512
+ mcqRatio: resolved.mcqRatio ?? null,
513
+ questionTypes: resolved.questionTypes ?? null,
514
+ };
515
+ await PusherService.emitWorksheetGenerationStart(input.workspaceId);
392
516
  const artifact = await ctx.db.artifact.create({
393
517
  data: {
394
518
  workspaceId: input.workspaceId,
395
519
  type: ArtifactType.WORKSHEET,
396
- title: input.title || `Worksheet - ${new Date().toLocaleString()}`,
520
+ title: input.title || (resolved.mode === 'quiz' ? `Quiz - ${new Date().toLocaleString()}` : `Worksheet - ${new Date().toLocaleString()}`),
397
521
  createdById: ctx.session.user.id,
398
- difficulty: (input.difficulty.toUpperCase()),
522
+ difficulty: difficultyUpper,
399
523
  estimatedTime: input.estimatedTime,
400
524
  generating: true,
401
- generatingMetadata: { quantity: input.numQuestions, difficulty: input.difficulty.toLowerCase() },
525
+ generatingMetadata: {
526
+ quantity: resolved.numQuestions,
527
+ difficulty: resolved.difficulty,
528
+ mode: resolved.mode,
529
+ presetId: input.presetId ?? null,
530
+ },
531
+ worksheetConfig: worksheetConfigSnapshot,
402
532
  },
403
533
  });
404
- await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: input.numQuestions });
405
- try {
406
- const content = await aiSessionService.generateWorksheetQuestions(input.workspaceId, ctx.session.user.id, input.numQuestions, input.difficulty);
534
+ await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: resolved.numQuestions });
535
+ await PusherService.emitWorksheetNew(input.workspaceId, artifact);
536
+ const userId = ctx.session.user.id;
537
+ const workspaceId = input.workspaceId;
538
+ const promptText = input.prompt;
539
+ const estTime = input.estimatedTime;
540
+ // Launch generation in the background to free up the connection pool immediately
541
+ (async () => {
407
542
  try {
408
- const worksheetData = JSON.parse(content);
409
- let actualWorksheetData = worksheetData;
410
- if (worksheetData.last_response) {
411
- try {
412
- actualWorksheetData = JSON.parse(worksheetData.last_response);
543
+ const content = await aiSessionService.generateWorksheetQuestions(workspaceId, userId, resolved.numQuestions, difficultyUpper, {
544
+ mode: resolved.mode,
545
+ mcqRatio: resolved.mcqRatio,
546
+ questionTypes: resolved.questionTypes,
547
+ prompt: promptText,
548
+ });
549
+ let problems = [];
550
+ try {
551
+ const worksheetData = JSON.parse(content);
552
+ let actualWorksheetData = worksheetData;
553
+ if (worksheetData.last_response) {
554
+ try {
555
+ actualWorksheetData = JSON.parse(worksheetData.last_response);
556
+ }
557
+ catch { /* noop */ }
413
558
  }
414
- catch { }
559
+ problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
560
+ if (!Array.isArray(problems))
561
+ problems = [];
415
562
  }
416
- const problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
417
- for (let i = 0; i < Math.min(problems.length, input.numQuestions); i++) {
418
- const problem = problems[i];
419
- const prompt = problem.question || problem.prompt || `Question ${i + 1}`;
420
- const answer = problem.answer || problem.solution || `Answer ${i + 1}`;
421
- const type = problem.type || 'TEXT';
422
- const options = problem.options || [];
423
- await ctx.db.worksheetQuestion.create({
424
- data: {
425
- artifactId: artifact.id,
426
- prompt,
427
- answer,
428
- difficulty: (input.difficulty.toUpperCase()),
429
- order: i,
430
- type,
431
- meta: {
432
- options: options.length > 0 ? options : undefined,
433
- markScheme: problem.mark_scheme || undefined,
434
- },
435
- },
563
+ catch (parseError) {
564
+ logger.error('Failed to parse worksheet JSON', parseError);
565
+ throw new Error('Failed to parse worksheet JSON');
566
+ }
567
+ const forceMcq = resolved.mode === 'quiz';
568
+ const questionsToCreate = [];
569
+ for (let i = 0; i < Math.min(problems.length, resolved.numQuestions); i++) {
570
+ const problem = problems[i] && typeof problems[i] === 'object' ? problems[i] : {};
571
+ const row = normalizeWorksheetProblemForDb(problem, i, difficultyUpper, forceMcq);
572
+ const metaPayload = {};
573
+ if (row.meta.options?.length)
574
+ metaPayload.options = row.meta.options;
575
+ if (row.meta.markScheme != null)
576
+ metaPayload.markScheme = row.meta.markScheme;
577
+ questionsToCreate.push({
578
+ artifactId: artifact.id,
579
+ prompt: row.prompt,
580
+ answer: row.answer ?? '',
581
+ difficulty: row.difficulty,
582
+ order: row.order,
583
+ type: row.type,
584
+ meta: Object.keys(metaPayload).length ? metaPayload : undefined,
436
585
  });
437
586
  }
438
- }
439
- catch {
440
- logger.error('Failed to parse worksheet JSON,');
441
- await ctx.db.artifact.delete({
587
+ if (questionsToCreate.length > 0) {
588
+ await ctx.db.worksheetQuestion.createMany({
589
+ data: questionsToCreate,
590
+ });
591
+ }
592
+ let parsedTitle;
593
+ let parsedDescription;
594
+ let parsedEstimated;
595
+ try {
596
+ const worksheetData = JSON.parse(content);
597
+ let actualWorksheetData = worksheetData;
598
+ if (worksheetData.last_response) {
599
+ try {
600
+ actualWorksheetData = JSON.parse(worksheetData.last_response);
601
+ }
602
+ catch { /* noop */ }
603
+ }
604
+ parsedTitle = typeof actualWorksheetData.title === 'string' ? actualWorksheetData.title : undefined;
605
+ parsedDescription = typeof actualWorksheetData.description === 'string' ? actualWorksheetData.description : undefined;
606
+ parsedEstimated = typeof actualWorksheetData.estimatedTime === 'string' ? actualWorksheetData.estimatedTime : undefined;
607
+ }
608
+ catch { /* noop */ }
609
+ await ctx.db.artifact.update({
610
+ where: { id: artifact.id },
611
+ data: {
612
+ generating: false,
613
+ title: parsedTitle || artifact.title,
614
+ description: parsedDescription ?? undefined,
615
+ estimatedTime: estTime || parsedEstimated || undefined,
616
+ worksheetConfig: worksheetConfigSnapshot,
617
+ },
618
+ });
619
+ const updatedWorksheet = await ctx.db.artifact.findFirst({
442
620
  where: { id: artifact.id },
621
+ include: { questions: true }
443
622
  });
444
- throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to parse worksheet JSON' });
623
+ await PusherService.emitWorksheetGenerationComplete(workspaceId, updatedWorksheet);
624
+ await PusherService.emitWorksheetComplete(workspaceId, artifact);
625
+ await notifyArtifactReady(ctx.db, {
626
+ userId,
627
+ workspaceId,
628
+ artifactId: artifact.id,
629
+ artifactType: ArtifactType.WORKSHEET,
630
+ title: updatedWorksheet?.title ?? artifact.title,
631
+ }).catch(() => { });
445
632
  }
446
- await ctx.db.artifact.update({
447
- where: { id: artifact.id },
448
- data: { generating: false },
449
- });
450
- await PusherService.emitWorksheetComplete(input.workspaceId, artifact);
451
- }
452
- catch (error) {
453
- await ctx.db.artifact.delete({
454
- where: { id: artifact.id },
455
- });
456
- await PusherService.emitError(input.workspaceId, `Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`, 'worksheet_generation');
457
- throw error;
458
- }
633
+ catch (error) {
634
+ logger.error(`Background generation failed for artifact ${artifact.id}:`, error);
635
+ await notifyArtifactFailed(ctx.db, {
636
+ userId,
637
+ workspaceId,
638
+ artifactId: artifact.id,
639
+ artifactType: ArtifactType.WORKSHEET,
640
+ title: artifact.title,
641
+ message: `Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`,
642
+ }).catch(() => { });
643
+ await ctx.db.artifact.deleteMany({
644
+ where: { id: artifact.id },
645
+ }).catch(() => { }); // Ignore delete errors if already gone
646
+ await PusherService.emitError(workspaceId, `Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`, 'worksheet_generation');
647
+ }
648
+ })();
459
649
  return { artifact };
460
650
  }),
461
651
  checkAnswer: authedProcedure
@@ -465,7 +655,7 @@ export const worksheets = router({
465
655
  answer: z.string().min(1),
466
656
  }))
467
657
  .mutation(async ({ ctx, input }) => {
468
- const worksheet = await ctx.db.artifact.findFirst({ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } }, include: { workspace: true } });
658
+ const worksheet = await ctx.db.artifact.findFirst({ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) }, include: { workspace: true } });
469
659
  if (!worksheet)
470
660
  throw new TRPCError({ code: 'NOT_FOUND' });
471
661
  const question = await ctx.db.worksheetQuestion.findFirst({ where: { id: input.questionId, artifactId: input.worksheetId } });