@goscribe/server 1.1.2 → 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 +12 -2
- package/src/lib/ai-session.ts +3 -21
- package/src/routers/flashcards.ts +0 -16
- package/src/routers/members.ts +13 -2
- package/src/routers/workspace.ts +468 -439
- 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
package/src/routers/workspace.ts
CHANGED
|
@@ -26,17 +26,17 @@ async function updateAnalysisProgress(
|
|
|
26
26
|
function calculateRelevance(query: string, ...texts: (string | null | undefined)[]): number {
|
|
27
27
|
const queryLower = query.toLowerCase();
|
|
28
28
|
let score = 0;
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
for (const text of texts) {
|
|
31
31
|
if (!text) continue;
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
const textLower = text.toLowerCase();
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
// Exact match gets highest score
|
|
36
36
|
if (textLower.includes(queryLower)) {
|
|
37
37
|
score += 10;
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
// Word boundary matches get good score
|
|
41
41
|
const words = queryLower.split(/\s+/);
|
|
42
42
|
for (const word of words) {
|
|
@@ -44,7 +44,7 @@ function calculateRelevance(query: string, ...texts: (string | null | undefined)
|
|
|
44
44
|
score += 5;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
// Partial matches get lower score
|
|
49
49
|
const queryChars = queryLower.split('');
|
|
50
50
|
let consecutiveMatches = 0;
|
|
@@ -57,7 +57,7 @@ function calculateRelevance(query: string, ...texts: (string | null | undefined)
|
|
|
57
57
|
}
|
|
58
58
|
score += consecutiveMatches * 0.1;
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
return score;
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -65,8 +65,8 @@ export const workspace = router({
|
|
|
65
65
|
// List current user's workspaces
|
|
66
66
|
list: authedProcedure
|
|
67
67
|
.input(z.object({
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
parentId: z.string().optional(),
|
|
69
|
+
}))
|
|
70
70
|
.query(async ({ ctx, input }) => {
|
|
71
71
|
const workspaces = await ctx.db.workspace.findMany({
|
|
72
72
|
where: {
|
|
@@ -85,14 +85,14 @@ export const workspace = router({
|
|
|
85
85
|
|
|
86
86
|
return { workspaces, folders };
|
|
87
87
|
}),
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
create: authedProcedure
|
|
90
90
|
.input(z.object({
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.mutation(async ({ ctx, input}) => {
|
|
91
|
+
name: z.string().min(1).max(100),
|
|
92
|
+
description: z.string().max(500).optional(),
|
|
93
|
+
parentId: z.string().optional(),
|
|
94
|
+
}))
|
|
95
|
+
.mutation(async ({ ctx, input }) => {
|
|
96
96
|
const ws = await ctx.db.workspace.create({
|
|
97
97
|
data: {
|
|
98
98
|
title: input.name,
|
|
@@ -119,10 +119,10 @@ export const workspace = router({
|
|
|
119
119
|
}),
|
|
120
120
|
createFolder: authedProcedure
|
|
121
121
|
.input(z.object({
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
name: z.string().min(1).max(100),
|
|
123
|
+
color: z.string().optional(),
|
|
124
|
+
parentId: z.string().optional(),
|
|
125
|
+
}))
|
|
126
126
|
.mutation(async ({ ctx, input }) => {
|
|
127
127
|
const folder = await ctx.db.folder.create({
|
|
128
128
|
data: {
|
|
@@ -136,26 +136,26 @@ export const workspace = router({
|
|
|
136
136
|
}),
|
|
137
137
|
updateFolder: authedProcedure
|
|
138
138
|
.input(z.object({
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
id: z.string(),
|
|
140
|
+
name: z.string().min(1).max(100).optional(),
|
|
141
|
+
color: z.string().optional(),
|
|
142
|
+
}))
|
|
143
143
|
.mutation(async ({ ctx, input }) => {
|
|
144
144
|
const folder = await ctx.db.folder.update({ where: { id: input.id }, data: { name: input.name, color: input.color ?? '#9D00FF' } });
|
|
145
145
|
return folder;
|
|
146
146
|
}),
|
|
147
147
|
deleteFolder: authedProcedure
|
|
148
148
|
.input(z.object({
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
id: z.string(),
|
|
150
|
+
}))
|
|
151
151
|
.mutation(async ({ ctx, input }) => {
|
|
152
152
|
const folder = await ctx.db.folder.delete({ where: { id: input.id } });
|
|
153
153
|
return folder;
|
|
154
154
|
}),
|
|
155
155
|
get: authedProcedure
|
|
156
156
|
.input(z.object({
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
id: z.string(),
|
|
158
|
+
}))
|
|
159
159
|
.query(async ({ ctx, input }) => {
|
|
160
160
|
const ws = await ctx.db.workspace.findFirst({
|
|
161
161
|
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
@@ -171,13 +171,13 @@ export const workspace = router({
|
|
|
171
171
|
getStats: authedProcedure
|
|
172
172
|
.query(async ({ ctx }) => {
|
|
173
173
|
const workspaces = await ctx.db.workspace.findMany({
|
|
174
|
-
where: {OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }]},
|
|
174
|
+
where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
|
|
175
175
|
});
|
|
176
176
|
const folders = await ctx.db.folder.findMany({
|
|
177
|
-
where: {OR: [{ ownerId: ctx.session.user.id }
|
|
177
|
+
where: { OR: [{ ownerId: ctx.session.user.id }] },
|
|
178
178
|
});
|
|
179
179
|
const lastUpdated = await ctx.db.workspace.findFirst({
|
|
180
|
-
where: {OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }]},
|
|
180
|
+
where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
|
|
181
181
|
orderBy: { updatedAt: 'desc' },
|
|
182
182
|
});
|
|
183
183
|
|
|
@@ -196,12 +196,12 @@ export const workspace = router({
|
|
|
196
196
|
}),
|
|
197
197
|
update: authedProcedure
|
|
198
198
|
.input(z.object({
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
id: z.string(),
|
|
200
|
+
name: z.string().min(1).max(100).optional(),
|
|
201
|
+
description: z.string().max(500).optional(),
|
|
202
|
+
color: z.string().optional(),
|
|
203
|
+
icon: z.string().optional(),
|
|
204
|
+
}))
|
|
205
205
|
.mutation(async ({ ctx, input }) => {
|
|
206
206
|
const existed = await ctx.db.workspace.findFirst({
|
|
207
207
|
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
@@ -216,12 +216,12 @@ export const workspace = router({
|
|
|
216
216
|
icon: input.icon ?? existed.icon,
|
|
217
217
|
},
|
|
218
218
|
});
|
|
219
|
-
return updated;
|
|
220
|
-
}),
|
|
221
|
-
|
|
219
|
+
return updated;
|
|
220
|
+
}),
|
|
221
|
+
delete: authedProcedure
|
|
222
222
|
.input(z.object({
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
id: z.string(),
|
|
224
|
+
}))
|
|
225
225
|
.mutation(async ({ ctx, input }) => {
|
|
226
226
|
const deleted = await ctx.db.workspace.deleteMany({
|
|
227
227
|
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
@@ -229,15 +229,15 @@ export const workspace = router({
|
|
|
229
229
|
if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
230
230
|
return true;
|
|
231
231
|
}),
|
|
232
|
-
|
|
232
|
+
getFolderInformation: authedProcedure
|
|
233
233
|
.input(z.object({
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
id: z.string(),
|
|
235
|
+
}))
|
|
236
236
|
.query(async ({ ctx, input }) => {
|
|
237
237
|
const folder = await ctx.db.folder.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
|
|
238
238
|
// find all of its parents
|
|
239
239
|
if (!folder) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
const parents = [];
|
|
242
242
|
let current = folder;
|
|
243
243
|
|
|
@@ -247,20 +247,36 @@ export const workspace = router({
|
|
|
247
247
|
parents.push(parent);
|
|
248
248
|
current = parent;
|
|
249
249
|
}
|
|
250
|
-
|
|
250
|
+
|
|
251
251
|
return { folder, parents };
|
|
252
252
|
}),
|
|
253
|
-
|
|
253
|
+
|
|
254
|
+
getSharedWith: authedProcedure
|
|
254
255
|
.input(z.object({
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
id: z.string(),
|
|
257
|
+
}))
|
|
258
|
+
.query(async ({ ctx, input }) => {
|
|
259
|
+
|
|
260
|
+
const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
|
|
261
|
+
if (!user || !user.email) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
262
|
+
const sharedWith = await ctx.db.workspace.findMany({ where: { members: { some: { userId: ctx.session.user.id } } } });
|
|
263
|
+
const invitations = await ctx.db.workspaceInvitation.findMany({ where: { email: user.email, acceptedAt: null }, include: {
|
|
264
|
+
workspace: true,
|
|
265
|
+
} });
|
|
266
|
+
|
|
267
|
+
return { shared: sharedWith, invitations };
|
|
268
|
+
}),
|
|
269
|
+
uploadFiles: authedProcedure
|
|
270
|
+
.input(z.object({
|
|
271
|
+
id: z.string(),
|
|
272
|
+
files: z.array(
|
|
273
|
+
z.object({
|
|
274
|
+
filename: z.string().min(1).max(255),
|
|
275
|
+
contentType: z.string().min(1).max(100),
|
|
276
|
+
size: z.number().min(1), // size in bytes
|
|
277
|
+
})
|
|
278
|
+
),
|
|
279
|
+
}))
|
|
264
280
|
.mutation(async ({ ctx, input }) => {
|
|
265
281
|
// ensure workspace belongs to user
|
|
266
282
|
const ws = await ctx.db.workspace.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
|
|
@@ -275,7 +291,7 @@ export const workspace = router({
|
|
|
275
291
|
name: file.filename,
|
|
276
292
|
mimeType: file.contentType,
|
|
277
293
|
size: file.size,
|
|
278
|
-
workspaceId: input.id,
|
|
294
|
+
workspaceId: input.id,
|
|
279
295
|
},
|
|
280
296
|
});
|
|
281
297
|
|
|
@@ -286,9 +302,9 @@ export const workspace = router({
|
|
|
286
302
|
.createSignedUploadUrl(objectKey); // 5 minutes
|
|
287
303
|
|
|
288
304
|
if (signedUrlError) {
|
|
289
|
-
throw new TRPCError({
|
|
290
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
291
|
-
message: `Failed to generate upload URL: ${signedUrlError.message}`
|
|
305
|
+
throw new TRPCError({
|
|
306
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
307
|
+
message: `Failed to generate upload URL: ${signedUrlError.message}`
|
|
292
308
|
});
|
|
293
309
|
}
|
|
294
310
|
|
|
@@ -299,7 +315,7 @@ export const workspace = router({
|
|
|
299
315
|
bucket: 'files',
|
|
300
316
|
objectKey: objectKey,
|
|
301
317
|
},
|
|
302
|
-
});
|
|
318
|
+
});
|
|
303
319
|
|
|
304
320
|
results.push({
|
|
305
321
|
fileId: record.id,
|
|
@@ -310,11 +326,11 @@ export const workspace = router({
|
|
|
310
326
|
return results;
|
|
311
327
|
|
|
312
328
|
}),
|
|
313
|
-
|
|
329
|
+
deleteFiles: authedProcedure
|
|
314
330
|
.input(z.object({
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
fileId: z.array(z.string()),
|
|
332
|
+
id: z.string(),
|
|
333
|
+
}))
|
|
318
334
|
.mutation(async ({ ctx, input }) => {
|
|
319
335
|
// ensure files are in the user's workspace
|
|
320
336
|
const files = await ctx.db.fileAsset.findMany({
|
|
@@ -345,7 +361,7 @@ export const workspace = router({
|
|
|
345
361
|
});
|
|
346
362
|
return true;
|
|
347
363
|
}),
|
|
348
|
-
|
|
364
|
+
getFileUploadUrl: authedProcedure
|
|
349
365
|
.input(z.object({
|
|
350
366
|
workspaceId: z.string(),
|
|
351
367
|
filename: z.string(),
|
|
@@ -376,13 +392,13 @@ export const workspace = router({
|
|
|
376
392
|
where: { id: input.workspaceId },
|
|
377
393
|
data: { needsAnalysis: true },
|
|
378
394
|
});
|
|
379
|
-
|
|
395
|
+
|
|
380
396
|
return {
|
|
381
397
|
fileId: fileAsset.id,
|
|
382
398
|
uploadUrl: signedUrlData.signedUrl,
|
|
383
399
|
};
|
|
384
400
|
}),
|
|
385
|
-
|
|
401
|
+
uploadAndAnalyzeMedia: authedProcedure
|
|
386
402
|
.input(z.object({
|
|
387
403
|
workspaceId: z.string(),
|
|
388
404
|
files: z.array(z.object({
|
|
@@ -404,9 +420,9 @@ export const workspace = router({
|
|
|
404
420
|
|
|
405
421
|
// Check if analysis is already in progress
|
|
406
422
|
if (workspace.fileBeingAnalyzed) {
|
|
407
|
-
throw new TRPCError({
|
|
408
|
-
code: 'CONFLICT',
|
|
409
|
-
message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
|
|
423
|
+
throw new TRPCError({
|
|
424
|
+
code: 'CONFLICT',
|
|
425
|
+
message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
|
|
410
426
|
});
|
|
411
427
|
}
|
|
412
428
|
|
|
@@ -420,18 +436,18 @@ export const workspace = router({
|
|
|
420
436
|
});
|
|
421
437
|
|
|
422
438
|
if (files.length === 0) {
|
|
423
|
-
throw new TRPCError({
|
|
424
|
-
code: 'NOT_FOUND',
|
|
425
|
-
message: 'No files found with the provided IDs'
|
|
439
|
+
throw new TRPCError({
|
|
440
|
+
code: 'NOT_FOUND',
|
|
441
|
+
message: 'No files found with the provided IDs'
|
|
426
442
|
});
|
|
427
443
|
}
|
|
428
444
|
|
|
429
445
|
// Validate all files have bucket and objectKey
|
|
430
446
|
for (const file of files) {
|
|
431
447
|
if (!file.bucket || !file.objectKey) {
|
|
432
|
-
throw new TRPCError({
|
|
433
|
-
code: 'BAD_REQUEST',
|
|
434
|
-
message: `File ${file.id} does not have bucket or objectKey set`
|
|
448
|
+
throw new TRPCError({
|
|
449
|
+
code: 'BAD_REQUEST',
|
|
450
|
+
message: `File ${file.id} does not have bucket or objectKey set`
|
|
435
451
|
});
|
|
436
452
|
}
|
|
437
453
|
}
|
|
@@ -439,186 +455,71 @@ export const workspace = router({
|
|
|
439
455
|
// Use the first file for progress tracking and artifact naming
|
|
440
456
|
const primaryFile = files[0];
|
|
441
457
|
const fileType = primaryFile.mimeType.startsWith('image/') ? 'image' : 'pdf';
|
|
442
|
-
|
|
443
|
-
// Set analysis in progress flag
|
|
444
|
-
await ctx.db.workspace.update({
|
|
445
|
-
where: { id: input.workspaceId },
|
|
446
|
-
data: { fileBeingAnalyzed: true },
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
PusherService.emitAnalysisProgress(input.workspaceId, {
|
|
450
|
-
status: 'starting',
|
|
451
|
-
filename: primaryFile.name,
|
|
452
|
-
fileType,
|
|
453
|
-
startedAt: new Date().toISOString(),
|
|
454
|
-
steps: {
|
|
455
|
-
fileUpload: { order: 1, status: 'pending' },
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
|
|
459
458
|
try {
|
|
460
|
-
|
|
459
|
+
// Set analysis in progress flag
|
|
460
|
+
await ctx.db.workspace.update({
|
|
461
|
+
where: { id: input.workspaceId },
|
|
462
|
+
data: { fileBeingAnalyzed: true },
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
PusherService.emitAnalysisProgress(input.workspaceId, {
|
|
461
466
|
status: 'starting',
|
|
462
467
|
filename: primaryFile.name,
|
|
463
468
|
fileType,
|
|
464
469
|
startedAt: new Date().toISOString(),
|
|
465
470
|
steps: {
|
|
466
|
-
fileUpload: {
|
|
467
|
-
order: 1,
|
|
468
|
-
status: 'pending',
|
|
469
|
-
},
|
|
470
|
-
fileAnalysis: {
|
|
471
|
-
order: 2,
|
|
472
|
-
status: 'pending',
|
|
473
|
-
},
|
|
474
|
-
studyGuide: {
|
|
475
|
-
order: 3,
|
|
476
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
477
|
-
},
|
|
478
|
-
flashcards: {
|
|
479
|
-
order: 4,
|
|
480
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
481
|
-
},
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
} catch (error) {
|
|
485
|
-
console.error('❌ Failed to update analysis progress:', error);
|
|
486
|
-
await ctx.db.workspace.update({
|
|
487
|
-
where: { id: input.workspaceId },
|
|
488
|
-
data: { fileBeingAnalyzed: false },
|
|
489
|
-
});
|
|
490
|
-
await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
|
|
491
|
-
throw error;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
495
|
-
status: 'uploading',
|
|
496
|
-
filename: primaryFile.name,
|
|
497
|
-
fileType,
|
|
498
|
-
startedAt: new Date().toISOString(),
|
|
499
|
-
steps: {
|
|
500
|
-
fileUpload: {
|
|
501
|
-
order: 1,
|
|
502
|
-
status: 'in_progress',
|
|
503
|
-
},
|
|
504
|
-
fileAnalysis: {
|
|
505
|
-
order: 2,
|
|
506
|
-
status: 'pending',
|
|
507
|
-
},
|
|
508
|
-
studyGuide: {
|
|
509
|
-
order: 3,
|
|
510
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
471
|
+
fileUpload: { order: 1, status: 'pending' },
|
|
511
472
|
},
|
|
512
|
-
|
|
513
|
-
order: 4,
|
|
514
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
515
|
-
},
|
|
516
|
-
}
|
|
517
|
-
});
|
|
473
|
+
});
|
|
518
474
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
475
|
+
try {
|
|
476
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
477
|
+
status: 'starting',
|
|
478
|
+
filename: primaryFile.name,
|
|
479
|
+
fileType,
|
|
480
|
+
startedAt: new Date().toISOString(),
|
|
481
|
+
steps: {
|
|
482
|
+
fileUpload: {
|
|
483
|
+
order: 1,
|
|
484
|
+
status: 'pending',
|
|
485
|
+
},
|
|
486
|
+
fileAnalysis: {
|
|
487
|
+
order: 2,
|
|
488
|
+
status: 'pending',
|
|
489
|
+
},
|
|
490
|
+
studyGuide: {
|
|
491
|
+
order: 3,
|
|
492
|
+
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
493
|
+
},
|
|
494
|
+
flashcards: {
|
|
495
|
+
order: 4,
|
|
496
|
+
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
497
|
+
},
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.error('❌ Failed to update analysis progress:', error);
|
|
531
502
|
await ctx.db.workspace.update({
|
|
532
503
|
where: { id: input.workspaceId },
|
|
533
504
|
data: { fileBeingAnalyzed: false },
|
|
534
505
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
message: `Failed to generate signed URL for file ${file.name}: ${signedUrlError.message}`
|
|
538
|
-
});
|
|
506
|
+
await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
|
|
507
|
+
throw error;
|
|
539
508
|
}
|
|
540
|
-
|
|
541
|
-
const fileUrl = signedUrlData.signedUrl;
|
|
542
|
-
const currentFileType = file.mimeType.startsWith('image/') ? 'image' : 'pdf';
|
|
543
|
-
|
|
544
|
-
// Use maxPages for large PDFs (>50 pages) to limit processing
|
|
545
|
-
const maxPages = currentFileType === 'pdf' && file.size && file.size > 50 ? 50 : undefined;
|
|
546
|
-
|
|
547
|
-
const processResult = await aiSessionService.processFile(
|
|
548
|
-
input.workspaceId,
|
|
549
|
-
ctx.session.user.id,
|
|
550
|
-
fileUrl,
|
|
551
|
-
currentFileType,
|
|
552
|
-
maxPages
|
|
553
|
-
);
|
|
554
|
-
|
|
555
|
-
if (processResult.status === 'error') {
|
|
556
|
-
logger.error(`Failed to process file ${file.name}:`, processResult.error);
|
|
557
|
-
// Continue processing other files even if one fails
|
|
558
|
-
// Optionally, you could throw an error or mark this file as failed
|
|
559
|
-
} else {
|
|
560
|
-
logger.info(`Successfully processed file ${file.name}: ${processResult.pageCount} pages`);
|
|
561
|
-
|
|
562
|
-
// Store the comprehensive description in aiTranscription field
|
|
563
|
-
await ctx.db.fileAsset.update({
|
|
564
|
-
where: { id: file.id },
|
|
565
|
-
data: {
|
|
566
|
-
aiTranscription: processResult.comprehensiveDescription || processResult.textContent || null,
|
|
567
|
-
}
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
509
|
|
|
572
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
573
|
-
status: 'analyzing',
|
|
574
|
-
filename: primaryFile.name,
|
|
575
|
-
fileType,
|
|
576
|
-
startedAt: new Date().toISOString(),
|
|
577
|
-
steps: {
|
|
578
|
-
fileUpload: {
|
|
579
|
-
order: 1,
|
|
580
|
-
status: 'completed',
|
|
581
|
-
},
|
|
582
|
-
fileAnalysis: {
|
|
583
|
-
order: 2,
|
|
584
|
-
status: 'in_progress',
|
|
585
|
-
},
|
|
586
|
-
studyGuide: {
|
|
587
|
-
order: 3,
|
|
588
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
589
|
-
},
|
|
590
|
-
flashcards: {
|
|
591
|
-
order: 4,
|
|
592
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
593
|
-
},
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
try {
|
|
598
|
-
// Analyze all files - use PDF analysis if any file is a PDF, otherwise use image analysis
|
|
599
|
-
// const hasPDF = files.some(f => !f.mimeType.startsWith('image/'));
|
|
600
|
-
// if (hasPDF) {
|
|
601
|
-
// await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id, file.id);
|
|
602
|
-
// } else {
|
|
603
|
-
// // If all files are images, analyze them
|
|
604
|
-
// for (const file of files) {
|
|
605
|
-
// await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id, file.id);
|
|
606
|
-
// }
|
|
607
|
-
// }
|
|
608
|
-
|
|
609
510
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
610
|
-
status: '
|
|
511
|
+
status: 'uploading',
|
|
611
512
|
filename: primaryFile.name,
|
|
612
513
|
fileType,
|
|
613
514
|
startedAt: new Date().toISOString(),
|
|
614
515
|
steps: {
|
|
615
516
|
fileUpload: {
|
|
616
517
|
order: 1,
|
|
617
|
-
status: '
|
|
518
|
+
status: 'in_progress',
|
|
618
519
|
},
|
|
619
520
|
fileAnalysis: {
|
|
620
521
|
order: 2,
|
|
621
|
-
status: '
|
|
522
|
+
status: 'pending',
|
|
622
523
|
},
|
|
623
524
|
studyGuide: {
|
|
624
525
|
order: 3,
|
|
@@ -630,60 +531,66 @@ export const workspace = router({
|
|
|
630
531
|
},
|
|
631
532
|
}
|
|
632
533
|
});
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
error: `Failed to analyze ${fileType}: ${error}`,
|
|
640
|
-
startedAt: new Date().toISOString(),
|
|
641
|
-
steps: {
|
|
642
|
-
fileUpload: {
|
|
643
|
-
order: 1,
|
|
644
|
-
status: 'completed',
|
|
645
|
-
},
|
|
646
|
-
fileAnalysis: {
|
|
647
|
-
order: 2,
|
|
648
|
-
status: 'error',
|
|
649
|
-
},
|
|
650
|
-
studyGuide: {
|
|
651
|
-
order: 3,
|
|
652
|
-
status: 'skipped',
|
|
653
|
-
},
|
|
654
|
-
flashcards: {
|
|
655
|
-
order: 4,
|
|
656
|
-
status: 'skipped',
|
|
657
|
-
},
|
|
534
|
+
|
|
535
|
+
// Process all files using the new process_file endpoint
|
|
536
|
+
for (const file of files) {
|
|
537
|
+
// TypeScript: We already validated bucket and objectKey exist above
|
|
538
|
+
if (!file.bucket || !file.objectKey) {
|
|
539
|
+
continue; // Skip if somehow missing (shouldn't happen due to validation above)
|
|
658
540
|
}
|
|
659
|
-
});
|
|
660
|
-
await ctx.db.workspace.update({
|
|
661
|
-
where: { id: input.workspaceId },
|
|
662
|
-
data: { fileBeingAnalyzed: false },
|
|
663
|
-
});
|
|
664
|
-
throw error;
|
|
665
|
-
}
|
|
666
541
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
542
|
+
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
543
|
+
.from(file.bucket)
|
|
544
|
+
.createSignedUrl(file.objectKey, 24 * 60 * 60); // 24 hours expiry
|
|
545
|
+
|
|
546
|
+
if (signedUrlError) {
|
|
547
|
+
await ctx.db.workspace.update({
|
|
548
|
+
where: { id: input.workspaceId },
|
|
549
|
+
data: { fileBeingAnalyzed: false },
|
|
550
|
+
});
|
|
551
|
+
throw new TRPCError({
|
|
552
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
553
|
+
message: `Failed to generate signed URL for file ${file.name}: ${signedUrlError.message}`
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const fileUrl = signedUrlData.signedUrl;
|
|
558
|
+
const currentFileType = file.mimeType.startsWith('image/') ? 'image' : 'pdf';
|
|
559
|
+
|
|
560
|
+
// Use maxPages for large PDFs (>50 pages) to limit processing
|
|
561
|
+
const maxPages = currentFileType === 'pdf' && file.size && file.size > 50 ? 50 : undefined;
|
|
562
|
+
|
|
563
|
+
const processResult = await aiSessionService.processFile(
|
|
564
|
+
input.workspaceId,
|
|
565
|
+
ctx.session.user.id,
|
|
566
|
+
fileUrl,
|
|
567
|
+
currentFileType,
|
|
568
|
+
maxPages
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
if (processResult.status === 'error') {
|
|
572
|
+
logger.error(`Failed to process file ${file.name}:`, processResult.error);
|
|
573
|
+
// Continue processing other files even if one fails
|
|
574
|
+
// Optionally, you could throw an error or mark this file as failed
|
|
575
|
+
} else {
|
|
576
|
+
logger.info(`Successfully processed file ${file.name}: ${processResult.pageCount} pages`);
|
|
577
|
+
|
|
578
|
+
// Store the comprehensive description in aiTranscription field
|
|
579
|
+
await ctx.db.fileAsset.update({
|
|
580
|
+
where: { id: file.id },
|
|
581
|
+
data: {
|
|
582
|
+
aiTranscription: {
|
|
583
|
+
comprehensiveDescription: processResult.comprehensiveDescription,
|
|
584
|
+
textContent: processResult.textContent,
|
|
585
|
+
imageDescriptions: processResult.imageDescriptions,
|
|
586
|
+
},
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
682
591
|
|
|
683
|
-
// Generate artifacts
|
|
684
|
-
if (input.generateStudyGuide) {
|
|
685
592
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
686
|
-
status: '
|
|
593
|
+
status: 'analyzing',
|
|
687
594
|
filename: primaryFile.name,
|
|
688
595
|
fileType,
|
|
689
596
|
startedAt: new Date().toISOString(),
|
|
@@ -694,11 +601,11 @@ export const workspace = router({
|
|
|
694
601
|
},
|
|
695
602
|
fileAnalysis: {
|
|
696
603
|
order: 2,
|
|
697
|
-
status: '
|
|
604
|
+
status: 'in_progress',
|
|
698
605
|
},
|
|
699
606
|
studyGuide: {
|
|
700
607
|
order: 3,
|
|
701
|
-
status: '
|
|
608
|
+
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
702
609
|
},
|
|
703
610
|
flashcards: {
|
|
704
611
|
order: 4,
|
|
@@ -706,42 +613,242 @@ export const workspace = router({
|
|
|
706
613
|
},
|
|
707
614
|
}
|
|
708
615
|
});
|
|
709
|
-
|
|
710
|
-
const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
|
|
711
616
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
617
|
+
try {
|
|
618
|
+
// Analyze all files - use PDF analysis if any file is a PDF, otherwise use image analysis
|
|
619
|
+
// const hasPDF = files.some(f => !f.mimeType.startsWith('image/'));
|
|
620
|
+
// if (hasPDF) {
|
|
621
|
+
// await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id, file.id);
|
|
622
|
+
// } else {
|
|
623
|
+
// // If all files are images, analyze them
|
|
624
|
+
// for (const file of files) {
|
|
625
|
+
// await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id, file.id);
|
|
626
|
+
// }
|
|
627
|
+
// }
|
|
628
|
+
|
|
629
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
630
|
+
status: 'generating_artifacts',
|
|
631
|
+
filename: primaryFile.name,
|
|
632
|
+
fileType,
|
|
633
|
+
startedAt: new Date().toISOString(),
|
|
634
|
+
steps: {
|
|
635
|
+
fileUpload: {
|
|
636
|
+
order: 1,
|
|
637
|
+
status: 'completed',
|
|
638
|
+
},
|
|
639
|
+
fileAnalysis: {
|
|
640
|
+
order: 2,
|
|
641
|
+
status: 'completed',
|
|
642
|
+
},
|
|
643
|
+
studyGuide: {
|
|
644
|
+
order: 3,
|
|
645
|
+
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
646
|
+
},
|
|
647
|
+
flashcards: {
|
|
648
|
+
order: 4,
|
|
649
|
+
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
650
|
+
},
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
} catch (error) {
|
|
654
|
+
console.error('❌ Failed to analyze files:', error);
|
|
655
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
656
|
+
status: 'error',
|
|
657
|
+
filename: primaryFile.name,
|
|
658
|
+
fileType,
|
|
659
|
+
error: `Failed to analyze ${fileType}: ${error}`,
|
|
660
|
+
startedAt: new Date().toISOString(),
|
|
661
|
+
steps: {
|
|
662
|
+
fileUpload: {
|
|
663
|
+
order: 1,
|
|
664
|
+
status: 'completed',
|
|
665
|
+
},
|
|
666
|
+
fileAnalysis: {
|
|
667
|
+
order: 2,
|
|
668
|
+
status: 'error',
|
|
669
|
+
},
|
|
670
|
+
studyGuide: {
|
|
671
|
+
order: 3,
|
|
672
|
+
status: 'skipped',
|
|
673
|
+
},
|
|
674
|
+
flashcards: {
|
|
675
|
+
order: 4,
|
|
676
|
+
status: 'skipped',
|
|
677
|
+
},
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
await ctx.db.workspace.update({
|
|
681
|
+
where: { id: input.workspaceId },
|
|
682
|
+
data: { fileBeingAnalyzed: false },
|
|
683
|
+
});
|
|
684
|
+
throw error;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const results: {
|
|
688
|
+
filename: string;
|
|
689
|
+
artifacts: {
|
|
690
|
+
studyGuide: any | null;
|
|
691
|
+
flashcards: any | null;
|
|
692
|
+
worksheet: any | null;
|
|
693
|
+
};
|
|
694
|
+
} = {
|
|
695
|
+
filename: primaryFile.name,
|
|
696
|
+
artifacts: {
|
|
697
|
+
studyGuide: null,
|
|
698
|
+
flashcards: null,
|
|
699
|
+
worksheet: null,
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// Generate artifacts
|
|
704
|
+
if (input.generateStudyGuide) {
|
|
705
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
706
|
+
status: 'generating_study_guide',
|
|
707
|
+
filename: primaryFile.name,
|
|
708
|
+
fileType,
|
|
709
|
+
startedAt: new Date().toISOString(),
|
|
710
|
+
steps: {
|
|
711
|
+
fileUpload: {
|
|
712
|
+
order: 1,
|
|
713
|
+
status: 'completed',
|
|
714
|
+
},
|
|
715
|
+
fileAnalysis: {
|
|
716
|
+
order: 2,
|
|
717
|
+
status: 'completed',
|
|
718
|
+
},
|
|
719
|
+
studyGuide: {
|
|
720
|
+
order: 3,
|
|
721
|
+
status: 'in_progress',
|
|
722
|
+
},
|
|
723
|
+
flashcards: {
|
|
724
|
+
order: 4,
|
|
725
|
+
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
726
|
+
},
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
|
|
731
|
+
|
|
732
|
+
let artifact = await ctx.db.artifact.findFirst({
|
|
733
|
+
where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
|
|
734
|
+
});
|
|
735
|
+
if (!artifact) {
|
|
736
|
+
const fileNames = files.map(f => f.name).join(', ');
|
|
737
|
+
artifact = await ctx.db.artifact.create({
|
|
738
|
+
data: {
|
|
739
|
+
workspaceId: input.workspaceId,
|
|
740
|
+
type: ArtifactType.STUDY_GUIDE,
|
|
741
|
+
title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
|
|
742
|
+
createdById: ctx.session.user.id,
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const lastVersion = await ctx.db.artifactVersion.findFirst({
|
|
748
|
+
where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
|
|
749
|
+
orderBy: { version: 'desc' },
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
await ctx.db.artifactVersion.create({
|
|
753
|
+
data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
results.artifacts.studyGuide = artifact;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (input.generateFlashcards) {
|
|
760
|
+
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
761
|
+
status: 'generating_flashcards',
|
|
762
|
+
filename: primaryFile.name,
|
|
763
|
+
fileType,
|
|
764
|
+
startedAt: new Date().toISOString(),
|
|
765
|
+
steps: {
|
|
766
|
+
fileUpload: {
|
|
767
|
+
order: 1,
|
|
768
|
+
status: 'completed',
|
|
769
|
+
},
|
|
770
|
+
fileAnalysis: {
|
|
771
|
+
order: 2,
|
|
772
|
+
status: 'completed',
|
|
773
|
+
},
|
|
774
|
+
studyGuide: {
|
|
775
|
+
order: 3,
|
|
776
|
+
status: input.generateStudyGuide ? 'completed' : 'skipped',
|
|
777
|
+
},
|
|
778
|
+
flashcards: {
|
|
779
|
+
order: 4,
|
|
780
|
+
status: 'in_progress',
|
|
781
|
+
},
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
|
|
786
|
+
|
|
787
|
+
const artifact = await ctx.db.artifact.create({
|
|
718
788
|
data: {
|
|
719
789
|
workspaceId: input.workspaceId,
|
|
720
|
-
type: ArtifactType.
|
|
721
|
-
title: files.length === 1 ? `
|
|
790
|
+
type: ArtifactType.FLASHCARD_SET,
|
|
791
|
+
title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
|
|
722
792
|
createdById: ctx.session.user.id,
|
|
723
793
|
},
|
|
724
794
|
});
|
|
795
|
+
|
|
796
|
+
// Parse JSON flashcard content
|
|
797
|
+
try {
|
|
798
|
+
const flashcardData: any = content;
|
|
799
|
+
|
|
800
|
+
let createdCards = 0;
|
|
801
|
+
for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
|
|
802
|
+
const card = flashcardData[i];
|
|
803
|
+
const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
|
|
804
|
+
const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
|
|
805
|
+
|
|
806
|
+
await ctx.db.flashcard.create({
|
|
807
|
+
data: {
|
|
808
|
+
artifactId: artifact.id,
|
|
809
|
+
front: front,
|
|
810
|
+
back: back,
|
|
811
|
+
order: i,
|
|
812
|
+
tags: ['ai-generated', 'medium'],
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
createdCards++;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
} catch (parseError) {
|
|
819
|
+
// Fallback to text parsing if JSON fails
|
|
820
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
821
|
+
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
822
|
+
const line = lines[i];
|
|
823
|
+
if (line.includes(' - ')) {
|
|
824
|
+
const [front, back] = line.split(' - ');
|
|
825
|
+
await ctx.db.flashcard.create({
|
|
826
|
+
data: {
|
|
827
|
+
artifactId: artifact.id,
|
|
828
|
+
front: front.trim(),
|
|
829
|
+
back: back.trim(),
|
|
830
|
+
order: i,
|
|
831
|
+
tags: ['ai-generated', 'medium'],
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
results.artifacts.flashcards = artifact;
|
|
725
839
|
}
|
|
726
|
-
|
|
727
|
-
const lastVersion = await ctx.db.artifactVersion.findFirst({
|
|
728
|
-
where: { artifact: {workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE} },
|
|
729
|
-
orderBy: { version: 'desc' },
|
|
730
|
-
});
|
|
731
840
|
|
|
732
|
-
await ctx.db.
|
|
733
|
-
|
|
841
|
+
await ctx.db.workspace.update({
|
|
842
|
+
where: { id: input.workspaceId },
|
|
843
|
+
data: { fileBeingAnalyzed: false },
|
|
734
844
|
});
|
|
735
845
|
|
|
736
|
-
results.artifacts.studyGuide = artifact;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (input.generateFlashcards) {
|
|
740
846
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
741
|
-
status: '
|
|
847
|
+
status: 'completed',
|
|
742
848
|
filename: primaryFile.name,
|
|
743
849
|
fileType,
|
|
744
850
|
startedAt: new Date().toISOString(),
|
|
851
|
+
completedAt: new Date().toISOString(),
|
|
745
852
|
steps: {
|
|
746
853
|
fileUpload: {
|
|
747
854
|
order: 1,
|
|
@@ -757,149 +864,71 @@ export const workspace = router({
|
|
|
757
864
|
},
|
|
758
865
|
flashcards: {
|
|
759
866
|
order: 4,
|
|
760
|
-
status: '
|
|
867
|
+
status: input.generateFlashcards ? 'completed' : 'skipped',
|
|
761
868
|
},
|
|
762
869
|
}
|
|
870
|
+
|
|
763
871
|
});
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
type: ArtifactType.FLASHCARD_SET,
|
|
771
|
-
title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
|
|
772
|
-
createdById: ctx.session.user.id,
|
|
773
|
-
},
|
|
872
|
+
return results;
|
|
873
|
+
} catch (error) {
|
|
874
|
+
console.error('❌ Failed to update analysis progress:', error);
|
|
875
|
+
await ctx.db.workspace.update({
|
|
876
|
+
where: { id: input.workspaceId },
|
|
877
|
+
data: { fileBeingAnalyzed: false },
|
|
774
878
|
});
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
879
|
+
await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
}),
|
|
883
|
+
search: authedProcedure
|
|
884
|
+
.input(z.object({
|
|
885
|
+
query: z.string(),
|
|
886
|
+
limit: z.number().min(1).max(100).default(20),
|
|
887
|
+
}))
|
|
888
|
+
.query(async ({ ctx, input }) => {
|
|
889
|
+
const { query } = input;
|
|
890
|
+
const workspaces = await ctx.db.workspace.findMany({
|
|
891
|
+
where: {
|
|
892
|
+
ownerId: ctx.session.user.id,
|
|
893
|
+
OR: [
|
|
894
|
+
{
|
|
895
|
+
title: {
|
|
896
|
+
contains: query,
|
|
897
|
+
mode: 'insensitive',
|
|
793
898
|
},
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
description: {
|
|
902
|
+
contains: query,
|
|
903
|
+
mode: 'insensitive',
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
],
|
|
907
|
+
},
|
|
908
|
+
orderBy: {
|
|
909
|
+
updatedAt: 'desc',
|
|
910
|
+
},
|
|
911
|
+
take: input.limit,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// Update analysisProgress for each workspace with search metadata
|
|
915
|
+
const workspaceUpdates = workspaces.map(ws =>
|
|
916
|
+
ctx.db.workspace.update({
|
|
917
|
+
where: { id: ws.id },
|
|
918
|
+
data: {
|
|
919
|
+
analysisProgress: {
|
|
920
|
+
lastSearched: new Date().toISOString(),
|
|
921
|
+
searchQuery: query,
|
|
922
|
+
matchedIn: ws.title.toLowerCase().includes(query.toLowerCase()) ? 'title' : 'description',
|
|
814
923
|
}
|
|
815
924
|
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
results.artifacts.flashcards = artifact;
|
|
819
|
-
}
|
|
925
|
+
})
|
|
926
|
+
);
|
|
820
927
|
|
|
821
|
-
await
|
|
822
|
-
where: { id: input.workspaceId },
|
|
823
|
-
data: { fileBeingAnalyzed: false },
|
|
824
|
-
});
|
|
928
|
+
await Promise.all(workspaceUpdates);
|
|
825
929
|
|
|
826
|
-
|
|
827
|
-
status: 'completed',
|
|
828
|
-
filename: primaryFile.name,
|
|
829
|
-
fileType,
|
|
830
|
-
startedAt: new Date().toISOString(),
|
|
831
|
-
completedAt: new Date().toISOString(),
|
|
832
|
-
steps: {
|
|
833
|
-
fileUpload: {
|
|
834
|
-
order: 1,
|
|
835
|
-
status: 'completed',
|
|
836
|
-
},
|
|
837
|
-
fileAnalysis: {
|
|
838
|
-
order: 2,
|
|
839
|
-
status: 'completed',
|
|
840
|
-
},
|
|
841
|
-
studyGuide: {
|
|
842
|
-
order: 3,
|
|
843
|
-
status: input.generateStudyGuide ? 'completed' : 'skipped',
|
|
844
|
-
},
|
|
845
|
-
flashcards: {
|
|
846
|
-
order: 4,
|
|
847
|
-
status: input.generateFlashcards ? 'completed' : 'skipped',
|
|
848
|
-
},
|
|
849
|
-
}
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
return results;
|
|
930
|
+
return workspaces;
|
|
853
931
|
}),
|
|
854
|
-
search: authedProcedure
|
|
855
|
-
.input(z.object({
|
|
856
|
-
query: z.string(),
|
|
857
|
-
limit: z.number().min(1).max(100).default(20),
|
|
858
|
-
}))
|
|
859
|
-
.query(async ({ ctx, input }) => {
|
|
860
|
-
const { query } = input;
|
|
861
|
-
const workspaces = await ctx.db.workspace.findMany({
|
|
862
|
-
where: {
|
|
863
|
-
ownerId: ctx.session.user.id,
|
|
864
|
-
OR: [
|
|
865
|
-
{
|
|
866
|
-
title: {
|
|
867
|
-
contains: query,
|
|
868
|
-
mode: 'insensitive',
|
|
869
|
-
},
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
description: {
|
|
873
|
-
contains: query,
|
|
874
|
-
mode: 'insensitive',
|
|
875
|
-
},
|
|
876
|
-
},
|
|
877
|
-
],
|
|
878
|
-
},
|
|
879
|
-
orderBy: {
|
|
880
|
-
updatedAt: 'desc',
|
|
881
|
-
},
|
|
882
|
-
take: input.limit,
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
// Update analysisProgress for each workspace with search metadata
|
|
886
|
-
const workspaceUpdates = workspaces.map(ws =>
|
|
887
|
-
ctx.db.workspace.update({
|
|
888
|
-
where: { id: ws.id },
|
|
889
|
-
data: {
|
|
890
|
-
analysisProgress: {
|
|
891
|
-
lastSearched: new Date().toISOString(),
|
|
892
|
-
searchQuery: query,
|
|
893
|
-
matchedIn: ws.title.toLowerCase().includes(query.toLowerCase()) ? 'title' : 'description',
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
})
|
|
897
|
-
);
|
|
898
|
-
|
|
899
|
-
await Promise.all(workspaceUpdates);
|
|
900
|
-
|
|
901
|
-
return workspaces;
|
|
902
|
-
}),
|
|
903
932
|
|
|
904
933
|
// Members sub-router
|
|
905
934
|
members,
|