@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,6 +1,6 @@
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 { v4 as uuidv4 } from 'uuid';
5
5
  import inference from '../lib/inference.js';
6
6
  import { uploadToSupabase, generateSignedUrl, deleteFromSupabase } from '../lib/storage.js';
@@ -9,6 +9,7 @@ import { aiSessionService } from '../lib/ai-session.js';
9
9
  import { ArtifactType } from '../lib/constants.js';
10
10
  import { workspaceAccessFilter } from '../lib/workspace-access.js';
11
11
  import { logger } from '../lib/logger.js';
12
+ import { notifyArtifactFailed, notifyArtifactReady } from '../lib/notification-service.js';
12
13
 
13
14
  // Podcast segment schema
14
15
  const podcastSegmentSchema = z.object({
@@ -72,11 +73,11 @@ export const podcast = router({
72
73
  // Check if workspace exists
73
74
 
74
75
  if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
75
-
76
+
76
77
  const artifacts = await ctx.db.artifact.findMany({
77
- where: {
78
- workspaceId: input.workspaceId,
79
- type: ArtifactType.PODCAST_EPISODE
78
+ where: {
79
+ workspaceId: input.workspaceId,
80
+ type: ArtifactType.PODCAST_EPISODE
80
81
  },
81
82
  include: {
82
83
  versions: {
@@ -89,7 +90,7 @@ export const podcast = router({
89
90
  },
90
91
  orderBy: { updatedAt: 'desc' },
91
92
  });
92
-
93
+
93
94
  logger.debug(`Found ${artifacts.length} podcast artifacts`);
94
95
  artifacts.forEach((artifact, i) => {
95
96
  logger.debug(` Podcast ${i + 1}: "${artifact.title}" - ${artifact.podcastSegments.length} segments`);
@@ -105,7 +106,7 @@ export const podcast = router({
105
106
  if (artifact.imageObjectKey) {
106
107
  objectUrl = await generateSignedUrl(artifact.imageObjectKey, 24);
107
108
  }
108
-
109
+
109
110
  // Generate fresh signed URLs for all segments
110
111
  const segmentsWithUrls = await Promise.all(
111
112
  artifact.podcastSegments.map(async (segment) => {
@@ -150,7 +151,7 @@ export const podcast = router({
150
151
  let metadata = null;
151
152
  if (latestVersion) {
152
153
  try {
153
- logger.debug(latestVersion.data)
154
+ logger.debug(JSON.stringify(latestVersion.data))
154
155
  metadata = podcastMetadataSchema.parse(latestVersion.data);
155
156
  } catch (error) {
156
157
  logger.error('Failed to parse podcast metadata:', error);
@@ -184,7 +185,7 @@ export const podcast = router({
184
185
  .input(z.object({ episodeId: z.string() }))
185
186
  .query(async ({ ctx, input }) => {
186
187
  const episode = await ctx.db.artifact.findFirst({
187
- where: {
188
+ where: {
188
189
  id: input.episodeId,
189
190
  type: ArtifactType.PODCAST_EPISODE,
190
191
  workspace: workspaceAccessFilter(ctx.session.user.id)
@@ -200,14 +201,14 @@ export const podcast = router({
200
201
  },
201
202
  });
202
203
 
203
- logger.debug(episode)
204
+ logger.debug(JSON.stringify(episode))
204
205
 
205
206
  if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
206
-
207
+
207
208
  const latestVersion = episode.versions[0];
208
209
  if (!latestVersion) throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
209
-
210
- logger.debug(latestVersion)
210
+
211
+ logger.debug(JSON.stringify(latestVersion))
211
212
  try {
212
213
  const metadata = podcastMetadataSchema.parse(latestVersion.data);
213
214
  } catch (error) {
@@ -216,7 +217,7 @@ export const podcast = router({
216
217
  const metadata = podcastMetadataSchema.parse(latestVersion.data);
217
218
 
218
219
  const imageUrl = episode.imageObjectKey ? await generateSignedUrl(episode.imageObjectKey, 24) : null;
219
-
220
+
220
221
 
221
222
  // Generate fresh signed URLs for all segments
222
223
  const segmentsWithUrls = await Promise.all(
@@ -263,7 +264,7 @@ export const podcast = router({
263
264
  };
264
265
  })
265
266
  );
266
-
267
+
267
268
  return {
268
269
  id: episode.id,
269
270
  title: metadata.title, // Use title from version metadata
@@ -278,7 +279,7 @@ export const podcast = router({
278
279
  }),
279
280
 
280
281
  // Generate podcast episode from text input
281
- generateEpisode: authedProcedure
282
+ generateEpisode: limitedProcedure
282
283
  .input(z.object({
283
284
  workspaceId: z.string(),
284
285
  podcastData: podcastInputSchema,
@@ -288,33 +289,33 @@ export const podcast = router({
288
289
  where: { id: input.workspaceId, ownerId: ctx.session.user.id },
289
290
  });
290
291
  if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
291
-
292
- // Emit podcast generation start notification
293
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_generation_start', {
294
- title: input.podcastData.title
295
- });
296
292
 
297
- const BEGIN_PODCAST_GENERATION_MESSAGE = 'Structuring podcast contents...';
293
+ // Emit podcast generation start notification
294
+ await PusherService.emitTaskComplete(input.workspaceId, 'podcast_generation_start', {
295
+ title: input.podcastData.title
296
+ });
298
297
 
299
- const newArtifact = await ctx.db.artifact.create({
300
- data: {
301
- title: '----',
302
- type: ArtifactType.PODCAST_EPISODE,
303
- generating: true,
304
- generatingMetadata: {
305
- message: BEGIN_PODCAST_GENERATION_MESSAGE,
306
- },
307
- workspace: {
308
- connect: {
309
- id: input.workspaceId,
310
- }
298
+ const BEGIN_PODCAST_GENERATION_MESSAGE = 'Structuring podcast contents...';
299
+
300
+ const newArtifact = await ctx.db.artifact.create({
301
+ data: {
302
+ title: '----',
303
+ type: ArtifactType.PODCAST_EPISODE,
304
+ generating: true,
305
+ generatingMetadata: {
306
+ message: BEGIN_PODCAST_GENERATION_MESSAGE,
307
+ },
308
+ workspace: {
309
+ connect: {
310
+ id: input.workspaceId,
311
311
  }
312
312
  }
313
- });
313
+ }
314
+ });
314
315
 
315
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
316
- message: BEGIN_PODCAST_GENERATION_MESSAGE,
317
- });
316
+ await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
317
+ message: BEGIN_PODCAST_GENERATION_MESSAGE,
318
+ });
318
319
 
319
320
  try {
320
321
 
@@ -328,9 +329,9 @@ export const podcast = router({
328
329
  );
329
330
 
330
331
  if (!structureResult.success || !structureResult.structure) {
331
- throw new TRPCError({
332
- code: 'INTERNAL_SERVER_ERROR',
333
- message: 'Failed to generate podcast structure'
332
+ throw new TRPCError({
333
+ code: 'INTERNAL_SERVER_ERROR',
334
+ message: 'Failed to generate podcast structure'
334
335
  });
335
336
  }
336
337
 
@@ -362,7 +363,7 @@ export const podcast = router({
362
363
  }
363
364
  });
364
365
 
365
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
366
+ await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
366
367
  message: `Generating podcast image...`,
367
368
  });
368
369
 
@@ -405,7 +406,7 @@ export const podcast = router({
405
406
  }
406
407
  });
407
408
 
408
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
409
+ await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
409
410
  message: `Generating audio for segment ${i + 1} of ${structure.segments.length}...`,
410
411
  });
411
412
 
@@ -445,42 +446,51 @@ export const podcast = router({
445
446
  error: errorMessage,
446
447
  stack: audioError instanceof Error ? audioError.stack : undefined,
447
448
  });
448
-
449
+
449
450
  // Track failed segment
450
451
  failedSegments.push({
451
452
  index: i + 1,
452
453
  title: segment.title || `Segment ${i + 1}`,
453
454
  error: errorMessage,
454
455
  });
455
-
456
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_segment_error', {
456
+
457
+ await PusherService.emitTaskComplete(input.workspaceId, 'podcast_segment_error', {
457
458
  segmentIndex: i + 1,
458
459
  segmentTitle: segment.title || `Segment ${i + 1}`,
459
460
  error: errorMessage,
460
461
  successfulSegments: segments.length,
461
462
  failedSegments: failedSegments.length,
462
463
  });
463
-
464
+
464
465
  // Continue with other segments even if one fails
465
466
  }
466
467
  }
467
-
468
+
468
469
  // Check if any segments were successfully generated
469
470
  if (segments.length === 0) {
470
471
  logger.error('No segments were successfully generated');
471
- await PusherService.emitError(input.workspaceId,
472
- `Failed to generate any segments. ${failedSegments.length} segment(s) failed.`,
472
+ await PusherService.emitError(input.workspaceId,
473
+ `Failed to generate any segments. ${failedSegments.length} segment(s) failed.`,
473
474
  'podcast'
474
475
  );
475
-
476
+
476
477
  // Cleanup the artifact
478
+ await notifyArtifactFailed(ctx.db, {
479
+ userId: ctx.session.user.id,
480
+ workspaceId: input.workspaceId,
481
+ artifactType: ArtifactType.PODCAST_EPISODE,
482
+ artifactId: newArtifact.id,
483
+ title: input.podcastData.title,
484
+ message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`,
485
+ }).catch(() => {});
486
+
477
487
  await ctx.db.artifact.delete({
478
488
  where: { id: newArtifact.id },
479
489
  });
480
-
481
- throw new TRPCError({
482
- code: 'INTERNAL_SERVER_ERROR',
483
- message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`
490
+
491
+ throw new TRPCError({
492
+ code: 'INTERNAL_SERVER_ERROR',
493
+ message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`
484
494
  });
485
495
  }
486
496
 
@@ -496,7 +506,7 @@ export const podcast = router({
496
506
  }
497
507
  });
498
508
 
499
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
509
+ await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
500
510
  message: `Preparing podcast summary...`,
501
511
  });
502
512
 
@@ -523,7 +533,7 @@ export const podcast = router({
523
533
  Podcast Title: ${structure.episodeTitle}
524
534
  Segments: ${JSON.stringify(segments.map(s => ({ title: s.title, keyPoints: s.keyPoints })))}`;
525
535
 
526
- const summaryResponse = await inference(summaryPrompt);
536
+ const summaryResponse = await inference([{ role: "user", content: summaryPrompt }]);
527
537
  const summaryContent: string = summaryResponse.choices[0].message.content || '';
528
538
 
529
539
  let episodeSummary;
@@ -536,7 +546,7 @@ export const podcast = router({
536
546
  episodeSummary = JSON.parse(jsonMatch[0]);
537
547
  } catch (parseError) {
538
548
  logger.error('Failed to parse summary response:', summaryContent);
539
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_summary_error', {
549
+ await PusherService.emitTaskComplete(input.workspaceId, 'podcast_summary_error', {
540
550
  error: 'Failed to parse summary response'
541
551
  });
542
552
  episodeSummary = {
@@ -551,13 +561,13 @@ export const podcast = router({
551
561
  }
552
562
 
553
563
  // Emit summary generation completion notification
554
- await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
564
+ await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
555
565
  message: `Podcast summary generated.`,
556
566
  });
557
567
 
558
568
  // Step 4: Create artifact and initial version
559
569
  const episodeTitle = structure.episodeTitle || input.podcastData.title;
560
-
570
+
561
571
  await ctx.db.artifact.update({
562
572
  where: {
563
573
  id: newArtifact.id,
@@ -570,7 +580,7 @@ export const podcast = router({
570
580
  createdById: ctx.session.user.id,
571
581
  },
572
582
  });
573
-
583
+
574
584
  const createdSegments = await ctx.db.podcastSegment.createMany({
575
585
  data: segments.map(segment => ({
576
586
  artifactId: newArtifact.id,
@@ -587,7 +597,7 @@ export const podcast = router({
587
597
  },
588
598
  })),
589
599
  });
590
-
600
+
591
601
  const metadata = {
592
602
  title: episodeTitle,
593
603
  description: input.podcastData.description,
@@ -617,7 +627,14 @@ export const podcast = router({
617
627
  });
618
628
 
619
629
  // Emit podcast generation completion notification
620
- await PusherService.emitPodcastComplete(input.workspaceId, {});
630
+ await PusherService.emitPodcastComplete(input.workspaceId, newArtifact);
631
+ await notifyArtifactReady(ctx.db, {
632
+ userId: ctx.session.user.id,
633
+ workspaceId: input.workspaceId,
634
+ artifactId: newArtifact.id,
635
+ artifactType: ArtifactType.PODCAST_EPISODE,
636
+ title: metadata.title,
637
+ }).catch(() => {});
621
638
 
622
639
  return {
623
640
  id: newArtifact.id,
@@ -631,15 +648,27 @@ export const podcast = router({
631
648
 
632
649
  logger.error('Error generating podcast episode:', error);
633
650
 
651
+ await notifyArtifactFailed(ctx.db, {
652
+ userId: ctx.session.user.id,
653
+ workspaceId: input.workspaceId,
654
+ artifactType: ArtifactType.PODCAST_EPISODE,
655
+ artifactId: newArtifact.id,
656
+ title: input.podcastData.title,
657
+ message:
658
+ error instanceof Error
659
+ ? error.message
660
+ : 'Podcast generation failed.',
661
+ }).catch(() => {});
662
+
634
663
  await ctx.db.artifact.delete({
635
664
  where: {
636
665
  id: newArtifact.id,
637
666
  },
638
667
  });
639
668
  await PusherService.emitError(input.workspaceId, `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`, 'podcast');
640
- throw new TRPCError({
641
- code: 'INTERNAL_SERVER_ERROR',
642
- message: `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`
669
+ throw new TRPCError({
670
+ code: 'INTERNAL_SERVER_ERROR',
671
+ message: `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`
643
672
  });
644
673
  }
645
674
  }),
@@ -656,7 +685,7 @@ export const podcast = router({
656
685
  .input(z.object({ episodeId: z.string() }))
657
686
  .query(async ({ ctx, input }) => {
658
687
  const episode = await ctx.db.artifact.findFirst({
659
- where: {
688
+ where: {
660
689
  id: input.episodeId,
661
690
  type: ArtifactType.PODCAST_EPISODE,
662
691
  workspace: workspaceAccessFilter(ctx.session.user.id)
@@ -671,14 +700,14 @@ export const podcast = router({
671
700
  },
672
701
  },
673
702
  });
674
-
703
+
675
704
  if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
676
-
705
+
677
706
  const latestVersion = episode.versions[0];
678
707
  if (!latestVersion) throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
679
708
 
680
709
  const metadata = podcastMetadataSchema.parse(latestVersion.data);
681
-
710
+
682
711
  return {
683
712
  segments: episode.podcastSegments.map(s => ({
684
713
  id: s.id,
@@ -707,7 +736,7 @@ export const podcast = router({
707
736
  }))
708
737
  .mutation(async ({ ctx, input }) => {
709
738
  const episode = await ctx.db.artifact.findFirst({
710
- where: {
739
+ where: {
711
740
  id: input.episodeId,
712
741
  type: ArtifactType.PODCAST_EPISODE,
713
742
  workspace: workspaceAccessFilter(ctx.session.user.id)
@@ -719,14 +748,14 @@ export const podcast = router({
719
748
  },
720
749
  },
721
750
  });
722
-
751
+
723
752
  if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
724
-
753
+
725
754
  const latestVersion = episode.versions[0];
726
755
  if (!latestVersion) throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
727
756
 
728
757
  const metadata = podcastMetadataSchema.parse(latestVersion.data);
729
-
758
+
730
759
  // Update metadata
731
760
  if (input.title) metadata.title = input.title;
732
761
  if (input.description) metadata.description = input.description;
@@ -742,7 +771,7 @@ export const podcast = router({
742
771
  createdById: ctx.session.user.id,
743
772
  },
744
773
  });
745
-
774
+
746
775
  // Update the artifact with basic info for listing/searching
747
776
  return ctx.db.artifact.update({
748
777
  where: { id: input.episodeId },
@@ -759,7 +788,7 @@ export const podcast = router({
759
788
  .input(z.object({ episodeId: z.string() }))
760
789
  .mutation(async ({ ctx, input }) => {
761
790
  const episode = await ctx.db.artifact.findFirst({
762
- where: {
791
+ where: {
763
792
  id: input.episodeId,
764
793
  type: ArtifactType.PODCAST_EPISODE,
765
794
  workspace: workspaceAccessFilter(ctx.session.user.id)
@@ -771,12 +800,12 @@ export const podcast = router({
771
800
  },
772
801
  },
773
802
  });
774
-
803
+
775
804
  if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
776
805
 
777
806
  try {
778
807
  // Emit episode deletion start notification
779
- await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_start', {
808
+ await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_start', {
780
809
  episodeId: input.episodeId,
781
810
  episodeTitle: episode.title || 'Untitled Episode'
782
811
  });
@@ -813,7 +842,7 @@ export const podcast = router({
813
842
  });
814
843
 
815
844
  // Emit episode deletion completion notification
816
- await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_complete', {
845
+ await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_complete', {
817
846
  episodeId: input.episodeId,
818
847
  episodeTitle: episode.title || 'Untitled Episode'
819
848
  });
@@ -823,9 +852,9 @@ export const podcast = router({
823
852
  } catch (error) {
824
853
  logger.error('Error deleting episode:', error);
825
854
  await PusherService.emitError(episode.workspaceId, `Failed to delete episode: ${error instanceof Error ? error.message : 'Unknown error'}`, 'podcast');
826
- throw new TRPCError({
827
- code: 'INTERNAL_SERVER_ERROR',
828
- message: 'Failed to delete episode'
855
+ throw new TRPCError({
856
+ code: 'INTERNAL_SERVER_ERROR',
857
+ message: 'Failed to delete episode'
829
858
  });
830
859
  }
831
860
  }),
@@ -835,7 +864,7 @@ export const podcast = router({
835
864
  .input(z.object({ segmentId: z.string() }))
836
865
  .query(async ({ ctx, input }) => {
837
866
  const segment = await ctx.db.podcastSegment.findFirst({
838
- where: {
867
+ where: {
839
868
  id: input.segmentId,
840
869
  artifact: {
841
870
  workspace: workspaceAccessFilter(ctx.session.user.id)
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import { router, authedProcedure } from '../trpc.js';
4
4
  import { ArtifactType } from '../lib/constants.js';
5
+ import { getUserUsage, getUserPlanLimits } from '../lib/usage_service.js';
5
6
  import { workspaceAccessFilter } from '../lib/workspace-access.js';
6
7
 
7
8
  const initializeEditorJsEmptyBlock = () => ({
@@ -38,6 +39,17 @@ export const studyguide = router({
38
39
  });
39
40
 
40
41
  if (!artifact) {
42
+ const [usage, limits] = await Promise.all([
43
+ getUserUsage(ctx.session.user.id),
44
+ getUserPlanLimits(ctx.session.user.id)
45
+ ]);
46
+ if (limits && usage.studyGuides >= limits.maxStudyGuides) {
47
+ throw new TRPCError({
48
+ code: 'FORBIDDEN',
49
+ message: 'Study Guide limit reached. Please upgrade your plan to create more workspaces with study guides.'
50
+ });
51
+ }
52
+
41
53
  artifact = await ctx.db.artifact.create({
42
54
  data: {
43
55
  workspaceId: input.workspaceId,