@goscribe/server 1.1.2 → 1.1.4

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 +3 -2
  16. package/prisma/schema.prisma +196 -184
  17. package/src/lib/ai-session.ts +3 -21
  18. package/src/routers/auth.ts +50 -2
  19. package/src/routers/flashcards.ts +0 -16
  20. package/src/routers/members.ts +27 -6
  21. package/src/routers/workspace.ts +468 -439
  22. package/src/server.ts +13 -0
  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
@@ -26,17 +26,17 @@ async function updateAnalysisProgress(
26
26
  function calculateRelevance(query: string, ...texts: (string | null | undefined)[]): number {
27
27
  const queryLower = query.toLowerCase();
28
28
  let score = 0;
29
-
29
+
30
30
  for (const text of texts) {
31
31
  if (!text) continue;
32
-
32
+
33
33
  const textLower = text.toLowerCase();
34
-
34
+
35
35
  // Exact match gets highest score
36
36
  if (textLower.includes(queryLower)) {
37
37
  score += 10;
38
38
  }
39
-
39
+
40
40
  // Word boundary matches get good score
41
41
  const words = queryLower.split(/\s+/);
42
42
  for (const word of words) {
@@ -44,7 +44,7 @@ function calculateRelevance(query: string, ...texts: (string | null | undefined)
44
44
  score += 5;
45
45
  }
46
46
  }
47
-
47
+
48
48
  // Partial matches get lower score
49
49
  const queryChars = queryLower.split('');
50
50
  let consecutiveMatches = 0;
@@ -57,7 +57,7 @@ function calculateRelevance(query: string, ...texts: (string | null | undefined)
57
57
  }
58
58
  score += consecutiveMatches * 0.1;
59
59
  }
60
-
60
+
61
61
  return score;
62
62
  }
63
63
 
@@ -65,8 +65,8 @@ export const workspace = router({
65
65
  // List current user's workspaces
66
66
  list: authedProcedure
67
67
  .input(z.object({
68
- parentId: z.string().optional(),
69
- }))
68
+ parentId: z.string().optional(),
69
+ }))
70
70
  .query(async ({ ctx, input }) => {
71
71
  const workspaces = await ctx.db.workspace.findMany({
72
72
  where: {
@@ -85,14 +85,14 @@ export const workspace = router({
85
85
 
86
86
  return { workspaces, folders };
87
87
  }),
88
-
88
+
89
89
  create: authedProcedure
90
90
  .input(z.object({
91
- name: z.string().min(1).max(100),
92
- description: z.string().max(500).optional(),
93
- parentId: z.string().optional(),
94
- }))
95
- .mutation(async ({ ctx, input}) => {
91
+ name: z.string().min(1).max(100),
92
+ description: z.string().max(500).optional(),
93
+ parentId: z.string().optional(),
94
+ }))
95
+ .mutation(async ({ ctx, input }) => {
96
96
  const ws = await ctx.db.workspace.create({
97
97
  data: {
98
98
  title: input.name,
@@ -119,10 +119,10 @@ export const workspace = router({
119
119
  }),
120
120
  createFolder: authedProcedure
121
121
  .input(z.object({
122
- name: z.string().min(1).max(100),
123
- color: z.string().optional(),
124
- parentId: z.string().optional(),
125
- }))
122
+ name: z.string().min(1).max(100),
123
+ color: z.string().optional(),
124
+ parentId: z.string().optional(),
125
+ }))
126
126
  .mutation(async ({ ctx, input }) => {
127
127
  const folder = await ctx.db.folder.create({
128
128
  data: {
@@ -136,26 +136,26 @@ export const workspace = router({
136
136
  }),
137
137
  updateFolder: authedProcedure
138
138
  .input(z.object({
139
- id: z.string(),
140
- name: z.string().min(1).max(100).optional(),
141
- color: z.string().optional(),
142
- }))
139
+ id: z.string(),
140
+ name: z.string().min(1).max(100).optional(),
141
+ color: z.string().optional(),
142
+ }))
143
143
  .mutation(async ({ ctx, input }) => {
144
144
  const folder = await ctx.db.folder.update({ where: { id: input.id }, data: { name: input.name, color: input.color ?? '#9D00FF' } });
145
145
  return folder;
146
146
  }),
147
147
  deleteFolder: authedProcedure
148
148
  .input(z.object({
149
- id: z.string(),
150
- }))
149
+ id: z.string(),
150
+ }))
151
151
  .mutation(async ({ ctx, input }) => {
152
152
  const folder = await ctx.db.folder.delete({ where: { id: input.id } });
153
153
  return folder;
154
154
  }),
155
155
  get: authedProcedure
156
156
  .input(z.object({
157
- id: z.string(),
158
- }))
157
+ id: z.string(),
158
+ }))
159
159
  .query(async ({ ctx, input }) => {
160
160
  const ws = await ctx.db.workspace.findFirst({
161
161
  where: { id: input.id, ownerId: ctx.session.user.id },
@@ -171,13 +171,13 @@ export const workspace = router({
171
171
  getStats: authedProcedure
172
172
  .query(async ({ ctx }) => {
173
173
  const workspaces = await ctx.db.workspace.findMany({
174
- where: {OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }]},
174
+ where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
175
175
  });
176
176
  const folders = await ctx.db.folder.findMany({
177
- where: {OR: [{ ownerId: ctx.session.user.id } ]},
177
+ where: { OR: [{ ownerId: ctx.session.user.id }] },
178
178
  });
179
179
  const lastUpdated = await ctx.db.workspace.findFirst({
180
- where: {OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }]},
180
+ where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
181
181
  orderBy: { updatedAt: 'desc' },
182
182
  });
183
183
 
@@ -196,12 +196,12 @@ export const workspace = router({
196
196
  }),
197
197
  update: authedProcedure
198
198
  .input(z.object({
199
- id: z.string(),
200
- name: z.string().min(1).max(100).optional(),
201
- description: z.string().max(500).optional(),
202
- color: z.string().optional(),
203
- icon: z.string().optional(),
204
- }))
199
+ id: z.string(),
200
+ name: z.string().min(1).max(100).optional(),
201
+ description: z.string().max(500).optional(),
202
+ color: z.string().optional(),
203
+ icon: z.string().optional(),
204
+ }))
205
205
  .mutation(async ({ ctx, input }) => {
206
206
  const existed = await ctx.db.workspace.findFirst({
207
207
  where: { id: input.id, ownerId: ctx.session.user.id },
@@ -216,12 +216,12 @@ export const workspace = router({
216
216
  icon: input.icon ?? existed.icon,
217
217
  },
218
218
  });
219
- return updated;
220
- }),
221
- delete: authedProcedure
219
+ return updated;
220
+ }),
221
+ delete: authedProcedure
222
222
  .input(z.object({
223
- id: z.string(),
224
- }))
223
+ id: z.string(),
224
+ }))
225
225
  .mutation(async ({ ctx, input }) => {
226
226
  const deleted = await ctx.db.workspace.deleteMany({
227
227
  where: { id: input.id, ownerId: ctx.session.user.id },
@@ -229,15 +229,15 @@ export const workspace = router({
229
229
  if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
230
230
  return true;
231
231
  }),
232
- getFolderInformation: authedProcedure
232
+ getFolderInformation: authedProcedure
233
233
  .input(z.object({
234
- id: z.string(),
235
- }))
234
+ id: z.string(),
235
+ }))
236
236
  .query(async ({ ctx, input }) => {
237
237
  const folder = await ctx.db.folder.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
238
238
  // find all of its parents
239
239
  if (!folder) throw new TRPCError({ code: 'NOT_FOUND' });
240
-
240
+
241
241
  const parents = [];
242
242
  let current = folder;
243
243
 
@@ -247,20 +247,36 @@ export const workspace = router({
247
247
  parents.push(parent);
248
248
  current = parent;
249
249
  }
250
-
250
+
251
251
  return { folder, parents };
252
252
  }),
253
- uploadFiles: authedProcedure
253
+
254
+ getSharedWith: authedProcedure
254
255
  .input(z.object({
255
- id: z.string(),
256
- files: z.array(
257
- z.object({
258
- filename: z.string().min(1).max(255),
259
- contentType: z.string().min(1).max(100),
260
- size: z.number().min(1), // size in bytes
261
- })
262
- ),
263
- }))
256
+ id: z.string(),
257
+ }))
258
+ .query(async ({ ctx, input }) => {
259
+
260
+ const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
261
+ if (!user || !user.email) throw new TRPCError({ code: 'NOT_FOUND' });
262
+ const sharedWith = await ctx.db.workspace.findMany({ where: { members: { some: { userId: ctx.session.user.id } } } });
263
+ const invitations = await ctx.db.workspaceInvitation.findMany({ where: { email: user.email, acceptedAt: null }, include: {
264
+ workspace: true,
265
+ } });
266
+
267
+ return { shared: sharedWith, invitations };
268
+ }),
269
+ uploadFiles: authedProcedure
270
+ .input(z.object({
271
+ id: z.string(),
272
+ files: z.array(
273
+ z.object({
274
+ filename: z.string().min(1).max(255),
275
+ contentType: z.string().min(1).max(100),
276
+ size: z.number().min(1), // size in bytes
277
+ })
278
+ ),
279
+ }))
264
280
  .mutation(async ({ ctx, input }) => {
265
281
  // ensure workspace belongs to user
266
282
  const ws = await ctx.db.workspace.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
@@ -275,7 +291,7 @@ export const workspace = router({
275
291
  name: file.filename,
276
292
  mimeType: file.contentType,
277
293
  size: file.size,
278
- workspaceId: input.id,
294
+ workspaceId: input.id,
279
295
  },
280
296
  });
281
297
 
@@ -286,9 +302,9 @@ export const workspace = router({
286
302
  .createSignedUploadUrl(objectKey); // 5 minutes
287
303
 
288
304
  if (signedUrlError) {
289
- throw new TRPCError({
290
- code: 'INTERNAL_SERVER_ERROR',
291
- message: `Failed to generate upload URL: ${signedUrlError.message}`
305
+ throw new TRPCError({
306
+ code: 'INTERNAL_SERVER_ERROR',
307
+ message: `Failed to generate upload URL: ${signedUrlError.message}`
292
308
  });
293
309
  }
294
310
 
@@ -299,7 +315,7 @@ export const workspace = router({
299
315
  bucket: 'files',
300
316
  objectKey: objectKey,
301
317
  },
302
- });
318
+ });
303
319
 
304
320
  results.push({
305
321
  fileId: record.id,
@@ -310,11 +326,11 @@ export const workspace = router({
310
326
  return results;
311
327
 
312
328
  }),
313
- deleteFiles: authedProcedure
329
+ deleteFiles: authedProcedure
314
330
  .input(z.object({
315
- fileId: z.array(z.string()),
316
- id: z.string(),
317
- }))
331
+ fileId: z.array(z.string()),
332
+ id: z.string(),
333
+ }))
318
334
  .mutation(async ({ ctx, input }) => {
319
335
  // ensure files are in the user's workspace
320
336
  const files = await ctx.db.fileAsset.findMany({
@@ -345,7 +361,7 @@ export const workspace = router({
345
361
  });
346
362
  return true;
347
363
  }),
348
- getFileUploadUrl: authedProcedure
364
+ getFileUploadUrl: authedProcedure
349
365
  .input(z.object({
350
366
  workspaceId: z.string(),
351
367
  filename: z.string(),
@@ -376,13 +392,13 @@ export const workspace = router({
376
392
  where: { id: input.workspaceId },
377
393
  data: { needsAnalysis: true },
378
394
  });
379
-
395
+
380
396
  return {
381
397
  fileId: fileAsset.id,
382
398
  uploadUrl: signedUrlData.signedUrl,
383
399
  };
384
400
  }),
385
- uploadAndAnalyzeMedia: authedProcedure
401
+ uploadAndAnalyzeMedia: authedProcedure
386
402
  .input(z.object({
387
403
  workspaceId: z.string(),
388
404
  files: z.array(z.object({
@@ -404,9 +420,9 @@ export const workspace = router({
404
420
 
405
421
  // Check if analysis is already in progress
406
422
  if (workspace.fileBeingAnalyzed) {
407
- throw new TRPCError({
408
- code: 'CONFLICT',
409
- message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
423
+ throw new TRPCError({
424
+ code: 'CONFLICT',
425
+ message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
410
426
  });
411
427
  }
412
428
 
@@ -420,18 +436,18 @@ export const workspace = router({
420
436
  });
421
437
 
422
438
  if (files.length === 0) {
423
- throw new TRPCError({
424
- code: 'NOT_FOUND',
425
- message: 'No files found with the provided IDs'
439
+ throw new TRPCError({
440
+ code: 'NOT_FOUND',
441
+ message: 'No files found with the provided IDs'
426
442
  });
427
443
  }
428
444
 
429
445
  // Validate all files have bucket and objectKey
430
446
  for (const file of files) {
431
447
  if (!file.bucket || !file.objectKey) {
432
- throw new TRPCError({
433
- code: 'BAD_REQUEST',
434
- message: `File ${file.id} does not have bucket or objectKey set`
448
+ throw new TRPCError({
449
+ code: 'BAD_REQUEST',
450
+ message: `File ${file.id} does not have bucket or objectKey set`
435
451
  });
436
452
  }
437
453
  }
@@ -439,186 +455,71 @@ export const workspace = router({
439
455
  // Use the first file for progress tracking and artifact naming
440
456
  const primaryFile = files[0];
441
457
  const fileType = primaryFile.mimeType.startsWith('image/') ? 'image' : 'pdf';
442
-
443
- // Set analysis in progress flag
444
- await ctx.db.workspace.update({
445
- where: { id: input.workspaceId },
446
- data: { fileBeingAnalyzed: true },
447
- });
448
-
449
- PusherService.emitAnalysisProgress(input.workspaceId, {
450
- status: 'starting',
451
- filename: primaryFile.name,
452
- fileType,
453
- startedAt: new Date().toISOString(),
454
- steps: {
455
- fileUpload: { order: 1, status: 'pending' },
456
- },
457
- });
458
-
459
458
  try {
460
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
459
+ // Set analysis in progress flag
460
+ await ctx.db.workspace.update({
461
+ where: { id: input.workspaceId },
462
+ data: { fileBeingAnalyzed: true },
463
+ });
464
+
465
+ PusherService.emitAnalysisProgress(input.workspaceId, {
461
466
  status: 'starting',
462
467
  filename: primaryFile.name,
463
468
  fileType,
464
469
  startedAt: new Date().toISOString(),
465
470
  steps: {
466
- fileUpload: {
467
- order: 1,
468
- status: 'pending',
469
- },
470
- fileAnalysis: {
471
- order: 2,
472
- status: 'pending',
473
- },
474
- studyGuide: {
475
- order: 3,
476
- status: input.generateStudyGuide ? 'pending' : 'skipped',
477
- },
478
- flashcards: {
479
- order: 4,
480
- status: input.generateFlashcards ? 'pending' : 'skipped',
481
- },
482
- }
483
- });
484
- } catch (error) {
485
- console.error('❌ Failed to update analysis progress:', error);
486
- await ctx.db.workspace.update({
487
- where: { id: input.workspaceId },
488
- data: { fileBeingAnalyzed: false },
489
- });
490
- await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
491
- throw error;
492
- }
493
-
494
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
495
- status: 'uploading',
496
- filename: primaryFile.name,
497
- fileType,
498
- startedAt: new Date().toISOString(),
499
- steps: {
500
- fileUpload: {
501
- order: 1,
502
- status: 'in_progress',
503
- },
504
- fileAnalysis: {
505
- order: 2,
506
- status: 'pending',
507
- },
508
- studyGuide: {
509
- order: 3,
510
- status: input.generateStudyGuide ? 'pending' : 'skipped',
471
+ fileUpload: { order: 1, status: 'pending' },
511
472
  },
512
- flashcards: {
513
- order: 4,
514
- status: input.generateFlashcards ? 'pending' : 'skipped',
515
- },
516
- }
517
- });
473
+ });
518
474
 
519
- // Process all files using the new process_file endpoint
520
- for (const file of files) {
521
- // TypeScript: We already validated bucket and objectKey exist above
522
- if (!file.bucket || !file.objectKey) {
523
- continue; // Skip if somehow missing (shouldn't happen due to validation above)
524
- }
525
-
526
- const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
527
- .from(file.bucket)
528
- .createSignedUrl(file.objectKey, 24 * 60 * 60); // 24 hours expiry
529
-
530
- if (signedUrlError) {
475
+ 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
+ });
500
+ } catch (error) {
501
+ console.error('❌ Failed to update analysis progress:', error);
531
502
  await ctx.db.workspace.update({
532
503
  where: { id: input.workspaceId },
533
504
  data: { fileBeingAnalyzed: false },
534
505
  });
535
- throw new TRPCError({
536
- code: 'INTERNAL_SERVER_ERROR',
537
- message: `Failed to generate signed URL for file ${file.name}: ${signedUrlError.message}`
538
- });
506
+ await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
507
+ throw error;
539
508
  }
540
-
541
- const fileUrl = signedUrlData.signedUrl;
542
- const currentFileType = file.mimeType.startsWith('image/') ? 'image' : 'pdf';
543
-
544
- // Use maxPages for large PDFs (>50 pages) to limit processing
545
- const maxPages = currentFileType === 'pdf' && file.size && file.size > 50 ? 50 : undefined;
546
-
547
- const processResult = await aiSessionService.processFile(
548
- input.workspaceId,
549
- ctx.session.user.id,
550
- fileUrl,
551
- currentFileType,
552
- maxPages
553
- );
554
-
555
- if (processResult.status === 'error') {
556
- logger.error(`Failed to process file ${file.name}:`, processResult.error);
557
- // Continue processing other files even if one fails
558
- // Optionally, you could throw an error or mark this file as failed
559
- } else {
560
- logger.info(`Successfully processed file ${file.name}: ${processResult.pageCount} pages`);
561
-
562
- // Store the comprehensive description in aiTranscription field
563
- await ctx.db.fileAsset.update({
564
- where: { id: file.id },
565
- data: {
566
- aiTranscription: processResult.comprehensiveDescription || processResult.textContent || null,
567
- }
568
- });
569
- }
570
- }
571
509
 
572
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
573
- status: 'analyzing',
574
- filename: primaryFile.name,
575
- fileType,
576
- startedAt: new Date().toISOString(),
577
- steps: {
578
- fileUpload: {
579
- order: 1,
580
- status: 'completed',
581
- },
582
- fileAnalysis: {
583
- order: 2,
584
- status: 'in_progress',
585
- },
586
- studyGuide: {
587
- order: 3,
588
- status: input.generateStudyGuide ? 'pending' : 'skipped',
589
- },
590
- flashcards: {
591
- order: 4,
592
- status: input.generateFlashcards ? 'pending' : 'skipped',
593
- },
594
- }
595
- });
596
-
597
- try {
598
- // Analyze all files - use PDF analysis if any file is a PDF, otherwise use image analysis
599
- // const hasPDF = files.some(f => !f.mimeType.startsWith('image/'));
600
- // if (hasPDF) {
601
- // await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id, file.id);
602
- // } else {
603
- // // If all files are images, analyze them
604
- // for (const file of files) {
605
- // await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id, file.id);
606
- // }
607
- // }
608
-
609
510
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
610
- status: 'generating_artifacts',
511
+ status: 'uploading',
611
512
  filename: primaryFile.name,
612
513
  fileType,
613
514
  startedAt: new Date().toISOString(),
614
515
  steps: {
615
516
  fileUpload: {
616
517
  order: 1,
617
- status: 'completed',
518
+ status: 'in_progress',
618
519
  },
619
520
  fileAnalysis: {
620
521
  order: 2,
621
- status: 'completed',
522
+ status: 'pending',
622
523
  },
623
524
  studyGuide: {
624
525
  order: 3,
@@ -630,60 +531,66 @@ export const workspace = router({
630
531
  },
631
532
  }
632
533
  });
633
- } catch (error) {
634
- console.error('❌ Failed to analyze files:', error);
635
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
636
- status: 'error',
637
- filename: primaryFile.name,
638
- fileType,
639
- error: `Failed to analyze ${fileType}: ${error}`,
640
- startedAt: new Date().toISOString(),
641
- steps: {
642
- fileUpload: {
643
- order: 1,
644
- status: 'completed',
645
- },
646
- fileAnalysis: {
647
- order: 2,
648
- status: 'error',
649
- },
650
- studyGuide: {
651
- order: 3,
652
- status: 'skipped',
653
- },
654
- flashcards: {
655
- order: 4,
656
- status: 'skipped',
657
- },
534
+
535
+ // Process all files using the new process_file endpoint
536
+ for (const file of files) {
537
+ // TypeScript: We already validated bucket and objectKey exist above
538
+ if (!file.bucket || !file.objectKey) {
539
+ continue; // Skip if somehow missing (shouldn't happen due to validation above)
658
540
  }
659
- });
660
- await ctx.db.workspace.update({
661
- where: { id: input.workspaceId },
662
- data: { fileBeingAnalyzed: false },
663
- });
664
- throw error;
665
- }
666
541
 
667
- const results: {
668
- filename: string;
669
- artifacts: {
670
- studyGuide: any | null;
671
- flashcards: any | null;
672
- worksheet: any | null;
673
- };
674
- } = {
675
- filename: primaryFile.name,
676
- artifacts: {
677
- studyGuide: null,
678
- flashcards: null,
679
- worksheet: null,
680
- }
681
- };
542
+ const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
543
+ .from(file.bucket)
544
+ .createSignedUrl(file.objectKey, 24 * 60 * 60); // 24 hours expiry
545
+
546
+ if (signedUrlError) {
547
+ await ctx.db.workspace.update({
548
+ where: { id: input.workspaceId },
549
+ data: { fileBeingAnalyzed: false },
550
+ });
551
+ throw new TRPCError({
552
+ code: 'INTERNAL_SERVER_ERROR',
553
+ message: `Failed to generate signed URL for file ${file.name}: ${signedUrlError.message}`
554
+ });
555
+ }
556
+
557
+ const fileUrl = signedUrlData.signedUrl;
558
+ const currentFileType = file.mimeType.startsWith('image/') ? 'image' : 'pdf';
559
+
560
+ // Use maxPages for large PDFs (>50 pages) to limit processing
561
+ const maxPages = currentFileType === 'pdf' && file.size && file.size > 50 ? 50 : undefined;
562
+
563
+ const processResult = await aiSessionService.processFile(
564
+ input.workspaceId,
565
+ ctx.session.user.id,
566
+ fileUrl,
567
+ currentFileType,
568
+ maxPages
569
+ );
570
+
571
+ if (processResult.status === 'error') {
572
+ logger.error(`Failed to process file ${file.name}:`, processResult.error);
573
+ // Continue processing other files even if one fails
574
+ // Optionally, you could throw an error or mark this file as failed
575
+ } else {
576
+ logger.info(`Successfully processed file ${file.name}: ${processResult.pageCount} pages`);
577
+
578
+ // Store the comprehensive description in aiTranscription field
579
+ await ctx.db.fileAsset.update({
580
+ where: { id: file.id },
581
+ data: {
582
+ aiTranscription: {
583
+ comprehensiveDescription: processResult.comprehensiveDescription,
584
+ textContent: processResult.textContent,
585
+ imageDescriptions: processResult.imageDescriptions,
586
+ },
587
+ }
588
+ });
589
+ }
590
+ }
682
591
 
683
- // Generate artifacts
684
- if (input.generateStudyGuide) {
685
592
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
686
- status: 'generating_study_guide',
593
+ status: 'analyzing',
687
594
  filename: primaryFile.name,
688
595
  fileType,
689
596
  startedAt: new Date().toISOString(),
@@ -694,11 +601,11 @@ export const workspace = router({
694
601
  },
695
602
  fileAnalysis: {
696
603
  order: 2,
697
- status: 'completed',
604
+ status: 'in_progress',
698
605
  },
699
606
  studyGuide: {
700
607
  order: 3,
701
- status: 'in_progress',
608
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
702
609
  },
703
610
  flashcards: {
704
611
  order: 4,
@@ -706,42 +613,242 @@ export const workspace = router({
706
613
  },
707
614
  }
708
615
  });
709
-
710
- const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
711
616
 
712
- let artifact = await ctx.db.artifact.findFirst({
713
- where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
714
- });
715
- if (!artifact) {
716
- const fileNames = files.map(f => f.name).join(', ');
717
- artifact = await ctx.db.artifact.create({
617
+ try {
618
+ // Analyze all files - use PDF analysis if any file is a PDF, otherwise use image analysis
619
+ // const hasPDF = files.some(f => !f.mimeType.startsWith('image/'));
620
+ // if (hasPDF) {
621
+ // await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id, file.id);
622
+ // } else {
623
+ // // If all files are images, analyze them
624
+ // for (const file of files) {
625
+ // await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id, file.id);
626
+ // }
627
+ // }
628
+
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
+ });
653
+ } 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
+ });
680
+ await ctx.db.workspace.update({
681
+ where: { id: input.workspaceId },
682
+ data: { fileBeingAnalyzed: false },
683
+ });
684
+ throw error;
685
+ }
686
+
687
+ const results: {
688
+ filename: string;
689
+ artifacts: {
690
+ studyGuide: any | null;
691
+ flashcards: any | null;
692
+ worksheet: any | null;
693
+ };
694
+ } = {
695
+ filename: primaryFile.name,
696
+ artifacts: {
697
+ studyGuide: null,
698
+ flashcards: null,
699
+ worksheet: null,
700
+ }
701
+ };
702
+
703
+ // Generate artifacts
704
+ 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
+ });
729
+
730
+ const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
731
+
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
+ },
744
+ });
745
+ }
746
+
747
+ const lastVersion = await ctx.db.artifactVersion.findFirst({
748
+ where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
749
+ orderBy: { version: 'desc' },
750
+ });
751
+
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
+ });
755
+
756
+ results.artifacts.studyGuide = artifact;
757
+ }
758
+
759
+ 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');
786
+
787
+ const artifact = await ctx.db.artifact.create({
718
788
  data: {
719
789
  workspaceId: input.workspaceId,
720
- type: ArtifactType.STUDY_GUIDE,
721
- title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
790
+ type: ArtifactType.FLASHCARD_SET,
791
+ title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
722
792
  createdById: ctx.session.user.id,
723
793
  },
724
794
  });
795
+
796
+ // Parse JSON flashcard content
797
+ try {
798
+ const flashcardData: any = content;
799
+
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}`;
805
+
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
+ }
817
+
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
+ await ctx.db.flashcard.create({
826
+ data: {
827
+ artifactId: artifact.id,
828
+ front: front.trim(),
829
+ back: back.trim(),
830
+ order: i,
831
+ tags: ['ai-generated', 'medium'],
832
+ },
833
+ });
834
+ }
835
+ }
836
+ }
837
+
838
+ results.artifacts.flashcards = artifact;
725
839
  }
726
-
727
- const lastVersion = await ctx.db.artifactVersion.findFirst({
728
- where: { artifact: {workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE} },
729
- orderBy: { version: 'desc' },
730
- });
731
840
 
732
- await ctx.db.artifactVersion.create({
733
- data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
841
+ await ctx.db.workspace.update({
842
+ where: { id: input.workspaceId },
843
+ data: { fileBeingAnalyzed: false },
734
844
  });
735
845
 
736
- results.artifacts.studyGuide = artifact;
737
- }
738
-
739
- if (input.generateFlashcards) {
740
846
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
741
- status: 'generating_flashcards',
847
+ status: 'completed',
742
848
  filename: primaryFile.name,
743
849
  fileType,
744
850
  startedAt: new Date().toISOString(),
851
+ completedAt: new Date().toISOString(),
745
852
  steps: {
746
853
  fileUpload: {
747
854
  order: 1,
@@ -757,149 +864,71 @@ export const workspace = router({
757
864
  },
758
865
  flashcards: {
759
866
  order: 4,
760
- status: 'in_progress',
867
+ status: input.generateFlashcards ? 'completed' : 'skipped',
761
868
  },
762
869
  }
870
+
763
871
  });
764
-
765
- const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
766
-
767
- const artifact = await ctx.db.artifact.create({
768
- data: {
769
- workspaceId: input.workspaceId,
770
- type: ArtifactType.FLASHCARD_SET,
771
- title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
772
- createdById: ctx.session.user.id,
773
- },
872
+ return results;
873
+ } catch (error) {
874
+ console.error('❌ Failed to update analysis progress:', error);
875
+ await ctx.db.workspace.update({
876
+ where: { id: input.workspaceId },
877
+ data: { fileBeingAnalyzed: false },
774
878
  });
775
-
776
- // Parse JSON flashcard content
777
- try {
778
- const flashcardData: any = content;
779
-
780
- let createdCards = 0;
781
- for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
782
- const card = flashcardData[i];
783
- const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
784
- const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
785
-
786
- await ctx.db.flashcard.create({
787
- data: {
788
- artifactId: artifact.id,
789
- front: front,
790
- back: back,
791
- order: i,
792
- tags: ['ai-generated', 'medium'],
879
+ await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
880
+ throw error;
881
+ }
882
+ }),
883
+ search: authedProcedure
884
+ .input(z.object({
885
+ query: z.string(),
886
+ limit: z.number().min(1).max(100).default(20),
887
+ }))
888
+ .query(async ({ ctx, input }) => {
889
+ const { query } = input;
890
+ const workspaces = await ctx.db.workspace.findMany({
891
+ where: {
892
+ ownerId: ctx.session.user.id,
893
+ OR: [
894
+ {
895
+ title: {
896
+ contains: query,
897
+ mode: 'insensitive',
793
898
  },
794
- });
795
- createdCards++;
796
- }
797
-
798
- } catch (parseError) {
799
- // Fallback to text parsing if JSON fails
800
- const lines = content.split('\n').filter(line => line.trim());
801
- for (let i = 0; i < Math.min(lines.length, 10); i++) {
802
- const line = lines[i];
803
- if (line.includes(' - ')) {
804
- const [front, back] = line.split(' - ');
805
- await ctx.db.flashcard.create({
806
- data: {
807
- artifactId: artifact.id,
808
- front: front.trim(),
809
- back: back.trim(),
810
- order: i,
811
- tags: ['ai-generated', 'medium'],
812
- },
813
- });
899
+ },
900
+ {
901
+ description: {
902
+ contains: query,
903
+ mode: 'insensitive',
904
+ },
905
+ },
906
+ ],
907
+ },
908
+ orderBy: {
909
+ updatedAt: 'desc',
910
+ },
911
+ take: input.limit,
912
+ });
913
+
914
+ // Update analysisProgress for each workspace with search metadata
915
+ const workspaceUpdates = workspaces.map(ws =>
916
+ ctx.db.workspace.update({
917
+ where: { id: ws.id },
918
+ data: {
919
+ analysisProgress: {
920
+ lastSearched: new Date().toISOString(),
921
+ searchQuery: query,
922
+ matchedIn: ws.title.toLowerCase().includes(query.toLowerCase()) ? 'title' : 'description',
814
923
  }
815
924
  }
816
- }
817
-
818
- results.artifacts.flashcards = artifact;
819
- }
925
+ })
926
+ );
820
927
 
821
- await ctx.db.workspace.update({
822
- where: { id: input.workspaceId },
823
- data: { fileBeingAnalyzed: false },
824
- });
928
+ await Promise.all(workspaceUpdates);
825
929
 
826
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
827
- status: 'completed',
828
- filename: primaryFile.name,
829
- fileType,
830
- startedAt: new Date().toISOString(),
831
- completedAt: new Date().toISOString(),
832
- steps: {
833
- fileUpload: {
834
- order: 1,
835
- status: 'completed',
836
- },
837
- fileAnalysis: {
838
- order: 2,
839
- status: 'completed',
840
- },
841
- studyGuide: {
842
- order: 3,
843
- status: input.generateStudyGuide ? 'completed' : 'skipped',
844
- },
845
- flashcards: {
846
- order: 4,
847
- status: input.generateFlashcards ? 'completed' : 'skipped',
848
- },
849
- }
850
- });
851
-
852
- return results;
930
+ return workspaces;
853
931
  }),
854
- search: authedProcedure
855
- .input(z.object({
856
- query: z.string(),
857
- limit: z.number().min(1).max(100).default(20),
858
- }))
859
- .query(async ({ ctx, input }) => {
860
- const { query } = input;
861
- const workspaces = await ctx.db.workspace.findMany({
862
- where: {
863
- ownerId: ctx.session.user.id,
864
- OR: [
865
- {
866
- title: {
867
- contains: query,
868
- mode: 'insensitive',
869
- },
870
- },
871
- {
872
- description: {
873
- contains: query,
874
- mode: 'insensitive',
875
- },
876
- },
877
- ],
878
- },
879
- orderBy: {
880
- updatedAt: 'desc',
881
- },
882
- take: input.limit,
883
- });
884
-
885
- // Update analysisProgress for each workspace with search metadata
886
- const workspaceUpdates = workspaces.map(ws =>
887
- ctx.db.workspace.update({
888
- where: { id: ws.id },
889
- data: {
890
- analysisProgress: {
891
- lastSearched: new Date().toISOString(),
892
- searchQuery: query,
893
- matchedIn: ws.title.toLowerCase().includes(query.toLowerCase()) ? 'title' : 'description',
894
- }
895
- }
896
- })
897
- );
898
-
899
- await Promise.all(workspaceUpdates);
900
-
901
- return workspaces;
902
- }),
903
932
 
904
933
  // Members sub-router
905
934
  members,