@goscribe/server 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goscribe/server",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -130,6 +130,8 @@ model Workspace {
130
130
 
131
131
  analysisProgress Json?
132
132
 
133
+ needsAnalysis Boolean @default(false)
134
+
133
135
  // Raw uploads attached to this workspace
134
136
  uploads FileAsset[]
135
137
 
@@ -191,6 +193,7 @@ model FileAsset {
191
193
  objectKey String?
192
194
  url String? // optional if serving via signed GET per-view
193
195
  checksum String? // optional server-side integrity
196
+ aiTranscription String?
194
197
 
195
198
  meta Json? // arbitrary metadata
196
199
 
@@ -26,6 +26,19 @@ export interface AISession {
26
26
 
27
27
  const IMITATE_WAIT_TIME_MS = MOCK_MODE ? 1000 * 10 : 0;
28
28
 
29
+ export interface ProcessFileResult {
30
+ status: 'success' | 'error';
31
+ textContent: string | null;
32
+ imageDescriptions: Array<{
33
+ page: number;
34
+ description: string;
35
+ hasVisualContent: boolean;
36
+ }>;
37
+ comprehensiveDescription: string | null;
38
+ pageCount: number;
39
+ error?: string;
40
+ }
41
+
29
42
  export class AISessionService {
30
43
  private sessions = new Map<string, AISession>();
31
44
 
@@ -115,56 +128,107 @@ export class AISessionService {
115
128
  });
116
129
  }
117
130
 
118
- // Upload file to AI session
119
- async uploadFile(sessionId: string, user: string, file: File, fileType: 'image' | 'pdf'): Promise<void> {
131
+ // Process file (PDF/image) and return comprehensive text descriptions
132
+ async processFile(
133
+ sessionId: string,
134
+ user: string,
135
+ fileUrl: string,
136
+ fileType: 'image' | 'pdf',
137
+ maxPages?: number
138
+ ): Promise<ProcessFileResult> {
120
139
  const session = this.sessions.get(sessionId);
121
140
  if (!session) {
122
141
  throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
123
142
  }
124
143
 
125
144
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
126
- // Mock mode - simulate successful file upload
145
+
146
+ // Mock mode - return fake processing result
127
147
  if (MOCK_MODE) {
128
- console.log(`🎭 MOCK MODE: Uploading ${fileType} file "${file.name}" to session ${sessionId}`);
129
- session.files.push(file.name);
130
- session.updatedAt = new Date();
131
- this.sessions.set(sessionId, session);
132
- return;
148
+ logger.info(`🎭 MOCK MODE: Processing ${fileType} file from URL for session ${sessionId}`);
149
+ const mockPageCount = fileType === 'pdf' ? 15 : 1;
150
+ return {
151
+ status: 'success',
152
+ textContent: `Mock extracted text content from ${fileType} file. This would contain the full text extracted from the document.`,
153
+ imageDescriptions: Array.from({ length: mockPageCount }, (_, i) => ({
154
+ page: i + 1,
155
+ description: `Page ${i + 1} contains educational content with diagrams and text.`,
156
+ hasVisualContent: true,
157
+ })),
158
+ comprehensiveDescription: `DOCUMENT SUMMARY (${mockPageCount} ${mockPageCount === 1 ? 'page' : 'pages'})\n\nTEXT CONTENT:\nMock extracted text content...\n\nVISUAL CONTENT DESCRIPTIONS:\nPage-by-page descriptions of visual elements.`,
159
+ pageCount: mockPageCount,
160
+ };
133
161
  }
134
162
 
135
- const command = fileType === 'image' ? 'append_image' : 'append_pdflike';
136
-
137
163
  const formData = new FormData();
138
- formData.append('command', command);
139
- formData.append('file', file);
140
- formData.append('session', sessionId);
141
- formData.append('user', user);
142
- try {
143
- const response = await fetch(AI_SERVICE_URL, {
144
- method: 'POST',
145
- body: formData,
146
- });
164
+ formData.append('command', 'process_file');
165
+ formData.append('fileUrl', fileUrl);
166
+ formData.append('fileType', fileType);
167
+ if (maxPages) {
168
+ formData.append('maxPages', maxPages.toString());
169
+ }
147
170
 
148
- if (!response.ok) {
149
- throw new Error(`AI service error: ${response.status} ${response.statusText}`);
150
- }
171
+ // Retry logic for file processing
172
+ const maxRetries = 3;
173
+ let lastError: Error | null = null;
151
174
 
152
- const result = await response.json();
153
- console.log(`📋 Upload result:`, result);
154
- if (!result.message) {
155
- throw new Error(`AI service error: No response message`);
156
- }
175
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
176
+ try {
177
+ logger.info(`📄 Processing ${fileType} file attempt ${attempt}/${maxRetries} for session ${sessionId}`);
178
+
179
+ // Set timeout for large files (5 minutes)
180
+ const controller = new AbortController();
181
+ const timeoutId = setTimeout(() => controller.abort(), 300000); // 5 min timeout
157
182
 
158
- // Update session
159
- session.files.push(file.name);
160
- session.updatedAt = new Date();
161
- this.sessions.set(sessionId, session);
162
- } catch (error) {
163
- throw new TRPCError({
164
- code: 'INTERNAL_SERVER_ERROR',
165
- message: `Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}`,
166
- });
183
+ const response = await fetch(AI_SERVICE_URL, {
184
+ method: 'POST',
185
+ body: formData,
186
+ signal: controller.signal,
187
+ });
188
+
189
+ clearTimeout(timeoutId);
190
+
191
+ if (!response.ok) {
192
+ const errorText = await response.text();
193
+ logger.error(`❌ File processing error response:`, errorText);
194
+ throw new Error(`AI service error: ${response.status} ${response.statusText} - ${errorText}`);
195
+ }
196
+
197
+ const result = await response.json();
198
+ logger.info(`📋 File processing result: status=${result.status}, pageCount=${result.pageCount}`);
199
+
200
+ if (result.status === 'error') {
201
+ throw new Error(result.error || 'File processing failed');
202
+ }
203
+
204
+ // Update session
205
+ session.files.push(fileUrl);
206
+ session.updatedAt = new Date();
207
+ this.sessions.set(sessionId, session);
208
+
209
+ return result as ProcessFileResult;
210
+
211
+ } catch (error) {
212
+ lastError = error instanceof Error ? error : new Error('Unknown error');
213
+ logger.error(`❌ File processing attempt ${attempt} failed:`, lastError.message);
214
+
215
+ if (attempt < maxRetries) {
216
+ const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s
217
+ logger.info(`⏳ Retrying file processing in ${delay}ms...`);
218
+ await new Promise(resolve => setTimeout(resolve, delay));
219
+ }
220
+ }
167
221
  }
222
+
223
+ logger.error(`💥 All ${maxRetries} file processing attempts failed. Last error:`, lastError?.message);
224
+ return {
225
+ status: 'error',
226
+ textContent: null,
227
+ imageDescriptions: [],
228
+ comprehensiveDescription: null,
229
+ pageCount: 0,
230
+ error: `Failed to process file after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`,
231
+ };
168
232
  }
169
233
 
170
234
 
@@ -503,113 +567,6 @@ This mock study guide demonstrates the structure and format that would be genera
503
567
  }
504
568
 
505
569
 
506
- // Analyse PDF
507
- async analysePDF(sessionId: string, user: string): Promise<string> {
508
- const session = this.sessions.get(sessionId);
509
- if (!session) {
510
- throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
511
- }
512
-
513
- await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
514
- // Mock mode - return fake PDF analysis
515
- if (MOCK_MODE) {
516
- console.log(`🎭 MOCK MODE: Analysing PDF for session ${sessionId}`);
517
- return `Mock PDF Analysis Results:
518
-
519
- Document Type: Educational Material
520
- Pages: 15
521
- Language: English
522
- Key Topics Identified:
523
- - Introduction to Machine Learning
524
- - Data Preprocessing Techniques
525
- - Model Training and Validation
526
- - Performance Metrics
527
-
528
- Summary: This mock PDF analysis shows the structure and content that would be extracted from an uploaded PDF document. The analysis includes document metadata, key topics, and a summary of the content.
529
-
530
- Note: This is a mock response generated when DONT_TEST_INFERENCE=true`;
531
- }
532
-
533
- const formData = new FormData();
534
- formData.append('command', 'analyse_pdf');
535
- formData.append('session', sessionId);
536
- formData.append('user', user);
537
- try {
538
- const response = await fetch(AI_SERVICE_URL, {
539
- method: 'POST',
540
- body: formData,
541
- });
542
-
543
- if (!response.ok) {
544
- throw new Error(`AI service error: ${response.status} ${response.statusText}`);
545
- }
546
-
547
- const result = await response.json();
548
- return result.message || 'PDF analysis completed';
549
- } catch (error) {
550
- throw new TRPCError({
551
- code: 'INTERNAL_SERVER_ERROR',
552
- message: `Failed to analyse PDF: ${error instanceof Error ? error.message : 'Unknown error'}`,
553
- });
554
- }
555
- }
556
-
557
- // Analyse Image
558
- async analyseImage(sessionId: string, user: string): Promise<string> {
559
- const session = this.sessions.get(sessionId);
560
- if (!session) {
561
- throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
562
- }
563
-
564
- await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
565
- // Mock mode - return fake image analysis
566
- if (MOCK_MODE) {
567
- console.log(`🎭 MOCK MODE: Analysing image for session ${sessionId}`);
568
- return `Mock Image Analysis Results:
569
-
570
- Image Type: Educational Diagram
571
- Format: PNG
572
- Dimensions: 1920x1080
573
- Content Description:
574
- - Contains a flowchart or diagram
575
- - Shows a process or system architecture
576
- - Includes text labels and annotations
577
- - Educational or instructional content
578
-
579
- Key Elements Identified:
580
- - Process flow arrows
581
- - Decision points
582
- - Input/output elements
583
- - Descriptive text
584
-
585
- Summary: This mock image analysis demonstrates the type of content extraction that would be performed on uploaded images. The analysis identifies visual elements, text content, and overall structure.
586
-
587
- Note: This is a mock response generated when DONT_TEST_INFERENCE=true`;
588
- }
589
-
590
- const formData = new FormData();
591
- formData.append('command', 'analyse_img');
592
- formData.append('session', sessionId);
593
- formData.append('user', user);
594
- try {
595
- const response = await fetch(AI_SERVICE_URL, {
596
- method: 'POST',
597
- body: formData,
598
- });
599
-
600
- if (!response.ok) {
601
- throw new Error(`AI service error: ${response.status} ${response.statusText}`);
602
- }
603
-
604
- const result = await response.json();
605
- return result.message || 'Image analysis completed';
606
- } catch (error) {
607
- throw new TRPCError({
608
- code: 'INTERNAL_SERVER_ERROR',
609
- message: `Failed to analyse image: ${error instanceof Error ? error.message : 'Unknown error'}`,
610
- });
611
- }
612
- }
613
570
 
614
571
  async generatePodcastImage(sessionId: string, user: string, summary: string): Promise<string> {
615
572
 
@@ -877,7 +877,6 @@ export const podcast = router({
877
877
  updatedAt: segment.updatedAt,
878
878
  };
879
879
  }),
880
-
881
880
  // Get available voices for TTS
882
881
  getAvailableVoices: authedProcedure
883
882
  .query(async () => {
@@ -281,6 +281,7 @@ export const worksheets = router({
281
281
  userAnswer: input.answer,
282
282
  completedAt: input.completed ? new Date() : null,
283
283
  attempts: { increment: 1 },
284
+ correct: input.correct,
284
285
  },
285
286
  });
286
287
 
@@ -444,10 +445,10 @@ export const worksheets = router({
444
445
  answer,
445
446
  difficulty: (input.difficulty.toUpperCase()) as any,
446
447
  order: i,
448
+ type,
447
449
  meta: {
448
- type,
449
450
  options: options.length > 0 ? options : undefined,
450
- mark_scheme: problem.mark_scheme || undefined,
451
+ markScheme: problem.mark_scheme || undefined,
451
452
  },
452
453
  },
453
454
  });
@@ -490,7 +491,7 @@ export const worksheets = router({
490
491
 
491
492
  // Parse question meta to get mark_scheme
492
493
  const questionMeta = question.meta ? (typeof question.meta === 'object' ? question.meta : JSON.parse(question.meta as any)) : {} as any;
493
- const markScheme = questionMeta.mark_scheme;
494
+ const markScheme = questionMeta.markScheme;
494
495
 
495
496
  let isCorrect = false;
496
497
  let userMarkScheme = null;
@@ -517,7 +518,7 @@ export const worksheets = router({
517
518
  }
518
519
  } else {
519
520
  // Simple string comparison if no mark scheme
520
- isCorrect = question.answer === input.answer;
521
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'No mark scheme found for question' });
521
522
  }
522
523
 
523
524
 
@@ -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
@@ -344,29 +345,54 @@ export const workspace = router({
344
345
  });
345
346
  return true;
346
347
  }),
348
+ getFileUploadUrl: authedProcedure
349
+ .input(z.object({
350
+ workspaceId: z.string(),
351
+ filename: z.string(),
352
+ contentType: z.string(),
353
+ size: z.number(),
354
+ }))
355
+ .query(async ({ ctx, input }) => {
356
+ const objectKey = `workspace_${ctx.session.user.id}/${input.workspaceId}-file_${input.filename}`;
357
+ const fileAsset = await ctx.db.fileAsset.create({
358
+ data: {
359
+ workspaceId: input.workspaceId,
360
+ name: input.filename,
361
+ mimeType: input.contentType,
362
+ size: input.size,
363
+ userId: ctx.session.user.id,
364
+ bucket: 'media',
365
+ objectKey: objectKey,
366
+ },
367
+ });
368
+ const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
369
+ .from('media')
370
+ .createSignedUploadUrl(objectKey); // 5 minutes
371
+ if (signedUrlError) {
372
+ throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
373
+ }
374
+
375
+ await ctx.db.workspace.update({
376
+ where: { id: input.workspaceId },
377
+ data: { needsAnalysis: true },
378
+ });
379
+
380
+ return {
381
+ fileId: fileAsset.id,
382
+ uploadUrl: signedUrlData.signedUrl,
383
+ };
384
+ }),
347
385
  uploadAndAnalyzeMedia: authedProcedure
348
386
  .input(z.object({
349
387
  workspaceId: z.string(),
350
- file: z.object({
351
- filename: z.string(),
352
- contentType: z.string(),
353
- size: z.number(),
354
- content: z.string(), // Base64 encoded file content
355
- }),
388
+ files: z.array(z.object({
389
+ id: z.string(),
390
+ })),
356
391
  generateStudyGuide: z.boolean().default(true),
357
392
  generateFlashcards: z.boolean().default(true),
358
393
  generateWorksheet: z.boolean().default(true),
359
394
  }))
360
395
  .mutation(async ({ ctx, input }) => {
361
- console.log('🚀 uploadAndAnalyzeMedia started', {
362
- workspaceId: input.workspaceId,
363
- filename: input.file.filename,
364
- fileSize: input.file.size,
365
- generateStudyGuide: input.generateStudyGuide,
366
- generateFlashcards: input.generateFlashcards,
367
- generateWorksheet: input.generateWorksheet
368
- });
369
-
370
396
  // Verify workspace ownership
371
397
  const workspace = await ctx.db.workspace.findFirst({
372
398
  where: { id: input.workspaceId, ownerId: ctx.session.user.id }
@@ -376,39 +402,85 @@ export const workspace = router({
376
402
  throw new TRPCError({ code: 'NOT_FOUND' });
377
403
  }
378
404
 
379
- const fileType = input.file.contentType.startsWith('image/') ? 'image' : 'pdf';
405
+ // Check if analysis is already in progress
406
+ 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.'
410
+ });
411
+ }
412
+
413
+ // Fetch files from database
414
+ const files = await ctx.db.fileAsset.findMany({
415
+ where: {
416
+ id: { in: input.files.map(file => file.id) },
417
+ workspaceId: input.workspaceId,
418
+ userId: ctx.session.user.id,
419
+ },
420
+ });
421
+
422
+ if (files.length === 0) {
423
+ throw new TRPCError({
424
+ code: 'NOT_FOUND',
425
+ message: 'No files found with the provided IDs'
426
+ });
427
+ }
428
+
429
+ // Validate all files have bucket and objectKey
430
+ for (const file of files) {
431
+ 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`
435
+ });
436
+ }
437
+ }
438
+
439
+ // Use the first file for progress tracking and artifact naming
440
+ const primaryFile = files[0];
441
+ const fileType = primaryFile.mimeType.startsWith('image/') ? 'image' : 'pdf';
380
442
 
443
+ // Set analysis in progress flag
381
444
  await ctx.db.workspace.update({
382
445
  where: { id: input.workspaceId },
383
446
  data: { fileBeingAnalyzed: true },
384
447
  });
385
448
 
386
- try {
387
-
388
- await updateAnalysisProgress(ctx.db, input.workspaceId, {
449
+ PusherService.emitAnalysisProgress(input.workspaceId, {
389
450
  status: 'starting',
390
- filename: input.file.filename,
451
+ filename: primaryFile.name,
391
452
  fileType,
392
453
  startedAt: new Date().toISOString(),
393
454
  steps: {
394
- fileUpload: {
395
- order: 1,
396
- status: 'pending',
397
- },
398
- fileAnalysis: {
399
- order: 2,
400
- status: 'pending',
401
- },
402
- studyGuide: {
403
- order: 3,
404
- status: input.generateStudyGuide ? 'pending' : 'skipped',
405
- },
406
- flashcards: {
407
- order: 4,
408
- status: input.generateFlashcards ? 'pending' : 'skipped',
409
- },
410
- }
455
+ fileUpload: { order: 1, status: 'pending' },
456
+ },
411
457
  });
458
+
459
+ try {
460
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
461
+ status: 'starting',
462
+ filename: primaryFile.name,
463
+ fileType,
464
+ startedAt: new Date().toISOString(),
465
+ 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
+ });
412
484
  } catch (error) {
413
485
  console.error('❌ Failed to update analysis progress:', error);
414
486
  await ctx.db.workspace.update({
@@ -419,26 +491,9 @@ export const workspace = router({
419
491
  throw error;
420
492
  }
421
493
 
422
- const fileBuffer = Buffer.from(input.file.content, 'base64');
423
-
424
- // // Check AI service health first
425
- // console.log('🏥 Checking AI service health...');
426
- // const isHealthy = await aiSessionService.checkHealth();
427
- // if (!isHealthy) {
428
- // console.error('❌ AI service is not available');
429
- // await PusherService.emitError(input.workspaceId, 'AI service is currently unavailable');
430
- // throw new TRPCError({
431
- // code: 'SERVICE_UNAVAILABLE',
432
- // message: 'AI service is currently unavailable. Please try again later.',
433
- // });
434
- // }
435
- // console.log('✅ AI service is healthy');
436
-
437
- const fileObj = new File([fileBuffer], input.file.filename, { type: input.file.contentType });
438
-
439
494
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
440
495
  status: 'uploading',
441
- filename: input.file.filename,
496
+ filename: primaryFile.name,
442
497
  fileType,
443
498
  startedAt: new Date().toISOString(),
444
499
  steps: {
@@ -461,11 +516,62 @@ export const workspace = router({
461
516
  }
462
517
  });
463
518
 
464
- await aiSessionService.uploadFile(input.workspaceId, ctx.session.user.id, fileObj, fileType);
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) {
531
+ await ctx.db.workspace.update({
532
+ where: { id: input.workspaceId },
533
+ data: { fileBeingAnalyzed: false },
534
+ });
535
+ throw new TRPCError({
536
+ code: 'INTERNAL_SERVER_ERROR',
537
+ message: `Failed to generate signed URL for file ${file.name}: ${signedUrlError.message}`
538
+ });
539
+ }
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
+ }
465
571
 
466
572
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
467
573
  status: 'analyzing',
468
- filename: input.file.filename,
574
+ filename: primaryFile.name,
469
575
  fileType,
470
576
  startedAt: new Date().toISOString(),
471
577
  steps: {
@@ -489,15 +595,20 @@ export const workspace = router({
489
595
  });
490
596
 
491
597
  try {
492
- if (fileType === 'image') {
493
- await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id);
494
- } else {
495
- await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id );
496
- }
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
+ // }
497
608
 
498
609
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
499
610
  status: 'generating_artifacts',
500
- filename: input.file.filename,
611
+ filename: primaryFile.name,
501
612
  fileType,
502
613
  startedAt: new Date().toISOString(),
503
614
  steps: {
@@ -520,10 +631,10 @@ export const workspace = router({
520
631
  }
521
632
  });
522
633
  } catch (error) {
523
- console.error('❌ Failed to analyze file:', error);
634
+ console.error('❌ Failed to analyze files:', error);
524
635
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
525
636
  status: 'error',
526
- filename: input.file.filename,
637
+ filename: primaryFile.name,
527
638
  fileType,
528
639
  error: `Failed to analyze ${fileType}: ${error}`,
529
640
  startedAt: new Date().toISOString(),
@@ -546,6 +657,10 @@ export const workspace = router({
546
657
  },
547
658
  }
548
659
  });
660
+ await ctx.db.workspace.update({
661
+ where: { id: input.workspaceId },
662
+ data: { fileBeingAnalyzed: false },
663
+ });
549
664
  throw error;
550
665
  }
551
666
 
@@ -557,7 +672,7 @@ export const workspace = router({
557
672
  worksheet: any | null;
558
673
  };
559
674
  } = {
560
- filename: input.file.filename,
675
+ filename: primaryFile.name,
561
676
  artifacts: {
562
677
  studyGuide: null,
563
678
  flashcards: null,
@@ -569,7 +684,7 @@ export const workspace = router({
569
684
  if (input.generateStudyGuide) {
570
685
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
571
686
  status: 'generating_study_guide',
572
- filename: input.file.filename,
687
+ filename: primaryFile.name,
573
688
  fileType,
574
689
  startedAt: new Date().toISOString(),
575
690
  steps: {
@@ -598,11 +713,12 @@ export const workspace = router({
598
713
  where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
599
714
  });
600
715
  if (!artifact) {
716
+ const fileNames = files.map(f => f.name).join(', ');
601
717
  artifact = await ctx.db.artifact.create({
602
718
  data: {
603
719
  workspaceId: input.workspaceId,
604
720
  type: ArtifactType.STUDY_GUIDE,
605
- title: `Study Guide - ${input.file.filename}`,
721
+ title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
606
722
  createdById: ctx.session.user.id,
607
723
  },
608
724
  });
@@ -623,7 +739,7 @@ export const workspace = router({
623
739
  if (input.generateFlashcards) {
624
740
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
625
741
  status: 'generating_flashcards',
626
- filename: input.file.filename,
742
+ filename: primaryFile.name,
627
743
  fileType,
628
744
  startedAt: new Date().toISOString(),
629
745
  steps: {
@@ -652,7 +768,7 @@ export const workspace = router({
652
768
  data: {
653
769
  workspaceId: input.workspaceId,
654
770
  type: ArtifactType.FLASHCARD_SET,
655
- title: `Flashcards - ${input.file.filename}`,
771
+ title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
656
772
  createdById: ctx.session.user.id,
657
773
  },
658
774
  });
@@ -709,7 +825,7 @@ export const workspace = router({
709
825
 
710
826
  await updateAnalysisProgress(ctx.db, input.workspaceId, {
711
827
  status: 'completed',
712
- filename: input.file.filename,
828
+ filename: primaryFile.name,
713
829
  fileType,
714
830
  startedAt: new Date().toISOString(),
715
831
  completedAt: new Date().toISOString(),