@goscribe/server 1.1.6 → 1.2.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/.env.example +43 -0
- package/dist/routers/_app.d.ts +1 -1
- package/dist/routers/auth.js +9 -3
- package/dist/routers/workspace.d.ts +1 -1
- package/dist/routers/workspace.js +1 -1
- package/package.json +2 -1
- package/prisma/schema.prisma +46 -0
- package/src/lib/ai-session.ts +117 -63
- package/src/lib/constants.ts +14 -0
- package/src/lib/email.ts +46 -0
- package/src/lib/inference.ts +1 -1
- package/src/lib/logger.ts +26 -9
- package/src/lib/pusher.ts +4 -4
- package/src/lib/retry.ts +61 -0
- package/src/lib/storage.ts +2 -2
- package/src/lib/workspace-access.ts +13 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/annotations.ts +186 -0
- package/src/routers/auth.ts +98 -9
- package/src/routers/flashcards.ts +7 -13
- package/src/routers/podcast.ts +24 -28
- package/src/routers/studyguide.ts +68 -74
- package/src/routers/worksheets.ts +11 -14
- package/src/routers/workspace.ts +275 -272
- package/src/server.ts +4 -2
- package/src/services/flashcard-progress.service.ts +3 -6
- package/src/routers/meetingsummary.ts +0 -416
package/src/routers/workspace.ts
CHANGED
|
@@ -22,6 +22,65 @@ async function updateAnalysisProgress(
|
|
|
22
22
|
await PusherService.emitAnalysisProgress(workspaceId, progress);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// DRY helper to build progress steps for artifact generation pipeline
|
|
26
|
+
type StepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'error';
|
|
27
|
+
const PIPELINE_STEPS = ['fileUpload', 'fileAnalysis', 'studyGuide', 'flashcards'] as const;
|
|
28
|
+
|
|
29
|
+
function buildProgressSteps(
|
|
30
|
+
currentStep: typeof PIPELINE_STEPS[number],
|
|
31
|
+
currentStatus: StepStatus,
|
|
32
|
+
config: { generateStudyGuide: boolean; generateFlashcards: boolean },
|
|
33
|
+
overrides?: Partial<Record<typeof PIPELINE_STEPS[number], StepStatus>>
|
|
34
|
+
): Record<string, { order: number; status: StepStatus }> {
|
|
35
|
+
const stepIndex = PIPELINE_STEPS.indexOf(currentStep);
|
|
36
|
+
const steps: Record<string, { order: number; status: StepStatus }> = {};
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < PIPELINE_STEPS.length; i++) {
|
|
39
|
+
const step = PIPELINE_STEPS[i];
|
|
40
|
+
let status: StepStatus;
|
|
41
|
+
|
|
42
|
+
if (overrides?.[step]) {
|
|
43
|
+
status = overrides[step]!;
|
|
44
|
+
} else if (i < stepIndex) {
|
|
45
|
+
status = 'completed';
|
|
46
|
+
} else if (i === stepIndex) {
|
|
47
|
+
status = currentStatus;
|
|
48
|
+
} else {
|
|
49
|
+
// Future steps: check if they're configured
|
|
50
|
+
if (step === 'studyGuide' && !config.generateStudyGuide) {
|
|
51
|
+
status = 'skipped';
|
|
52
|
+
} else if (step === 'flashcards' && !config.generateFlashcards) {
|
|
53
|
+
status = 'skipped';
|
|
54
|
+
} else {
|
|
55
|
+
status = 'pending';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
steps[step] = { order: i + 1, status };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return steps;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildProgress(
|
|
66
|
+
status: string,
|
|
67
|
+
filename: string,
|
|
68
|
+
fileType: string,
|
|
69
|
+
currentStep: typeof PIPELINE_STEPS[number],
|
|
70
|
+
currentStepStatus: StepStatus,
|
|
71
|
+
config: { generateStudyGuide: boolean; generateFlashcards: boolean },
|
|
72
|
+
extra?: Record<string, any>
|
|
73
|
+
) {
|
|
74
|
+
return {
|
|
75
|
+
status,
|
|
76
|
+
filename,
|
|
77
|
+
fileType,
|
|
78
|
+
startedAt: new Date().toISOString(),
|
|
79
|
+
steps: buildProgressSteps(currentStep, currentStepStatus, config, extra as any),
|
|
80
|
+
...extra,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
25
84
|
// Helper function to calculate search relevance score
|
|
26
85
|
function calculateRelevance(query: string, ...texts: (string | null | undefined)[]): number {
|
|
27
86
|
const queryLower = query.toLowerCase();
|
|
@@ -194,6 +253,112 @@ export const workspace = router({
|
|
|
194
253
|
spaceTotal: 1000000000,
|
|
195
254
|
};
|
|
196
255
|
}),
|
|
256
|
+
|
|
257
|
+
// Study analytics: streaks, flashcard mastery, worksheet accuracy
|
|
258
|
+
getStudyAnalytics: authedProcedure
|
|
259
|
+
.query(async ({ ctx }) => {
|
|
260
|
+
const userId = ctx.session.user.id;
|
|
261
|
+
|
|
262
|
+
// Gather all study activity dates
|
|
263
|
+
const flashcardProgress = await ctx.db.flashcardProgress.findMany({
|
|
264
|
+
where: { userId },
|
|
265
|
+
select: { lastStudiedAt: true },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const worksheetProgress = await ctx.db.worksheetQuestionProgress.findMany({
|
|
269
|
+
where: { userId },
|
|
270
|
+
select: { updatedAt: true, completedAt: true },
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Build a set of unique study days (YYYY-MM-DD)
|
|
274
|
+
const studyDays = new Set<string>();
|
|
275
|
+
for (const fp of flashcardProgress) {
|
|
276
|
+
if (fp.lastStudiedAt) {
|
|
277
|
+
studyDays.add(fp.lastStudiedAt.toISOString().split('T')[0]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const wp of worksheetProgress) {
|
|
281
|
+
if (wp.completedAt) {
|
|
282
|
+
studyDays.add(wp.completedAt.toISOString().split('T')[0]);
|
|
283
|
+
} else {
|
|
284
|
+
studyDays.add(wp.updatedAt.toISOString().split('T')[0]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Calculate streak (consecutive days ending today or yesterday)
|
|
289
|
+
const sortedDays = [...studyDays].sort().reverse();
|
|
290
|
+
let streak = 0;
|
|
291
|
+
|
|
292
|
+
if (sortedDays.length > 0) {
|
|
293
|
+
const today = new Date();
|
|
294
|
+
today.setHours(0, 0, 0, 0);
|
|
295
|
+
const yesterday = new Date(today);
|
|
296
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
297
|
+
|
|
298
|
+
const todayStr = today.toISOString().split('T')[0];
|
|
299
|
+
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
|
300
|
+
|
|
301
|
+
// Streak only counts if the most recent study day is today or yesterday
|
|
302
|
+
if (sortedDays[0] === todayStr || sortedDays[0] === yesterdayStr) {
|
|
303
|
+
streak = 1;
|
|
304
|
+
for (let i = 1; i < sortedDays.length; i++) {
|
|
305
|
+
const current = new Date(sortedDays[i - 1]);
|
|
306
|
+
const prev = new Date(sortedDays[i]);
|
|
307
|
+
const diffDays = (current.getTime() - prev.getTime()) / (1000 * 60 * 60 * 24);
|
|
308
|
+
if (diffDays === 1) {
|
|
309
|
+
streak++;
|
|
310
|
+
} else {
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Weekly activity (last 7 days)
|
|
318
|
+
const weeklyActivity: boolean[] = [];
|
|
319
|
+
const today = new Date();
|
|
320
|
+
today.setHours(0, 0, 0, 0);
|
|
321
|
+
for (let i = 6; i >= 0; i--) {
|
|
322
|
+
const d = new Date(today);
|
|
323
|
+
d.setDate(d.getDate() - i);
|
|
324
|
+
const dayStr = d.toISOString().split('T')[0];
|
|
325
|
+
weeklyActivity.push(studyDays.has(dayStr));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Flashcard stats
|
|
329
|
+
const totalCards = await ctx.db.flashcardProgress.count({ where: { userId } });
|
|
330
|
+
const masteredCards = await ctx.db.flashcardProgress.count({
|
|
331
|
+
where: { userId, masteryLevel: { gte: 80 } },
|
|
332
|
+
});
|
|
333
|
+
const dueCards = await ctx.db.flashcardProgress.count({
|
|
334
|
+
where: { userId, nextReviewAt: { lte: new Date() } },
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Worksheet stats
|
|
338
|
+
const completedQuestions = await ctx.db.worksheetQuestionProgress.count({
|
|
339
|
+
where: { userId, completedAt: { not: null } },
|
|
340
|
+
});
|
|
341
|
+
const correctQuestions = await ctx.db.worksheetQuestionProgress.count({
|
|
342
|
+
where: { userId, correct: true },
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
streak,
|
|
347
|
+
totalStudyDays: studyDays.size,
|
|
348
|
+
weeklyActivity,
|
|
349
|
+
flashcards: {
|
|
350
|
+
total: totalCards,
|
|
351
|
+
mastered: masteredCards,
|
|
352
|
+
dueForReview: dueCards,
|
|
353
|
+
},
|
|
354
|
+
worksheets: {
|
|
355
|
+
completed: completedQuestions,
|
|
356
|
+
correct: correctQuestions,
|
|
357
|
+
accuracy: completedQuestions > 0 ? Math.round((correctQuestions / completedQuestions) * 100) : 0,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}),
|
|
361
|
+
|
|
197
362
|
update: authedProcedure
|
|
198
363
|
.input(z.object({
|
|
199
364
|
id: z.string(),
|
|
@@ -304,7 +469,7 @@ export const workspace = router({
|
|
|
304
469
|
if (signedUrlError) {
|
|
305
470
|
throw new TRPCError({
|
|
306
471
|
code: 'INTERNAL_SERVER_ERROR',
|
|
307
|
-
message: `Failed to
|
|
472
|
+
message: `Failed to upload file`
|
|
308
473
|
});
|
|
309
474
|
}
|
|
310
475
|
|
|
@@ -347,7 +512,7 @@ export const workspace = router({
|
|
|
347
512
|
.from(file.bucket)
|
|
348
513
|
.remove([file.objectKey])
|
|
349
514
|
.catch((err: unknown) => {
|
|
350
|
-
|
|
515
|
+
logger.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
|
|
351
516
|
});
|
|
352
517
|
}
|
|
353
518
|
}
|
|
@@ -385,7 +550,7 @@ export const workspace = router({
|
|
|
385
550
|
.from('media')
|
|
386
551
|
.createSignedUploadUrl(objectKey); // 5 minutes
|
|
387
552
|
if (signedUrlError) {
|
|
388
|
-
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to
|
|
553
|
+
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to upload file` });
|
|
389
554
|
}
|
|
390
555
|
|
|
391
556
|
await ctx.db.workspace.update({
|
|
@@ -414,7 +579,7 @@ export const workspace = router({
|
|
|
414
579
|
where: { id: input.workspaceId, ownerId: ctx.session.user.id }
|
|
415
580
|
});
|
|
416
581
|
if (!workspace) {
|
|
417
|
-
|
|
582
|
+
logger.error('Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
|
|
418
583
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
419
584
|
}
|
|
420
585
|
|
|
@@ -462,43 +627,18 @@ export const workspace = router({
|
|
|
462
627
|
data: { fileBeingAnalyzed: true },
|
|
463
628
|
});
|
|
464
629
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
fileType,
|
|
469
|
-
|
|
470
|
-
steps: {
|
|
471
|
-
fileUpload: { order: 1, status: 'pending' },
|
|
472
|
-
},
|
|
473
|
-
});
|
|
630
|
+
const genConfig = { generateStudyGuide: input.generateStudyGuide, generateFlashcards: input.generateFlashcards };
|
|
631
|
+
|
|
632
|
+
PusherService.emitAnalysisProgress(input.workspaceId,
|
|
633
|
+
buildProgress('starting', primaryFile.name, fileType, 'fileUpload', 'pending', genConfig)
|
|
634
|
+
);
|
|
474
635
|
|
|
475
636
|
try {
|
|
476
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
fileType,
|
|
480
|
-
startedAt: new Date().toISOString(),
|
|
481
|
-
steps: {
|
|
482
|
-
fileUpload: {
|
|
483
|
-
order: 1,
|
|
484
|
-
status: 'pending',
|
|
485
|
-
},
|
|
486
|
-
fileAnalysis: {
|
|
487
|
-
order: 2,
|
|
488
|
-
status: 'pending',
|
|
489
|
-
},
|
|
490
|
-
studyGuide: {
|
|
491
|
-
order: 3,
|
|
492
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
493
|
-
},
|
|
494
|
-
flashcards: {
|
|
495
|
-
order: 4,
|
|
496
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
497
|
-
},
|
|
498
|
-
}
|
|
499
|
-
});
|
|
637
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
638
|
+
buildProgress('starting', primaryFile.name, fileType, 'fileUpload', 'pending', genConfig)
|
|
639
|
+
);
|
|
500
640
|
} catch (error) {
|
|
501
|
-
|
|
641
|
+
logger.error('Failed to update analysis progress:', error);
|
|
502
642
|
await ctx.db.workspace.update({
|
|
503
643
|
where: { id: input.workspaceId },
|
|
504
644
|
data: { fileBeingAnalyzed: false },
|
|
@@ -507,30 +647,9 @@ export const workspace = router({
|
|
|
507
647
|
throw error;
|
|
508
648
|
}
|
|
509
649
|
|
|
510
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
fileType,
|
|
514
|
-
startedAt: new Date().toISOString(),
|
|
515
|
-
steps: {
|
|
516
|
-
fileUpload: {
|
|
517
|
-
order: 1,
|
|
518
|
-
status: 'in_progress',
|
|
519
|
-
},
|
|
520
|
-
fileAnalysis: {
|
|
521
|
-
order: 2,
|
|
522
|
-
status: 'pending',
|
|
523
|
-
},
|
|
524
|
-
studyGuide: {
|
|
525
|
-
order: 3,
|
|
526
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
527
|
-
},
|
|
528
|
-
flashcards: {
|
|
529
|
-
order: 4,
|
|
530
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
531
|
-
},
|
|
532
|
-
}
|
|
533
|
-
});
|
|
650
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
651
|
+
buildProgress('uploading', primaryFile.name, fileType, 'fileUpload', 'in_progress', genConfig)
|
|
652
|
+
);
|
|
534
653
|
|
|
535
654
|
// Process all files using the new process_file endpoint
|
|
536
655
|
for (const file of files) {
|
|
@@ -550,7 +669,7 @@ export const workspace = router({
|
|
|
550
669
|
});
|
|
551
670
|
throw new TRPCError({
|
|
552
671
|
code: 'INTERNAL_SERVER_ERROR',
|
|
553
|
-
message: `Failed to
|
|
672
|
+
message: `Failed to upload file`
|
|
554
673
|
});
|
|
555
674
|
}
|
|
556
675
|
|
|
@@ -589,30 +708,9 @@ export const workspace = router({
|
|
|
589
708
|
}
|
|
590
709
|
}
|
|
591
710
|
|
|
592
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
fileType,
|
|
596
|
-
startedAt: new Date().toISOString(),
|
|
597
|
-
steps: {
|
|
598
|
-
fileUpload: {
|
|
599
|
-
order: 1,
|
|
600
|
-
status: 'completed',
|
|
601
|
-
},
|
|
602
|
-
fileAnalysis: {
|
|
603
|
-
order: 2,
|
|
604
|
-
status: 'in_progress',
|
|
605
|
-
},
|
|
606
|
-
studyGuide: {
|
|
607
|
-
order: 3,
|
|
608
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
609
|
-
},
|
|
610
|
-
flashcards: {
|
|
611
|
-
order: 4,
|
|
612
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
613
|
-
},
|
|
614
|
-
}
|
|
615
|
-
});
|
|
711
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
712
|
+
buildProgress('analyzing', primaryFile.name, fileType, 'fileAnalysis', 'in_progress', genConfig)
|
|
713
|
+
);
|
|
616
714
|
|
|
617
715
|
try {
|
|
618
716
|
// Analyze all files - use PDF analysis if any file is a PDF, otherwise use image analysis
|
|
@@ -626,57 +724,17 @@ export const workspace = router({
|
|
|
626
724
|
// }
|
|
627
725
|
// }
|
|
628
726
|
|
|
629
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
fileType,
|
|
633
|
-
startedAt: new Date().toISOString(),
|
|
634
|
-
steps: {
|
|
635
|
-
fileUpload: {
|
|
636
|
-
order: 1,
|
|
637
|
-
status: 'completed',
|
|
638
|
-
},
|
|
639
|
-
fileAnalysis: {
|
|
640
|
-
order: 2,
|
|
641
|
-
status: 'completed',
|
|
642
|
-
},
|
|
643
|
-
studyGuide: {
|
|
644
|
-
order: 3,
|
|
645
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
646
|
-
},
|
|
647
|
-
flashcards: {
|
|
648
|
-
order: 4,
|
|
649
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
650
|
-
},
|
|
651
|
-
}
|
|
652
|
-
});
|
|
727
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
728
|
+
buildProgress('generating_artifacts', primaryFile.name, fileType, 'studyGuide', 'pending', genConfig)
|
|
729
|
+
);
|
|
653
730
|
} catch (error) {
|
|
654
|
-
|
|
655
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
steps: {
|
|
662
|
-
fileUpload: {
|
|
663
|
-
order: 1,
|
|
664
|
-
status: 'completed',
|
|
665
|
-
},
|
|
666
|
-
fileAnalysis: {
|
|
667
|
-
order: 2,
|
|
668
|
-
status: 'error',
|
|
669
|
-
},
|
|
670
|
-
studyGuide: {
|
|
671
|
-
order: 3,
|
|
672
|
-
status: 'skipped',
|
|
673
|
-
},
|
|
674
|
-
flashcards: {
|
|
675
|
-
order: 4,
|
|
676
|
-
status: 'skipped',
|
|
677
|
-
},
|
|
678
|
-
}
|
|
679
|
-
});
|
|
731
|
+
logger.error('Failed to analyze files:', error);
|
|
732
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
733
|
+
buildProgress('error', primaryFile.name, fileType, 'fileAnalysis', 'error', genConfig, {
|
|
734
|
+
error: `Failed to analyze ${fileType}: ${error}`,
|
|
735
|
+
studyGuide: 'skipped', flashcards: 'skipped',
|
|
736
|
+
})
|
|
737
|
+
);
|
|
680
738
|
await ctx.db.workspace.update({
|
|
681
739
|
where: { id: input.workspaceId },
|
|
682
740
|
data: { fileBeingAnalyzed: false },
|
|
@@ -700,142 +758,109 @@ export const workspace = router({
|
|
|
700
758
|
}
|
|
701
759
|
};
|
|
702
760
|
|
|
703
|
-
// Generate artifacts
|
|
761
|
+
// Generate artifacts - each step is isolated so failures don't block subsequent steps
|
|
704
762
|
if (input.generateStudyGuide) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
startedAt: new Date().toISOString(),
|
|
710
|
-
steps: {
|
|
711
|
-
fileUpload: {
|
|
712
|
-
order: 1,
|
|
713
|
-
status: 'completed',
|
|
714
|
-
},
|
|
715
|
-
fileAnalysis: {
|
|
716
|
-
order: 2,
|
|
717
|
-
status: 'completed',
|
|
718
|
-
},
|
|
719
|
-
studyGuide: {
|
|
720
|
-
order: 3,
|
|
721
|
-
status: 'in_progress',
|
|
722
|
-
},
|
|
723
|
-
flashcards: {
|
|
724
|
-
order: 4,
|
|
725
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
726
|
-
},
|
|
727
|
-
}
|
|
728
|
-
});
|
|
763
|
+
try {
|
|
764
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
765
|
+
buildProgress('generating_study_guide', primaryFile.name, fileType, 'studyGuide', 'in_progress', genConfig)
|
|
766
|
+
);
|
|
729
767
|
|
|
730
|
-
|
|
768
|
+
const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
|
|
731
769
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
});
|
|
735
|
-
if (!artifact) {
|
|
736
|
-
const fileNames = files.map(f => f.name).join(', ');
|
|
737
|
-
artifact = await ctx.db.artifact.create({
|
|
738
|
-
data: {
|
|
739
|
-
workspaceId: input.workspaceId,
|
|
740
|
-
type: ArtifactType.STUDY_GUIDE,
|
|
741
|
-
title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
|
|
742
|
-
createdById: ctx.session.user.id,
|
|
743
|
-
},
|
|
770
|
+
let artifact = await ctx.db.artifact.findFirst({
|
|
771
|
+
where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
|
|
744
772
|
});
|
|
745
|
-
|
|
773
|
+
if (!artifact) {
|
|
774
|
+
artifact = await ctx.db.artifact.create({
|
|
775
|
+
data: {
|
|
776
|
+
workspaceId: input.workspaceId,
|
|
777
|
+
type: ArtifactType.STUDY_GUIDE,
|
|
778
|
+
title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
|
|
779
|
+
createdById: ctx.session.user.id,
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
}
|
|
746
783
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
784
|
+
const lastVersion = await ctx.db.artifactVersion.findFirst({
|
|
785
|
+
where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
|
|
786
|
+
orderBy: { version: 'desc' },
|
|
787
|
+
});
|
|
751
788
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
789
|
+
await ctx.db.artifactVersion.create({
|
|
790
|
+
data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
|
|
791
|
+
});
|
|
755
792
|
|
|
756
|
-
|
|
793
|
+
results.artifacts.studyGuide = artifact;
|
|
794
|
+
} catch (sgError) {
|
|
795
|
+
logger.error('Study guide generation failed after retries:', sgError);
|
|
796
|
+
await PusherService.emitError(input.workspaceId, 'Study guide generation failed. Please try regenerating later.', 'study_guide');
|
|
797
|
+
// Continue to flashcards - don't abort the whole pipeline
|
|
798
|
+
}
|
|
757
799
|
}
|
|
758
800
|
|
|
759
801
|
if (input.generateFlashcards) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
fileUpload: {
|
|
767
|
-
order: 1,
|
|
768
|
-
status: 'completed',
|
|
769
|
-
},
|
|
770
|
-
fileAnalysis: {
|
|
771
|
-
order: 2,
|
|
772
|
-
status: 'completed',
|
|
773
|
-
},
|
|
774
|
-
studyGuide: {
|
|
775
|
-
order: 3,
|
|
776
|
-
status: input.generateStudyGuide ? 'completed' : 'skipped',
|
|
777
|
-
},
|
|
778
|
-
flashcards: {
|
|
779
|
-
order: 4,
|
|
780
|
-
status: 'in_progress',
|
|
781
|
-
},
|
|
782
|
-
}
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
|
|
802
|
+
try {
|
|
803
|
+
const sgStatus = input.generateStudyGuide ? (results.artifacts.studyGuide ? 'completed' : 'error') : 'skipped';
|
|
804
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
805
|
+
buildProgress('generating_flashcards', primaryFile.name, fileType, 'flashcards', 'in_progress', genConfig,
|
|
806
|
+
{ studyGuide: sgStatus } as any)
|
|
807
|
+
);
|
|
786
808
|
|
|
787
|
-
|
|
788
|
-
data: {
|
|
789
|
-
workspaceId: input.workspaceId,
|
|
790
|
-
type: ArtifactType.FLASHCARD_SET,
|
|
791
|
-
title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
|
|
792
|
-
createdById: ctx.session.user.id,
|
|
793
|
-
},
|
|
794
|
-
});
|
|
809
|
+
const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
|
|
795
810
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
811
|
+
const artifact = await ctx.db.artifact.create({
|
|
812
|
+
data: {
|
|
813
|
+
workspaceId: input.workspaceId,
|
|
814
|
+
type: ArtifactType.FLASHCARD_SET,
|
|
815
|
+
title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
|
|
816
|
+
createdById: ctx.session.user.id,
|
|
817
|
+
},
|
|
818
|
+
});
|
|
799
819
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const
|
|
803
|
-
const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
|
|
804
|
-
const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
|
|
820
|
+
// Parse JSON flashcard content
|
|
821
|
+
try {
|
|
822
|
+
const flashcardData: any = content;
|
|
805
823
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
back: back,
|
|
811
|
-
order: i,
|
|
812
|
-
tags: ['ai-generated', 'medium'],
|
|
813
|
-
},
|
|
814
|
-
});
|
|
815
|
-
createdCards++;
|
|
816
|
-
}
|
|
824
|
+
for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
|
|
825
|
+
const card = flashcardData[i];
|
|
826
|
+
const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
|
|
827
|
+
const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
|
|
817
828
|
|
|
818
|
-
} catch (parseError) {
|
|
819
|
-
// Fallback to text parsing if JSON fails
|
|
820
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
821
|
-
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
822
|
-
const line = lines[i];
|
|
823
|
-
if (line.includes(' - ')) {
|
|
824
|
-
const [front, back] = line.split(' - ');
|
|
825
829
|
await ctx.db.flashcard.create({
|
|
826
830
|
data: {
|
|
827
831
|
artifactId: artifact.id,
|
|
828
|
-
front: front
|
|
829
|
-
back: back
|
|
832
|
+
front: front,
|
|
833
|
+
back: back,
|
|
830
834
|
order: i,
|
|
831
835
|
tags: ['ai-generated', 'medium'],
|
|
832
836
|
},
|
|
833
837
|
});
|
|
834
838
|
}
|
|
839
|
+
} catch (parseError) {
|
|
840
|
+
// Fallback to text parsing if JSON fails
|
|
841
|
+
const lines = content.split('\n').filter((line: string) => line.trim());
|
|
842
|
+
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
843
|
+
const line = lines[i];
|
|
844
|
+
if (line.includes(' - ')) {
|
|
845
|
+
const [front, back] = line.split(' - ');
|
|
846
|
+
await ctx.db.flashcard.create({
|
|
847
|
+
data: {
|
|
848
|
+
artifactId: artifact.id,
|
|
849
|
+
front: front.trim(),
|
|
850
|
+
back: back.trim(),
|
|
851
|
+
order: i,
|
|
852
|
+
tags: ['ai-generated', 'medium'],
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
835
857
|
}
|
|
836
|
-
}
|
|
837
858
|
|
|
838
|
-
|
|
859
|
+
results.artifacts.flashcards = artifact;
|
|
860
|
+
} catch (fcError) {
|
|
861
|
+
logger.error('Flashcard generation failed after retries:', fcError);
|
|
862
|
+
await PusherService.emitError(input.workspaceId, 'Flashcard generation failed. Please try regenerating later.', 'flashcards');
|
|
863
|
+
}
|
|
839
864
|
}
|
|
840
865
|
|
|
841
866
|
await ctx.db.workspace.update({
|
|
@@ -844,34 +869,12 @@ export const workspace = router({
|
|
|
844
869
|
});
|
|
845
870
|
|
|
846
871
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
847
|
-
|
|
848
|
-
filename: primaryFile.name,
|
|
849
|
-
fileType,
|
|
850
|
-
startedAt: new Date().toISOString(),
|
|
872
|
+
...buildProgress('completed', primaryFile.name, fileType, 'flashcards', 'completed', genConfig),
|
|
851
873
|
completedAt: new Date().toISOString(),
|
|
852
|
-
steps: {
|
|
853
|
-
fileUpload: {
|
|
854
|
-
order: 1,
|
|
855
|
-
status: 'completed',
|
|
856
|
-
},
|
|
857
|
-
fileAnalysis: {
|
|
858
|
-
order: 2,
|
|
859
|
-
status: 'completed',
|
|
860
|
-
},
|
|
861
|
-
studyGuide: {
|
|
862
|
-
order: 3,
|
|
863
|
-
status: input.generateStudyGuide ? 'completed' : 'skipped',
|
|
864
|
-
},
|
|
865
|
-
flashcards: {
|
|
866
|
-
order: 4,
|
|
867
|
-
status: input.generateFlashcards ? 'completed' : 'skipped',
|
|
868
|
-
},
|
|
869
|
-
}
|
|
870
|
-
|
|
871
874
|
});
|
|
872
875
|
return results;
|
|
873
876
|
} catch (error) {
|
|
874
|
-
|
|
877
|
+
logger.error('Failed to update analysis progress:', error);
|
|
875
878
|
await ctx.db.workspace.update({
|
|
876
879
|
where: { id: input.workspaceId },
|
|
877
880
|
data: { fileBeingAnalyzed: false },
|
package/src/server.ts
CHANGED
|
@@ -18,15 +18,17 @@ async function main() {
|
|
|
18
18
|
const app = express();
|
|
19
19
|
|
|
20
20
|
// Middlewares
|
|
21
|
-
app.use(helmet());
|
|
22
21
|
app.use(cors({
|
|
23
|
-
origin:
|
|
22
|
+
origin: ['https://www.scribe.study', 'https://scribe.study', 'http://localhost:3000'],
|
|
24
23
|
credentials: true, // allow cookies
|
|
25
24
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
26
25
|
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'Set-Cookie'],
|
|
27
26
|
exposedHeaders: ['Set-Cookie'],
|
|
27
|
+
preflightContinue: false, // Important: stop further handling of OPTIONS
|
|
28
28
|
}));
|
|
29
29
|
|
|
30
|
+
app.use(helmet());
|
|
31
|
+
|
|
30
32
|
// Custom morgan middleware with logger integration
|
|
31
33
|
app.use(morgan('combined', {
|
|
32
34
|
stream: {
|