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