@goscribe/server 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/ai-session.d.ts +13 -3
- package/dist/lib/ai-session.js +66 -146
- package/dist/lib/pusher.js +1 -1
- package/dist/routers/_app.d.ts +114 -7
- package/dist/routers/chat.js +2 -23
- package/dist/routers/flashcards.d.ts +25 -1
- package/dist/routers/flashcards.js +0 -14
- package/dist/routers/members.d.ts +18 -0
- package/dist/routers/members.js +14 -1
- package/dist/routers/worksheets.js +5 -4
- package/dist/routers/workspace.d.ts +89 -6
- package/dist/routers/workspace.js +389 -259
- package/dist/services/flashcard-progress.service.d.ts +25 -1
- package/dist/services/flashcard-progress.service.js +70 -31
- package/package.json +2 -2
- package/prisma/schema.prisma +14 -1
- package/src/lib/ai-session.ts +97 -158
- package/src/routers/flashcards.ts +0 -16
- package/src/routers/members.ts +13 -2
- package/src/routers/podcast.ts +0 -1
- package/src/routers/worksheets.ts +3 -2
- package/src/routers/workspace.ts +516 -399
- package/ANALYSIS_PROGRESS_SPEC.md +0 -463
- package/PROGRESS_QUICK_REFERENCE.md +0 -239
- package/dist/lib/podcast-prompts.d.ts +0 -43
- package/dist/lib/podcast-prompts.js +0 -135
- package/dist/routers/ai-session.d.ts +0 -0
- package/dist/routers/ai-session.js +0 -1
- package/dist/services/flashcard.service.d.ts +0 -183
- package/dist/services/flashcard.service.js +0 -224
- package/dist/services/podcast-segment-reorder.d.ts +0 -0
- package/dist/services/podcast-segment-reorder.js +0 -107
- package/dist/services/podcast.service.d.ts +0 -0
- package/dist/services/podcast.service.js +0 -326
- package/dist/services/worksheet.service.d.ts +0 -0
- package/dist/services/worksheet.service.js +0 -295
package/src/routers/workspace.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { ArtifactType } from '@prisma/client';
|
|
|
6
6
|
import { aiSessionService } from '../lib/ai-session.js';
|
|
7
7
|
import PusherService from '../lib/pusher.js';
|
|
8
8
|
import { members } from './members.js';
|
|
9
|
+
import { logger } from '../lib/logger.js';
|
|
9
10
|
import type { PrismaClient } from '@prisma/client';
|
|
10
11
|
|
|
11
12
|
// Helper function to update and emit analysis progress
|
|
@@ -25,17 +26,17 @@ async function updateAnalysisProgress(
|
|
|
25
26
|
function calculateRelevance(query: string, ...texts: (string | null | undefined)[]): number {
|
|
26
27
|
const queryLower = query.toLowerCase();
|
|
27
28
|
let score = 0;
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
for (const text of texts) {
|
|
30
31
|
if (!text) continue;
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
const textLower = text.toLowerCase();
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
// Exact match gets highest score
|
|
35
36
|
if (textLower.includes(queryLower)) {
|
|
36
37
|
score += 10;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
// Word boundary matches get good score
|
|
40
41
|
const words = queryLower.split(/\s+/);
|
|
41
42
|
for (const word of words) {
|
|
@@ -43,7 +44,7 @@ function calculateRelevance(query: string, ...texts: (string | null | undefined)
|
|
|
43
44
|
score += 5;
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
// Partial matches get lower score
|
|
48
49
|
const queryChars = queryLower.split('');
|
|
49
50
|
let consecutiveMatches = 0;
|
|
@@ -56,7 +57,7 @@ function calculateRelevance(query: string, ...texts: (string | null | undefined)
|
|
|
56
57
|
}
|
|
57
58
|
score += consecutiveMatches * 0.1;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
|
|
60
61
|
return score;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -64,8 +65,8 @@ export const workspace = router({
|
|
|
64
65
|
// List current user's workspaces
|
|
65
66
|
list: authedProcedure
|
|
66
67
|
.input(z.object({
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
parentId: z.string().optional(),
|
|
69
|
+
}))
|
|
69
70
|
.query(async ({ ctx, input }) => {
|
|
70
71
|
const workspaces = await ctx.db.workspace.findMany({
|
|
71
72
|
where: {
|
|
@@ -84,14 +85,14 @@ export const workspace = router({
|
|
|
84
85
|
|
|
85
86
|
return { workspaces, folders };
|
|
86
87
|
}),
|
|
87
|
-
|
|
88
|
+
|
|
88
89
|
create: authedProcedure
|
|
89
90
|
.input(z.object({
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.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 }) => {
|
|
95
96
|
const ws = await ctx.db.workspace.create({
|
|
96
97
|
data: {
|
|
97
98
|
title: input.name,
|
|
@@ -118,10 +119,10 @@ export const workspace = router({
|
|
|
118
119
|
}),
|
|
119
120
|
createFolder: authedProcedure
|
|
120
121
|
.input(z.object({
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
name: z.string().min(1).max(100),
|
|
123
|
+
color: z.string().optional(),
|
|
124
|
+
parentId: z.string().optional(),
|
|
125
|
+
}))
|
|
125
126
|
.mutation(async ({ ctx, input }) => {
|
|
126
127
|
const folder = await ctx.db.folder.create({
|
|
127
128
|
data: {
|
|
@@ -135,26 +136,26 @@ export const workspace = router({
|
|
|
135
136
|
}),
|
|
136
137
|
updateFolder: authedProcedure
|
|
137
138
|
.input(z.object({
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
id: z.string(),
|
|
140
|
+
name: z.string().min(1).max(100).optional(),
|
|
141
|
+
color: z.string().optional(),
|
|
142
|
+
}))
|
|
142
143
|
.mutation(async ({ ctx, input }) => {
|
|
143
144
|
const folder = await ctx.db.folder.update({ where: { id: input.id }, data: { name: input.name, color: input.color ?? '#9D00FF' } });
|
|
144
145
|
return folder;
|
|
145
146
|
}),
|
|
146
147
|
deleteFolder: authedProcedure
|
|
147
148
|
.input(z.object({
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
id: z.string(),
|
|
150
|
+
}))
|
|
150
151
|
.mutation(async ({ ctx, input }) => {
|
|
151
152
|
const folder = await ctx.db.folder.delete({ where: { id: input.id } });
|
|
152
153
|
return folder;
|
|
153
154
|
}),
|
|
154
155
|
get: authedProcedure
|
|
155
156
|
.input(z.object({
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
id: z.string(),
|
|
158
|
+
}))
|
|
158
159
|
.query(async ({ ctx, input }) => {
|
|
159
160
|
const ws = await ctx.db.workspace.findFirst({
|
|
160
161
|
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
@@ -170,13 +171,13 @@ export const workspace = router({
|
|
|
170
171
|
getStats: authedProcedure
|
|
171
172
|
.query(async ({ ctx }) => {
|
|
172
173
|
const workspaces = await ctx.db.workspace.findMany({
|
|
173
|
-
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 } } }] },
|
|
174
175
|
});
|
|
175
176
|
const folders = await ctx.db.folder.findMany({
|
|
176
|
-
where: {OR: [{ ownerId: ctx.session.user.id }
|
|
177
|
+
where: { OR: [{ ownerId: ctx.session.user.id }] },
|
|
177
178
|
});
|
|
178
179
|
const lastUpdated = await ctx.db.workspace.findFirst({
|
|
179
|
-
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 } } }] },
|
|
180
181
|
orderBy: { updatedAt: 'desc' },
|
|
181
182
|
});
|
|
182
183
|
|
|
@@ -195,12 +196,12 @@ export const workspace = router({
|
|
|
195
196
|
}),
|
|
196
197
|
update: authedProcedure
|
|
197
198
|
.input(z.object({
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}))
|
|
204
205
|
.mutation(async ({ ctx, input }) => {
|
|
205
206
|
const existed = await ctx.db.workspace.findFirst({
|
|
206
207
|
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
@@ -215,12 +216,12 @@ export const workspace = router({
|
|
|
215
216
|
icon: input.icon ?? existed.icon,
|
|
216
217
|
},
|
|
217
218
|
});
|
|
218
|
-
return updated;
|
|
219
|
-
}),
|
|
220
|
-
|
|
219
|
+
return updated;
|
|
220
|
+
}),
|
|
221
|
+
delete: authedProcedure
|
|
221
222
|
.input(z.object({
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
id: z.string(),
|
|
224
|
+
}))
|
|
224
225
|
.mutation(async ({ ctx, input }) => {
|
|
225
226
|
const deleted = await ctx.db.workspace.deleteMany({
|
|
226
227
|
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
@@ -228,15 +229,15 @@ export const workspace = router({
|
|
|
228
229
|
if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
229
230
|
return true;
|
|
230
231
|
}),
|
|
231
|
-
|
|
232
|
+
getFolderInformation: authedProcedure
|
|
232
233
|
.input(z.object({
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
id: z.string(),
|
|
235
|
+
}))
|
|
235
236
|
.query(async ({ ctx, input }) => {
|
|
236
237
|
const folder = await ctx.db.folder.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
|
|
237
238
|
// find all of its parents
|
|
238
239
|
if (!folder) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
239
|
-
|
|
240
|
+
|
|
240
241
|
const parents = [];
|
|
241
242
|
let current = folder;
|
|
242
243
|
|
|
@@ -246,20 +247,36 @@ export const workspace = router({
|
|
|
246
247
|
parents.push(parent);
|
|
247
248
|
current = parent;
|
|
248
249
|
}
|
|
249
|
-
|
|
250
|
+
|
|
250
251
|
return { folder, parents };
|
|
251
252
|
}),
|
|
252
|
-
|
|
253
|
+
|
|
254
|
+
getSharedWith: authedProcedure
|
|
253
255
|
.input(z.object({
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
}))
|
|
263
280
|
.mutation(async ({ ctx, input }) => {
|
|
264
281
|
// ensure workspace belongs to user
|
|
265
282
|
const ws = await ctx.db.workspace.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
|
|
@@ -274,7 +291,7 @@ export const workspace = router({
|
|
|
274
291
|
name: file.filename,
|
|
275
292
|
mimeType: file.contentType,
|
|
276
293
|
size: file.size,
|
|
277
|
-
workspaceId: input.id,
|
|
294
|
+
workspaceId: input.id,
|
|
278
295
|
},
|
|
279
296
|
});
|
|
280
297
|
|
|
@@ -285,9 +302,9 @@ export const workspace = router({
|
|
|
285
302
|
.createSignedUploadUrl(objectKey); // 5 minutes
|
|
286
303
|
|
|
287
304
|
if (signedUrlError) {
|
|
288
|
-
throw new TRPCError({
|
|
289
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
290
|
-
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}`
|
|
291
308
|
});
|
|
292
309
|
}
|
|
293
310
|
|
|
@@ -298,7 +315,7 @@ export const workspace = router({
|
|
|
298
315
|
bucket: 'files',
|
|
299
316
|
objectKey: objectKey,
|
|
300
317
|
},
|
|
301
|
-
});
|
|
318
|
+
});
|
|
302
319
|
|
|
303
320
|
results.push({
|
|
304
321
|
fileId: record.id,
|
|
@@ -309,11 +326,11 @@ export const workspace = router({
|
|
|
309
326
|
return results;
|
|
310
327
|
|
|
311
328
|
}),
|
|
312
|
-
|
|
329
|
+
deleteFiles: authedProcedure
|
|
313
330
|
.input(z.object({
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
331
|
+
fileId: z.array(z.string()),
|
|
332
|
+
id: z.string(),
|
|
333
|
+
}))
|
|
317
334
|
.mutation(async ({ ctx, input }) => {
|
|
318
335
|
// ensure files are in the user's workspace
|
|
319
336
|
const files = await ctx.db.fileAsset.findMany({
|
|
@@ -344,7 +361,7 @@ export const workspace = router({
|
|
|
344
361
|
});
|
|
345
362
|
return true;
|
|
346
363
|
}),
|
|
347
|
-
|
|
364
|
+
getFileUploadUrl: authedProcedure
|
|
348
365
|
.input(z.object({
|
|
349
366
|
workspaceId: z.string(),
|
|
350
367
|
filename: z.string(),
|
|
@@ -353,7 +370,7 @@ export const workspace = router({
|
|
|
353
370
|
}))
|
|
354
371
|
.query(async ({ ctx, input }) => {
|
|
355
372
|
const objectKey = `workspace_${ctx.session.user.id}/${input.workspaceId}-file_${input.filename}`;
|
|
356
|
-
await ctx.db.fileAsset.create({
|
|
373
|
+
const fileAsset = await ctx.db.fileAsset.create({
|
|
357
374
|
data: {
|
|
358
375
|
workspaceId: input.workspaceId,
|
|
359
376
|
name: input.filename,
|
|
@@ -370,31 +387,28 @@ export const workspace = router({
|
|
|
370
387
|
if (signedUrlError) {
|
|
371
388
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
|
|
372
389
|
}
|
|
373
|
-
|
|
390
|
+
|
|
391
|
+
await ctx.db.workspace.update({
|
|
392
|
+
where: { id: input.workspaceId },
|
|
393
|
+
data: { needsAnalysis: true },
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
fileId: fileAsset.id,
|
|
398
|
+
uploadUrl: signedUrlData.signedUrl,
|
|
399
|
+
};
|
|
374
400
|
}),
|
|
375
|
-
|
|
401
|
+
uploadAndAnalyzeMedia: authedProcedure
|
|
376
402
|
.input(z.object({
|
|
377
403
|
workspaceId: z.string(),
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
size: z.number(),
|
|
382
|
-
content: z.string(), // Base64 encoded file content
|
|
383
|
-
}),
|
|
404
|
+
files: z.array(z.object({
|
|
405
|
+
id: z.string(),
|
|
406
|
+
})),
|
|
384
407
|
generateStudyGuide: z.boolean().default(true),
|
|
385
408
|
generateFlashcards: z.boolean().default(true),
|
|
386
409
|
generateWorksheet: z.boolean().default(true),
|
|
387
410
|
}))
|
|
388
411
|
.mutation(async ({ ctx, input }) => {
|
|
389
|
-
console.log('🚀 uploadAndAnalyzeMedia started', {
|
|
390
|
-
workspaceId: input.workspaceId,
|
|
391
|
-
filename: input.file.filename,
|
|
392
|
-
fileSize: input.file.size,
|
|
393
|
-
generateStudyGuide: input.generateStudyGuide,
|
|
394
|
-
generateFlashcards: input.generateFlashcards,
|
|
395
|
-
generateWorksheet: input.generateWorksheet
|
|
396
|
-
});
|
|
397
|
-
|
|
398
412
|
// Verify workspace ownership
|
|
399
413
|
const workspace = await ctx.db.workspace.findFirst({
|
|
400
414
|
where: { id: input.workspaceId, ownerId: ctx.session.user.id }
|
|
@@ -404,138 +418,108 @@ export const workspace = router({
|
|
|
404
418
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
405
419
|
}
|
|
406
420
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
421
|
+
// Check if analysis is already in progress
|
|
422
|
+
if (workspace.fileBeingAnalyzed) {
|
|
423
|
+
throw new TRPCError({
|
|
424
|
+
code: 'CONFLICT',
|
|
425
|
+
message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Fetch files from database
|
|
430
|
+
const files = await ctx.db.fileAsset.findMany({
|
|
431
|
+
where: {
|
|
432
|
+
id: { in: input.files.map(file => file.id) },
|
|
433
|
+
workspaceId: input.workspaceId,
|
|
434
|
+
userId: ctx.session.user.id,
|
|
435
|
+
},
|
|
412
436
|
});
|
|
413
437
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
status: 'pending',
|
|
429
|
-
},
|
|
430
|
-
studyGuide: {
|
|
431
|
-
order: 3,
|
|
432
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
433
|
-
},
|
|
434
|
-
flashcards: {
|
|
435
|
-
order: 4,
|
|
436
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
437
|
-
},
|
|
438
|
+
if (files.length === 0) {
|
|
439
|
+
throw new TRPCError({
|
|
440
|
+
code: 'NOT_FOUND',
|
|
441
|
+
message: 'No files found with the provided IDs'
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Validate all files have bucket and objectKey
|
|
446
|
+
for (const file of files) {
|
|
447
|
+
if (!file.bucket || !file.objectKey) {
|
|
448
|
+
throw new TRPCError({
|
|
449
|
+
code: 'BAD_REQUEST',
|
|
450
|
+
message: `File ${file.id} does not have bucket or objectKey set`
|
|
451
|
+
});
|
|
438
452
|
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Use the first file for progress tracking and artifact naming
|
|
456
|
+
const primaryFile = files[0];
|
|
457
|
+
const fileType = primaryFile.mimeType.startsWith('image/') ? 'image' : 'pdf';
|
|
458
|
+
try {
|
|
459
|
+
// Set analysis in progress flag
|
|
442
460
|
await ctx.db.workspace.update({
|
|
443
461
|
where: { id: input.workspaceId },
|
|
444
|
-
data: { fileBeingAnalyzed:
|
|
462
|
+
data: { fileBeingAnalyzed: true },
|
|
445
463
|
});
|
|
446
|
-
await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
|
|
447
|
-
throw error;
|
|
448
|
-
}
|
|
449
464
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// await PusherService.emitError(input.workspaceId, 'AI service is currently unavailable');
|
|
458
|
-
// throw new TRPCError({
|
|
459
|
-
// code: 'SERVICE_UNAVAILABLE',
|
|
460
|
-
// message: 'AI service is currently unavailable. Please try again later.',
|
|
461
|
-
// });
|
|
462
|
-
// }
|
|
463
|
-
// console.log('✅ AI service is healthy');
|
|
464
|
-
|
|
465
|
-
const fileObj = new File([fileBuffer], input.file.filename, { type: input.file.contentType });
|
|
466
|
-
|
|
467
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
468
|
-
status: 'uploading',
|
|
469
|
-
filename: input.file.filename,
|
|
470
|
-
fileType,
|
|
471
|
-
startedAt: new Date().toISOString(),
|
|
472
|
-
steps: {
|
|
473
|
-
fileUpload: {
|
|
474
|
-
order: 1,
|
|
475
|
-
status: 'in_progress',
|
|
476
|
-
},
|
|
477
|
-
fileAnalysis: {
|
|
478
|
-
order: 2,
|
|
479
|
-
status: 'pending',
|
|
480
|
-
},
|
|
481
|
-
studyGuide: {
|
|
482
|
-
order: 3,
|
|
483
|
-
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
484
|
-
},
|
|
485
|
-
flashcards: {
|
|
486
|
-
order: 4,
|
|
487
|
-
status: input.generateFlashcards ? 'pending' : 'skipped',
|
|
465
|
+
PusherService.emitAnalysisProgress(input.workspaceId, {
|
|
466
|
+
status: 'starting',
|
|
467
|
+
filename: primaryFile.name,
|
|
468
|
+
fileType,
|
|
469
|
+
startedAt: new Date().toISOString(),
|
|
470
|
+
steps: {
|
|
471
|
+
fileUpload: { order: 1, status: 'pending' },
|
|
488
472
|
},
|
|
489
|
-
}
|
|
490
|
-
});
|
|
473
|
+
});
|
|
491
474
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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);
|
|
502
|
+
await ctx.db.workspace.update({
|
|
503
|
+
where: { id: input.workspaceId },
|
|
504
|
+
data: { fileBeingAnalyzed: false },
|
|
505
|
+
});
|
|
506
|
+
await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
|
|
507
|
+
throw error;
|
|
516
508
|
}
|
|
517
|
-
});
|
|
518
509
|
|
|
519
|
-
try {
|
|
520
|
-
if (fileType === 'image') {
|
|
521
|
-
await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id);
|
|
522
|
-
} else {
|
|
523
|
-
await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id );
|
|
524
|
-
}
|
|
525
|
-
|
|
526
510
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
527
|
-
status: '
|
|
528
|
-
filename:
|
|
511
|
+
status: 'uploading',
|
|
512
|
+
filename: primaryFile.name,
|
|
529
513
|
fileType,
|
|
530
514
|
startedAt: new Date().toISOString(),
|
|
531
515
|
steps: {
|
|
532
516
|
fileUpload: {
|
|
533
517
|
order: 1,
|
|
534
|
-
status: '
|
|
518
|
+
status: 'in_progress',
|
|
535
519
|
},
|
|
536
520
|
fileAnalysis: {
|
|
537
521
|
order: 2,
|
|
538
|
-
status: '
|
|
522
|
+
status: 'pending',
|
|
539
523
|
},
|
|
540
524
|
studyGuide: {
|
|
541
525
|
order: 3,
|
|
@@ -547,57 +531,67 @@ export const workspace = router({
|
|
|
547
531
|
},
|
|
548
532
|
}
|
|
549
533
|
});
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
error: `Failed to analyze ${fileType}: ${error}`,
|
|
557
|
-
startedAt: new Date().toISOString(),
|
|
558
|
-
steps: {
|
|
559
|
-
fileUpload: {
|
|
560
|
-
order: 1,
|
|
561
|
-
status: 'completed',
|
|
562
|
-
},
|
|
563
|
-
fileAnalysis: {
|
|
564
|
-
order: 2,
|
|
565
|
-
status: 'error',
|
|
566
|
-
},
|
|
567
|
-
studyGuide: {
|
|
568
|
-
order: 3,
|
|
569
|
-
status: 'skipped',
|
|
570
|
-
},
|
|
571
|
-
flashcards: {
|
|
572
|
-
order: 4,
|
|
573
|
-
status: 'skipped',
|
|
574
|
-
},
|
|
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)
|
|
575
540
|
}
|
|
576
|
-
});
|
|
577
|
-
throw error;
|
|
578
|
-
}
|
|
579
541
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
+
}
|
|
595
591
|
|
|
596
|
-
// Generate artifacts
|
|
597
|
-
if (input.generateStudyGuide) {
|
|
598
592
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
599
|
-
status: '
|
|
600
|
-
filename:
|
|
593
|
+
status: 'analyzing',
|
|
594
|
+
filename: primaryFile.name,
|
|
601
595
|
fileType,
|
|
602
596
|
startedAt: new Date().toISOString(),
|
|
603
597
|
steps: {
|
|
@@ -607,11 +601,11 @@ export const workspace = router({
|
|
|
607
601
|
},
|
|
608
602
|
fileAnalysis: {
|
|
609
603
|
order: 2,
|
|
610
|
-
status: '
|
|
604
|
+
status: 'in_progress',
|
|
611
605
|
},
|
|
612
606
|
studyGuide: {
|
|
613
607
|
order: 3,
|
|
614
|
-
status: '
|
|
608
|
+
status: input.generateStudyGuide ? 'pending' : 'skipped',
|
|
615
609
|
},
|
|
616
610
|
flashcards: {
|
|
617
611
|
order: 4,
|
|
@@ -619,41 +613,242 @@ export const workspace = router({
|
|
|
619
613
|
},
|
|
620
614
|
}
|
|
621
615
|
});
|
|
622
|
-
|
|
623
|
-
const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
|
|
624
616
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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({
|
|
630
788
|
data: {
|
|
631
789
|
workspaceId: input.workspaceId,
|
|
632
|
-
type: ArtifactType.
|
|
633
|
-
title: `
|
|
790
|
+
type: ArtifactType.FLASHCARD_SET,
|
|
791
|
+
title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
|
|
634
792
|
createdById: ctx.session.user.id,
|
|
635
793
|
},
|
|
636
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;
|
|
637
839
|
}
|
|
638
|
-
|
|
639
|
-
const lastVersion = await ctx.db.artifactVersion.findFirst({
|
|
640
|
-
where: { artifact: {workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE} },
|
|
641
|
-
orderBy: { version: 'desc' },
|
|
642
|
-
});
|
|
643
840
|
|
|
644
|
-
await ctx.db.
|
|
645
|
-
|
|
841
|
+
await ctx.db.workspace.update({
|
|
842
|
+
where: { id: input.workspaceId },
|
|
843
|
+
data: { fileBeingAnalyzed: false },
|
|
646
844
|
});
|
|
647
845
|
|
|
648
|
-
results.artifacts.studyGuide = artifact;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (input.generateFlashcards) {
|
|
652
846
|
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
653
|
-
status: '
|
|
654
|
-
filename:
|
|
847
|
+
status: 'completed',
|
|
848
|
+
filename: primaryFile.name,
|
|
655
849
|
fileType,
|
|
656
850
|
startedAt: new Date().toISOString(),
|
|
851
|
+
completedAt: new Date().toISOString(),
|
|
657
852
|
steps: {
|
|
658
853
|
fileUpload: {
|
|
659
854
|
order: 1,
|
|
@@ -669,149 +864,71 @@ export const workspace = router({
|
|
|
669
864
|
},
|
|
670
865
|
flashcards: {
|
|
671
866
|
order: 4,
|
|
672
|
-
status: '
|
|
867
|
+
status: input.generateFlashcards ? 'completed' : 'skipped',
|
|
673
868
|
},
|
|
674
869
|
}
|
|
870
|
+
|
|
675
871
|
});
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
type: ArtifactType.FLASHCARD_SET,
|
|
683
|
-
title: `Flashcards - ${input.file.filename}`,
|
|
684
|
-
createdById: ctx.session.user.id,
|
|
685
|
-
},
|
|
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 },
|
|
686
878
|
});
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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',
|
|
705
898
|
},
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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',
|
|
726
923
|
}
|
|
727
924
|
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
results.artifacts.flashcards = artifact;
|
|
731
|
-
}
|
|
925
|
+
})
|
|
926
|
+
);
|
|
732
927
|
|
|
733
|
-
await
|
|
734
|
-
where: { id: input.workspaceId },
|
|
735
|
-
data: { fileBeingAnalyzed: false },
|
|
736
|
-
});
|
|
928
|
+
await Promise.all(workspaceUpdates);
|
|
737
929
|
|
|
738
|
-
|
|
739
|
-
status: 'completed',
|
|
740
|
-
filename: input.file.filename,
|
|
741
|
-
fileType,
|
|
742
|
-
startedAt: new Date().toISOString(),
|
|
743
|
-
completedAt: new Date().toISOString(),
|
|
744
|
-
steps: {
|
|
745
|
-
fileUpload: {
|
|
746
|
-
order: 1,
|
|
747
|
-
status: 'completed',
|
|
748
|
-
},
|
|
749
|
-
fileAnalysis: {
|
|
750
|
-
order: 2,
|
|
751
|
-
status: 'completed',
|
|
752
|
-
},
|
|
753
|
-
studyGuide: {
|
|
754
|
-
order: 3,
|
|
755
|
-
status: input.generateStudyGuide ? 'completed' : 'skipped',
|
|
756
|
-
},
|
|
757
|
-
flashcards: {
|
|
758
|
-
order: 4,
|
|
759
|
-
status: input.generateFlashcards ? 'completed' : 'skipped',
|
|
760
|
-
},
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
return results;
|
|
930
|
+
return workspaces;
|
|
765
931
|
}),
|
|
766
|
-
search: authedProcedure
|
|
767
|
-
.input(z.object({
|
|
768
|
-
query: z.string(),
|
|
769
|
-
limit: z.number().min(1).max(100).default(20),
|
|
770
|
-
}))
|
|
771
|
-
.query(async ({ ctx, input }) => {
|
|
772
|
-
const { query } = input;
|
|
773
|
-
const workspaces = await ctx.db.workspace.findMany({
|
|
774
|
-
where: {
|
|
775
|
-
ownerId: ctx.session.user.id,
|
|
776
|
-
OR: [
|
|
777
|
-
{
|
|
778
|
-
title: {
|
|
779
|
-
contains: query,
|
|
780
|
-
mode: 'insensitive',
|
|
781
|
-
},
|
|
782
|
-
},
|
|
783
|
-
{
|
|
784
|
-
description: {
|
|
785
|
-
contains: query,
|
|
786
|
-
mode: 'insensitive',
|
|
787
|
-
},
|
|
788
|
-
},
|
|
789
|
-
],
|
|
790
|
-
},
|
|
791
|
-
orderBy: {
|
|
792
|
-
updatedAt: 'desc',
|
|
793
|
-
},
|
|
794
|
-
take: input.limit,
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// Update analysisProgress for each workspace with search metadata
|
|
798
|
-
const workspaceUpdates = workspaces.map(ws =>
|
|
799
|
-
ctx.db.workspace.update({
|
|
800
|
-
where: { id: ws.id },
|
|
801
|
-
data: {
|
|
802
|
-
analysisProgress: {
|
|
803
|
-
lastSearched: new Date().toISOString(),
|
|
804
|
-
searchQuery: query,
|
|
805
|
-
matchedIn: ws.title.toLowerCase().includes(query.toLowerCase()) ? 'title' : 'description',
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
})
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
await Promise.all(workspaceUpdates);
|
|
812
|
-
|
|
813
|
-
return workspaces;
|
|
814
|
-
}),
|
|
815
932
|
|
|
816
933
|
// Members sub-router
|
|
817
934
|
members,
|