@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.
- package/dist/lib/ai-session.d.ts +13 -3
- package/dist/lib/ai-session.js +66 -146
- package/dist/lib/pusher.js +1 -1
- package/dist/routers/_app.d.ts +114 -7
- package/dist/routers/chat.js +2 -23
- package/dist/routers/flashcards.d.ts +25 -1
- package/dist/routers/flashcards.js +0 -14
- package/dist/routers/members.d.ts +18 -0
- package/dist/routers/members.js +14 -1
- package/dist/routers/worksheets.js +5 -4
- package/dist/routers/workspace.d.ts +89 -6
- package/dist/routers/workspace.js +389 -259
- package/dist/services/flashcard-progress.service.d.ts +25 -1
- package/dist/services/flashcard-progress.service.js +70 -31
- package/package.json +2 -2
- package/prisma/schema.prisma +14 -1
- package/src/lib/ai-session.ts +97 -158
- package/src/routers/flashcards.ts +0 -16
- package/src/routers/members.ts +13 -2
- package/src/routers/podcast.ts +0 -1
- package/src/routers/worksheets.ts +3 -2
- package/src/routers/workspace.ts +516 -399
- package/ANALYSIS_PROGRESS_SPEC.md +0 -463
- package/PROGRESS_QUICK_REFERENCE.md +0 -239
- package/dist/lib/podcast-prompts.d.ts +0 -43
- package/dist/lib/podcast-prompts.js +0 -135
- package/dist/routers/ai-session.d.ts +0 -0
- package/dist/routers/ai-session.js +0 -1
- package/dist/services/flashcard.service.d.ts +0 -183
- package/dist/services/flashcard.service.js +0 -224
- package/dist/services/podcast-segment-reorder.d.ts +0 -0
- package/dist/services/podcast-segment-reorder.js +0 -107
- package/dist/services/podcast.service.d.ts +0 -0
- package/dist/services/podcast.service.js +0 -326
- package/dist/services/worksheet.service.d.ts +0 -0
- 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
|
// Helper function to update and emit analysis progress
|
|
10
11
|
async function updateAnalysisProgress(db, workspaceId, progress) {
|
|
11
12
|
await db.workspace.update({
|
|
@@ -233,6 +234,20 @@ export const workspace = router({
|
|
|
233
234
|
}
|
|
234
235
|
return { folder, parents };
|
|
235
236
|
}),
|
|
237
|
+
getSharedWith: authedProcedure
|
|
238
|
+
.input(z.object({
|
|
239
|
+
id: z.string(),
|
|
240
|
+
}))
|
|
241
|
+
.query(async ({ ctx, input }) => {
|
|
242
|
+
const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
|
|
243
|
+
if (!user || !user.email)
|
|
244
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
245
|
+
const sharedWith = await ctx.db.workspace.findMany({ where: { members: { some: { userId: ctx.session.user.id } } } });
|
|
246
|
+
const invitations = await ctx.db.workspaceInvitation.findMany({ where: { email: user.email, acceptedAt: null }, include: {
|
|
247
|
+
workspace: true,
|
|
248
|
+
} });
|
|
249
|
+
return { shared: sharedWith, invitations };
|
|
250
|
+
}),
|
|
236
251
|
uploadFiles: authedProcedure
|
|
237
252
|
.input(z.object({
|
|
238
253
|
id: z.string(),
|
|
@@ -319,28 +334,52 @@ export const workspace = router({
|
|
|
319
334
|
});
|
|
320
335
|
return true;
|
|
321
336
|
}),
|
|
337
|
+
getFileUploadUrl: authedProcedure
|
|
338
|
+
.input(z.object({
|
|
339
|
+
workspaceId: z.string(),
|
|
340
|
+
filename: z.string(),
|
|
341
|
+
contentType: z.string(),
|
|
342
|
+
size: z.number(),
|
|
343
|
+
}))
|
|
344
|
+
.query(async ({ ctx, input }) => {
|
|
345
|
+
const objectKey = `workspace_${ctx.session.user.id}/${input.workspaceId}-file_${input.filename}`;
|
|
346
|
+
const fileAsset = await ctx.db.fileAsset.create({
|
|
347
|
+
data: {
|
|
348
|
+
workspaceId: input.workspaceId,
|
|
349
|
+
name: input.filename,
|
|
350
|
+
mimeType: input.contentType,
|
|
351
|
+
size: input.size,
|
|
352
|
+
userId: ctx.session.user.id,
|
|
353
|
+
bucket: 'media',
|
|
354
|
+
objectKey: objectKey,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
358
|
+
.from('media')
|
|
359
|
+
.createSignedUploadUrl(objectKey); // 5 minutes
|
|
360
|
+
if (signedUrlError) {
|
|
361
|
+
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
|
|
362
|
+
}
|
|
363
|
+
await ctx.db.workspace.update({
|
|
364
|
+
where: { id: input.workspaceId },
|
|
365
|
+
data: { needsAnalysis: true },
|
|
366
|
+
});
|
|
367
|
+
return {
|
|
368
|
+
fileId: fileAsset.id,
|
|
369
|
+
uploadUrl: signedUrlData.signedUrl,
|
|
370
|
+
};
|
|
371
|
+
}),
|
|
322
372
|
uploadAndAnalyzeMedia: authedProcedure
|
|
323
373
|
.input(z.object({
|
|
324
374
|
workspaceId: z.string(),
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
size: z.number(),
|
|
329
|
-
content: z.string(), // Base64 encoded file content
|
|
330
|
-
}),
|
|
375
|
+
files: z.array(z.object({
|
|
376
|
+
id: z.string(),
|
|
377
|
+
})),
|
|
331
378
|
generateStudyGuide: z.boolean().default(true),
|
|
332
379
|
generateFlashcards: z.boolean().default(true),
|
|
333
380
|
generateWorksheet: z.boolean().default(true),
|
|
334
381
|
}))
|
|
335
382
|
.mutation(async ({ ctx, input }) => {
|
|
336
|
-
console.log('🚀 uploadAndAnalyzeMedia started', {
|
|
337
|
-
workspaceId: input.workspaceId,
|
|
338
|
-
filename: input.file.filename,
|
|
339
|
-
fileSize: input.file.size,
|
|
340
|
-
generateStudyGuide: input.generateStudyGuide,
|
|
341
|
-
generateFlashcards: input.generateFlashcards,
|
|
342
|
-
generateWorksheet: input.generateWorksheet
|
|
343
|
-
});
|
|
344
383
|
// Verify workspace ownership
|
|
345
384
|
const workspace = await ctx.db.workspace.findFirst({
|
|
346
385
|
where: { id: input.workspaceId, ownerId: ctx.session.user.id }
|
|
@@ -349,129 +388,102 @@ export const workspace = router({
|
|
|
349
388
|
console.error('❌ Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
|
|
350
389
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
351
390
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
391
|
+
// Check if analysis is already in progress
|
|
392
|
+
if (workspace.fileBeingAnalyzed) {
|
|
393
|
+
throw new TRPCError({
|
|
394
|
+
code: 'CONFLICT',
|
|
395
|
+
message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
// Fetch files from database
|
|
399
|
+
const files = await ctx.db.fileAsset.findMany({
|
|
400
|
+
where: {
|
|
401
|
+
id: { in: input.files.map(file => file.id) },
|
|
402
|
+
workspaceId: input.workspaceId,
|
|
403
|
+
userId: ctx.session.user.id,
|
|
404
|
+
},
|
|
356
405
|
});
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
fileType,
|
|
362
|
-
startedAt: new Date().toISOString(),
|
|
363
|
-
steps: {
|
|
364
|
-
fileUpload: {
|
|
365
|
-
order: 1,
|
|
366
|
-
status: 'pending',
|
|
367
|
-
},
|
|
368
|
-
fileAnalysis: {
|
|
369
|
-
order: 2,
|
|
370
|
-
status: 'pending',
|
|
371
|
-
},
|
|
372
|
-
studyGuide: {
|
|
373
|
-
order: 3,
|
|
374
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
375
|
-
},
|
|
376
|
-
flashcards: {
|
|
377
|
-
order: 4,
|
|
378
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
379
|
-
},
|
|
380
|
-
}
|
|
406
|
+
if (files.length === 0) {
|
|
407
|
+
throw new TRPCError({
|
|
408
|
+
code: 'NOT_FOUND',
|
|
409
|
+
message: 'No files found with the provided IDs'
|
|
381
410
|
});
|
|
382
411
|
}
|
|
383
|
-
|
|
384
|
-
|
|
412
|
+
// Validate all files have bucket and objectKey
|
|
413
|
+
for (const file of files) {
|
|
414
|
+
if (!file.bucket || !file.objectKey) {
|
|
415
|
+
throw new TRPCError({
|
|
416
|
+
code: 'BAD_REQUEST',
|
|
417
|
+
message: `File ${file.id} does not have bucket or objectKey set`
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Use the first file for progress tracking and artifact naming
|
|
422
|
+
const primaryFile = files[0];
|
|
423
|
+
const fileType = primaryFile.mimeType.startsWith('image/') ? 'image' : 'pdf';
|
|
424
|
+
try {
|
|
425
|
+
// Set analysis in progress flag
|
|
385
426
|
await ctx.db.workspace.update({
|
|
386
427
|
where: { id: input.workspaceId },
|
|
387
|
-
data: { fileBeingAnalyzed:
|
|
428
|
+
data: { fileBeingAnalyzed: true },
|
|
388
429
|
});
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
// if (!isHealthy) {
|
|
397
|
-
// console.error('❌ AI service is not available');
|
|
398
|
-
// await PusherService.emitError(input.workspaceId, 'AI service is currently unavailable');
|
|
399
|
-
// throw new TRPCError({
|
|
400
|
-
// code: 'SERVICE_UNAVAILABLE',
|
|
401
|
-
// message: 'AI service is currently unavailable. Please try again later.',
|
|
402
|
-
// });
|
|
403
|
-
// }
|
|
404
|
-
// console.log('✅ AI service is healthy');
|
|
405
|
-
const fileObj = new File([fileBuffer], input.file.filename, { type: input.file.contentType });
|
|
406
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
407
|
-
status: 'uploading',
|
|
408
|
-
filename: input.file.filename,
|
|
409
|
-
fileType,
|
|
410
|
-
startedAt: new Date().toISOString(),
|
|
411
|
-
steps: {
|
|
412
|
-
fileUpload: {
|
|
413
|
-
order: 1,
|
|
414
|
-
status: 'in_progress',
|
|
415
|
-
},
|
|
416
|
-
fileAnalysis: {
|
|
417
|
-
order: 2,
|
|
418
|
-
status: 'pending',
|
|
419
|
-
},
|
|
420
|
-
studyGuide: {
|
|
421
|
-
order: 3,
|
|
422
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
423
|
-
},
|
|
424
|
-
flashcards: {
|
|
425
|
-
order: 4,
|
|
426
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
427
|
-
},
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
await aiSessionService.uploadFile(input.workspaceId, ctx.session.user.id, fileObj, fileType);
|
|
431
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
432
|
-
status: 'analyzing',
|
|
433
|
-
filename: input.file.filename,
|
|
434
|
-
fileType,
|
|
435
|
-
startedAt: new Date().toISOString(),
|
|
436
|
-
steps: {
|
|
437
|
-
fileUpload: {
|
|
438
|
-
order: 1,
|
|
439
|
-
status: 'completed',
|
|
440
|
-
},
|
|
441
|
-
fileAnalysis: {
|
|
442
|
-
order: 2,
|
|
443
|
-
status: 'in_progress',
|
|
444
|
-
},
|
|
445
|
-
studyGuide: {
|
|
446
|
-
order: 3,
|
|
447
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
448
|
-
},
|
|
449
|
-
flashcards: {
|
|
450
|
-
order: 4,
|
|
451
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
430
|
+
PusherService.emitAnalysisProgress(input.workspaceId, {
|
|
431
|
+
status: 'starting',
|
|
432
|
+
filename: primaryFile.name,
|
|
433
|
+
fileType,
|
|
434
|
+
startedAt: new Date().toISOString(),
|
|
435
|
+
steps: {
|
|
436
|
+
fileUpload: { order: 1, status: 'pending' },
|
|
452
437
|
},
|
|
438
|
+
});
|
|
439
|
+
try {
|
|
440
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
441
|
+
status: 'starting',
|
|
442
|
+
filename: primaryFile.name,
|
|
443
|
+
fileType,
|
|
444
|
+
startedAt: new Date().toISOString(),
|
|
445
|
+
steps: {
|
|
446
|
+
fileUpload: {
|
|
447
|
+
order: 1,
|
|
448
|
+
status: 'pending',
|
|
449
|
+
},
|
|
450
|
+
fileAnalysis: {
|
|
451
|
+
order: 2,
|
|
452
|
+
status: 'pending',
|
|
453
|
+
},
|
|
454
|
+
studyGuide: {
|
|
455
|
+
order: 3,
|
|
456
|
+
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
457
|
+
},
|
|
458
|
+
flashcards: {
|
|
459
|
+
order: 4,
|
|
460
|
+
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
461
|
+
},
|
|
462
|
+
}
|
|
463
|
+
});
|
|
453
464
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
await
|
|
465
|
+
catch (error) {
|
|
466
|
+
console.error('❌ Failed to update analysis progress:', error);
|
|
467
|
+
await ctx.db.workspace.update({
|
|
468
|
+
where: { id: input.workspaceId },
|
|
469
|
+
data: { fileBeingAnalyzed: false },
|
|
470
|
+
});
|
|
471
|
+
await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
|
|
472
|
+
throw error;
|
|
461
473
|
}
|
|
462
474
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
463
|
-
status: '
|
|
464
|
-
filename:
|
|
475
|
+
status: 'uploading',
|
|
476
|
+
filename: primaryFile.name,
|
|
465
477
|
fileType,
|
|
466
478
|
startedAt: new Date().toISOString(),
|
|
467
479
|
steps: {
|
|
468
480
|
fileUpload: {
|
|
469
481
|
order: 1,
|
|
470
|
-
status: '
|
|
482
|
+
status: 'in_progress',
|
|
471
483
|
},
|
|
472
484
|
fileAnalysis: {
|
|
473
485
|
order: 2,
|
|
474
|
-
status: '
|
|
486
|
+
status: 'pending',
|
|
475
487
|
},
|
|
476
488
|
studyGuide: {
|
|
477
489
|
order: 3,
|
|
@@ -483,49 +495,53 @@ export const workspace = router({
|
|
|
483
495
|
},
|
|
484
496
|
}
|
|
485
497
|
});
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
498
|
+
// Process all files using the new process_file endpoint
|
|
499
|
+
for (const file of files) {
|
|
500
|
+
// TypeScript: We already validated bucket and objectKey exist above
|
|
501
|
+
if (!file.bucket || !file.objectKey) {
|
|
502
|
+
continue; // Skip if somehow missing (shouldn't happen due to validation above)
|
|
503
|
+
}
|
|
504
|
+
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
505
|
+
.from(file.bucket)
|
|
506
|
+
.createSignedUrl(file.objectKey, 24 * 60 * 60); // 24 hours expiry
|
|
507
|
+
if (signedUrlError) {
|
|
508
|
+
await ctx.db.workspace.update({
|
|
509
|
+
where: { id: input.workspaceId },
|
|
510
|
+
data: { fileBeingAnalyzed: false },
|
|
511
|
+
});
|
|
512
|
+
throw new TRPCError({
|
|
513
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
514
|
+
message: `Failed to generate signed URL for file ${file.name}: ${signedUrlError.message}`
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
const fileUrl = signedUrlData.signedUrl;
|
|
518
|
+
const currentFileType = file.mimeType.startsWith('image/') ? 'image' : 'pdf';
|
|
519
|
+
// Use maxPages for large PDFs (>50 pages) to limit processing
|
|
520
|
+
const maxPages = currentFileType === 'pdf' && file.size && file.size > 50 ? 50 : undefined;
|
|
521
|
+
const processResult = await aiSessionService.processFile(input.workspaceId, ctx.session.user.id, fileUrl, currentFileType, maxPages);
|
|
522
|
+
if (processResult.status === 'error') {
|
|
523
|
+
logger.error(`Failed to process file ${file.name}:`, processResult.error);
|
|
524
|
+
// Continue processing other files even if one fails
|
|
525
|
+
// Optionally, you could throw an error or mark this file as failed
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
logger.info(`Successfully processed file ${file.name}: ${processResult.pageCount} pages`);
|
|
529
|
+
// Store the comprehensive description in aiTranscription field
|
|
530
|
+
await ctx.db.fileAsset.update({
|
|
531
|
+
where: { id: file.id },
|
|
532
|
+
data: {
|
|
533
|
+
aiTranscription: {
|
|
534
|
+
comprehensiveDescription: processResult.comprehensiveDescription,
|
|
535
|
+
textContent: processResult.textContent,
|
|
536
|
+
imageDescriptions: processResult.imageDescriptions,
|
|
537
|
+
},
|
|
538
|
+
}
|
|
539
|
+
});
|
|
512
540
|
}
|
|
513
|
-
});
|
|
514
|
-
throw error;
|
|
515
|
-
}
|
|
516
|
-
const results = {
|
|
517
|
-
filename: input.file.filename,
|
|
518
|
-
artifacts: {
|
|
519
|
-
studyGuide: null,
|
|
520
|
-
flashcards: null,
|
|
521
|
-
worksheet: null,
|
|
522
541
|
}
|
|
523
|
-
};
|
|
524
|
-
// Generate artifacts
|
|
525
|
-
if (input.generateStudyGuide) {
|
|
526
542
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
527
|
-
status: '
|
|
528
|
-
filename:
|
|
543
|
+
status: 'analyzing',
|
|
544
|
+
filename: primaryFile.name,
|
|
529
545
|
fileType,
|
|
530
546
|
startedAt: new Date().toISOString(),
|
|
531
547
|
steps: {
|
|
@@ -535,11 +551,11 @@ export const workspace = router({
|
|
|
535
551
|
},
|
|
536
552
|
fileAnalysis: {
|
|
537
553
|
order: 2,
|
|
538
|
-
status: '
|
|
554
|
+
status: 'in_progress',
|
|
539
555
|
},
|
|
540
556
|
studyGuide: {
|
|
541
557
|
order: 3,
|
|
542
|
-
status: '
|
|
558
|
+
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
543
559
|
},
|
|
544
560
|
flashcards: {
|
|
545
561
|
order: 4,
|
|
@@ -547,35 +563,218 @@ export const workspace = router({
|
|
|
547
563
|
},
|
|
548
564
|
}
|
|
549
565
|
});
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
566
|
+
try {
|
|
567
|
+
// Analyze all files - use PDF analysis if any file is a PDF, otherwise use image analysis
|
|
568
|
+
// const hasPDF = files.some(f => !f.mimeType.startsWith('image/'));
|
|
569
|
+
// if (hasPDF) {
|
|
570
|
+
// await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id, file.id);
|
|
571
|
+
// } else {
|
|
572
|
+
// // If all files are images, analyze them
|
|
573
|
+
// for (const file of files) {
|
|
574
|
+
// await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id, file.id);
|
|
575
|
+
// }
|
|
576
|
+
// }
|
|
577
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
578
|
+
status: 'generating_artifacts',
|
|
579
|
+
filename: primaryFile.name,
|
|
580
|
+
fileType,
|
|
581
|
+
startedAt: new Date().toISOString(),
|
|
582
|
+
steps: {
|
|
583
|
+
fileUpload: {
|
|
584
|
+
order: 1,
|
|
585
|
+
status: 'completed',
|
|
586
|
+
},
|
|
587
|
+
fileAnalysis: {
|
|
588
|
+
order: 2,
|
|
589
|
+
status: 'completed',
|
|
590
|
+
},
|
|
591
|
+
studyGuide: {
|
|
592
|
+
order: 3,
|
|
593
|
+
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
594
|
+
},
|
|
595
|
+
flashcards: {
|
|
596
|
+
order: 4,
|
|
597
|
+
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
598
|
+
},
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
console.error('❌ Failed to analyze files:', error);
|
|
604
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
605
|
+
status: 'error',
|
|
606
|
+
filename: primaryFile.name,
|
|
607
|
+
fileType,
|
|
608
|
+
error: `Failed to analyze ${fileType}: ${error}`,
|
|
609
|
+
startedAt: new Date().toISOString(),
|
|
610
|
+
steps: {
|
|
611
|
+
fileUpload: {
|
|
612
|
+
order: 1,
|
|
613
|
+
status: 'completed',
|
|
614
|
+
},
|
|
615
|
+
fileAnalysis: {
|
|
616
|
+
order: 2,
|
|
617
|
+
status: 'error',
|
|
618
|
+
},
|
|
619
|
+
studyGuide: {
|
|
620
|
+
order: 3,
|
|
621
|
+
status: 'skipped',
|
|
622
|
+
},
|
|
623
|
+
flashcards: {
|
|
624
|
+
order: 4,
|
|
625
|
+
status: 'skipped',
|
|
626
|
+
},
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
await ctx.db.workspace.update({
|
|
630
|
+
where: { id: input.workspaceId },
|
|
631
|
+
data: { fileBeingAnalyzed: false },
|
|
632
|
+
});
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
const results = {
|
|
636
|
+
filename: primaryFile.name,
|
|
637
|
+
artifacts: {
|
|
638
|
+
studyGuide: null,
|
|
639
|
+
flashcards: null,
|
|
640
|
+
worksheet: null,
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
// Generate artifacts
|
|
644
|
+
if (input.generateStudyGuide) {
|
|
645
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
646
|
+
status: 'generating_study_guide',
|
|
647
|
+
filename: primaryFile.name,
|
|
648
|
+
fileType,
|
|
649
|
+
startedAt: new Date().toISOString(),
|
|
650
|
+
steps: {
|
|
651
|
+
fileUpload: {
|
|
652
|
+
order: 1,
|
|
653
|
+
status: 'completed',
|
|
654
|
+
},
|
|
655
|
+
fileAnalysis: {
|
|
656
|
+
order: 2,
|
|
657
|
+
status: 'completed',
|
|
658
|
+
},
|
|
659
|
+
studyGuide: {
|
|
660
|
+
order: 3,
|
|
661
|
+
status: 'in_progress',
|
|
662
|
+
},
|
|
663
|
+
flashcards: {
|
|
664
|
+
order: 4,
|
|
665
|
+
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
666
|
+
},
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
|
|
670
|
+
let artifact = await ctx.db.artifact.findFirst({
|
|
671
|
+
where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
|
|
672
|
+
});
|
|
673
|
+
if (!artifact) {
|
|
674
|
+
const fileNames = files.map(f => f.name).join(', ');
|
|
675
|
+
artifact = await ctx.db.artifact.create({
|
|
676
|
+
data: {
|
|
677
|
+
workspaceId: input.workspaceId,
|
|
678
|
+
type: ArtifactType.STUDY_GUIDE,
|
|
679
|
+
title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
|
|
680
|
+
createdById: ctx.session.user.id,
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const lastVersion = await ctx.db.artifactVersion.findFirst({
|
|
685
|
+
where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
|
|
686
|
+
orderBy: { version: 'desc' },
|
|
687
|
+
});
|
|
688
|
+
await ctx.db.artifactVersion.create({
|
|
689
|
+
data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
|
|
690
|
+
});
|
|
691
|
+
results.artifacts.studyGuide = artifact;
|
|
692
|
+
}
|
|
693
|
+
if (input.generateFlashcards) {
|
|
694
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
695
|
+
status: 'generating_flashcards',
|
|
696
|
+
filename: primaryFile.name,
|
|
697
|
+
fileType,
|
|
698
|
+
startedAt: new Date().toISOString(),
|
|
699
|
+
steps: {
|
|
700
|
+
fileUpload: {
|
|
701
|
+
order: 1,
|
|
702
|
+
status: 'completed',
|
|
703
|
+
},
|
|
704
|
+
fileAnalysis: {
|
|
705
|
+
order: 2,
|
|
706
|
+
status: 'completed',
|
|
707
|
+
},
|
|
708
|
+
studyGuide: {
|
|
709
|
+
order: 3,
|
|
710
|
+
status: input.generateStudyGuide ? 'completed' : 'skipped',
|
|
711
|
+
},
|
|
712
|
+
flashcards: {
|
|
713
|
+
order: 4,
|
|
714
|
+
status: 'in_progress',
|
|
715
|
+
},
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
|
|
719
|
+
const artifact = await ctx.db.artifact.create({
|
|
556
720
|
data: {
|
|
557
721
|
workspaceId: input.workspaceId,
|
|
558
|
-
type: ArtifactType.
|
|
559
|
-
title: `
|
|
722
|
+
type: ArtifactType.FLASHCARD_SET,
|
|
723
|
+
title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
|
|
560
724
|
createdById: ctx.session.user.id,
|
|
561
725
|
},
|
|
562
726
|
});
|
|
727
|
+
// Parse JSON flashcard content
|
|
728
|
+
try {
|
|
729
|
+
const flashcardData = content;
|
|
730
|
+
let createdCards = 0;
|
|
731
|
+
for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
|
|
732
|
+
const card = flashcardData[i];
|
|
733
|
+
const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
|
|
734
|
+
const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
|
|
735
|
+
await ctx.db.flashcard.create({
|
|
736
|
+
data: {
|
|
737
|
+
artifactId: artifact.id,
|
|
738
|
+
front: front,
|
|
739
|
+
back: back,
|
|
740
|
+
order: i,
|
|
741
|
+
tags: ['ai-generated', 'medium'],
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
createdCards++;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
catch (parseError) {
|
|
748
|
+
// Fallback to text parsing if JSON fails
|
|
749
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
750
|
+
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
751
|
+
const line = lines[i];
|
|
752
|
+
if (line.includes(' - ')) {
|
|
753
|
+
const [front, back] = line.split(' - ');
|
|
754
|
+
await ctx.db.flashcard.create({
|
|
755
|
+
data: {
|
|
756
|
+
artifactId: artifact.id,
|
|
757
|
+
front: front.trim(),
|
|
758
|
+
back: back.trim(),
|
|
759
|
+
order: i,
|
|
760
|
+
tags: ['ai-generated', 'medium'],
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
results.artifacts.flashcards = artifact;
|
|
563
767
|
}
|
|
564
|
-
|
|
565
|
-
where: {
|
|
566
|
-
|
|
567
|
-
});
|
|
568
|
-
await ctx.db.artifactVersion.create({
|
|
569
|
-
data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
|
|
768
|
+
await ctx.db.workspace.update({
|
|
769
|
+
where: { id: input.workspaceId },
|
|
770
|
+
data: { fileBeingAnalyzed: false },
|
|
570
771
|
});
|
|
571
|
-
results.artifacts.studyGuide = artifact;
|
|
572
|
-
}
|
|
573
|
-
if (input.generateFlashcards) {
|
|
574
772
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
575
|
-
status: '
|
|
576
|
-
filename:
|
|
773
|
+
status: 'completed',
|
|
774
|
+
filename: primaryFile.name,
|
|
577
775
|
fileType,
|
|
578
776
|
startedAt: new Date().toISOString(),
|
|
777
|
+
completedAt: new Date().toISOString(),
|
|
579
778
|
steps: {
|
|
580
779
|
fileUpload: {
|
|
581
780
|
order: 1,
|
|
@@ -591,90 +790,21 @@ export const workspace = router({
|
|
|
591
790
|
},
|
|
592
791
|
flashcards: {
|
|
593
792
|
order: 4,
|
|
594
|
-
status: '
|
|
793
|
+
status: input.generateFlashcards ? 'completed' : 'skipped',
|
|
595
794
|
},
|
|
596
795
|
}
|
|
597
796
|
});
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
},
|
|
797
|
+
return results;
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
console.error('❌ Failed to update analysis progress:', error);
|
|
801
|
+
await ctx.db.workspace.update({
|
|
802
|
+
where: { id: input.workspaceId },
|
|
803
|
+
data: { fileBeingAnalyzed: false },
|
|
606
804
|
});
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const flashcardData = content;
|
|
610
|
-
let createdCards = 0;
|
|
611
|
-
for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
|
|
612
|
-
const card = flashcardData[i];
|
|
613
|
-
const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
|
|
614
|
-
const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
|
|
615
|
-
await ctx.db.flashcard.create({
|
|
616
|
-
data: {
|
|
617
|
-
artifactId: artifact.id,
|
|
618
|
-
front: front,
|
|
619
|
-
back: back,
|
|
620
|
-
order: i,
|
|
621
|
-
tags: ['ai-generated', 'medium'],
|
|
622
|
-
},
|
|
623
|
-
});
|
|
624
|
-
createdCards++;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
catch (parseError) {
|
|
628
|
-
// Fallback to text parsing if JSON fails
|
|
629
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
630
|
-
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
631
|
-
const line = lines[i];
|
|
632
|
-
if (line.includes(' - ')) {
|
|
633
|
-
const [front, back] = line.split(' - ');
|
|
634
|
-
await ctx.db.flashcard.create({
|
|
635
|
-
data: {
|
|
636
|
-
artifactId: artifact.id,
|
|
637
|
-
front: front.trim(),
|
|
638
|
-
back: back.trim(),
|
|
639
|
-
order: i,
|
|
640
|
-
tags: ['ai-generated', 'medium'],
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
results.artifacts.flashcards = artifact;
|
|
805
|
+
await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
|
|
806
|
+
throw error;
|
|
647
807
|
}
|
|
648
|
-
await ctx.db.workspace.update({
|
|
649
|
-
where: { id: input.workspaceId },
|
|
650
|
-
data: { fileBeingAnalyzed: false },
|
|
651
|
-
});
|
|
652
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
653
|
-
status: 'completed',
|
|
654
|
-
filename: input.file.filename,
|
|
655
|
-
fileType,
|
|
656
|
-
startedAt: new Date().toISOString(),
|
|
657
|
-
completedAt: new Date().toISOString(),
|
|
658
|
-
steps: {
|
|
659
|
-
fileUpload: {
|
|
660
|
-
order: 1,
|
|
661
|
-
status: 'completed',
|
|
662
|
-
},
|
|
663
|
-
fileAnalysis: {
|
|
664
|
-
order: 2,
|
|
665
|
-
status: 'completed',
|
|
666
|
-
},
|
|
667
|
-
studyGuide: {
|
|
668
|
-
order: 3,
|
|
669
|
-
status: input.generateStudyGuide ? 'completed' : 'skipped',
|
|
670
|
-
},
|
|
671
|
-
flashcards: {
|
|
672
|
-
order: 4,
|
|
673
|
-
status: input.generateFlashcards ? 'completed' : 'skipped',
|
|
674
|
-
},
|
|
675
|
-
}
|
|
676
|
-
});
|
|
677
|
-
return results;
|
|
678
808
|
}),
|
|
679
809
|
search: authedProcedure
|
|
680
810
|
.input(z.object({
|