@goscribe/server 1.1.1 → 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 +3 -2
- package/src/routers/workspace.ts +161 -73
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
|
|
|
@@ -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
|
|
@@ -353,7 +354,7 @@ export const workspace = router({
|
|
|
353
354
|
}))
|
|
354
355
|
.query(async ({ ctx, input }) => {
|
|
355
356
|
const objectKey = `workspace_${ctx.session.user.id}/${input.workspaceId}-file_${input.filename}`;
|
|
356
|
-
await ctx.db.fileAsset.create({
|
|
357
|
+
const fileAsset = await ctx.db.fileAsset.create({
|
|
357
358
|
data: {
|
|
358
359
|
workspaceId: input.workspaceId,
|
|
359
360
|
name: input.filename,
|
|
@@ -370,31 +371,28 @@ export const workspace = router({
|
|
|
370
371
|
if (signedUrlError) {
|
|
371
372
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
|
|
372
373
|
}
|
|
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
|
+
};
|
|
374
384
|
}),
|
|
375
385
|
uploadAndAnalyzeMedia: authedProcedure
|
|
376
386
|
.input(z.object({
|
|
377
387
|
workspaceId: z.string(),
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
size: z.number(),
|
|
382
|
-
content: z.string(), // Base64 encoded file content
|
|
383
|
-
}),
|
|
388
|
+
files: z.array(z.object({
|
|
389
|
+
id: z.string(),
|
|
390
|
+
})),
|
|
384
391
|
generateStudyGuide: z.boolean().default(true),
|
|
385
392
|
generateFlashcards: z.boolean().default(true),
|
|
386
393
|
generateWorksheet: z.boolean().default(true),
|
|
387
394
|
}))
|
|
388
395
|
.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
396
|
// Verify workspace ownership
|
|
399
397
|
const workspace = await ctx.db.workspace.findFirst({
|
|
400
398
|
where: { id: input.workspaceId, ownerId: ctx.session.user.id }
|
|
@@ -404,39 +402,85 @@ export const workspace = router({
|
|
|
404
402
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
405
403
|
}
|
|
406
404
|
|
|
407
|
-
|
|
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';
|
|
408
442
|
|
|
443
|
+
// Set analysis in progress flag
|
|
409
444
|
await ctx.db.workspace.update({
|
|
410
445
|
where: { id: input.workspaceId },
|
|
411
446
|
data: { fileBeingAnalyzed: true },
|
|
412
447
|
});
|
|
413
448
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
449
|
+
PusherService.emitAnalysisProgress(input.workspaceId, {
|
|
417
450
|
status: 'starting',
|
|
418
|
-
filename:
|
|
451
|
+
filename: primaryFile.name,
|
|
419
452
|
fileType,
|
|
420
453
|
startedAt: new Date().toISOString(),
|
|
421
454
|
steps: {
|
|
422
|
-
fileUpload: {
|
|
423
|
-
|
|
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
|
-
}
|
|
455
|
+
fileUpload: { order: 1, status: 'pending' },
|
|
456
|
+
},
|
|
439
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
|
+
});
|
|
440
484
|
} catch (error) {
|
|
441
485
|
console.error('❌ Failed to update analysis progress:', error);
|
|
442
486
|
await ctx.db.workspace.update({
|
|
@@ -447,26 +491,9 @@ export const workspace = router({
|
|
|
447
491
|
throw error;
|
|
448
492
|
}
|
|
449
493
|
|
|
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
494
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
468
495
|
status: 'uploading',
|
|
469
|
-
filename:
|
|
496
|
+
filename: primaryFile.name,
|
|
470
497
|
fileType,
|
|
471
498
|
startedAt: new Date().toISOString(),
|
|
472
499
|
steps: {
|
|
@@ -489,11 +516,62 @@ export const workspace = router({
|
|
|
489
516
|
}
|
|
490
517
|
});
|
|
491
518
|
|
|
492
|
-
|
|
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
|
+
}
|
|
493
571
|
|
|
494
572
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
495
573
|
status: 'analyzing',
|
|
496
|
-
filename:
|
|
574
|
+
filename: primaryFile.name,
|
|
497
575
|
fileType,
|
|
498
576
|
startedAt: new Date().toISOString(),
|
|
499
577
|
steps: {
|
|
@@ -517,15 +595,20 @@ export const workspace = router({
|
|
|
517
595
|
});
|
|
518
596
|
|
|
519
597
|
try {
|
|
520
|
-
if
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
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
|
+
// }
|
|
525
608
|
|
|
526
609
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
527
610
|
status: 'generating_artifacts',
|
|
528
|
-
filename:
|
|
611
|
+
filename: primaryFile.name,
|
|
529
612
|
fileType,
|
|
530
613
|
startedAt: new Date().toISOString(),
|
|
531
614
|
steps: {
|
|
@@ -548,10 +631,10 @@ export const workspace = router({
|
|
|
548
631
|
}
|
|
549
632
|
});
|
|
550
633
|
} catch (error) {
|
|
551
|
-
console.error('❌ Failed to analyze
|
|
634
|
+
console.error('❌ Failed to analyze files:', error);
|
|
552
635
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
553
636
|
status: 'error',
|
|
554
|
-
filename:
|
|
637
|
+
filename: primaryFile.name,
|
|
555
638
|
fileType,
|
|
556
639
|
error: `Failed to analyze ${fileType}: ${error}`,
|
|
557
640
|
startedAt: new Date().toISOString(),
|
|
@@ -574,6 +657,10 @@ export const workspace = router({
|
|
|
574
657
|
},
|
|
575
658
|
}
|
|
576
659
|
});
|
|
660
|
+
await ctx.db.workspace.update({
|
|
661
|
+
where: { id: input.workspaceId },
|
|
662
|
+
data: { fileBeingAnalyzed: false },
|
|
663
|
+
});
|
|
577
664
|
throw error;
|
|
578
665
|
}
|
|
579
666
|
|
|
@@ -585,7 +672,7 @@ export const workspace = router({
|
|
|
585
672
|
worksheet: any | null;
|
|
586
673
|
};
|
|
587
674
|
} = {
|
|
588
|
-
filename:
|
|
675
|
+
filename: primaryFile.name,
|
|
589
676
|
artifacts: {
|
|
590
677
|
studyGuide: null,
|
|
591
678
|
flashcards: null,
|
|
@@ -597,7 +684,7 @@ export const workspace = router({
|
|
|
597
684
|
if (input.generateStudyGuide) {
|
|
598
685
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
599
686
|
status: 'generating_study_guide',
|
|
600
|
-
filename:
|
|
687
|
+
filename: primaryFile.name,
|
|
601
688
|
fileType,
|
|
602
689
|
startedAt: new Date().toISOString(),
|
|
603
690
|
steps: {
|
|
@@ -626,11 +713,12 @@ export const workspace = router({
|
|
|
626
713
|
where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
|
|
627
714
|
});
|
|
628
715
|
if (!artifact) {
|
|
716
|
+
const fileNames = files.map(f => f.name).join(', ');
|
|
629
717
|
artifact = await ctx.db.artifact.create({
|
|
630
718
|
data: {
|
|
631
719
|
workspaceId: input.workspaceId,
|
|
632
720
|
type: ArtifactType.STUDY_GUIDE,
|
|
633
|
-
title: `Study Guide - ${
|
|
721
|
+
title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
|
|
634
722
|
createdById: ctx.session.user.id,
|
|
635
723
|
},
|
|
636
724
|
});
|
|
@@ -651,7 +739,7 @@ export const workspace = router({
|
|
|
651
739
|
if (input.generateFlashcards) {
|
|
652
740
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
653
741
|
status: 'generating_flashcards',
|
|
654
|
-
filename:
|
|
742
|
+
filename: primaryFile.name,
|
|
655
743
|
fileType,
|
|
656
744
|
startedAt: new Date().toISOString(),
|
|
657
745
|
steps: {
|
|
@@ -680,7 +768,7 @@ export const workspace = router({
|
|
|
680
768
|
data: {
|
|
681
769
|
workspaceId: input.workspaceId,
|
|
682
770
|
type: ArtifactType.FLASHCARD_SET,
|
|
683
|
-
title: `Flashcards - ${
|
|
771
|
+
title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
|
|
684
772
|
createdById: ctx.session.user.id,
|
|
685
773
|
},
|
|
686
774
|
});
|
|
@@ -737,7 +825,7 @@ export const workspace = router({
|
|
|
737
825
|
|
|
738
826
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
739
827
|
status: 'completed',
|
|
740
|
-
filename:
|
|
828
|
+
filename: primaryFile.name,
|
|
741
829
|
fileType,
|
|
742
830
|
startedAt: new Date().toISOString(),
|
|
743
831
|
completedAt: new Date().toISOString(),
|