@goscribe/server 1.1.7 → 1.3.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 (56) hide show
  1. package/.env.example +43 -0
  2. package/check-difficulty.cjs +14 -0
  3. package/check-questions.cjs +14 -0
  4. package/db-summary.cjs +22 -0
  5. package/dist/routers/auth.js +1 -1
  6. package/mcq-test.cjs +36 -0
  7. package/package.json +10 -2
  8. package/prisma/migrations/20260413143206_init/migration.sql +873 -0
  9. package/prisma/schema.prisma +485 -292
  10. package/src/context.ts +4 -1
  11. package/src/lib/activity_human_description.test.ts +28 -0
  12. package/src/lib/activity_human_description.ts +239 -0
  13. package/src/lib/activity_log_service.test.ts +37 -0
  14. package/src/lib/activity_log_service.ts +353 -0
  15. package/src/lib/ai-session.ts +194 -112
  16. package/src/lib/constants.ts +14 -0
  17. package/src/lib/email.ts +230 -0
  18. package/src/lib/env.ts +23 -6
  19. package/src/lib/inference.ts +3 -3
  20. package/src/lib/logger.ts +26 -9
  21. package/src/lib/notification-service.test.ts +106 -0
  22. package/src/lib/notification-service.ts +677 -0
  23. package/src/lib/prisma.ts +6 -1
  24. package/src/lib/pusher.ts +90 -6
  25. package/src/lib/retry.ts +61 -0
  26. package/src/lib/storage.ts +2 -2
  27. package/src/lib/stripe.ts +39 -0
  28. package/src/lib/subscription_service.ts +722 -0
  29. package/src/lib/usage_service.ts +74 -0
  30. package/src/lib/worksheet-generation.test.ts +31 -0
  31. package/src/lib/worksheet-generation.ts +139 -0
  32. package/src/lib/workspace-access.ts +13 -0
  33. package/src/routers/_app.ts +11 -0
  34. package/src/routers/admin.ts +710 -0
  35. package/src/routers/annotations.ts +227 -0
  36. package/src/routers/auth.ts +432 -33
  37. package/src/routers/copilot.ts +719 -0
  38. package/src/routers/flashcards.ts +207 -80
  39. package/src/routers/members.ts +280 -80
  40. package/src/routers/notifications.ts +142 -0
  41. package/src/routers/payment.ts +448 -0
  42. package/src/routers/podcast.ts +133 -108
  43. package/src/routers/studyguide.ts +80 -74
  44. package/src/routers/worksheets.ts +300 -80
  45. package/src/routers/workspace.ts +538 -328
  46. package/src/scripts/purge-deleted-users.ts +167 -0
  47. package/src/server.ts +140 -12
  48. package/src/services/flashcard-progress.service.ts +52 -43
  49. package/src/trpc.ts +184 -5
  50. package/test-generate.js +30 -0
  51. package/test-ratio.cjs +9 -0
  52. package/zod-test.cjs +22 -0
  53. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
  54. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
  55. package/prisma/seed.mjs +0 -135
  56. package/src/routers/meetingsummary.ts +0 -416
@@ -1,14 +1,19 @@
1
1
  import { z } from 'zod';
2
2
  import { TRPCError } from '@trpc/server';
3
- import { router, authedProcedure } from '../trpc.js';
3
+ import { router, authedProcedure, verifiedProcedure, 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
-
8
- // Avoid importing Prisma enums directly; mirror values as string literals
9
- const ArtifactType = {
10
- WORKSHEET: 'WORKSHEET',
11
- } as const;
8
+ import { ArtifactType } from '../lib/constants.js';
9
+ import { workspaceAccessFilter } from '../lib/workspace-access.js';
10
+ import {
11
+ mergeWorksheetGenerationConfig,
12
+ normalizeWorksheetProblemForDb,
13
+ parsePresetConfig,
14
+ worksheetModeSchema,
15
+ worksheetPresetConfigPartialSchema,
16
+ } from '../lib/worksheet-generation.js';
12
17
 
13
18
  const Difficulty = {
14
19
  EASY: 'EASY',
@@ -75,10 +80,91 @@ export const worksheets = router({
75
80
  return merged;
76
81
  }),
77
82
 
83
+ listPresets: authedProcedure
84
+ .input(z.object({ workspaceId: z.string() }))
85
+ .query(async ({ ctx, input }) => {
86
+ const ws = await ctx.db.workspace.findFirst({
87
+ where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
88
+ });
89
+ if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
90
+
91
+ return ctx.db.worksheetPreset.findMany({
92
+ where: {
93
+ OR: [
94
+ { isSystem: true },
95
+ {
96
+ userId: ctx.session.user.id,
97
+ OR: [{ workspaceId: input.workspaceId }, { workspaceId: null }],
98
+ },
99
+ ],
100
+ },
101
+ orderBy: [{ isSystem: 'desc' }, { name: 'asc' }],
102
+ });
103
+ }),
104
+
105
+ createPreset: authedProcedure
106
+ .input(z.object({
107
+ workspaceId: z.string().optional(),
108
+ name: z.string().min(1).max(120),
109
+ config: z.record(z.string(), z.unknown()),
110
+ }))
111
+ .mutation(async ({ ctx, input }) => {
112
+ if (input.workspaceId) {
113
+ const ws = await ctx.db.workspace.findFirst({
114
+ where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
115
+ });
116
+ if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
117
+ }
118
+ const config = parsePresetConfig(input.config);
119
+ return ctx.db.worksheetPreset.create({
120
+ data: {
121
+ userId: ctx.session.user.id,
122
+ workspaceId: input.workspaceId ?? null,
123
+ name: input.name,
124
+ isSystem: false,
125
+ config: config as object,
126
+ },
127
+ });
128
+ }),
129
+
130
+ updatePreset: authedProcedure
131
+ .input(z.object({
132
+ id: z.string(),
133
+ name: z.string().min(1).max(120).optional(),
134
+ config: z.record(z.string(), z.unknown()).optional(),
135
+ }).refine(d => d.name !== undefined || d.config !== undefined, { message: 'Provide name and/or config' }))
136
+ .mutation(async ({ ctx, input }) => {
137
+ const existing = await ctx.db.worksheetPreset.findFirst({
138
+ where: { id: input.id, userId: ctx.session.user.id, isSystem: false },
139
+ });
140
+ if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found or read-only' });
141
+
142
+ const data: { name?: string; config?: object } = {};
143
+ if (input.name !== undefined) data.name = input.name;
144
+ if (input.config !== undefined) data.config = parsePresetConfig(input.config) as object;
145
+
146
+ return ctx.db.worksheetPreset.update({
147
+ where: { id: input.id },
148
+ data,
149
+ });
150
+ }),
151
+
152
+ deletePreset: authedProcedure
153
+ .input(z.object({ id: z.string() }))
154
+ .mutation(async ({ ctx, input }) => {
155
+ const existing = await ctx.db.worksheetPreset.findFirst({
156
+ where: { id: input.id, userId: ctx.session.user.id, isSystem: false },
157
+ });
158
+ if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found or read-only' });
159
+
160
+ await ctx.db.worksheetPreset.delete({ where: { id: input.id } });
161
+ return true;
162
+ }),
163
+
78
164
  // Create a worksheet set
79
- create: authedProcedure
80
- .input(z.object({
81
- workspaceId: z.string(),
165
+ create: limitedProcedure
166
+ .input(z.object({
167
+ workspaceId: z.string(),
82
168
  title: z.string().min(1).max(120),
83
169
  description: z.string().optional(),
84
170
  difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
@@ -130,7 +216,7 @@ export const worksheets = router({
130
216
  where: {
131
217
  id: input.worksheetId,
132
218
  type: ArtifactType.WORKSHEET,
133
- workspace: { ownerId: ctx.session.user.id },
219
+ workspace: workspaceAccessFilter(ctx.session.user.id),
134
220
  },
135
221
  include: { questions: true },
136
222
  orderBy: { updatedAt: 'desc' },
@@ -169,7 +255,7 @@ export const worksheets = router({
169
255
  }),
170
256
 
171
257
  // Add a question to a worksheet
172
- createWorksheetQuestion: authedProcedure
258
+ createWorksheetQuestion: limitedProcedure
173
259
  .input(z.object({
174
260
  worksheetId: z.string(),
175
261
  prompt: z.string().min(1),
@@ -181,7 +267,7 @@ export const worksheets = router({
181
267
  }))
182
268
  .mutation(async ({ ctx, input }) => {
183
269
  const worksheet = await ctx.db.artifact.findFirst({
184
- where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
270
+ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) },
185
271
  });
186
272
  if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
187
273
  return ctx.db.worksheetQuestion.create({
@@ -210,7 +296,7 @@ export const worksheets = router({
210
296
  }))
211
297
  .mutation(async ({ ctx, input }) => {
212
298
  const q = await ctx.db.worksheetQuestion.findFirst({
213
- where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } } },
299
+ where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) } },
214
300
  });
215
301
  if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
216
302
  return ctx.db.worksheetQuestion.update({
@@ -231,7 +317,7 @@ export const worksheets = router({
231
317
  .input(z.object({ worksheetQuestionId: z.string() }))
232
318
  .mutation(async ({ ctx, input }) => {
233
319
  const q = await ctx.db.worksheetQuestion.findFirst({
234
- where: { id: input.worksheetQuestionId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
320
+ where: { id: input.worksheetQuestionId, artifact: { workspace: workspaceAccessFilter(ctx.session.user.id) } },
235
321
  });
236
322
  if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
237
323
  await ctx.db.worksheetQuestion.delete({ where: { id: input.worksheetQuestionId } });
@@ -253,7 +339,7 @@ export const worksheets = router({
253
339
  id: input.problemId,
254
340
  artifact: {
255
341
  type: ArtifactType.WORKSHEET,
256
- workspace: { ownerId: ctx.session.user.id },
342
+ workspace: workspaceAccessFilter(ctx.session.user.id),
257
343
  },
258
344
  },
259
345
  });
@@ -297,7 +383,7 @@ export const worksheets = router({
297
383
  where: {
298
384
  id: input.worksheetId,
299
385
  type: ArtifactType.WORKSHEET,
300
- workspace: { ownerId: ctx.session.user.id },
386
+ workspace: workspaceAccessFilter(ctx.session.user.id),
301
387
  },
302
388
  });
303
389
  if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
@@ -336,13 +422,13 @@ export const worksheets = router({
336
422
  }))
337
423
  .mutation(async ({ ctx, input }) => {
338
424
  const { id, problems, ...updateData } = input;
339
-
425
+
340
426
  // Verify worksheet ownership
341
427
  const existingWorksheet = await ctx.db.artifact.findFirst({
342
428
  where: {
343
429
  id,
344
430
  type: ArtifactType.WORKSHEET,
345
- workspace: { ownerId: ctx.session.user.id },
431
+ workspace: workspaceAccessFilter(ctx.session.user.id),
346
432
  },
347
433
  });
348
434
  if (!existingWorksheet) throw new TRPCError({ code: 'NOT_FOUND' });
@@ -388,107 +474,241 @@ export const worksheets = router({
388
474
  .input(z.object({ id: z.string() }))
389
475
  .mutation(async ({ ctx, input }) => {
390
476
  const deleted = await ctx.db.artifact.deleteMany({
391
- where: { id: input.id, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
477
+ where: { id: input.id, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) },
392
478
  });
393
479
  if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
394
480
  return true;
395
481
  }),
396
482
 
397
483
  // Generate a worksheet from a user prompt
398
- generateFromPrompt: authedProcedure
484
+ generateFromPrompt: limitedProcedure
399
485
  .input(z.object({
400
486
  workspaceId: z.string(),
401
487
  prompt: z.string().min(1),
402
488
  numQuestions: z.number().int().min(1).max(20).default(8),
403
489
  difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
490
+ mode: worksheetModeSchema.optional(),
491
+ presetId: z.string().optional(),
492
+ configOverride: worksheetPresetConfigPartialSchema.optional(),
404
493
  title: z.string().optional(),
405
494
  estimatedTime: z.string().optional(),
406
495
  }))
407
496
  .mutation(async ({ ctx, input }) => {
408
- const workspace = await ctx.db.workspace.findFirst({ where: { id: input.workspaceId, ownerId: ctx.session.user.id } });
497
+ const workspace = await ctx.db.workspace.findFirst({
498
+ where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
499
+ });
409
500
  if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
410
501
 
502
+ let presetRow: Awaited<ReturnType<typeof ctx.db.worksheetPreset.findFirst>> = null;
503
+ if (input.presetId) {
504
+ presetRow = await ctx.db.worksheetPreset.findFirst({
505
+ where: {
506
+ id: input.presetId,
507
+ OR: [
508
+ { isSystem: true },
509
+ {
510
+ userId: ctx.session.user.id,
511
+ OR: [{ workspaceId: input.workspaceId }, { workspaceId: null }],
512
+ },
513
+ ],
514
+ },
515
+ });
516
+ if (!presetRow) throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found' });
517
+ }
518
+
519
+ const presetConfig = presetRow?.config
520
+ ? parsePresetConfig(presetRow.config)
521
+ : undefined;
522
+
523
+ const resolved = mergeWorksheetGenerationConfig(
524
+ presetConfig,
525
+ input.configOverride ?? undefined,
526
+ {
527
+ numQuestions: input.numQuestions,
528
+ difficulty: input.difficulty,
529
+ mode: input.mode,
530
+ },
531
+ );
532
+
533
+ const difficultyUpper = resolved.difficulty.toUpperCase() as 'EASY' | 'MEDIUM' | 'HARD';
534
+
535
+ console.log("[DEBUG TRPC PAYLOAD] input =", input);
536
+ console.log("[DEBUG TRPC PAYLOAD] presetConfig =", presetConfig);
537
+ console.log("[DEBUG TRPC PAYLOAD] legacy merged legacy values:", { numQuestions: input.numQuestions, difficulty: input.difficulty, mode: input.mode });
538
+ console.log("[DEBUG TRPC PAYLOAD] RESOLVED =", resolved);
539
+
540
+ const worksheetConfigSnapshot = {
541
+ mode: resolved.mode,
542
+ presetId: input.presetId ?? null,
543
+ presetName: presetRow?.name ?? null,
544
+ numQuestions: resolved.numQuestions,
545
+ difficulty: resolved.difficulty,
546
+ mcqRatio: resolved.mcqRatio ?? null,
547
+ questionTypes: resolved.questionTypes ?? null,
548
+ };
549
+
550
+ await PusherService.emitWorksheetGenerationStart(input.workspaceId);
551
+
411
552
  const artifact = await ctx.db.artifact.create({
412
553
  data: {
413
554
  workspaceId: input.workspaceId,
414
555
  type: ArtifactType.WORKSHEET,
415
- title: input.title || `Worksheet - ${new Date().toLocaleString()}`,
556
+ title: input.title || (resolved.mode === 'quiz' ? `Quiz - ${new Date().toLocaleString()}` : `Worksheet - ${new Date().toLocaleString()}`),
416
557
  createdById: ctx.session.user.id,
417
- difficulty: (input.difficulty.toUpperCase()) as any,
558
+ difficulty: difficultyUpper as any,
418
559
  estimatedTime: input.estimatedTime,
419
560
  generating: true,
420
- generatingMetadata: { quantity: input.numQuestions, difficulty: input.difficulty.toLowerCase() },
561
+ generatingMetadata: {
562
+ quantity: resolved.numQuestions,
563
+ difficulty: resolved.difficulty,
564
+ mode: resolved.mode,
565
+ presetId: input.presetId ?? null,
566
+ },
567
+ worksheetConfig: worksheetConfigSnapshot as object,
421
568
  },
422
569
  });
423
- await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: input.numQuestions });
424
- try {
425
-
426
- const content = await aiSessionService.generateWorksheetQuestions(input.workspaceId, ctx.session.user.id, input.numQuestions, input.difficulty as any);
427
- try {
428
- const worksheetData = JSON.parse(content);
429
- let actualWorksheetData = worksheetData;
430
- if (worksheetData.last_response) {
431
- try { actualWorksheetData = JSON.parse(worksheetData.last_response); } catch {}
432
- }
433
- const problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
434
- for (let i = 0; i < Math.min(problems.length, input.numQuestions); i++) {
435
- const problem = problems[i];
436
- const prompt = problem.question || problem.prompt || `Question ${i + 1}`;
437
- const answer = problem.answer || problem.solution || `Answer ${i + 1}`;
438
- const type = problem.type || 'TEXT';
439
- const options = problem.options || [];
440
-
441
- await ctx.db.worksheetQuestion.create({
442
- data: {
570
+
571
+ await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: resolved.numQuestions });
572
+ await PusherService.emitWorksheetNew(input.workspaceId, artifact);
573
+
574
+ const userId = ctx.session.user.id;
575
+ const workspaceId = input.workspaceId;
576
+ const promptText = input.prompt;
577
+ const estTime = input.estimatedTime;
578
+
579
+ // Launch generation in the background to free up the connection pool immediately
580
+ (async () => {
581
+ try {
582
+ const content = await aiSessionService.generateWorksheetQuestions(
583
+ workspaceId,
584
+ userId,
585
+ resolved.numQuestions,
586
+ difficultyUpper,
587
+ {
588
+ mode: resolved.mode,
589
+ mcqRatio: resolved.mcqRatio,
590
+ questionTypes: resolved.questionTypes,
591
+ prompt: promptText,
592
+ },
593
+ );
594
+
595
+ let problems: any[] = [];
596
+ try {
597
+ const worksheetData = JSON.parse(content);
598
+ let actualWorksheetData: any = worksheetData;
599
+ if (worksheetData.last_response) {
600
+ try { actualWorksheetData = JSON.parse(worksheetData.last_response); } catch { /* noop */ }
601
+ }
602
+ problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
603
+ if (!Array.isArray(problems)) problems = [];
604
+ } catch (parseError) {
605
+ logger.error('Failed to parse worksheet JSON', parseError);
606
+ throw new Error('Failed to parse worksheet JSON');
607
+ }
608
+
609
+ const forceMcq = resolved.mode === 'quiz';
610
+ const questionsToCreate = [];
611
+ for (let i = 0; i < Math.min(problems.length, resolved.numQuestions); i++) {
612
+ const problem = problems[i] && typeof problems[i] === 'object' ? problems[i] as Record<string, unknown> : {};
613
+ const row = normalizeWorksheetProblemForDb(problem, i, difficultyUpper, forceMcq);
614
+ const metaPayload: Record<string, unknown> = {};
615
+ if (row.meta.options?.length) metaPayload.options = row.meta.options;
616
+ if (row.meta.markScheme != null) metaPayload.markScheme = row.meta.markScheme;
617
+
618
+ questionsToCreate.push({
443
619
  artifactId: artifact.id,
444
- prompt,
445
- answer,
446
- difficulty: (input.difficulty.toUpperCase()) as any,
447
- order: i,
448
- type,
449
- meta: {
450
- options: options.length > 0 ? options : undefined,
451
- markScheme: problem.mark_scheme || undefined,
452
- },
620
+ prompt: row.prompt,
621
+ answer: row.answer ?? '',
622
+ difficulty: row.difficulty as any,
623
+ order: row.order,
624
+ type: row.type as any,
625
+ meta: Object.keys(metaPayload).length ? (metaPayload as object) : undefined,
626
+ });
627
+ }
628
+
629
+ if (questionsToCreate.length > 0) {
630
+ await ctx.db.worksheetQuestion.createMany({
631
+ data: questionsToCreate,
632
+ });
633
+ }
634
+
635
+ let parsedTitle: string | undefined;
636
+ let parsedDescription: string | undefined;
637
+ let parsedEstimated: string | undefined;
638
+ try {
639
+ const worksheetData = JSON.parse(content);
640
+ let actualWorksheetData: any = worksheetData;
641
+ if (worksheetData.last_response) {
642
+ try { actualWorksheetData = JSON.parse(worksheetData.last_response); } catch { /* noop */ }
643
+ }
644
+ parsedTitle = typeof actualWorksheetData.title === 'string' ? actualWorksheetData.title : undefined;
645
+ parsedDescription = typeof actualWorksheetData.description === 'string' ? actualWorksheetData.description : undefined;
646
+ parsedEstimated = typeof actualWorksheetData.estimatedTime === 'string' ? actualWorksheetData.estimatedTime : undefined;
647
+ } catch { /* noop */ }
648
+
649
+ await ctx.db.artifact.update({
650
+ where: { id: artifact.id },
651
+ data: {
652
+ generating: false,
653
+ title: parsedTitle || artifact.title,
654
+ description: parsedDescription ?? undefined,
655
+ estimatedTime: estTime || parsedEstimated || undefined,
656
+ worksheetConfig: worksheetConfigSnapshot as object,
453
657
  },
454
658
  });
455
- }
456
- } catch {
457
- logger.error('Failed to parse worksheet JSON,');
458
- await ctx.db.artifact.delete({
459
- where: { id: artifact.id },
460
- });
461
- throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to parse worksheet JSON' });
462
- }
463
659
 
464
- await ctx.db.artifact.update({
465
- where: { id: artifact.id },
466
- data: { generating: false },
467
- });
660
+ const updatedWorksheet = await ctx.db.artifact.findFirst({
661
+ where: { id: artifact.id },
662
+ include: { questions: true }
663
+ });
468
664
 
469
- await PusherService.emitWorksheetComplete(input.workspaceId, artifact);
470
- } catch (error) {
471
- await ctx.db.artifact.delete({
472
- where: { id: artifact.id },
473
- });
474
- await PusherService.emitError(input.workspaceId, `Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`, 'worksheet_generation');
475
- throw error;
476
- }
665
+ await PusherService.emitWorksheetGenerationComplete(workspaceId, updatedWorksheet);
666
+ await PusherService.emitWorksheetComplete(workspaceId, artifact);
667
+
668
+ await notifyArtifactReady(ctx.db, {
669
+ userId,
670
+ workspaceId,
671
+ artifactId: artifact.id,
672
+ artifactType: ArtifactType.WORKSHEET,
673
+ title: updatedWorksheet?.title ?? artifact.title,
674
+ }).catch(() => {});
675
+ } catch (error) {
676
+ logger.error(`Background generation failed for artifact ${artifact.id}:`, error);
677
+ await notifyArtifactFailed(ctx.db, {
678
+ userId,
679
+ workspaceId,
680
+ artifactId: artifact.id,
681
+ artifactType: ArtifactType.WORKSHEET,
682
+ title: artifact.title,
683
+ message: `Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`,
684
+ }).catch(() => {});
685
+
686
+ await ctx.db.artifact.deleteMany({
687
+ where: { id: artifact.id },
688
+ }).catch(() => { }); // Ignore delete errors if already gone
689
+
690
+ await PusherService.emitError(
691
+ workspaceId,
692
+ `Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`,
693
+ 'worksheet_generation'
694
+ );
695
+ }
696
+ })();
477
697
 
478
698
  return { artifact };
479
699
  }),
480
- checkAnswer: authedProcedure
700
+ checkAnswer: authedProcedure
481
701
  .input(z.object({
482
702
  worksheetId: z.string(),
483
703
  questionId: z.string(),
484
704
  answer: z.string().min(1),
485
705
  }))
486
706
  .mutation(async ({ ctx, input }) => {
487
- const worksheet = await ctx.db.artifact.findFirst({ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } }, include: { workspace: true } });
707
+ const worksheet = await ctx.db.artifact.findFirst({ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) }, include: { workspace: true } });
488
708
  if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
489
709
  const question = await ctx.db.worksheetQuestion.findFirst({ where: { id: input.questionId, artifactId: input.worksheetId } });
490
710
  if (!question) throw new TRPCError({ code: 'NOT_FOUND' });
491
-
711
+
492
712
  // Parse question meta to get mark_scheme
493
713
  const questionMeta = question.meta ? (typeof question.meta === 'object' ? question.meta : JSON.parse(question.meta as any)) : {} as any;
494
714
  const markScheme = questionMeta.markScheme;
@@ -506,11 +726,11 @@ export const worksheets = router({
506
726
  input.answer,
507
727
  markScheme
508
728
  );
509
-
729
+
510
730
  // Determine if correct by comparing achieved points vs total points
511
731
  const achievedTotal = userMarkScheme.points.reduce((sum: number, p: any) => sum + (p.achievedPoints || 0), 0);
512
732
  isCorrect = achievedTotal === markScheme.totalPoints;
513
-
733
+
514
734
  } catch (error) {
515
735
  logger.error('Failed to check answer with AI', error instanceof Error ? error.message : 'Unknown error');
516
736
  // Fallback to simple string comparison
@@ -522,7 +742,7 @@ export const worksheets = router({
522
742
  }
523
743
 
524
744
 
525
-
745
+
526
746
  // @todo: figure out this wierd fix
527
747
  const progress = await ctx.db.worksheetQuestionProgress.upsert({
528
748
  where: {
@@ -555,6 +775,6 @@ export const worksheets = router({
555
775
 
556
776
  return { isCorrect, userMarkScheme, progress };
557
777
  }),
558
- });
778
+ });
559
779
 
560
780