@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
  import type { PrismaClient } from '@prisma/client';
10
11
 
11
12
  // Helper function to update and emit analysis progress
@@ -25,17 +26,17 @@ async function updateAnalysisProgress(
25
26
  function calculateRelevance(query: string, ...texts: (string | null | undefined)[]): number {
26
27
  const queryLower = query.toLowerCase();
27
28
  let score = 0;
28
-
29
+
29
30
  for (const text of texts) {
30
31
  if (!text) continue;
31
-
32
+
32
33
  const textLower = text.toLowerCase();
33
-
34
+
34
35
  // Exact match gets highest score
35
36
  if (textLower.includes(queryLower)) {
36
37
  score += 10;
37
38
  }
38
-
39
+
39
40
  // Word boundary matches get good score
40
41
  const words = queryLower.split(/\s+/);
41
42
  for (const word of words) {
@@ -43,7 +44,7 @@ function calculateRelevance(query: string, ...texts: (string | null | undefined)
43
44
  score += 5;
44
45
  }
45
46
  }
46
-
47
+
47
48
  // Partial matches get lower score
48
49
  const queryChars = queryLower.split('');
49
50
  let consecutiveMatches = 0;
@@ -56,7 +57,7 @@ function calculateRelevance(query: string, ...texts: (string | null | undefined)
56
57
  }
57
58
  score += consecutiveMatches * 0.1;
58
59
  }
59
-
60
+
60
61
  return score;
61
62
  }
62
63
 
@@ -64,8 +65,8 @@ export const workspace = router({
64
65
  // List current user's workspaces
65
66
  list: authedProcedure
66
67
  .input(z.object({
67
- parentId: z.string().optional(),
68
- }))
68
+ parentId: z.string().optional(),
69
+ }))
69
70
  .query(async ({ ctx, input }) => {
70
71
  const workspaces = await ctx.db.workspace.findMany({
71
72
  where: {
@@ -84,14 +85,14 @@ export const workspace = router({
84
85
 
85
86
  return { workspaces, folders };
86
87
  }),
87
-
88
+
88
89
  create: authedProcedure
89
90
  .input(z.object({
90
- name: z.string().min(1).max(100),
91
- description: z.string().max(500).optional(),
92
- parentId: z.string().optional(),
93
- }))
94
- .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 }) => {
95
96
  const ws = await ctx.db.workspace.create({
96
97
  data: {
97
98
  title: input.name,
@@ -118,10 +119,10 @@ export const workspace = router({
118
119
  }),
119
120
  createFolder: authedProcedure
120
121
  .input(z.object({
121
- name: z.string().min(1).max(100),
122
- color: z.string().optional(),
123
- parentId: z.string().optional(),
124
- }))
122
+ name: z.string().min(1).max(100),
123
+ color: z.string().optional(),
124
+ parentId: z.string().optional(),
125
+ }))
125
126
  .mutation(async ({ ctx, input }) => {
126
127
  const folder = await ctx.db.folder.create({
127
128
  data: {
@@ -135,26 +136,26 @@ export const workspace = router({
135
136
  }),
136
137
  updateFolder: authedProcedure
137
138
  .input(z.object({
138
- id: z.string(),
139
- name: z.string().min(1).max(100).optional(),
140
- color: z.string().optional(),
141
- }))
139
+ id: z.string(),
140
+ name: z.string().min(1).max(100).optional(),
141
+ color: z.string().optional(),
142
+ }))
142
143
  .mutation(async ({ ctx, input }) => {
143
144
  const folder = await ctx.db.folder.update({ where: { id: input.id }, data: { name: input.name, color: input.color ?? '#9D00FF' } });
144
145
  return folder;
145
146
  }),
146
147
  deleteFolder: authedProcedure
147
148
  .input(z.object({
148
- id: z.string(),
149
- }))
149
+ id: z.string(),
150
+ }))
150
151
  .mutation(async ({ ctx, input }) => {
151
152
  const folder = await ctx.db.folder.delete({ where: { id: input.id } });
152
153
  return folder;
153
154
  }),
154
155
  get: authedProcedure
155
156
  .input(z.object({
156
- id: z.string(),
157
- }))
157
+ id: z.string(),
158
+ }))
158
159
  .query(async ({ ctx, input }) => {
159
160
  const ws = await ctx.db.workspace.findFirst({
160
161
  where: { id: input.id, ownerId: ctx.session.user.id },
@@ -170,13 +171,13 @@ export const workspace = router({
170
171
  getStats: authedProcedure
171
172
  .query(async ({ ctx }) => {
172
173
  const workspaces = await ctx.db.workspace.findMany({
173
- 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 } } }] },
174
175
  });
175
176
  const folders = await ctx.db.folder.findMany({
176
- where: {OR: [{ ownerId: ctx.session.user.id } ]},
177
+ where: { OR: [{ ownerId: ctx.session.user.id }] },
177
178
  });
178
179
  const lastUpdated = await ctx.db.workspace.findFirst({
179
- 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 } } }] },
180
181
  orderBy: { updatedAt: 'desc' },
181
182
  });
182
183
 
@@ -195,12 +196,12 @@ export const workspace = router({
195
196
  }),
196
197
  update: authedProcedure
197
198
  .input(z.object({
198
- id: z.string(),
199
- name: z.string().min(1).max(100).optional(),
200
- description: z.string().max(500).optional(),
201
- color: z.string().optional(),
202
- icon: z.string().optional(),
203
- }))
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
+ }))
204
205
  .mutation(async ({ ctx, input }) => {
205
206
  const existed = await ctx.db.workspace.findFirst({
206
207
  where: { id: input.id, ownerId: ctx.session.user.id },
@@ -215,12 +216,12 @@ export const workspace = router({
215
216
  icon: input.icon ?? existed.icon,
216
217
  },
217
218
  });
218
- return updated;
219
- }),
220
- delete: authedProcedure
219
+ return updated;
220
+ }),
221
+ delete: authedProcedure
221
222
  .input(z.object({
222
- id: z.string(),
223
- }))
223
+ id: z.string(),
224
+ }))
224
225
  .mutation(async ({ ctx, input }) => {
225
226
  const deleted = await ctx.db.workspace.deleteMany({
226
227
  where: { id: input.id, ownerId: ctx.session.user.id },
@@ -228,15 +229,15 @@ export const workspace = router({
228
229
  if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
229
230
  return true;
230
231
  }),
231
- getFolderInformation: authedProcedure
232
+ getFolderInformation: authedProcedure
232
233
  .input(z.object({
233
- id: z.string(),
234
- }))
234
+ id: z.string(),
235
+ }))
235
236
  .query(async ({ ctx, input }) => {
236
237
  const folder = await ctx.db.folder.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
237
238
  // find all of its parents
238
239
  if (!folder) throw new TRPCError({ code: 'NOT_FOUND' });
239
-
240
+
240
241
  const parents = [];
241
242
  let current = folder;
242
243
 
@@ -246,20 +247,36 @@ export const workspace = router({
246
247
  parents.push(parent);
247
248
  current = parent;
248
249
  }
249
-
250
+
250
251
  return { folder, parents };
251
252
  }),
252
- uploadFiles: authedProcedure
253
+
254
+ getSharedWith: authedProcedure
253
255
  .input(z.object({
254
- id: z.string(),
255
- files: z.array(
256
- z.object({
257
- filename: z.string().min(1).max(255),
258
- contentType: z.string().min(1).max(100),
259
- size: z.number().min(1), // size in bytes
260
- })
261
- ),
262
- }))
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
+ }))
263
280
  .mutation(async ({ ctx, input }) => {
264
281
  // ensure workspace belongs to user
265
282
  const ws = await ctx.db.workspace.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
@@ -274,7 +291,7 @@ export const workspace = router({
274
291
  name: file.filename,
275
292
  mimeType: file.contentType,
276
293
  size: file.size,
277
- workspaceId: input.id,
294
+ workspaceId: input.id,
278
295
  },
279
296
  });
280
297
 
@@ -285,9 +302,9 @@ export const workspace = router({
285
302
  .createSignedUploadUrl(objectKey); // 5 minutes
286
303
 
287
304
  if (signedUrlError) {
288
- throw new TRPCError({
289
- code: 'INTERNAL_SERVER_ERROR',
290
- 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}`
291
308
  });
292
309
  }
293
310
 
@@ -298,7 +315,7 @@ export const workspace = router({
298
315
  bucket: 'files',
299
316
  objectKey: objectKey,
300
317
  },
301
- });
318
+ });
302
319
 
303
320
  results.push({
304
321
  fileId: record.id,
@@ -309,11 +326,11 @@ export const workspace = router({
309
326
  return results;
310
327
 
311
328
  }),
312
- deleteFiles: authedProcedure
329
+ deleteFiles: authedProcedure
313
330
  .input(z.object({
314
- fileId: z.array(z.string()),
315
- id: z.string(),
316
- }))
331
+ fileId: z.array(z.string()),
332
+ id: z.string(),
333
+ }))
317
334
  .mutation(async ({ ctx, input }) => {
318
335
  // ensure files are in the user's workspace
319
336
  const files = await ctx.db.fileAsset.findMany({
@@ -344,7 +361,7 @@ export const workspace = router({
344
361
  });
345
362
  return true;
346
363
  }),
347
- getFileUploadUrl: authedProcedure
364
+ getFileUploadUrl: authedProcedure
348
365
  .input(z.object({
349
366
  workspaceId: z.string(),
350
367
  filename: z.string(),
@@ -353,7 +370,7 @@ export const workspace = router({
353
370
  }))
354
371
  .query(async ({ ctx, input }) => {
355
372
  const objectKey = `workspace_${ctx.session.user.id}/${input.workspaceId}-file_${input.filename}`;
356
- await ctx.db.fileAsset.create({
373
+ const fileAsset = await ctx.db.fileAsset.create({
357
374
  data: {
358
375
  workspaceId: input.workspaceId,
359
376
  name: input.filename,
@@ -370,31 +387,28 @@ export const workspace = router({
370
387
  if (signedUrlError) {
371
388
  throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
372
389
  }
373
- return signedUrlData.signedUrl;
390
+
391
+ await ctx.db.workspace.update({
392
+ where: { id: input.workspaceId },
393
+ data: { needsAnalysis: true },
394
+ });
395
+
396
+ return {
397
+ fileId: fileAsset.id,
398
+ uploadUrl: signedUrlData.signedUrl,
399
+ };
374
400
  }),
375
- uploadAndAnalyzeMedia: authedProcedure
401
+ uploadAndAnalyzeMedia: authedProcedure
376
402
  .input(z.object({
377
403
  workspaceId: z.string(),
378
- file: z.object({
379
- filename: z.string(),
380
- contentType: z.string(),
381
- size: z.number(),
382
- content: z.string(), // Base64 encoded file content
383
- }),
404
+ files: z.array(z.object({
405
+ id: z.string(),
406
+ })),
384
407
  generateStudyGuide: z.boolean().default(true),
385
408
  generateFlashcards: z.boolean().default(true),
386
409
  generateWorksheet: z.boolean().default(true),
387
410
  }))
388
411
  .mutation(async ({ ctx, input }) => {
389
- console.log('🚀 uploadAndAnalyzeMedia started', {
390
- workspaceId: input.workspaceId,
391
- filename: input.file.filename,
392
- fileSize: input.file.size,
393
- generateStudyGuide: input.generateStudyGuide,
394
- generateFlashcards: input.generateFlashcards,
395
- generateWorksheet: input.generateWorksheet
396
- });
397
-
398
412
  // Verify workspace ownership
399
413
  const workspace = await ctx.db.workspace.findFirst({
400
414
  where: { id: input.workspaceId, ownerId: ctx.session.user.id }
@@ -404,138 +418,108 @@ export const workspace = router({
404
418
  throw new TRPCError({ code: 'NOT_FOUND' });
405
419
  }
406
420
 
407
- const fileType = input.file.contentType.startsWith('image/') ? 'image' : 'pdf';
408
-
409
- await ctx.db.workspace.update({
410
- where: { id: input.workspaceId },
411
- data: { fileBeingAnalyzed: true },
421
+ // Check if analysis is already in progress
422
+ if (workspace.fileBeingAnalyzed) {
423
+ throw new TRPCError({
424
+ code: 'CONFLICT',
425
+ message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
426
+ });
427
+ }
428
+
429
+ // Fetch files from database
430
+ const files = await ctx.db.fileAsset.findMany({
431
+ where: {
432
+ id: { in: input.files.map(file => file.id) },
433
+ workspaceId: input.workspaceId,
434
+ userId: ctx.session.user.id,
435
+ },
412
436
  });
413
437
 
414
- try {
415
-
416
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
417
- status: 'starting',
418
- filename: input.file.filename,
419
- fileType,
420
- startedAt: new Date().toISOString(),
421
- steps: {
422
- fileUpload: {
423
- order: 1,
424
- status: 'pending',
425
- },
426
- fileAnalysis: {
427
- order: 2,
428
- status: 'pending',
429
- },
430
- studyGuide: {
431
- order: 3,
432
- status: input.generateStudyGuide ? 'pending' : 'skipped',
433
- },
434
- flashcards: {
435
- order: 4,
436
- status: input.generateFlashcards ? 'pending' : 'skipped',
437
- },
438
+ if (files.length === 0) {
439
+ throw new TRPCError({
440
+ code: 'NOT_FOUND',
441
+ message: 'No files found with the provided IDs'
442
+ });
443
+ }
444
+
445
+ // Validate all files have bucket and objectKey
446
+ for (const file of files) {
447
+ if (!file.bucket || !file.objectKey) {
448
+ throw new TRPCError({
449
+ code: 'BAD_REQUEST',
450
+ message: `File ${file.id} does not have bucket or objectKey set`
451
+ });
438
452
  }
439
- });
440
- } catch (error) {
441
- console.error('❌ Failed to update analysis progress:', error);
453
+ }
454
+
455
+ // Use the first file for progress tracking and artifact naming
456
+ const primaryFile = files[0];
457
+ const fileType = primaryFile.mimeType.startsWith('image/') ? 'image' : 'pdf';
458
+ try {
459
+ // Set analysis in progress flag
442
460
  await ctx.db.workspace.update({
443
461
  where: { id: input.workspaceId },
444
- data: { fileBeingAnalyzed: false },
462
+ data: { fileBeingAnalyzed: true },
445
463
  });
446
- await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
447
- throw error;
448
- }
449
464
 
450
- const fileBuffer = Buffer.from(input.file.content, 'base64');
451
-
452
- // // Check AI service health first
453
- // console.log('🏥 Checking AI service health...');
454
- // const isHealthy = await aiSessionService.checkHealth();
455
- // if (!isHealthy) {
456
- // console.error('❌ AI service is not available');
457
- // await PusherService.emitError(input.workspaceId, 'AI service is currently unavailable');
458
- // throw new TRPCError({
459
- // code: 'SERVICE_UNAVAILABLE',
460
- // message: 'AI service is currently unavailable. Please try again later.',
461
- // });
462
- // }
463
- // console.log('✅ AI service is healthy');
464
-
465
- const fileObj = new File([fileBuffer], input.file.filename, { type: input.file.contentType });
466
-
467
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
468
- status: 'uploading',
469
- filename: input.file.filename,
470
- fileType,
471
- startedAt: new Date().toISOString(),
472
- steps: {
473
- fileUpload: {
474
- order: 1,
475
- status: 'in_progress',
476
- },
477
- fileAnalysis: {
478
- order: 2,
479
- status: 'pending',
480
- },
481
- studyGuide: {
482
- order: 3,
483
- status: input.generateStudyGuide ? 'pending' : 'skipped',
484
- },
485
- flashcards: {
486
- order: 4,
487
- status: input.generateFlashcards ? 'pending' : 'skipped',
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' },
488
472
  },
489
- }
490
- });
473
+ });
491
474
 
492
- await aiSessionService.uploadFile(input.workspaceId, ctx.session.user.id, fileObj, fileType);
493
-
494
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
495
- status: 'analyzing',
496
- filename: input.file.filename,
497
- fileType,
498
- startedAt: new Date().toISOString(),
499
- steps: {
500
- fileUpload: {
501
- order: 1,
502
- status: 'completed',
503
- },
504
- fileAnalysis: {
505
- order: 2,
506
- status: 'in_progress',
507
- },
508
- studyGuide: {
509
- order: 3,
510
- status: input.generateStudyGuide ? 'pending' : 'skipped',
511
- },
512
- flashcards: {
513
- order: 4,
514
- status: input.generateFlashcards ? 'pending' : 'skipped',
515
- },
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);
502
+ await ctx.db.workspace.update({
503
+ where: { id: input.workspaceId },
504
+ data: { fileBeingAnalyzed: false },
505
+ });
506
+ await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
507
+ throw error;
516
508
  }
517
- });
518
509
 
519
- try {
520
- if (fileType === 'image') {
521
- await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id);
522
- } else {
523
- await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id );
524
- }
525
-
526
510
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
527
- status: 'generating_artifacts',
528
- filename: input.file.filename,
511
+ status: 'uploading',
512
+ filename: primaryFile.name,
529
513
  fileType,
530
514
  startedAt: new Date().toISOString(),
531
515
  steps: {
532
516
  fileUpload: {
533
517
  order: 1,
534
- status: 'completed',
518
+ status: 'in_progress',
535
519
  },
536
520
  fileAnalysis: {
537
521
  order: 2,
538
- status: 'completed',
522
+ status: 'pending',
539
523
  },
540
524
  studyGuide: {
541
525
  order: 3,
@@ -547,57 +531,67 @@ export const workspace = router({
547
531
  },
548
532
  }
549
533
  });
550
- } catch (error) {
551
- console.error('❌ Failed to analyze file:', error);
552
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
553
- status: 'error',
554
- filename: input.file.filename,
555
- fileType,
556
- error: `Failed to analyze ${fileType}: ${error}`,
557
- startedAt: new Date().toISOString(),
558
- steps: {
559
- fileUpload: {
560
- order: 1,
561
- status: 'completed',
562
- },
563
- fileAnalysis: {
564
- order: 2,
565
- status: 'error',
566
- },
567
- studyGuide: {
568
- order: 3,
569
- status: 'skipped',
570
- },
571
- flashcards: {
572
- order: 4,
573
- status: 'skipped',
574
- },
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)
575
540
  }
576
- });
577
- throw error;
578
- }
579
541
 
580
- const results: {
581
- filename: string;
582
- artifacts: {
583
- studyGuide: any | null;
584
- flashcards: any | null;
585
- worksheet: any | null;
586
- };
587
- } = {
588
- filename: input.file.filename,
589
- artifacts: {
590
- studyGuide: null,
591
- flashcards: null,
592
- worksheet: null,
593
- }
594
- };
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
+ }
595
591
 
596
- // Generate artifacts
597
- if (input.generateStudyGuide) {
598
592
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
599
- status: 'generating_study_guide',
600
- filename: input.file.filename,
593
+ status: 'analyzing',
594
+ filename: primaryFile.name,
601
595
  fileType,
602
596
  startedAt: new Date().toISOString(),
603
597
  steps: {
@@ -607,11 +601,11 @@ export const workspace = router({
607
601
  },
608
602
  fileAnalysis: {
609
603
  order: 2,
610
- status: 'completed',
604
+ status: 'in_progress',
611
605
  },
612
606
  studyGuide: {
613
607
  order: 3,
614
- status: 'in_progress',
608
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
615
609
  },
616
610
  flashcards: {
617
611
  order: 4,
@@ -619,41 +613,242 @@ export const workspace = router({
619
613
  },
620
614
  }
621
615
  });
622
-
623
- const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
624
616
 
625
- let artifact = await ctx.db.artifact.findFirst({
626
- where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
627
- });
628
- if (!artifact) {
629
- 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({
630
788
  data: {
631
789
  workspaceId: input.workspaceId,
632
- type: ArtifactType.STUDY_GUIDE,
633
- title: `Study Guide - ${input.file.filename}`,
790
+ type: ArtifactType.FLASHCARD_SET,
791
+ title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
634
792
  createdById: ctx.session.user.id,
635
793
  },
636
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;
637
839
  }
638
-
639
- const lastVersion = await ctx.db.artifactVersion.findFirst({
640
- where: { artifact: {workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE} },
641
- orderBy: { version: 'desc' },
642
- });
643
840
 
644
- await ctx.db.artifactVersion.create({
645
- 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 },
646
844
  });
647
845
 
648
- results.artifacts.studyGuide = artifact;
649
- }
650
-
651
- if (input.generateFlashcards) {
652
846
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
653
- status: 'generating_flashcards',
654
- filename: input.file.filename,
847
+ status: 'completed',
848
+ filename: primaryFile.name,
655
849
  fileType,
656
850
  startedAt: new Date().toISOString(),
851
+ completedAt: new Date().toISOString(),
657
852
  steps: {
658
853
  fileUpload: {
659
854
  order: 1,
@@ -669,149 +864,71 @@ export const workspace = router({
669
864
  },
670
865
  flashcards: {
671
866
  order: 4,
672
- status: 'in_progress',
867
+ status: input.generateFlashcards ? 'completed' : 'skipped',
673
868
  },
674
869
  }
870
+
675
871
  });
676
-
677
- const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
678
-
679
- const artifact = await ctx.db.artifact.create({
680
- data: {
681
- workspaceId: input.workspaceId,
682
- type: ArtifactType.FLASHCARD_SET,
683
- title: `Flashcards - ${input.file.filename}`,
684
- createdById: ctx.session.user.id,
685
- },
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 },
686
878
  });
687
-
688
- // Parse JSON flashcard content
689
- try {
690
- const flashcardData: any = content;
691
-
692
- let createdCards = 0;
693
- for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
694
- const card = flashcardData[i];
695
- const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
696
- const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
697
-
698
- await ctx.db.flashcard.create({
699
- data: {
700
- artifactId: artifact.id,
701
- front: front,
702
- back: back,
703
- order: i,
704
- 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',
705
898
  },
706
- });
707
- createdCards++;
708
- }
709
-
710
- } catch (parseError) {
711
- // Fallback to text parsing if JSON fails
712
- const lines = content.split('\n').filter(line => line.trim());
713
- for (let i = 0; i < Math.min(lines.length, 10); i++) {
714
- const line = lines[i];
715
- if (line.includes(' - ')) {
716
- const [front, back] = line.split(' - ');
717
- await ctx.db.flashcard.create({
718
- data: {
719
- artifactId: artifact.id,
720
- front: front.trim(),
721
- back: back.trim(),
722
- order: i,
723
- tags: ['ai-generated', 'medium'],
724
- },
725
- });
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',
726
923
  }
727
924
  }
728
- }
729
-
730
- results.artifacts.flashcards = artifact;
731
- }
925
+ })
926
+ );
732
927
 
733
- await ctx.db.workspace.update({
734
- where: { id: input.workspaceId },
735
- data: { fileBeingAnalyzed: false },
736
- });
928
+ await Promise.all(workspaceUpdates);
737
929
 
738
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
739
- status: 'completed',
740
- filename: input.file.filename,
741
- fileType,
742
- startedAt: new Date().toISOString(),
743
- completedAt: new Date().toISOString(),
744
- steps: {
745
- fileUpload: {
746
- order: 1,
747
- status: 'completed',
748
- },
749
- fileAnalysis: {
750
- order: 2,
751
- status: 'completed',
752
- },
753
- studyGuide: {
754
- order: 3,
755
- status: input.generateStudyGuide ? 'completed' : 'skipped',
756
- },
757
- flashcards: {
758
- order: 4,
759
- status: input.generateFlashcards ? 'completed' : 'skipped',
760
- },
761
- }
762
- });
763
-
764
- return results;
930
+ return workspaces;
765
931
  }),
766
- search: authedProcedure
767
- .input(z.object({
768
- query: z.string(),
769
- limit: z.number().min(1).max(100).default(20),
770
- }))
771
- .query(async ({ ctx, input }) => {
772
- const { query } = input;
773
- const workspaces = await ctx.db.workspace.findMany({
774
- where: {
775
- ownerId: ctx.session.user.id,
776
- OR: [
777
- {
778
- title: {
779
- contains: query,
780
- mode: 'insensitive',
781
- },
782
- },
783
- {
784
- description: {
785
- contains: query,
786
- mode: 'insensitive',
787
- },
788
- },
789
- ],
790
- },
791
- orderBy: {
792
- updatedAt: 'desc',
793
- },
794
- take: input.limit,
795
- });
796
-
797
- // Update analysisProgress for each workspace with search metadata
798
- const workspaceUpdates = workspaces.map(ws =>
799
- ctx.db.workspace.update({
800
- where: { id: ws.id },
801
- data: {
802
- analysisProgress: {
803
- lastSearched: new Date().toISOString(),
804
- searchQuery: query,
805
- matchedIn: ws.title.toLowerCase().includes(query.toLowerCase()) ? 'title' : 'description',
806
- }
807
- }
808
- })
809
- );
810
-
811
- await Promise.all(workspaceUpdates);
812
-
813
- return workspaces;
814
- }),
815
932
 
816
933
  // Members sub-router
817
934
  members,