@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 +1 -1
- package/prisma/schema.prisma +3 -0
- package/src/lib/ai-session.ts +100 -143
- package/src/routers/podcast.ts +0 -1
- package/src/routers/worksheets.ts +5 -4
- package/src/routers/workspace.ts +187 -71
package/package.json
CHANGED
package/prisma/schema.prisma
CHANGED
|
@@ -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
|
|
package/src/lib/ai-session.ts
CHANGED
|
@@ -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
|
-
//
|
|
119
|
-
async
|
|
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
|
-
|
|
145
|
+
|
|
146
|
+
// Mock mode - return fake processing result
|
|
127
147
|
if (MOCK_MODE) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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',
|
|
139
|
-
formData.append('
|
|
140
|
-
formData.append('
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
171
|
+
// Retry logic for file processing
|
|
172
|
+
const maxRetries = 3;
|
|
173
|
+
let lastError: Error | null = null;
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
package/src/routers/podcast.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
521
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'No mark scheme found for question' });
|
|
521
522
|
}
|
|
522
523
|
|
|
523
524
|
|
package/src/routers/workspace.ts
CHANGED
|
@@ -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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
449
|
+
PusherService.emitAnalysisProgress(input.workspaceId, {
|
|
389
450
|
status: 'starting',
|
|
390
|
-
filename:
|
|
451
|
+
filename: primaryFile.name,
|
|
391
452
|
fileType,
|
|
392
453
|
startedAt: new Date().toISOString(),
|
|
393
454
|
steps: {
|
|
394
|
-
fileUpload: {
|
|
395
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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:
|
|
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
|
|
634
|
+
console.error('❌ Failed to analyze files:', error);
|
|
524
635
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
525
636
|
status: 'error',
|
|
526
|
-
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:
|
|
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:
|
|
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 - ${
|
|
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:
|
|
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 - ${
|
|
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:
|
|
828
|
+
filename: primaryFile.name,
|
|
713
829
|
fileType,
|
|
714
830
|
startedAt: new Date().toISOString(),
|
|
715
831
|
completedAt: new Date().toISOString(),
|