@goscribe/server 1.0.11 → 1.1.0

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 +117 -35
  70. package/src/routers/workspace.ts +328 -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,19 @@
1
1
  import { z } from 'zod';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import { router, 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
+ // Helper function to update and emit analysis progress
10
+ async function updateAnalysisProgress(db, workspaceId, progress) {
11
+ await db.workspace.update({
12
+ where: { id: workspaceId },
13
+ data: { analysisProgress: progress }
14
+ });
15
+ await PusherService.emitAnalysisProgress(workspaceId, progress);
16
+ }
8
17
  // Helper function to calculate search relevance score
9
18
  function calculateRelevance(query, ...texts) {
10
19
  const queryLower = query.toLowerCase();
@@ -88,11 +97,13 @@ export const workspace = router({
88
97
  },
89
98
  },
90
99
  });
100
+ aiSessionService.initSession(ws.id, ctx.session.user.id);
91
101
  return ws;
92
102
  }),
93
103
  createFolder: authedProcedure
94
104
  .input(z.object({
95
105
  name: z.string().min(1).max(100),
106
+ color: z.string().optional(),
96
107
  parentId: z.string().optional(),
97
108
  }))
98
109
  .mutation(async ({ ctx, input }) => {
@@ -100,11 +111,30 @@ export const workspace = router({
100
111
  data: {
101
112
  name: input.name,
102
113
  ownerId: ctx.session.user.id,
114
+ color: input.color ?? '#9D00FF',
103
115
  parentId: input.parentId ?? null,
104
116
  },
105
117
  });
106
118
  return folder;
107
119
  }),
120
+ updateFolder: authedProcedure
121
+ .input(z.object({
122
+ id: z.string(),
123
+ name: z.string().min(1).max(100).optional(),
124
+ color: z.string().optional(),
125
+ }))
126
+ .mutation(async ({ ctx, input }) => {
127
+ const folder = await ctx.db.folder.update({ where: { id: input.id }, data: { name: input.name, color: input.color ?? '#9D00FF' } });
128
+ return folder;
129
+ }),
130
+ deleteFolder: authedProcedure
131
+ .input(z.object({
132
+ id: z.string(),
133
+ }))
134
+ .mutation(async ({ ctx, input }) => {
135
+ const folder = await ctx.db.folder.delete({ where: { id: input.id } });
136
+ return folder;
137
+ }),
108
138
  get: authedProcedure
109
139
  .input(z.object({
110
140
  id: z.string(),
@@ -122,57 +152,28 @@ export const workspace = router({
122
152
  throw new TRPCError({ code: 'NOT_FOUND' });
123
153
  return ws;
124
154
  }),
125
- share: authedProcedure
126
- .input(z.object({
127
- id: z.string(),
128
- }))
129
- .query(async ({ ctx, input }) => {
130
- const ws = await ctx.db.workspace.findFirst({
131
- where: { id: input.id, ownerId: ctx.session.user.id },
155
+ getStats: authedProcedure
156
+ .query(async ({ ctx }) => {
157
+ const workspaces = await ctx.db.workspace.findMany({
158
+ where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
132
159
  });
133
- if (!ws)
134
- throw new TRPCError({ code: 'NOT_FOUND' });
135
- // generate a unique share link if not exists
136
- if (!ws.shareLink) {
137
- const shareLink = [...Array(30)].map(() => (Math.random() * 36 | 0).toString(36)).join('');
138
- const updated = await ctx.db.workspace.update({
139
- where: { id: ws.id },
140
- data: { shareLink },
141
- });
142
- return { shareLink: updated.shareLink };
143
- }
144
- }),
145
- join: authedProcedure
146
- .input(z.object({
147
- shareLink: z.string().min(10).max(100),
148
- }))
149
- .mutation(async ({ ctx, input }) => {
150
- const ws = await ctx.db.workspace.findFirst({
151
- where: { shareLink: input.shareLink },
160
+ const folders = await ctx.db.folder.findMany({
161
+ where: { OR: [{ ownerId: ctx.session.user.id }] },
152
162
  });
153
- if (!ws)
154
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' });
155
- if (ws.ownerId === ctx.session.user.id) {
156
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot join your own workspace' });
157
- }
158
- // add to sharedWith if not already
159
- const alreadyShared = await ctx.db.workspace.findFirst({
160
- where: { id: ws.id, sharedWith: { some: { id: ctx.session.user.id } } },
163
+ const lastUpdated = await ctx.db.workspace.findFirst({
164
+ where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
165
+ orderBy: { updatedAt: 'desc' },
161
166
  });
162
- if (alreadyShared) {
163
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'Already joined this workspace' });
164
- }
165
- await ctx.db.workspace.update({
166
- where: { id: ws.id },
167
- data: { sharedWith: { connect: { id: ctx.session.user.id } } },
167
+ const spaceLeft = await ctx.db.fileAsset.aggregate({
168
+ where: { workspaceId: { in: workspaces.map(ws => ws.id) }, userId: ctx.session.user.id },
169
+ _sum: { size: true },
168
170
  });
169
171
  return {
170
- id: ws.id,
171
- title: ws.title,
172
- description: ws.description,
173
- ownerId: ws.ownerId,
174
- createdAt: ws.createdAt,
175
- updatedAt: ws.updatedAt,
172
+ workspaces: workspaces.length,
173
+ folders: folders.length,
174
+ lastUpdated: lastUpdated?.updatedAt,
175
+ spaceUsed: spaceLeft._sum?.size ?? 0,
176
+ spaceLeft: 1000000000 - (spaceLeft._sum?.size ?? 0) || 0,
176
177
  };
177
178
  }),
178
179
  update: authedProcedure
@@ -180,6 +181,8 @@ export const workspace = router({
180
181
  id: z.string(),
181
182
  name: z.string().min(1).max(100).optional(),
182
183
  description: z.string().max(500).optional(),
184
+ color: z.string().optional(),
185
+ icon: z.string().optional(),
183
186
  }))
184
187
  .mutation(async ({ ctx, input }) => {
185
188
  const existed = await ctx.db.workspace.findFirst({
@@ -192,6 +195,8 @@ export const workspace = router({
192
195
  data: {
193
196
  title: input.name ?? existed.title,
194
197
  description: input.description,
198
+ color: input.color ?? existed.color,
199
+ icon: input.icon ?? existed.icon,
195
200
  },
196
201
  });
197
202
  return updated;
@@ -255,24 +260,27 @@ export const workspace = router({
255
260
  },
256
261
  });
257
262
  // 2. Generate signed URL for direct upload
258
- const [url] = await bucket
259
- .file(`${ctx.session.user.id}/${record.id}-${file.filename}`)
260
- .getSignedUrl({
261
- action: "write",
262
- expires: Date.now() + 5 * 60 * 1000, // 5 min
263
- contentType: file.contentType,
264
- });
263
+ const objectKey = `${ctx.session.user.id}/${record.id}-${file.filename}`;
264
+ const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
265
+ .from('files')
266
+ .createSignedUploadUrl(objectKey); // 5 minutes
267
+ if (signedUrlError) {
268
+ throw new TRPCError({
269
+ code: 'INTERNAL_SERVER_ERROR',
270
+ message: `Failed to generate upload URL: ${signedUrlError.message}`
271
+ });
272
+ }
265
273
  // 3. Update record with bucket info
266
274
  await ctx.db.fileAsset.update({
267
275
  where: { id: record.id },
268
276
  data: {
269
- bucket: bucket.name,
270
- objectKey: `${ctx.session.user.id}/${record.id}-${file.filename}`,
277
+ bucket: 'files',
278
+ objectKey: objectKey,
271
279
  },
272
280
  });
273
281
  results.push({
274
282
  fileId: record.id,
275
- uploadUrl: url,
283
+ uploadUrl: signedUrlData.signedUrl,
276
284
  });
277
285
  }
278
286
  return results;
@@ -291,11 +299,13 @@ export const workspace = router({
291
299
  userId: ctx.session.user.id,
292
300
  },
293
301
  });
294
- // Delete from GCS (best-effort)
302
+ // Delete from Supabase Storage (best-effort)
295
303
  for (const file of files) {
296
304
  if (file.bucket && file.objectKey) {
297
- const gcsFile = bucket.file(file.objectKey);
298
- gcsFile.delete({ ignoreNotFound: true }).catch((err) => {
305
+ supabaseClient.storage
306
+ .from(file.bucket)
307
+ .remove([file.objectKey])
308
+ .catch((err) => {
299
309
  console.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
300
310
  });
301
311
  }
@@ -339,11 +349,47 @@ export const workspace = router({
339
349
  console.error('❌ Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
340
350
  throw new TRPCError({ code: 'NOT_FOUND' });
341
351
  }
342
- console.log(' Workspace verified', { workspaceId: workspace.id, workspaceTitle: workspace.title });
343
- // Convert base64 to buffer
344
- console.log('📁 Converting base64 to buffer...');
352
+ const fileType = input.file.contentType.startsWith('image/') ? 'image' : 'pdf';
353
+ await ctx.db.workspace.update({
354
+ where: { id: input.workspaceId },
355
+ data: { fileBeingAnalyzed: true },
356
+ });
357
+ try {
358
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
359
+ status: 'starting',
360
+ filename: input.file.filename,
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
+ }
381
+ });
382
+ }
383
+ catch (error) {
384
+ console.error('❌ Failed to update analysis progress:', error);
385
+ await ctx.db.workspace.update({
386
+ where: { id: input.workspaceId },
387
+ data: { fileBeingAnalyzed: false },
388
+ });
389
+ await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
390
+ throw error;
391
+ }
345
392
  const fileBuffer = Buffer.from(input.file.content, 'base64');
346
- console.log('✅ File buffer created', { bufferSize: fileBuffer.length });
347
393
  // // Check AI service health first
348
394
  // console.log('🏥 Checking AI service health...');
349
395
  // const isHealthy = await aiSessionService.checkHealth();
@@ -356,41 +402,115 @@ export const workspace = router({
356
402
  // });
357
403
  // }
358
404
  // console.log('✅ AI service is healthy');
359
- // Initialize AI session
360
- console.log('🤖 Initializing AI session...');
361
- const session = await aiSessionService.initSession(input.workspaceId);
362
- console.log('✅ AI session initialized', { sessionId: session.id });
363
405
  const fileObj = new File([fileBuffer], input.file.filename, { type: input.file.contentType });
364
- const fileType = input.file.contentType.startsWith('image/') ? 'image' : 'pdf';
365
- console.log('📤 Uploading file to AI service...', { filename: input.file.filename, fileType });
366
- await aiSessionService.uploadFile(session.id, fileObj, fileType);
367
- console.log('✅ File uploaded to AI service');
368
- console.log('🚀 Starting LLM session...');
369
- try {
370
- await aiSessionService.startLLMSession(session.id);
371
- console.log('✅ LLM session started');
372
- }
373
- catch (error) {
374
- console.error('❌ Failed to start LLM session:', error);
375
- throw error;
376
- }
377
- // Analyze the file first
378
- console.log('🔍 Analyzing file...', { fileType });
379
- await PusherService.emitTaskComplete(input.workspaceId, 'file_analysis_start', { filename: input.file.filename, fileType });
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',
452
+ },
453
+ }
454
+ });
380
455
  try {
381
456
  if (fileType === 'image') {
382
- await aiSessionService.analyseImage(session.id);
383
- console.log('✅ Image analysis completed');
457
+ await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id);
384
458
  }
385
459
  else {
386
- await aiSessionService.analysePDF(session.id);
387
- console.log('✅ PDF analysis completed');
460
+ await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id);
388
461
  }
389
- await PusherService.emitTaskComplete(input.workspaceId, 'file_analysis_complete', { filename: input.file.filename, fileType });
462
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
463
+ status: 'generating_artifacts',
464
+ filename: input.file.filename,
465
+ fileType,
466
+ startedAt: new Date().toISOString(),
467
+ steps: {
468
+ fileUpload: {
469
+ order: 1,
470
+ status: 'completed',
471
+ },
472
+ fileAnalysis: {
473
+ order: 2,
474
+ status: 'completed',
475
+ },
476
+ studyGuide: {
477
+ order: 3,
478
+ status: input.generateStudyGuide ? 'pending' : 'skipped',
479
+ },
480
+ flashcards: {
481
+ order: 4,
482
+ status: input.generateFlashcards ? 'pending' : 'skipped',
483
+ },
484
+ }
485
+ });
390
486
  }
391
487
  catch (error) {
392
488
  console.error('❌ Failed to analyze file:', error);
393
- await PusherService.emitError(input.workspaceId, `Failed to analyze ${fileType}: ${error}`, 'file_analysis');
489
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
490
+ status: 'error',
491
+ filename: input.file.filename,
492
+ fileType,
493
+ error: `Failed to analyze ${fileType}: ${error}`,
494
+ startedAt: new Date().toISOString(),
495
+ steps: {
496
+ fileUpload: {
497
+ order: 1,
498
+ status: 'completed',
499
+ },
500
+ fileAnalysis: {
501
+ order: 2,
502
+ status: 'error',
503
+ },
504
+ studyGuide: {
505
+ order: 3,
506
+ status: 'skipped',
507
+ },
508
+ flashcards: {
509
+ order: 4,
510
+ status: 'skipped',
511
+ },
512
+ }
513
+ });
394
514
  throw error;
395
515
  }
396
516
  const results = {
@@ -403,9 +523,31 @@ export const workspace = router({
403
523
  };
404
524
  // Generate artifacts
405
525
  if (input.generateStudyGuide) {
406
- await PusherService.emitTaskComplete(input.workspaceId, 'study_guide_load_start', { filename: input.file.filename });
407
- const content = await aiSessionService.generateStudyGuide(session.id);
408
- await PusherService.emitTaskComplete(input.workspaceId, 'study_guide_info', { contentLength: content.length });
526
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
527
+ status: 'generating_study_guide',
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: 'in_progress',
543
+ },
544
+ flashcards: {
545
+ order: 4,
546
+ status: input.generateFlashcards ? 'pending' : 'skipped',
547
+ },
548
+ }
549
+ });
550
+ const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
409
551
  let artifact = await ctx.db.artifact.findFirst({
410
552
  where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
411
553
  });
@@ -427,13 +569,33 @@ export const workspace = router({
427
569
  data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
428
570
  });
429
571
  results.artifacts.studyGuide = artifact;
430
- // Emit Pusher notification
431
- await PusherService.emitStudyGuideComplete(input.workspaceId, artifact);
432
572
  }
433
573
  if (input.generateFlashcards) {
434
- await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_load_start', { filename: input.file.filename });
435
- const content = await aiSessionService.generateFlashcardQuestions(session.id, 10, 'medium');
436
- await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { contentLength: content.length });
574
+ await updateAnalysisProgress(ctx.db, input.workspaceId, {
575
+ status: 'generating_flashcards',
576
+ filename: input.file.filename,
577
+ fileType,
578
+ startedAt: new Date().toISOString(),
579
+ steps: {
580
+ fileUpload: {
581
+ order: 1,
582
+ status: 'completed',
583
+ },
584
+ fileAnalysis: {
585
+ order: 2,
586
+ status: 'completed',
587
+ },
588
+ studyGuide: {
589
+ order: 3,
590
+ status: input.generateStudyGuide ? 'completed' : 'skipped',
591
+ },
592
+ flashcards: {
593
+ order: 4,
594
+ status: 'in_progress',
595
+ },
596
+ }
597
+ });
598
+ const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
437
599
  const artifact = await ctx.db.artifact.create({
438
600
  data: {
439
601
  workspaceId: input.workspaceId,
@@ -444,7 +606,7 @@ export const workspace = router({
444
606
  });
445
607
  // Parse JSON flashcard content
446
608
  try {
447
- const flashcardData = JSON.parse(content);
609
+ const flashcardData = content;
448
610
  let createdCards = 0;
449
611
  for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
450
612
  const card = flashcardData[i];
@@ -482,90 +644,36 @@ export const workspace = router({
482
644
  }
483
645
  }
484
646
  results.artifacts.flashcards = artifact;
485
- // Emit Pusher notification
486
- await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
487
647
  }
488
- if (input.generateWorksheet) {
489
- await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_load_start', { filename: input.file.filename });
490
- const content = await aiSessionService.generateWorksheetQuestions(session.id, 8, 'medium');
491
- await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: content.length });
492
- const artifact = await ctx.db.artifact.create({
493
- data: {
494
- workspaceId: input.workspaceId,
495
- type: ArtifactType.WORKSHEET,
496
- title: `Worksheet - ${input.file.filename}`,
497
- createdById: ctx.session.user.id,
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',
498
674
  },
499
- });
500
- // Parse JSON worksheet content
501
- try {
502
- const worksheetData = JSON.parse(content);
503
- // The actual worksheet data is in last_response as JSON
504
- let actualWorksheetData = worksheetData;
505
- if (worksheetData.last_response) {
506
- try {
507
- actualWorksheetData = JSON.parse(worksheetData.last_response);
508
- }
509
- catch (parseError) {
510
- console.error('❌ Failed to parse last_response JSON:', parseError);
511
- console.log('📋 Raw last_response:', worksheetData.last_response);
512
- }
513
- }
514
- // Handle different JSON structures
515
- const problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
516
- let createdQuestions = 0;
517
- for (let i = 0; i < Math.min(problems.length, 8); i++) {
518
- const problem = problems[i];
519
- const prompt = problem.question || problem.prompt || `Question ${i + 1}`;
520
- const answer = problem.answer || problem.solution || `Answer ${i + 1}`;
521
- const type = problem.type || 'TEXT';
522
- const options = problem.options || [];
523
- await ctx.db.worksheetQuestion.create({
524
- data: {
525
- artifactId: artifact.id,
526
- prompt: prompt,
527
- answer: answer,
528
- difficulty: 'MEDIUM',
529
- order: i,
530
- meta: {
531
- type: type,
532
- options: options.length > 0 ? options : undefined
533
- },
534
- },
535
- });
536
- createdQuestions++;
537
- }
538
- }
539
- catch (parseError) {
540
- console.error('❌ Failed to parse worksheet JSON, using fallback parsing:', parseError);
541
- // Fallback to text parsing if JSON fails
542
- const lines = content.split('\n').filter(line => line.trim());
543
- for (let i = 0; i < Math.min(lines.length, 8); i++) {
544
- const line = lines[i];
545
- if (line.includes(' - ')) {
546
- const [prompt, answer] = line.split(' - ');
547
- await ctx.db.worksheetQuestion.create({
548
- data: {
549
- artifactId: artifact.id,
550
- prompt: prompt.trim(),
551
- answer: answer.trim(),
552
- difficulty: 'MEDIUM',
553
- order: i,
554
- meta: { type: 'TEXT', },
555
- },
556
- });
557
- }
558
- }
559
675
  }
560
- results.artifacts.worksheet = artifact;
561
- // Emit Pusher notification
562
- await PusherService.emitWorksheetComplete(input.workspaceId, artifact);
563
- }
564
- await PusherService.emitTaskComplete(input.workspaceId, 'analysis_cleanup_start', { filename: input.file.filename });
565
- aiSessionService.deleteSession(session.id);
566
- await PusherService.emitTaskComplete(input.workspaceId, 'analysis_cleanup_complete', { filename: input.file.filename });
567
- // Emit overall completion notification
568
- await PusherService.emitOverallComplete(input.workspaceId, input.file.filename, results.artifacts);
676
+ });
569
677
  return results;
570
678
  }),
571
679
  search: authedProcedure
@@ -598,6 +706,20 @@ export const workspace = router({
598
706
  },
599
707
  take: input.limit,
600
708
  });
709
+ // Update analysisProgress for each workspace with search metadata
710
+ const workspaceUpdates = workspaces.map(ws => ctx.db.workspace.update({
711
+ where: { id: ws.id },
712
+ data: {
713
+ analysisProgress: {
714
+ lastSearched: new Date().toISOString(),
715
+ searchQuery: query,
716
+ matchedIn: ws.title.toLowerCase().includes(query.toLowerCase()) ? 'title' : 'description',
717
+ }
718
+ }
719
+ }));
720
+ await Promise.all(workspaceUpdates);
601
721
  return workspaces;
602
- })
722
+ }),
723
+ // Members sub-router
724
+ members,
603
725
  });
package/dist/server.js CHANGED
@@ -7,6 +7,7 @@ import compression from 'compression';
7
7
  import * as trpcExpress from '@trpc/server/adapters/express';
8
8
  import { appRouter } from './routers/_app.js';
9
9
  import { createContext } from './context.js';
10
+ import { logger } from './lib/logger.js';
10
11
  const PORT = process.env.PORT ? Number(process.env.PORT) : 3001;
11
12
  async function main() {
12
13
  const app = express();
@@ -19,7 +20,14 @@ async function main() {
19
20
  allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'Set-Cookie'],
20
21
  exposedHeaders: ['Set-Cookie'],
21
22
  }));
22
- app.use(morgan('dev'));
23
+ // Custom morgan middleware with logger integration
24
+ app.use(morgan('combined', {
25
+ stream: {
26
+ write: (message) => {
27
+ logger.info(message.trim(), 'HTTP');
28
+ }
29
+ }
30
+ }));
23
31
  app.use(compression());
24
32
  app.use(express.json({ limit: '50mb' }));
25
33
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
@@ -33,11 +41,11 @@ async function main() {
33
41
  createContext,
34
42
  }));
35
43
  app.listen(PORT, () => {
36
- console.log(`✅ Server ready on http://localhost:${PORT}`);
37
- console.log(`➡️ tRPC endpoint at http://localhost:${PORT}/trpc`);
44
+ logger.info(`Server ready on http://localhost:${PORT}`, 'SERVER');
45
+ logger.info(`tRPC endpoint at http://localhost:${PORT}/trpc`, 'SERVER');
38
46
  });
39
47
  }
40
48
  main().catch((err) => {
41
- console.error('Failed to start server', err);
49
+ logger.error('Failed to start server', 'SERVER', undefined, err);
42
50
  process.exit(1);
43
51
  });