@goscribe/server 1.0.11 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/ANALYSIS_PROGRESS_SPEC.md +463 -0
  2. package/PROGRESS_QUICK_REFERENCE.md +239 -0
  3. package/dist/lib/ai-session.d.ts +20 -9
  4. package/dist/lib/ai-session.js +316 -80
  5. package/dist/lib/auth.d.ts +35 -2
  6. package/dist/lib/auth.js +88 -15
  7. package/dist/lib/env.d.ts +32 -0
  8. package/dist/lib/env.js +46 -0
  9. package/dist/lib/errors.d.ts +33 -0
  10. package/dist/lib/errors.js +78 -0
  11. package/dist/lib/inference.d.ts +4 -1
  12. package/dist/lib/inference.js +9 -11
  13. package/dist/lib/logger.d.ts +62 -0
  14. package/dist/lib/logger.js +342 -0
  15. package/dist/lib/podcast-prompts.d.ts +43 -0
  16. package/dist/lib/podcast-prompts.js +135 -0
  17. package/dist/lib/pusher.d.ts +1 -0
  18. package/dist/lib/pusher.js +14 -2
  19. package/dist/lib/storage.d.ts +3 -3
  20. package/dist/lib/storage.js +51 -47
  21. package/dist/lib/validation.d.ts +51 -0
  22. package/dist/lib/validation.js +64 -0
  23. package/dist/routers/_app.d.ts +697 -111
  24. package/dist/routers/_app.js +5 -0
  25. package/dist/routers/auth.d.ts +11 -1
  26. package/dist/routers/chat.d.ts +11 -1
  27. package/dist/routers/flashcards.d.ts +205 -6
  28. package/dist/routers/flashcards.js +144 -66
  29. package/dist/routers/members.d.ts +165 -0
  30. package/dist/routers/members.js +531 -0
  31. package/dist/routers/podcast.d.ts +78 -63
  32. package/dist/routers/podcast.js +330 -393
  33. package/dist/routers/studyguide.d.ts +11 -1
  34. package/dist/routers/worksheets.d.ts +124 -13
  35. package/dist/routers/worksheets.js +123 -50
  36. package/dist/routers/workspace.d.ts +213 -26
  37. package/dist/routers/workspace.js +303 -181
  38. package/dist/server.js +12 -4
  39. package/dist/services/flashcard-progress.service.d.ts +183 -0
  40. package/dist/services/flashcard-progress.service.js +383 -0
  41. package/dist/services/flashcard.service.d.ts +183 -0
  42. package/dist/services/flashcard.service.js +224 -0
  43. package/dist/services/podcast-segment-reorder.d.ts +0 -0
  44. package/dist/services/podcast-segment-reorder.js +107 -0
  45. package/dist/services/podcast.service.d.ts +0 -0
  46. package/dist/services/podcast.service.js +326 -0
  47. package/dist/services/worksheet.service.d.ts +0 -0
  48. package/dist/services/worksheet.service.js +295 -0
  49. package/dist/trpc.d.ts +13 -2
  50. package/dist/trpc.js +55 -6
  51. package/dist/types/index.d.ts +126 -0
  52. package/dist/types/index.js +1 -0
  53. package/package.json +3 -2
  54. package/prisma/schema.prisma +142 -4
  55. package/src/lib/ai-session.ts +356 -85
  56. package/src/lib/auth.ts +113 -19
  57. package/src/lib/env.ts +59 -0
  58. package/src/lib/errors.ts +92 -0
  59. package/src/lib/inference.ts +11 -11
  60. package/src/lib/logger.ts +405 -0
  61. package/src/lib/pusher.ts +15 -3
  62. package/src/lib/storage.ts +56 -51
  63. package/src/lib/validation.ts +75 -0
  64. package/src/routers/_app.ts +5 -0
  65. package/src/routers/chat.ts +2 -23
  66. package/src/routers/flashcards.ts +108 -24
  67. package/src/routers/members.ts +586 -0
  68. package/src/routers/podcast.ts +385 -420
  69. package/src/routers/worksheets.ts +118 -36
  70. package/src/routers/workspace.ts +356 -195
  71. package/src/server.ts +13 -4
  72. package/src/services/flashcard-progress.service.ts +541 -0
  73. package/src/trpc.ts +59 -6
  74. package/src/types/index.ts +165 -0
  75. package/AUTH_FRONTEND_SPEC.md +0 -21
  76. package/CHAT_FRONTEND_SPEC.md +0 -474
  77. package/DATABASE_SETUP.md +0 -165
  78. package/MEETINGSUMMARY_FRONTEND_SPEC.md +0 -28
  79. package/PODCAST_FRONTEND_SPEC.md +0 -595
  80. package/STUDYGUIDE_FRONTEND_SPEC.md +0 -18
  81. package/WORKSHEETS_FRONTEND_SPEC.md +0 -26
  82. package/WORKSPACE_FRONTEND_SPEC.md +0 -47
  83. package/test-ai-integration.js +0 -134
@@ -1,10 +1,25 @@
1
1
  import { z } from 'zod';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import { router, publicProcedure, authedProcedure } from '../trpc.js';
4
- import { bucket } from '../lib/storage.js';
4
+ import { supabaseClient } from '../lib/storage.js';
5
5
  import { ArtifactType } from '@prisma/client';
6
6
  import { aiSessionService } from '../lib/ai-session.js';
7
7
  import PusherService from '../lib/pusher.js';
8
+ import { members } from './members.js';
9
+ import type { PrismaClient } from '@prisma/client';
10
+
11
+ // Helper function to update and emit analysis progress
12
+ async function updateAnalysisProgress(
13
+ db: PrismaClient,
14
+ workspaceId: string,
15
+ progress: any
16
+ ) {
17
+ await db.workspace.update({
18
+ where: { id: workspaceId },
19
+ data: { analysisProgress: progress }
20
+ });
21
+ await PusherService.emitAnalysisProgress(workspaceId, progress);
22
+ }
8
23
 
9
24
  // Helper function to calculate search relevance score
10
25
  function calculateRelevance(query: string, ...texts: (string | null | undefined)[]): number {
@@ -97,11 +112,14 @@ export const workspace = router({
97
112
  },
98
113
  },
99
114
  });
115
+
116
+ aiSessionService.initSession(ws.id, ctx.session.user.id);
100
117
  return ws;
101
118
  }),
102
119
  createFolder: authedProcedure
103
120
  .input(z.object({
104
121
  name: z.string().min(1).max(100),
122
+ color: z.string().optional(),
105
123
  parentId: z.string().optional(),
106
124
  }))
107
125
  .mutation(async ({ ctx, input }) => {
@@ -109,11 +127,30 @@ export const workspace = router({
109
127
  data: {
110
128
  name: input.name,
111
129
  ownerId: ctx.session.user.id,
130
+ color: input.color ?? '#9D00FF',
112
131
  parentId: input.parentId ?? null,
113
132
  },
114
133
  });
115
134
  return folder;
116
135
  }),
136
+ updateFolder: authedProcedure
137
+ .input(z.object({
138
+ id: z.string(),
139
+ name: z.string().min(1).max(100).optional(),
140
+ color: z.string().optional(),
141
+ }))
142
+ .mutation(async ({ ctx, input }) => {
143
+ const folder = await ctx.db.folder.update({ where: { id: input.id }, data: { name: input.name, color: input.color ?? '#9D00FF' } });
144
+ return folder;
145
+ }),
146
+ deleteFolder: authedProcedure
147
+ .input(z.object({
148
+ id: z.string(),
149
+ }))
150
+ .mutation(async ({ ctx, input }) => {
151
+ const folder = await ctx.db.folder.delete({ where: { id: input.id } });
152
+ return folder;
153
+ }),
117
154
  get: authedProcedure
118
155
  .input(z.object({
119
156
  id: z.string(),
@@ -130,56 +167,30 @@ export const workspace = router({
130
167
  if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
131
168
  return ws;
132
169
  }),
133
- share: authedProcedure
134
- .input(z.object({
135
- id: z.string(),
136
- }))
137
- .query(async ({ ctx, input }) => {
138
- const ws = await ctx.db.workspace.findFirst({
139
- where: { id: input.id, ownerId: ctx.session.user.id },
170
+ getStats: authedProcedure
171
+ .query(async ({ ctx }) => {
172
+ const workspaces = await ctx.db.workspace.findMany({
173
+ where: {OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }]},
140
174
  });
141
- if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
142
-
143
- // generate a unique share link if not exists
144
- if (!ws.shareLink) {
145
- const shareLink = [...Array(30)].map(() => (Math.random() * 36 | 0).toString(36)).join('');
146
- const updated = await ctx.db.workspace.update({
147
- where: { id: ws.id },
148
- data: { shareLink },
149
- });
150
- return { shareLink: updated.shareLink };
151
- }
152
- }),
153
- join: authedProcedure
154
- .input(z.object({
155
- shareLink: z.string().min(10).max(100),
156
- }))
157
- .mutation(async ({ ctx, input }) => {
158
- const ws = await ctx.db.workspace.findFirst({
159
- where: { shareLink: input.shareLink },
175
+ const folders = await ctx.db.folder.findMany({
176
+ where: {OR: [{ ownerId: ctx.session.user.id } ]},
160
177
  });
161
- if (!ws) throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' });
162
- if (ws.ownerId === ctx.session.user.id) {
163
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot join your own workspace' });
164
- }
165
- // add to sharedWith if not already
166
- const alreadyShared = await ctx.db.workspace.findFirst({
167
- where: { id: ws.id, sharedWith: { some: { id: ctx.session.user.id } } },
178
+ const lastUpdated = await ctx.db.workspace.findFirst({
179
+ where: {OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }]},
180
+ orderBy: { updatedAt: 'desc' },
168
181
  });
169
- if (alreadyShared) {
170
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'Already joined this workspace' });
171
- }
172
- await ctx.db.workspace.update({
173
- where: { id: ws.id },
174
- data: { sharedWith: { connect: { id: ctx.session.user.id } } },
182
+
183
+ const spaceLeft = await ctx.db.fileAsset.aggregate({
184
+ where: { workspaceId: { in: workspaces.map(ws => ws.id) }, userId: ctx.session.user.id },
185
+ _sum: { size: true },
175
186
  });
187
+
176
188
  return {
177
- id: ws.id,
178
- title: ws.title,
179
- description: ws.description,
180
- ownerId: ws.ownerId,
181
- createdAt: ws.createdAt,
182
- updatedAt: ws.updatedAt,
189
+ workspaces: workspaces.length,
190
+ folders: folders.length,
191
+ lastUpdated: lastUpdated?.updatedAt,
192
+ spaceUsed: spaceLeft._sum?.size ?? 0,
193
+ spaceLeft: 1000000000 - (spaceLeft._sum?.size ?? 0) || 0,
183
194
  };
184
195
  }),
185
196
  update: authedProcedure
@@ -187,6 +198,8 @@ export const workspace = router({
187
198
  id: z.string(),
188
199
  name: z.string().min(1).max(100).optional(),
189
200
  description: z.string().max(500).optional(),
201
+ color: z.string().optional(),
202
+ icon: z.string().optional(),
190
203
  }))
191
204
  .mutation(async ({ ctx, input }) => {
192
205
  const existed = await ctx.db.workspace.findFirst({
@@ -198,6 +211,8 @@ export const workspace = router({
198
211
  data: {
199
212
  title: input.name ?? existed.title,
200
213
  description: input.description,
214
+ color: input.color ?? existed.color,
215
+ icon: input.icon ?? existed.icon,
201
216
  },
202
217
  });
203
218
  return updated;
@@ -264,26 +279,30 @@ export const workspace = router({
264
279
  });
265
280
 
266
281
  // 2. Generate signed URL for direct upload
267
- const [url] = await bucket
268
- .file(`${ctx.session.user.id}/${record.id}-${file.filename}`)
269
- .getSignedUrl({
270
- action: "write",
271
- expires: Date.now() + 5 * 60 * 1000, // 5 min
272
- contentType: file.contentType,
282
+ const objectKey = `${ctx.session.user.id}/${record.id}-${file.filename}`;
283
+ const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
284
+ .from('files')
285
+ .createSignedUploadUrl(objectKey); // 5 minutes
286
+
287
+ if (signedUrlError) {
288
+ throw new TRPCError({
289
+ code: 'INTERNAL_SERVER_ERROR',
290
+ message: `Failed to generate upload URL: ${signedUrlError.message}`
273
291
  });
292
+ }
274
293
 
275
294
  // 3. Update record with bucket info
276
295
  await ctx.db.fileAsset.update({
277
296
  where: { id: record.id },
278
297
  data: {
279
- bucket: bucket.name,
280
- objectKey: `${ctx.session.user.id}/${record.id}-${file.filename}`,
298
+ bucket: 'files',
299
+ objectKey: objectKey,
281
300
  },
282
301
  });
283
302
 
284
303
  results.push({
285
304
  fileId: record.id,
286
- uploadUrl: url,
305
+ uploadUrl: signedUrlData.signedUrl,
287
306
  });
288
307
  }
289
308
 
@@ -304,13 +323,15 @@ export const workspace = router({
304
323
  userId: ctx.session.user.id,
305
324
  },
306
325
  });
307
- // Delete from GCS (best-effort)
326
+ // Delete from Supabase Storage (best-effort)
308
327
  for (const file of files) {
309
328
  if (file.bucket && file.objectKey) {
310
- const gcsFile: import('@google-cloud/storage').File = bucket.file(file.objectKey);
311
- gcsFile.delete({ ignoreNotFound: true }).catch((err: unknown) => {
312
- console.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
313
- });
329
+ supabaseClient.storage
330
+ .from(file.bucket)
331
+ .remove([file.objectKey])
332
+ .catch((err: unknown) => {
333
+ console.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
334
+ });
314
335
  }
315
336
  }
316
337
 
@@ -323,6 +344,34 @@ export const workspace = router({
323
344
  });
324
345
  return true;
325
346
  }),
347
+ getFileUploadUrl: authedProcedure
348
+ .input(z.object({
349
+ workspaceId: z.string(),
350
+ filename: z.string(),
351
+ contentType: z.string(),
352
+ size: z.number(),
353
+ }))
354
+ .query(async ({ ctx, input }) => {
355
+ const objectKey = `workspace_${ctx.session.user.id}/${input.workspaceId}-file_${input.filename}`;
356
+ await ctx.db.fileAsset.create({
357
+ data: {
358
+ workspaceId: input.workspaceId,
359
+ name: input.filename,
360
+ mimeType: input.contentType,
361
+ size: input.size,
362
+ userId: ctx.session.user.id,
363
+ bucket: 'media',
364
+ objectKey: objectKey,
365
+ },
366
+ });
367
+ const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
368
+ .from('media')
369
+ .createSignedUploadUrl(objectKey); // 5 minutes
370
+ if (signedUrlError) {
371
+ throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
372
+ }
373
+ return signedUrlData.signedUrl;
374
+ }),
326
375
  uploadAndAnalyzeMedia: authedProcedure
327
376
  .input(z.object({
328
377
  workspaceId: z.string(),
@@ -354,12 +403,51 @@ export const workspace = router({
354
403
  console.error('❌ Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
355
404
  throw new TRPCError({ code: 'NOT_FOUND' });
356
405
  }
357
- console.log('✅ Workspace verified', { workspaceId: workspace.id, workspaceTitle: workspace.title });
358
406
 
359
- // Convert base64 to buffer
360
- console.log('📁 Converting base64 to buffer...');
407
+ const fileType = input.file.contentType.startsWith('image/') ? 'image' : 'pdf';
408
+
409
+ await ctx.db.workspace.update({
410
+ where: { id: input.workspaceId },
411
+ data: { fileBeingAnalyzed: true },
412
+ });
413
+
414
+ try {
415
+
416
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
417
+ status: 'starting',
418
+ filename: input.file.filename,
419
+ fileType,
420
+ startedAt: new Date().toISOString(),
421
+ steps: {
422
+ fileUpload: {
423
+ order: 1,
424
+ status: 'pending',
425
+ },
426
+ fileAnalysis: {
427
+ order: 2,
428
+ status: 'pending',
429
+ },
430
+ studyGuide: {
431
+ order: 3,
432
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
433
+ },
434
+ flashcards: {
435
+ order: 4,
436
+ status: input.generateFlashcards ? 'pending' : 'skipped',
437
+ },
438
+ }
439
+ });
440
+ } catch (error) {
441
+ console.error('❌ Failed to update analysis progress:', error);
442
+ await ctx.db.workspace.update({
443
+ where: { id: input.workspaceId },
444
+ data: { fileBeingAnalyzed: false },
445
+ });
446
+ await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
447
+ throw error;
448
+ }
449
+
361
450
  const fileBuffer = Buffer.from(input.file.content, 'base64');
362
- console.log('✅ File buffer created', { bufferSize: fileBuffer.length });
363
451
 
364
452
  // // Check AI service health first
365
453
  // console.log('🏥 Checking AI service health...');
@@ -374,41 +462,118 @@ export const workspace = router({
374
462
  // }
375
463
  // console.log('✅ AI service is healthy');
376
464
 
377
- // Initialize AI session
378
- console.log('🤖 Initializing AI session...');
379
- const session = await aiSessionService.initSession(input.workspaceId, ctx.session.user.id);
380
- console.log('✅ AI session initialized', { sessionId: session.id });
381
-
382
465
  const fileObj = new File([fileBuffer], input.file.filename, { type: input.file.contentType });
383
- const fileType = input.file.contentType.startsWith('image/') ? 'image' : 'pdf';
384
- console.log('📤 Uploading file to AI service...', { filename: input.file.filename, fileType });
385
- await aiSessionService.uploadFile(session.id, fileObj, fileType);
386
- console.log('✅ File uploaded to AI service');
387
-
388
- console.log('🚀 Starting LLM session...');
389
- try {
390
- await aiSessionService.startLLMSession(session.id);
391
- console.log('✅ LLM session started');
392
- } catch (error) {
393
- console.error('❌ Failed to start LLM session:', error);
394
- throw error;
395
- }
396
466
 
397
- // Analyze the file first
398
- console.log('🔍 Analyzing file...', { fileType });
399
- await PusherService.emitTaskComplete(input.workspaceId, 'file_analysis_start', { filename: input.file.filename, fileType });
467
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
468
+ status: 'uploading',
469
+ filename: input.file.filename,
470
+ fileType,
471
+ startedAt: new Date().toISOString(),
472
+ steps: {
473
+ fileUpload: {
474
+ order: 1,
475
+ status: 'in_progress',
476
+ },
477
+ fileAnalysis: {
478
+ order: 2,
479
+ status: 'pending',
480
+ },
481
+ studyGuide: {
482
+ order: 3,
483
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
484
+ },
485
+ flashcards: {
486
+ order: 4,
487
+ status: input.generateFlashcards ? 'pending' : 'skipped',
488
+ },
489
+ }
490
+ });
491
+
492
+ await aiSessionService.uploadFile(input.workspaceId, ctx.session.user.id, fileObj, fileType);
493
+
494
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
495
+ status: 'analyzing',
496
+ filename: input.file.filename,
497
+ fileType,
498
+ startedAt: new Date().toISOString(),
499
+ steps: {
500
+ fileUpload: {
501
+ order: 1,
502
+ status: 'completed',
503
+ },
504
+ fileAnalysis: {
505
+ order: 2,
506
+ status: 'in_progress',
507
+ },
508
+ studyGuide: {
509
+ order: 3,
510
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
511
+ },
512
+ flashcards: {
513
+ order: 4,
514
+ status: input.generateFlashcards ? 'pending' : 'skipped',
515
+ },
516
+ }
517
+ });
518
+
400
519
  try {
401
520
  if (fileType === 'image') {
402
- await aiSessionService.analyseImage(session.id);
403
- console.log('✅ Image analysis completed');
521
+ await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id);
404
522
  } else {
405
- await aiSessionService.analysePDF(session.id);
406
- console.log('✅ PDF analysis completed');
523
+ await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id );
407
524
  }
408
- await PusherService.emitTaskComplete(input.workspaceId, 'file_analysis_complete', { filename: input.file.filename, fileType });
525
+
526
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
527
+ status: 'generating_artifacts',
528
+ filename: input.file.filename,
529
+ fileType,
530
+ startedAt: new Date().toISOString(),
531
+ steps: {
532
+ fileUpload: {
533
+ order: 1,
534
+ status: 'completed',
535
+ },
536
+ fileAnalysis: {
537
+ order: 2,
538
+ status: 'completed',
539
+ },
540
+ studyGuide: {
541
+ order: 3,
542
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
543
+ },
544
+ flashcards: {
545
+ order: 4,
546
+ status: input.generateFlashcards ? 'pending' : 'skipped',
547
+ },
548
+ }
549
+ });
409
550
  } catch (error) {
410
551
  console.error('❌ Failed to analyze file:', error);
411
- await PusherService.emitError(input.workspaceId, `Failed to analyze ${fileType}: ${error}`, 'file_analysis');
552
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
553
+ status: 'error',
554
+ filename: input.file.filename,
555
+ fileType,
556
+ error: `Failed to analyze ${fileType}: ${error}`,
557
+ startedAt: new Date().toISOString(),
558
+ steps: {
559
+ fileUpload: {
560
+ order: 1,
561
+ status: 'completed',
562
+ },
563
+ fileAnalysis: {
564
+ order: 2,
565
+ status: 'error',
566
+ },
567
+ studyGuide: {
568
+ order: 3,
569
+ status: 'skipped',
570
+ },
571
+ flashcards: {
572
+ order: 4,
573
+ status: 'skipped',
574
+ },
575
+ }
576
+ });
412
577
  throw error;
413
578
  }
414
579
 
@@ -430,11 +595,32 @@ export const workspace = router({
430
595
 
431
596
  // Generate artifacts
432
597
  if (input.generateStudyGuide) {
433
- await PusherService.emitTaskComplete(input.workspaceId, 'study_guide_load_start', { filename: input.file.filename });
434
- const content = await aiSessionService.generateStudyGuide(session.id);
435
-
436
- await PusherService.emitTaskComplete(input.workspaceId, 'study_guide_info', { contentLength: content.length });
598
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
599
+ status: 'generating_study_guide',
600
+ filename: input.file.filename,
601
+ fileType,
602
+ startedAt: new Date().toISOString(),
603
+ steps: {
604
+ fileUpload: {
605
+ order: 1,
606
+ status: 'completed',
607
+ },
608
+ fileAnalysis: {
609
+ order: 2,
610
+ status: 'completed',
611
+ },
612
+ studyGuide: {
613
+ order: 3,
614
+ status: 'in_progress',
615
+ },
616
+ flashcards: {
617
+ order: 4,
618
+ status: input.generateFlashcards ? 'pending' : 'skipped',
619
+ },
620
+ }
621
+ });
437
622
 
623
+ const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
438
624
 
439
625
  let artifact = await ctx.db.artifact.findFirst({
440
626
  where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
@@ -460,16 +646,35 @@ export const workspace = router({
460
646
  });
461
647
 
462
648
  results.artifacts.studyGuide = artifact;
463
-
464
- // Emit Pusher notification
465
- await PusherService.emitStudyGuideComplete(input.workspaceId, artifact);
466
649
  }
467
650
 
468
651
  if (input.generateFlashcards) {
469
- await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_load_start', { filename: input.file.filename });
470
- const content = await aiSessionService.generateFlashcardQuestions(session.id, 10, 'medium');
471
-
472
- await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { contentLength: content.length });
652
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
653
+ status: 'generating_flashcards',
654
+ filename: input.file.filename,
655
+ fileType,
656
+ startedAt: new Date().toISOString(),
657
+ steps: {
658
+ fileUpload: {
659
+ order: 1,
660
+ status: 'completed',
661
+ },
662
+ fileAnalysis: {
663
+ order: 2,
664
+ status: 'completed',
665
+ },
666
+ studyGuide: {
667
+ order: 3,
668
+ status: input.generateStudyGuide ? 'completed' : 'skipped',
669
+ },
670
+ flashcards: {
671
+ order: 4,
672
+ status: 'in_progress',
673
+ },
674
+ }
675
+ });
676
+
677
+ const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
473
678
 
474
679
  const artifact = await ctx.db.artifact.create({
475
680
  data: {
@@ -482,7 +687,7 @@ export const workspace = router({
482
687
 
483
688
  // Parse JSON flashcard content
484
689
  try {
485
- const flashcardData = JSON.parse(content);
690
+ const flashcardData: any = content;
486
691
 
487
692
  let createdCards = 0;
488
693
  for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
@@ -523,101 +728,38 @@ export const workspace = router({
523
728
  }
524
729
 
525
730
  results.artifacts.flashcards = artifact;
526
-
527
- // Emit Pusher notification
528
- await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
529
731
  }
530
732
 
531
- if (input.generateWorksheet) {
532
- await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_load_start', { filename: input.file.filename });
533
- const content = await aiSessionService.generateWorksheetQuestions(session.id, 8, 'medium');
534
- await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: content.length });
535
-
536
- const artifact = await ctx.db.artifact.create({
537
- data: {
538
- workspaceId: input.workspaceId,
539
- type: ArtifactType.WORKSHEET,
540
- title: `Worksheet - ${input.file.filename}`,
541
- createdById: ctx.session.user.id,
733
+ await ctx.db.workspace.update({
734
+ where: { id: input.workspaceId },
735
+ data: { fileBeingAnalyzed: false },
736
+ });
737
+
738
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
739
+ status: 'completed',
740
+ filename: input.file.filename,
741
+ fileType,
742
+ startedAt: new Date().toISOString(),
743
+ completedAt: new Date().toISOString(),
744
+ steps: {
745
+ fileUpload: {
746
+ order: 1,
747
+ status: 'completed',
748
+ },
749
+ fileAnalysis: {
750
+ order: 2,
751
+ status: 'completed',
752
+ },
753
+ studyGuide: {
754
+ order: 3,
755
+ status: input.generateStudyGuide ? 'completed' : 'skipped',
756
+ },
757
+ flashcards: {
758
+ order: 4,
759
+ status: input.generateFlashcards ? 'completed' : 'skipped',
542
760
  },
543
- });
544
-
545
- // Parse JSON worksheet content
546
- try {
547
- const worksheetData = JSON.parse(content);
548
-
549
- // The actual worksheet data is in last_response as JSON
550
- let actualWorksheetData = worksheetData;
551
- if (worksheetData.last_response) {
552
- try {
553
- actualWorksheetData = JSON.parse(worksheetData.last_response);
554
- } catch (parseError) {
555
- console.error('❌ Failed to parse last_response JSON:', parseError);
556
- console.log('📋 Raw last_response:', worksheetData.last_response);
557
- }
558
- }
559
-
560
- // Handle different JSON structures
561
- const problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
562
- let createdQuestions = 0;
563
-
564
- for (let i = 0; i < Math.min(problems.length, 8); i++) {
565
- const problem = problems[i];
566
- const prompt = problem.question || problem.prompt || `Question ${i + 1}`;
567
- const answer = problem.answer || problem.solution || `Answer ${i + 1}`;
568
- const type = problem.type || 'TEXT';
569
- const options = problem.options || [];
570
-
571
- await ctx.db.worksheetQuestion.create({
572
- data: {
573
- artifactId: artifact.id,
574
- prompt: prompt,
575
- answer: answer,
576
- difficulty: 'MEDIUM' as any,
577
- order: i,
578
- meta: {
579
- type: type,
580
- options: options.length > 0 ? options : undefined
581
- },
582
- },
583
- });
584
- createdQuestions++;
585
- }
586
-
587
- } catch (parseError) {
588
- console.error('❌ Failed to parse worksheet JSON, using fallback parsing:', parseError);
589
- // Fallback to text parsing if JSON fails
590
- const lines = content.split('\n').filter(line => line.trim());
591
- for (let i = 0; i < Math.min(lines.length, 8); i++) {
592
- const line = lines[i];
593
- if (line.includes(' - ')) {
594
- const [prompt, answer] = line.split(' - ');
595
- await ctx.db.worksheetQuestion.create({
596
- data: {
597
- artifactId: artifact.id,
598
- prompt: prompt.trim(),
599
- answer: answer.trim(),
600
- difficulty: 'MEDIUM' as any,
601
- order: i,
602
- meta: { type: 'TEXT', },
603
- },
604
- });
605
- }
606
- }
607
761
  }
608
-
609
- results.artifacts.worksheet = artifact;
610
-
611
- // Emit Pusher notification
612
- await PusherService.emitWorksheetComplete(input.workspaceId, artifact);
613
- }
614
-
615
- await PusherService.emitTaskComplete(input.workspaceId, 'analysis_cleanup_start', { filename: input.file.filename });
616
- aiSessionService.deleteSession(session.id);
617
- await PusherService.emitTaskComplete(input.workspaceId, 'analysis_cleanup_complete', { filename: input.file.filename });
618
-
619
- // Emit overall completion notification
620
- await PusherService.emitOverallComplete(input.workspaceId, input.file.filename, results.artifacts);
762
+ });
621
763
 
622
764
  return results;
623
765
  }),
@@ -651,7 +793,26 @@ export const workspace = router({
651
793
  },
652
794
  take: input.limit,
653
795
  });
796
+
797
+ // Update analysisProgress for each workspace with search metadata
798
+ const workspaceUpdates = workspaces.map(ws =>
799
+ ctx.db.workspace.update({
800
+ where: { id: ws.id },
801
+ data: {
802
+ analysisProgress: {
803
+ lastSearched: new Date().toISOString(),
804
+ searchQuery: query,
805
+ matchedIn: ws.title.toLowerCase().includes(query.toLowerCase()) ? 'title' : 'description',
806
+ }
807
+ }
808
+ })
809
+ );
810
+
811
+ await Promise.all(workspaceUpdates);
812
+
654
813
  return workspaces;
655
- })
814
+ }),
656
815
 
816
+ // Members sub-router
817
+ members,
657
818
  });