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