@goscribe/server 1.0.8 → 1.0.10

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 (58) hide show
  1. package/AUTH_FRONTEND_SPEC.md +21 -0
  2. package/CHAT_FRONTEND_SPEC.md +474 -0
  3. package/DATABASE_SETUP.md +165 -0
  4. package/MEETINGSUMMARY_FRONTEND_SPEC.md +28 -0
  5. package/PODCAST_FRONTEND_SPEC.md +595 -0
  6. package/STUDYGUIDE_FRONTEND_SPEC.md +18 -0
  7. package/WORKSHEETS_FRONTEND_SPEC.md +26 -0
  8. package/WORKSPACE_FRONTEND_SPEC.md +47 -0
  9. package/dist/lib/ai-session.d.ts +26 -0
  10. package/dist/lib/ai-session.js +343 -0
  11. package/dist/lib/inference.d.ts +2 -0
  12. package/dist/lib/inference.js +21 -0
  13. package/dist/lib/pusher.d.ts +14 -0
  14. package/dist/lib/pusher.js +94 -0
  15. package/dist/lib/storage.d.ts +10 -2
  16. package/dist/lib/storage.js +63 -6
  17. package/dist/routers/_app.d.ts +840 -58
  18. package/dist/routers/_app.js +6 -0
  19. package/dist/routers/ai-session.d.ts +0 -0
  20. package/dist/routers/ai-session.js +1 -0
  21. package/dist/routers/auth.d.ts +1 -0
  22. package/dist/routers/auth.js +6 -4
  23. package/dist/routers/chat.d.ts +171 -0
  24. package/dist/routers/chat.js +270 -0
  25. package/dist/routers/flashcards.d.ts +37 -0
  26. package/dist/routers/flashcards.js +128 -0
  27. package/dist/routers/meetingsummary.d.ts +0 -0
  28. package/dist/routers/meetingsummary.js +377 -0
  29. package/dist/routers/podcast.d.ts +277 -0
  30. package/dist/routers/podcast.js +847 -0
  31. package/dist/routers/studyguide.d.ts +54 -0
  32. package/dist/routers/studyguide.js +125 -0
  33. package/dist/routers/worksheets.d.ts +138 -51
  34. package/dist/routers/worksheets.js +317 -7
  35. package/dist/routers/workspace.d.ts +162 -7
  36. package/dist/routers/workspace.js +440 -8
  37. package/dist/server.js +6 -2
  38. package/package.json +11 -4
  39. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
  40. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
  41. package/prisma/migrations/migration_lock.toml +3 -0
  42. package/prisma/schema.prisma +87 -6
  43. package/prisma/seed.mjs +135 -0
  44. package/src/lib/ai-session.ts +412 -0
  45. package/src/lib/inference.ts +21 -0
  46. package/src/lib/pusher.ts +104 -0
  47. package/src/lib/storage.ts +89 -6
  48. package/src/routers/_app.ts +6 -0
  49. package/src/routers/auth.ts +8 -4
  50. package/src/routers/chat.ts +275 -0
  51. package/src/routers/flashcards.ts +142 -0
  52. package/src/routers/meetingsummary.ts +416 -0
  53. package/src/routers/podcast.ts +934 -0
  54. package/src/routers/studyguide.ts +144 -0
  55. package/src/routers/worksheets.ts +336 -7
  56. package/src/routers/workspace.ts +487 -8
  57. package/src/server.ts +7 -2
  58. package/test-ai-integration.js +134 -0
@@ -3,24 +3,78 @@ import { TRPCError } from '@trpc/server';
3
3
  import { router, publicProcedure, authedProcedure } from '../trpc.js';
4
4
  import { bucket } from '../lib/storage.js';
5
5
  import { ArtifactType } from '@prisma/client';
6
+ import { aiSessionService } from '../lib/ai-session.js';
7
+ import PusherService from '../lib/pusher.js';
8
+
9
+ // Helper function to calculate search relevance score
10
+ function calculateRelevance(query: string, ...texts: (string | null | undefined)[]): number {
11
+ const queryLower = query.toLowerCase();
12
+ let score = 0;
13
+
14
+ for (const text of texts) {
15
+ if (!text) continue;
16
+
17
+ const textLower = text.toLowerCase();
18
+
19
+ // Exact match gets highest score
20
+ if (textLower.includes(queryLower)) {
21
+ score += 10;
22
+ }
23
+
24
+ // Word boundary matches get good score
25
+ const words = queryLower.split(/\s+/);
26
+ for (const word of words) {
27
+ if (word.length > 2 && textLower.includes(word)) {
28
+ score += 5;
29
+ }
30
+ }
31
+
32
+ // Partial matches get lower score
33
+ const queryChars = queryLower.split('');
34
+ let consecutiveMatches = 0;
35
+ for (const char of queryChars) {
36
+ if (textLower.includes(char)) {
37
+ consecutiveMatches++;
38
+ } else {
39
+ consecutiveMatches = 0;
40
+ }
41
+ }
42
+ score += consecutiveMatches * 0.1;
43
+ }
44
+
45
+ return score;
46
+ }
6
47
 
7
48
  export const workspace = router({
8
49
  // List current user's workspaces
9
50
  list: authedProcedure
10
- .query(async ({ ctx }) => {
51
+ .input(z.object({
52
+ parentId: z.string().optional(),
53
+ }))
54
+ .query(async ({ ctx, input }) => {
11
55
  const workspaces = await ctx.db.workspace.findMany({
12
56
  where: {
13
57
  ownerId: ctx.session.user.id,
58
+ folderId: input.parentId ?? null,
14
59
  },
15
60
  orderBy: { updatedAt: 'desc' },
16
61
  });
17
- return workspaces;
62
+
63
+ const folders = await ctx.db.folder.findMany({
64
+ where: {
65
+ ownerId: ctx.session.user.id,
66
+ parentId: input.parentId ?? null,
67
+ },
68
+ });
69
+
70
+ return { workspaces, folders };
18
71
  }),
19
72
 
20
73
  create: authedProcedure
21
74
  .input(z.object({
22
75
  name: z.string().min(1).max(100),
23
76
  description: z.string().max(500).optional(),
77
+ parentId: z.string().optional(),
24
78
  }))
25
79
  .mutation(async ({ ctx, input}) => {
26
80
  const ws = await ctx.db.workspace.create({
@@ -28,6 +82,7 @@ export const workspace = router({
28
82
  title: input.name,
29
83
  description: input.description,
30
84
  ownerId: ctx.session.user.id,
85
+ folderId: input.parentId ?? null,
31
86
  artifacts: {
32
87
  create: {
33
88
  type: ArtifactType.FLASHCARD_SET,
@@ -44,20 +99,92 @@ export const workspace = router({
44
99
  });
45
100
  return ws;
46
101
  }),
102
+ createFolder: authedProcedure
103
+ .input(z.object({
104
+ name: z.string().min(1).max(100),
105
+ parentId: z.string().optional(),
106
+ }))
107
+ .mutation(async ({ ctx, input }) => {
108
+ const folder = await ctx.db.folder.create({
109
+ data: {
110
+ name: input.name,
111
+ ownerId: ctx.session.user.id,
112
+ parentId: input.parentId ?? null,
113
+ },
114
+ });
115
+ return folder;
116
+ }),
47
117
  get: authedProcedure
48
118
  .input(z.object({
49
- id: z.string().uuid(),
119
+ id: z.string(),
50
120
  }))
51
121
  .query(async ({ ctx, input }) => {
52
122
  const ws = await ctx.db.workspace.findFirst({
53
123
  where: { id: input.id, ownerId: ctx.session.user.id },
124
+ include: {
125
+ artifacts: true,
126
+ folder: true,
127
+ uploads: true,
128
+ },
54
129
  });
55
130
  if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
56
131
  return ws;
57
132
  }),
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 },
140
+ });
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 },
160
+ });
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 } } },
168
+ });
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 } } },
175
+ });
176
+ 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,
183
+ };
184
+ }),
58
185
  update: authedProcedure
59
186
  .input(z.object({
60
- id: z.string().uuid(),
187
+ id: z.string(),
61
188
  name: z.string().min(1).max(100).optional(),
62
189
  description: z.string().max(500).optional(),
63
190
  }))
@@ -77,7 +204,7 @@ export const workspace = router({
77
204
  }),
78
205
  delete: authedProcedure
79
206
  .input(z.object({
80
- id: z.string().uuid(),
207
+ id: z.string(),
81
208
  }))
82
209
  .mutation(async ({ ctx, input }) => {
83
210
  const deleted = await ctx.db.workspace.deleteMany({
@@ -86,9 +213,30 @@ export const workspace = router({
86
213
  if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
87
214
  return true;
88
215
  }),
216
+ getFolderInformation: authedProcedure
217
+ .input(z.object({
218
+ id: z.string(),
219
+ }))
220
+ .query(async ({ ctx, input }) => {
221
+ const folder = await ctx.db.folder.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
222
+ // find all of its parents
223
+ if (!folder) throw new TRPCError({ code: 'NOT_FOUND' });
224
+
225
+ const parents = [];
226
+ let current = folder;
227
+
228
+ while (current.parentId) {
229
+ const parent = await ctx.db.folder.findFirst({ where: { id: current.parentId, ownerId: ctx.session.user.id } });
230
+ if (!parent) break;
231
+ parents.push(parent);
232
+ current = parent;
233
+ }
234
+
235
+ return { folder, parents };
236
+ }),
89
237
  uploadFiles: authedProcedure
90
238
  .input(z.object({
91
- id: z.string().uuid(),
239
+ id: z.string(),
92
240
  files: z.array(
93
241
  z.object({
94
242
  filename: z.string().min(1).max(255),
@@ -144,8 +292,8 @@ export const workspace = router({
144
292
  }),
145
293
  deleteFiles: authedProcedure
146
294
  .input(z.object({
147
- fileId: z.array(z.string().uuid()),
148
- id: z.string().uuid(),
295
+ fileId: z.array(z.string()),
296
+ id: z.string(),
149
297
  }))
150
298
  .mutation(async ({ ctx, input }) => {
151
299
  // ensure files are in the user's workspace
@@ -175,4 +323,335 @@ export const workspace = router({
175
323
  });
176
324
  return true;
177
325
  }),
326
+ uploadAndAnalyzeMedia: authedProcedure
327
+ .input(z.object({
328
+ workspaceId: z.string(),
329
+ file: z.object({
330
+ filename: z.string(),
331
+ contentType: z.string(),
332
+ size: z.number(),
333
+ content: z.string(), // Base64 encoded file content
334
+ }),
335
+ generateStudyGuide: z.boolean().default(true),
336
+ generateFlashcards: z.boolean().default(true),
337
+ generateWorksheet: z.boolean().default(true),
338
+ }))
339
+ .mutation(async ({ ctx, input }) => {
340
+ console.log('🚀 uploadAndAnalyzeMedia started', {
341
+ workspaceId: input.workspaceId,
342
+ filename: input.file.filename,
343
+ fileSize: input.file.size,
344
+ generateStudyGuide: input.generateStudyGuide,
345
+ generateFlashcards: input.generateFlashcards,
346
+ generateWorksheet: input.generateWorksheet
347
+ });
348
+
349
+ // Verify workspace ownership
350
+ const workspace = await ctx.db.workspace.findFirst({
351
+ where: { id: input.workspaceId, ownerId: ctx.session.user.id }
352
+ });
353
+ if (!workspace) {
354
+ console.error('❌ Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
355
+ throw new TRPCError({ code: 'NOT_FOUND' });
356
+ }
357
+ console.log('✅ Workspace verified', { workspaceId: workspace.id, workspaceTitle: workspace.title });
358
+
359
+ // Convert base64 to buffer
360
+ console.log('📁 Converting base64 to buffer...');
361
+ const fileBuffer = Buffer.from(input.file.content, 'base64');
362
+ console.log('✅ File buffer created', { bufferSize: fileBuffer.length });
363
+
364
+ // // Check AI service health first
365
+ // console.log('🏥 Checking AI service health...');
366
+ // const isHealthy = await aiSessionService.checkHealth();
367
+ // if (!isHealthy) {
368
+ // console.error('❌ AI service is not available');
369
+ // await PusherService.emitError(input.workspaceId, 'AI service is currently unavailable');
370
+ // throw new TRPCError({
371
+ // code: 'SERVICE_UNAVAILABLE',
372
+ // message: 'AI service is currently unavailable. Please try again later.',
373
+ // });
374
+ // }
375
+ // console.log('✅ AI service is healthy');
376
+
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
+ 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
+
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 });
400
+ try {
401
+ if (fileType === 'image') {
402
+ await aiSessionService.analyseImage(session.id);
403
+ console.log('✅ Image analysis completed');
404
+ } else {
405
+ await aiSessionService.analysePDF(session.id);
406
+ console.log('✅ PDF analysis completed');
407
+ }
408
+ await PusherService.emitTaskComplete(input.workspaceId, 'file_analysis_complete', { filename: input.file.filename, fileType });
409
+ } catch (error) {
410
+ console.error('❌ Failed to analyze file:', error);
411
+ await PusherService.emitError(input.workspaceId, `Failed to analyze ${fileType}: ${error}`, 'file_analysis');
412
+ throw error;
413
+ }
414
+
415
+ const results: {
416
+ filename: string;
417
+ artifacts: {
418
+ studyGuide: any | null;
419
+ flashcards: any | null;
420
+ worksheet: any | null;
421
+ };
422
+ } = {
423
+ filename: input.file.filename,
424
+ artifacts: {
425
+ studyGuide: null,
426
+ flashcards: null,
427
+ worksheet: null,
428
+ }
429
+ };
430
+
431
+ // Generate artifacts
432
+ 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 });
437
+
438
+
439
+ let artifact = await ctx.db.artifact.findFirst({
440
+ where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
441
+ });
442
+ if (!artifact) {
443
+ artifact = await ctx.db.artifact.create({
444
+ data: {
445
+ workspaceId: input.workspaceId,
446
+ type: ArtifactType.STUDY_GUIDE,
447
+ title: `Study Guide - ${input.file.filename}`,
448
+ createdById: ctx.session.user.id,
449
+ },
450
+ });
451
+ }
452
+
453
+ const lastVersion = await ctx.db.artifactVersion.findFirst({
454
+ where: { artifact: {workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE} },
455
+ orderBy: { version: 'desc' },
456
+ });
457
+
458
+ await ctx.db.artifactVersion.create({
459
+ data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
460
+ });
461
+
462
+ results.artifacts.studyGuide = artifact;
463
+
464
+ // Emit Pusher notification
465
+ await PusherService.emitStudyGuideComplete(input.workspaceId, artifact);
466
+ }
467
+
468
+ 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 });
473
+
474
+ const artifact = await ctx.db.artifact.create({
475
+ data: {
476
+ workspaceId: input.workspaceId,
477
+ type: ArtifactType.FLASHCARD_SET,
478
+ title: `Flashcards - ${input.file.filename}`,
479
+ createdById: ctx.session.user.id,
480
+ },
481
+ });
482
+
483
+ // Parse JSON flashcard content
484
+ try {
485
+ const flashcardData = JSON.parse(content);
486
+
487
+ let createdCards = 0;
488
+ for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
489
+ const card = flashcardData[i];
490
+ const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
491
+ const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
492
+
493
+ await ctx.db.flashcard.create({
494
+ data: {
495
+ artifactId: artifact.id,
496
+ front: front,
497
+ back: back,
498
+ order: i,
499
+ tags: ['ai-generated', 'medium'],
500
+ },
501
+ });
502
+ createdCards++;
503
+ }
504
+
505
+ } catch (parseError) {
506
+ // Fallback to text parsing if JSON fails
507
+ const lines = content.split('\n').filter(line => line.trim());
508
+ for (let i = 0; i < Math.min(lines.length, 10); i++) {
509
+ const line = lines[i];
510
+ if (line.includes(' - ')) {
511
+ const [front, back] = line.split(' - ');
512
+ await ctx.db.flashcard.create({
513
+ data: {
514
+ artifactId: artifact.id,
515
+ front: front.trim(),
516
+ back: back.trim(),
517
+ order: i,
518
+ tags: ['ai-generated', 'medium'],
519
+ },
520
+ });
521
+ }
522
+ }
523
+ }
524
+
525
+ results.artifacts.flashcards = artifact;
526
+
527
+ // Emit Pusher notification
528
+ await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
529
+ }
530
+
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,
542
+ },
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
+ }
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);
621
+
622
+ return results;
623
+ }),
624
+ search: authedProcedure
625
+ .input(z.object({
626
+ query: z.string(),
627
+ limit: z.number().min(1).max(100).default(20),
628
+ }))
629
+ .query(async ({ ctx, input }) => {
630
+ const { query } = input;
631
+ const workspaces = await ctx.db.workspace.findMany({
632
+ where: {
633
+ ownerId: ctx.session.user.id,
634
+ OR: [
635
+ {
636
+ title: {
637
+ contains: query,
638
+ mode: 'insensitive',
639
+ },
640
+ },
641
+ {
642
+ description: {
643
+ contains: query,
644
+ mode: 'insensitive',
645
+ },
646
+ },
647
+ ],
648
+ },
649
+ orderBy: {
650
+ updatedAt: 'desc',
651
+ },
652
+ take: input.limit,
653
+ });
654
+ return workspaces;
655
+ })
656
+
178
657
  });
package/src/server.ts CHANGED
@@ -8,6 +8,7 @@ import * as trpcExpress from '@trpc/server/adapters/express';
8
8
 
9
9
  import { appRouter } from './routers/_app.js';
10
10
  import { createContext } from './context.js';
11
+ import { prisma } from './lib/prisma.js';
11
12
 
12
13
  const PORT = process.env.PORT ? Number(process.env.PORT) : 3001;
13
14
 
@@ -17,13 +18,17 @@ async function main() {
17
18
  // Middlewares
18
19
  app.use(helmet());
19
20
  app.use(cors({
20
- origin: "http://localhost:3000", // your Next.js dev URL
21
+ origin: process.env.FRONTEND_URL || "http://localhost:3000",
21
22
  credentials: true, // allow cookies
23
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
24
+ allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'Set-Cookie'],
25
+ exposedHeaders: ['Set-Cookie'],
22
26
  }));
23
27
 
24
28
  app.use(morgan('dev'));
25
29
  app.use(compression());
26
- app.use(express.json());
30
+ app.use(express.json({ limit: '50mb' }));
31
+ app.use(express.urlencoded({ limit: '50mb', extended: true }));
27
32
 
28
33
 
29
34
  // Health (plain Express)