@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.
@@ -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 generate upload URL: ${signedUrlError.message}`
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
- console.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
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 generate upload URL: ${signedUrlError.message}` });
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
- console.error('Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
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
- PusherService.emitAnalysisProgress(input.workspaceId, {
466
- status: 'starting',
467
- filename: primaryFile.name,
468
- fileType,
469
- startedAt: new Date().toISOString(),
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
- status: 'starting',
478
- filename: primaryFile.name,
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
- console.error('Failed to update analysis progress:', error);
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
- status: 'uploading',
512
- filename: primaryFile.name,
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 generate signed URL for file ${file.name}: ${signedUrlError.message}`
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
- status: 'analyzing',
594
- filename: primaryFile.name,
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
- status: 'generating_artifacts',
631
- filename: primaryFile.name,
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
- console.error('Failed to analyze files:', error);
655
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
656
- status: 'error',
657
- filename: primaryFile.name,
658
- fileType,
659
- error: `Failed to analyze ${fileType}: ${error}`,
660
- startedAt: new Date().toISOString(),
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
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
706
- status: 'generating_study_guide',
707
- filename: primaryFile.name,
708
- fileType,
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
- const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
768
+ const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
731
769
 
732
- let artifact = await ctx.db.artifact.findFirst({
733
- where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
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
- const lastVersion = await ctx.db.artifactVersion.findFirst({
748
- where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
749
- orderBy: { version: 'desc' },
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
- await ctx.db.artifactVersion.create({
753
- data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
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
- results.artifacts.studyGuide = artifact;
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
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
761
- status: 'generating_flashcards',
762
- filename: primaryFile.name,
763
- fileType,
764
- startedAt: new Date().toISOString(),
765
- steps: {
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
- const artifact = await ctx.db.artifact.create({
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
- // Parse JSON flashcard content
797
- try {
798
- const flashcardData: any = content;
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
- let createdCards = 0;
801
- for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
802
- const card = flashcardData[i];
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
- await ctx.db.flashcard.create({
807
- data: {
808
- artifactId: artifact.id,
809
- front: front,
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.trim(),
829
- back: back.trim(),
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
- results.artifacts.flashcards = artifact;
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
- status: 'completed',
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
- console.error('Failed to update analysis progress:', error);
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: process.env.FRONTEND_URL || "http://localhost:3000",
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: {