@goscribe/server 1.2.0 → 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.
- package/check-difficulty.cjs +14 -0
- package/check-questions.cjs +14 -0
- package/db-summary.cjs +22 -0
- package/mcq-test.cjs +36 -0
- package/package.json +9 -2
- package/prisma/migrations/20260413143206_init/migration.sql +873 -0
- package/prisma/schema.prisma +471 -324
- package/src/context.ts +4 -1
- package/src/lib/activity_human_description.test.ts +28 -0
- package/src/lib/activity_human_description.ts +239 -0
- package/src/lib/activity_log_service.test.ts +37 -0
- package/src/lib/activity_log_service.ts +353 -0
- package/src/lib/ai-session.ts +79 -51
- package/src/lib/email.ts +213 -29
- package/src/lib/env.ts +23 -6
- package/src/lib/inference.ts +2 -2
- package/src/lib/notification-service.test.ts +106 -0
- package/src/lib/notification-service.ts +677 -0
- package/src/lib/prisma.ts +6 -1
- package/src/lib/pusher.ts +86 -2
- package/src/lib/stripe.ts +39 -0
- package/src/lib/subscription_service.ts +722 -0
- package/src/lib/usage_service.ts +74 -0
- package/src/lib/worksheet-generation.test.ts +31 -0
- package/src/lib/worksheet-generation.ts +139 -0
- package/src/routers/_app.ts +9 -0
- package/src/routers/admin.ts +710 -0
- package/src/routers/annotations.ts +41 -0
- package/src/routers/auth.ts +338 -28
- package/src/routers/copilot.ts +719 -0
- package/src/routers/flashcards.ts +201 -68
- package/src/routers/members.ts +280 -80
- package/src/routers/notifications.ts +142 -0
- package/src/routers/payment.ts +448 -0
- package/src/routers/podcast.ts +112 -83
- package/src/routers/studyguide.ts +12 -0
- package/src/routers/worksheets.ts +289 -66
- package/src/routers/workspace.ts +329 -122
- package/src/scripts/purge-deleted-users.ts +167 -0
- package/src/server.ts +137 -11
- package/src/services/flashcard-progress.service.ts +49 -37
- package/src/trpc.ts +184 -5
- package/test-generate.js +30 -0
- package/test-ratio.cjs +9 -0
- package/zod-test.cjs +22 -0
- package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
- package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
- package/prisma/seed.mjs +0 -135
package/src/routers/podcast.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
293
|
+
// Emit podcast generation start notification
|
|
294
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_generation_start', {
|
|
295
|
+
title: input.podcastData.title
|
|
296
|
+
});
|
|
298
297
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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,
|