@goscribe/server 1.1.1 → 1.1.3

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 (36) hide show
  1. package/dist/lib/ai-session.d.ts +13 -3
  2. package/dist/lib/ai-session.js +66 -146
  3. package/dist/lib/pusher.js +1 -1
  4. package/dist/routers/_app.d.ts +114 -7
  5. package/dist/routers/chat.js +2 -23
  6. package/dist/routers/flashcards.d.ts +25 -1
  7. package/dist/routers/flashcards.js +0 -14
  8. package/dist/routers/members.d.ts +18 -0
  9. package/dist/routers/members.js +14 -1
  10. package/dist/routers/worksheets.js +5 -4
  11. package/dist/routers/workspace.d.ts +89 -6
  12. package/dist/routers/workspace.js +389 -259
  13. package/dist/services/flashcard-progress.service.d.ts +25 -1
  14. package/dist/services/flashcard-progress.service.js +70 -31
  15. package/package.json +2 -2
  16. package/prisma/schema.prisma +14 -1
  17. package/src/lib/ai-session.ts +97 -158
  18. package/src/routers/flashcards.ts +0 -16
  19. package/src/routers/members.ts +13 -2
  20. package/src/routers/podcast.ts +0 -1
  21. package/src/routers/worksheets.ts +3 -2
  22. package/src/routers/workspace.ts +516 -399
  23. package/ANALYSIS_PROGRESS_SPEC.md +0 -463
  24. package/PROGRESS_QUICK_REFERENCE.md +0 -239
  25. package/dist/lib/podcast-prompts.d.ts +0 -43
  26. package/dist/lib/podcast-prompts.js +0 -135
  27. package/dist/routers/ai-session.d.ts +0 -0
  28. package/dist/routers/ai-session.js +0 -1
  29. package/dist/services/flashcard.service.d.ts +0 -183
  30. package/dist/services/flashcard.service.js +0 -224
  31. package/dist/services/podcast-segment-reorder.d.ts +0 -0
  32. package/dist/services/podcast-segment-reorder.js +0 -107
  33. package/dist/services/podcast.service.d.ts +0 -0
  34. package/dist/services/podcast.service.js +0 -326
  35. package/dist/services/worksheet.service.d.ts +0 -0
  36. package/dist/services/worksheet.service.js +0 -295
@@ -6,6 +6,7 @@ import { ArtifactType } from '@prisma/client';
6
6
  import { aiSessionService } from '../lib/ai-session.js';
7
7
  import PusherService from '../lib/pusher.js';
8
8
  import { members } from './members.js';
9
+ import { logger } from '../lib/logger.js';
9
10
  // Helper function to update and emit analysis progress
10
11
  async function updateAnalysisProgress(db, workspaceId, progress) {
11
12
  await db.workspace.update({
@@ -233,6 +234,20 @@ export const workspace = router({
233
234
  }
234
235
  return { folder, parents };
235
236
  }),
237
+ getSharedWith: authedProcedure
238
+ .input(z.object({
239
+ id: z.string(),
240
+ }))
241
+ .query(async ({ ctx, input }) => {
242
+ const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
243
+ if (!user || !user.email)
244
+ throw new TRPCError({ code: 'NOT_FOUND' });
245
+ const sharedWith = await ctx.db.workspace.findMany({ where: { members: { some: { userId: ctx.session.user.id } } } });
246
+ const invitations = await ctx.db.workspaceInvitation.findMany({ where: { email: user.email, acceptedAt: null }, include: {
247
+ workspace: true,
248
+ } });
249
+ return { shared: sharedWith, invitations };
250
+ }),
236
251
  uploadFiles: authedProcedure
237
252
  .input(z.object({
238
253
  id: z.string(),
@@ -319,28 +334,52 @@ export const workspace = router({
319
334
  });
320
335
  return true;
321
336
  }),
337
+ getFileUploadUrl: authedProcedure
338
+ .input(z.object({
339
+ workspaceId: z.string(),
340
+ filename: z.string(),
341
+ contentType: z.string(),
342
+ size: z.number(),
343
+ }))
344
+ .query(async ({ ctx, input }) => {
345
+ const objectKey = `workspace_${ctx.session.user.id}/${input.workspaceId}-file_${input.filename}`;
346
+ const fileAsset = await ctx.db.fileAsset.create({
347
+ data: {
348
+ workspaceId: input.workspaceId,
349
+ name: input.filename,
350
+ mimeType: input.contentType,
351
+ size: input.size,
352
+ userId: ctx.session.user.id,
353
+ bucket: 'media',
354
+ objectKey: objectKey,
355
+ },
356
+ });
357
+ const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
358
+ .from('media')
359
+ .createSignedUploadUrl(objectKey); // 5 minutes
360
+ if (signedUrlError) {
361
+ throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
362
+ }
363
+ await ctx.db.workspace.update({
364
+ where: { id: input.workspaceId },
365
+ data: { needsAnalysis: true },
366
+ });
367
+ return {
368
+ fileId: fileAsset.id,
369
+ uploadUrl: signedUrlData.signedUrl,
370
+ };
371
+ }),
322
372
  uploadAndAnalyzeMedia: authedProcedure
323
373
  .input(z.object({
324
374
  workspaceId: z.string(),
325
- file: z.object({
326
- filename: z.string(),
327
- contentType: z.string(),
328
- size: z.number(),
329
- content: z.string(), // Base64 encoded file content
330
- }),
375
+ files: z.array(z.object({
376
+ id: z.string(),
377
+ })),
331
378
  generateStudyGuide: z.boolean().default(true),
332
379
  generateFlashcards: z.boolean().default(true),
333
380
  generateWorksheet: z.boolean().default(true),
334
381
  }))
335
382
  .mutation(async ({ ctx, input }) => {
336
- console.log('🚀 uploadAndAnalyzeMedia started', {
337
- workspaceId: input.workspaceId,
338
- filename: input.file.filename,
339
- fileSize: input.file.size,
340
- generateStudyGuide: input.generateStudyGuide,
341
- generateFlashcards: input.generateFlashcards,
342
- generateWorksheet: input.generateWorksheet
343
- });
344
383
  // Verify workspace ownership
345
384
  const workspace = await ctx.db.workspace.findFirst({
346
385
  where: { id: input.workspaceId, ownerId: ctx.session.user.id }
@@ -349,129 +388,102 @@ export const workspace = router({
349
388
  console.error('❌ Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
350
389
  throw new TRPCError({ code: 'NOT_FOUND' });
351
390
  }
352
- const fileType = input.file.contentType.startsWith('image/') ? 'image' : 'pdf';
353
- await ctx.db.workspace.update({
354
- where: { id: input.workspaceId },
355
- data: { fileBeingAnalyzed: true },
391
+ // Check if analysis is already in progress
392
+ if (workspace.fileBeingAnalyzed) {
393
+ throw new TRPCError({
394
+ code: 'CONFLICT',
395
+ message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
396
+ });
397
+ }
398
+ // Fetch files from database
399
+ const files = await ctx.db.fileAsset.findMany({
400
+ where: {
401
+ id: { in: input.files.map(file => file.id) },
402
+ workspaceId: input.workspaceId,
403
+ userId: ctx.session.user.id,
404
+ },
356
405
  });
357
- try {
358
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
359
- status: 'starting',
360
- filename: input.file.filename,
361
- fileType,
362
- startedAt: new Date().toISOString(),
363
- steps: {
364
- fileUpload: {
365
- order: 1,
366
- status: 'pending',
367
- },
368
- fileAnalysis: {
369
- order: 2,
370
- status: 'pending',
371
- },
372
- studyGuide: {
373
- order: 3,
374
- status: input.generateStudyGuide ? 'pending' : 'skipped',
375
- },
376
- flashcards: {
377
- order: 4,
378
- status: input.generateFlashcards ? 'pending' : 'skipped',
379
- },
380
- }
406
+ if (files.length === 0) {
407
+ throw new TRPCError({
408
+ code: 'NOT_FOUND',
409
+ message: 'No files found with the provided IDs'
381
410
  });
382
411
  }
383
- catch (error) {
384
- console.error('❌ Failed to update analysis progress:', error);
412
+ // Validate all files have bucket and objectKey
413
+ for (const file of files) {
414
+ if (!file.bucket || !file.objectKey) {
415
+ throw new TRPCError({
416
+ code: 'BAD_REQUEST',
417
+ message: `File ${file.id} does not have bucket or objectKey set`
418
+ });
419
+ }
420
+ }
421
+ // Use the first file for progress tracking and artifact naming
422
+ const primaryFile = files[0];
423
+ const fileType = primaryFile.mimeType.startsWith('image/') ? 'image' : 'pdf';
424
+ try {
425
+ // Set analysis in progress flag
385
426
  await ctx.db.workspace.update({
386
427
  where: { id: input.workspaceId },
387
- data: { fileBeingAnalyzed: false },
428
+ data: { fileBeingAnalyzed: true },
388
429
  });
389
- await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
390
- throw error;
391
- }
392
- const fileBuffer = Buffer.from(input.file.content, 'base64');
393
- // // Check AI service health first
394
- // console.log('🏥 Checking AI service health...');
395
- // const isHealthy = await aiSessionService.checkHealth();
396
- // if (!isHealthy) {
397
- // console.error('❌ AI service is not available');
398
- // await PusherService.emitError(input.workspaceId, 'AI service is currently unavailable');
399
- // throw new TRPCError({
400
- // code: 'SERVICE_UNAVAILABLE',
401
- // message: 'AI service is currently unavailable. Please try again later.',
402
- // });
403
- // }
404
- // console.log('✅ AI service is healthy');
405
- const fileObj = new File([fileBuffer], input.file.filename, { type: input.file.contentType });
406
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
407
- status: 'uploading',
408
- filename: input.file.filename,
409
- fileType,
410
- startedAt: new Date().toISOString(),
411
- steps: {
412
- fileUpload: {
413
- order: 1,
414
- status: 'in_progress',
415
- },
416
- fileAnalysis: {
417
- order: 2,
418
- status: 'pending',
419
- },
420
- studyGuide: {
421
- order: 3,
422
- status: input.generateStudyGuide ? 'pending' : 'skipped',
423
- },
424
- flashcards: {
425
- order: 4,
426
- status: input.generateFlashcards ? 'pending' : 'skipped',
427
- },
428
- }
429
- });
430
- await aiSessionService.uploadFile(input.workspaceId, ctx.session.user.id, fileObj, fileType);
431
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
432
- status: 'analyzing',
433
- filename: input.file.filename,
434
- fileType,
435
- startedAt: new Date().toISOString(),
436
- steps: {
437
- fileUpload: {
438
- order: 1,
439
- status: 'completed',
440
- },
441
- fileAnalysis: {
442
- order: 2,
443
- status: 'in_progress',
444
- },
445
- studyGuide: {
446
- order: 3,
447
- status: input.generateStudyGuide ? 'pending' : 'skipped',
448
- },
449
- flashcards: {
450
- order: 4,
451
- status: input.generateFlashcards ? 'pending' : 'skipped',
430
+ PusherService.emitAnalysisProgress(input.workspaceId, {
431
+ status: 'starting',
432
+ filename: primaryFile.name,
433
+ fileType,
434
+ startedAt: new Date().toISOString(),
435
+ steps: {
436
+ fileUpload: { order: 1, status: 'pending' },
452
437
  },
438
+ });
439
+ try {
440
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
441
+ status: 'starting',
442
+ filename: primaryFile.name,
443
+ fileType,
444
+ startedAt: new Date().toISOString(),
445
+ steps: {
446
+ fileUpload: {
447
+ order: 1,
448
+ status: 'pending',
449
+ },
450
+ fileAnalysis: {
451
+ order: 2,
452
+ status: 'pending',
453
+ },
454
+ studyGuide: {
455
+ order: 3,
456
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
457
+ },
458
+ flashcards: {
459
+ order: 4,
460
+ status: input.generateFlashcards ? 'pending' : 'skipped',
461
+ },
462
+ }
463
+ });
453
464
  }
454
- });
455
- try {
456
- if (fileType === 'image') {
457
- await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id);
458
- }
459
- else {
460
- await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id);
465
+ catch (error) {
466
+ console.error('❌ Failed to update analysis progress:', error);
467
+ await ctx.db.workspace.update({
468
+ where: { id: input.workspaceId },
469
+ data: { fileBeingAnalyzed: false },
470
+ });
471
+ await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
472
+ throw error;
461
473
  }
462
474
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
463
- status: 'generating_artifacts',
464
- filename: input.file.filename,
475
+ status: 'uploading',
476
+ filename: primaryFile.name,
465
477
  fileType,
466
478
  startedAt: new Date().toISOString(),
467
479
  steps: {
468
480
  fileUpload: {
469
481
  order: 1,
470
- status: 'completed',
482
+ status: 'in_progress',
471
483
  },
472
484
  fileAnalysis: {
473
485
  order: 2,
474
- status: 'completed',
486
+ status: 'pending',
475
487
  },
476
488
  studyGuide: {
477
489
  order: 3,
@@ -483,49 +495,53 @@ export const workspace = router({
483
495
  },
484
496
  }
485
497
  });
486
- }
487
- catch (error) {
488
- console.error('❌ Failed to analyze file:', error);
489
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
490
- status: 'error',
491
- filename: input.file.filename,
492
- fileType,
493
- error: `Failed to analyze ${fileType}: ${error}`,
494
- startedAt: new Date().toISOString(),
495
- steps: {
496
- fileUpload: {
497
- order: 1,
498
- status: 'completed',
499
- },
500
- fileAnalysis: {
501
- order: 2,
502
- status: 'error',
503
- },
504
- studyGuide: {
505
- order: 3,
506
- status: 'skipped',
507
- },
508
- flashcards: {
509
- order: 4,
510
- status: 'skipped',
511
- },
498
+ // Process all files using the new process_file endpoint
499
+ for (const file of files) {
500
+ // TypeScript: We already validated bucket and objectKey exist above
501
+ if (!file.bucket || !file.objectKey) {
502
+ continue; // Skip if somehow missing (shouldn't happen due to validation above)
503
+ }
504
+ const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
505
+ .from(file.bucket)
506
+ .createSignedUrl(file.objectKey, 24 * 60 * 60); // 24 hours expiry
507
+ if (signedUrlError) {
508
+ await ctx.db.workspace.update({
509
+ where: { id: input.workspaceId },
510
+ data: { fileBeingAnalyzed: false },
511
+ });
512
+ throw new TRPCError({
513
+ code: 'INTERNAL_SERVER_ERROR',
514
+ message: `Failed to generate signed URL for file ${file.name}: ${signedUrlError.message}`
515
+ });
516
+ }
517
+ const fileUrl = signedUrlData.signedUrl;
518
+ const currentFileType = file.mimeType.startsWith('image/') ? 'image' : 'pdf';
519
+ // Use maxPages for large PDFs (>50 pages) to limit processing
520
+ const maxPages = currentFileType === 'pdf' && file.size && file.size > 50 ? 50 : undefined;
521
+ const processResult = await aiSessionService.processFile(input.workspaceId, ctx.session.user.id, fileUrl, currentFileType, maxPages);
522
+ if (processResult.status === 'error') {
523
+ logger.error(`Failed to process file ${file.name}:`, processResult.error);
524
+ // Continue processing other files even if one fails
525
+ // Optionally, you could throw an error or mark this file as failed
526
+ }
527
+ else {
528
+ logger.info(`Successfully processed file ${file.name}: ${processResult.pageCount} pages`);
529
+ // Store the comprehensive description in aiTranscription field
530
+ await ctx.db.fileAsset.update({
531
+ where: { id: file.id },
532
+ data: {
533
+ aiTranscription: {
534
+ comprehensiveDescription: processResult.comprehensiveDescription,
535
+ textContent: processResult.textContent,
536
+ imageDescriptions: processResult.imageDescriptions,
537
+ },
538
+ }
539
+ });
512
540
  }
513
- });
514
- throw error;
515
- }
516
- const results = {
517
- filename: input.file.filename,
518
- artifacts: {
519
- studyGuide: null,
520
- flashcards: null,
521
- worksheet: null,
522
541
  }
523
- };
524
- // Generate artifacts
525
- if (input.generateStudyGuide) {
526
542
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
527
- status: 'generating_study_guide',
528
- filename: input.file.filename,
543
+ status: 'analyzing',
544
+ filename: primaryFile.name,
529
545
  fileType,
530
546
  startedAt: new Date().toISOString(),
531
547
  steps: {
@@ -535,11 +551,11 @@ export const workspace = router({
535
551
  },
536
552
  fileAnalysis: {
537
553
  order: 2,
538
- status: 'completed',
554
+ status: 'in_progress',
539
555
  },
540
556
  studyGuide: {
541
557
  order: 3,
542
- status: 'in_progress',
558
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
543
559
  },
544
560
  flashcards: {
545
561
  order: 4,
@@ -547,35 +563,218 @@ export const workspace = router({
547
563
  },
548
564
  }
549
565
  });
550
- const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
551
- let artifact = await ctx.db.artifact.findFirst({
552
- where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
553
- });
554
- if (!artifact) {
555
- artifact = await ctx.db.artifact.create({
566
+ try {
567
+ // Analyze all files - use PDF analysis if any file is a PDF, otherwise use image analysis
568
+ // const hasPDF = files.some(f => !f.mimeType.startsWith('image/'));
569
+ // if (hasPDF) {
570
+ // await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id, file.id);
571
+ // } else {
572
+ // // If all files are images, analyze them
573
+ // for (const file of files) {
574
+ // await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id, file.id);
575
+ // }
576
+ // }
577
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
578
+ status: 'generating_artifacts',
579
+ filename: primaryFile.name,
580
+ fileType,
581
+ startedAt: new Date().toISOString(),
582
+ steps: {
583
+ fileUpload: {
584
+ order: 1,
585
+ status: 'completed',
586
+ },
587
+ fileAnalysis: {
588
+ order: 2,
589
+ status: 'completed',
590
+ },
591
+ studyGuide: {
592
+ order: 3,
593
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
594
+ },
595
+ flashcards: {
596
+ order: 4,
597
+ status: input.generateFlashcards ? 'pending' : 'skipped',
598
+ },
599
+ }
600
+ });
601
+ }
602
+ catch (error) {
603
+ console.error('❌ Failed to analyze files:', error);
604
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
605
+ status: 'error',
606
+ filename: primaryFile.name,
607
+ fileType,
608
+ error: `Failed to analyze ${fileType}: ${error}`,
609
+ startedAt: new Date().toISOString(),
610
+ steps: {
611
+ fileUpload: {
612
+ order: 1,
613
+ status: 'completed',
614
+ },
615
+ fileAnalysis: {
616
+ order: 2,
617
+ status: 'error',
618
+ },
619
+ studyGuide: {
620
+ order: 3,
621
+ status: 'skipped',
622
+ },
623
+ flashcards: {
624
+ order: 4,
625
+ status: 'skipped',
626
+ },
627
+ }
628
+ });
629
+ await ctx.db.workspace.update({
630
+ where: { id: input.workspaceId },
631
+ data: { fileBeingAnalyzed: false },
632
+ });
633
+ throw error;
634
+ }
635
+ const results = {
636
+ filename: primaryFile.name,
637
+ artifacts: {
638
+ studyGuide: null,
639
+ flashcards: null,
640
+ worksheet: null,
641
+ }
642
+ };
643
+ // Generate artifacts
644
+ if (input.generateStudyGuide) {
645
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
646
+ status: 'generating_study_guide',
647
+ filename: primaryFile.name,
648
+ fileType,
649
+ startedAt: new Date().toISOString(),
650
+ steps: {
651
+ fileUpload: {
652
+ order: 1,
653
+ status: 'completed',
654
+ },
655
+ fileAnalysis: {
656
+ order: 2,
657
+ status: 'completed',
658
+ },
659
+ studyGuide: {
660
+ order: 3,
661
+ status: 'in_progress',
662
+ },
663
+ flashcards: {
664
+ order: 4,
665
+ status: input.generateFlashcards ? 'pending' : 'skipped',
666
+ },
667
+ }
668
+ });
669
+ const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
670
+ let artifact = await ctx.db.artifact.findFirst({
671
+ where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
672
+ });
673
+ if (!artifact) {
674
+ const fileNames = files.map(f => f.name).join(', ');
675
+ artifact = await ctx.db.artifact.create({
676
+ data: {
677
+ workspaceId: input.workspaceId,
678
+ type: ArtifactType.STUDY_GUIDE,
679
+ title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
680
+ createdById: ctx.session.user.id,
681
+ },
682
+ });
683
+ }
684
+ const lastVersion = await ctx.db.artifactVersion.findFirst({
685
+ where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
686
+ orderBy: { version: 'desc' },
687
+ });
688
+ await ctx.db.artifactVersion.create({
689
+ data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
690
+ });
691
+ results.artifacts.studyGuide = artifact;
692
+ }
693
+ if (input.generateFlashcards) {
694
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
695
+ status: 'generating_flashcards',
696
+ filename: primaryFile.name,
697
+ fileType,
698
+ startedAt: new Date().toISOString(),
699
+ steps: {
700
+ fileUpload: {
701
+ order: 1,
702
+ status: 'completed',
703
+ },
704
+ fileAnalysis: {
705
+ order: 2,
706
+ status: 'completed',
707
+ },
708
+ studyGuide: {
709
+ order: 3,
710
+ status: input.generateStudyGuide ? 'completed' : 'skipped',
711
+ },
712
+ flashcards: {
713
+ order: 4,
714
+ status: 'in_progress',
715
+ },
716
+ }
717
+ });
718
+ const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
719
+ const artifact = await ctx.db.artifact.create({
556
720
  data: {
557
721
  workspaceId: input.workspaceId,
558
- type: ArtifactType.STUDY_GUIDE,
559
- title: `Study Guide - ${input.file.filename}`,
722
+ type: ArtifactType.FLASHCARD_SET,
723
+ title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
560
724
  createdById: ctx.session.user.id,
561
725
  },
562
726
  });
727
+ // Parse JSON flashcard content
728
+ try {
729
+ const flashcardData = content;
730
+ let createdCards = 0;
731
+ for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
732
+ const card = flashcardData[i];
733
+ const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
734
+ const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
735
+ await ctx.db.flashcard.create({
736
+ data: {
737
+ artifactId: artifact.id,
738
+ front: front,
739
+ back: back,
740
+ order: i,
741
+ tags: ['ai-generated', 'medium'],
742
+ },
743
+ });
744
+ createdCards++;
745
+ }
746
+ }
747
+ catch (parseError) {
748
+ // Fallback to text parsing if JSON fails
749
+ const lines = content.split('\n').filter(line => line.trim());
750
+ for (let i = 0; i < Math.min(lines.length, 10); i++) {
751
+ const line = lines[i];
752
+ if (line.includes(' - ')) {
753
+ const [front, back] = line.split(' - ');
754
+ await ctx.db.flashcard.create({
755
+ data: {
756
+ artifactId: artifact.id,
757
+ front: front.trim(),
758
+ back: back.trim(),
759
+ order: i,
760
+ tags: ['ai-generated', 'medium'],
761
+ },
762
+ });
763
+ }
764
+ }
765
+ }
766
+ results.artifacts.flashcards = artifact;
563
767
  }
564
- const lastVersion = await ctx.db.artifactVersion.findFirst({
565
- where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
566
- orderBy: { version: 'desc' },
567
- });
568
- await ctx.db.artifactVersion.create({
569
- data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
768
+ await ctx.db.workspace.update({
769
+ where: { id: input.workspaceId },
770
+ data: { fileBeingAnalyzed: false },
570
771
  });
571
- results.artifacts.studyGuide = artifact;
572
- }
573
- if (input.generateFlashcards) {
574
772
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
575
- status: 'generating_flashcards',
576
- filename: input.file.filename,
773
+ status: 'completed',
774
+ filename: primaryFile.name,
577
775
  fileType,
578
776
  startedAt: new Date().toISOString(),
777
+ completedAt: new Date().toISOString(),
579
778
  steps: {
580
779
  fileUpload: {
581
780
  order: 1,
@@ -591,90 +790,21 @@ export const workspace = router({
591
790
  },
592
791
  flashcards: {
593
792
  order: 4,
594
- status: 'in_progress',
793
+ status: input.generateFlashcards ? 'completed' : 'skipped',
595
794
  },
596
795
  }
597
796
  });
598
- const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
599
- const artifact = await ctx.db.artifact.create({
600
- data: {
601
- workspaceId: input.workspaceId,
602
- type: ArtifactType.FLASHCARD_SET,
603
- title: `Flashcards - ${input.file.filename}`,
604
- createdById: ctx.session.user.id,
605
- },
797
+ return results;
798
+ }
799
+ catch (error) {
800
+ console.error('❌ Failed to update analysis progress:', error);
801
+ await ctx.db.workspace.update({
802
+ where: { id: input.workspaceId },
803
+ data: { fileBeingAnalyzed: false },
606
804
  });
607
- // Parse JSON flashcard content
608
- try {
609
- const flashcardData = content;
610
- let createdCards = 0;
611
- for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
612
- const card = flashcardData[i];
613
- const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
614
- const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
615
- await ctx.db.flashcard.create({
616
- data: {
617
- artifactId: artifact.id,
618
- front: front,
619
- back: back,
620
- order: i,
621
- tags: ['ai-generated', 'medium'],
622
- },
623
- });
624
- createdCards++;
625
- }
626
- }
627
- catch (parseError) {
628
- // Fallback to text parsing if JSON fails
629
- const lines = content.split('\n').filter(line => line.trim());
630
- for (let i = 0; i < Math.min(lines.length, 10); i++) {
631
- const line = lines[i];
632
- if (line.includes(' - ')) {
633
- const [front, back] = line.split(' - ');
634
- await ctx.db.flashcard.create({
635
- data: {
636
- artifactId: artifact.id,
637
- front: front.trim(),
638
- back: back.trim(),
639
- order: i,
640
- tags: ['ai-generated', 'medium'],
641
- },
642
- });
643
- }
644
- }
645
- }
646
- results.artifacts.flashcards = artifact;
805
+ await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
806
+ throw error;
647
807
  }
648
- await ctx.db.workspace.update({
649
- where: { id: input.workspaceId },
650
- data: { fileBeingAnalyzed: false },
651
- });
652
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
653
- status: 'completed',
654
- filename: input.file.filename,
655
- fileType,
656
- startedAt: new Date().toISOString(),
657
- completedAt: new Date().toISOString(),
658
- steps: {
659
- fileUpload: {
660
- order: 1,
661
- status: 'completed',
662
- },
663
- fileAnalysis: {
664
- order: 2,
665
- status: 'completed',
666
- },
667
- studyGuide: {
668
- order: 3,
669
- status: input.generateStudyGuide ? 'completed' : 'skipped',
670
- },
671
- flashcards: {
672
- order: 4,
673
- status: input.generateFlashcards ? 'completed' : 'skipped',
674
- },
675
- }
676
- });
677
- return results;
678
808
  }),
679
809
  search: authedProcedure
680
810
  .input(z.object({