@goscribe/server 1.0.7 → 1.0.9
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/AUTH_FRONTEND_SPEC.md +21 -0
- package/CHAT_FRONTEND_SPEC.md +474 -0
- package/MEETINGSUMMARY_FRONTEND_SPEC.md +28 -0
- package/PODCAST_FRONTEND_SPEC.md +595 -0
- package/STUDYGUIDE_FRONTEND_SPEC.md +18 -0
- package/WORKSHEETS_FRONTEND_SPEC.md +26 -0
- package/WORKSPACE_FRONTEND_SPEC.md +47 -0
- package/dist/context.d.ts +1 -1
- package/dist/lib/ai-session.d.ts +26 -0
- package/dist/lib/ai-session.js +343 -0
- package/dist/lib/auth.js +10 -6
- package/dist/lib/inference.d.ts +2 -0
- package/dist/lib/inference.js +21 -0
- package/dist/lib/pusher.d.ts +14 -0
- package/dist/lib/pusher.js +94 -0
- package/dist/lib/storage.d.ts +10 -2
- package/dist/lib/storage.js +63 -6
- package/dist/routers/_app.d.ts +878 -100
- package/dist/routers/_app.js +8 -2
- package/dist/routers/ai-session.d.ts +0 -0
- package/dist/routers/ai-session.js +1 -0
- package/dist/routers/auth.d.ts +13 -11
- package/dist/routers/auth.js +50 -21
- package/dist/routers/chat.d.ts +171 -0
- package/dist/routers/chat.js +270 -0
- package/dist/routers/flashcards.d.ts +51 -39
- package/dist/routers/flashcards.js +143 -31
- package/dist/routers/meetingsummary.d.ts +0 -0
- package/dist/routers/meetingsummary.js +377 -0
- package/dist/routers/podcast.d.ts +277 -0
- package/dist/routers/podcast.js +847 -0
- package/dist/routers/studyguide.d.ts +54 -0
- package/dist/routers/studyguide.js +125 -0
- package/dist/routers/worksheets.d.ts +147 -40
- package/dist/routers/worksheets.js +348 -33
- package/dist/routers/workspace.d.ts +163 -8
- package/dist/routers/workspace.js +453 -8
- package/dist/server.d.ts +1 -1
- package/dist/server.js +7 -2
- package/dist/trpc.d.ts +5 -5
- package/package.json +11 -3
- package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
- package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +87 -6
- package/prisma/seed.mjs +135 -0
- package/src/lib/ai-session.ts +411 -0
- package/src/lib/auth.ts +1 -1
- package/src/lib/inference.ts +21 -0
- package/src/lib/pusher.ts +104 -0
- package/src/lib/storage.ts +89 -6
- package/src/routers/_app.ts +6 -0
- package/src/routers/auth.ts +8 -4
- package/src/routers/chat.ts +275 -0
- package/src/routers/flashcards.ts +151 -33
- package/src/routers/meetingsummary.ts +416 -0
- package/src/routers/podcast.ts +934 -0
- package/src/routers/studyguide.ts +144 -0
- package/src/routers/worksheets.ts +346 -18
- package/src/routers/workspace.ts +500 -8
- package/src/server.ts +7 -2
- package/test-ai-integration.js +134 -0
- package/dist/context.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/auth.d.ts.map +0 -1
- package/dist/lib/file.d.ts.map +0 -1
- package/dist/lib/prisma.d.ts.map +0 -1
- package/dist/lib/storage.d.ts.map +0 -1
- package/dist/routers/_app.d.ts.map +0 -1
- package/dist/routers/auth.d.ts.map +0 -1
- package/dist/routers/sample.js +0 -21
- package/dist/routers/workspace.d.ts.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/trpc.d.ts.map +0 -1
|
@@ -2,22 +2,70 @@ import { z } from 'zod';
|
|
|
2
2
|
import { TRPCError } from '@trpc/server';
|
|
3
3
|
import { router, authedProcedure } from '../trpc.js';
|
|
4
4
|
import { bucket } from '../lib/storage.js';
|
|
5
|
+
import { ArtifactType } from '@prisma/client';
|
|
6
|
+
import { aiSessionService } from '../lib/ai-session.js';
|
|
7
|
+
import PusherService from '../lib/pusher.js';
|
|
8
|
+
// Helper function to calculate search relevance score
|
|
9
|
+
function calculateRelevance(query, ...texts) {
|
|
10
|
+
const queryLower = query.toLowerCase();
|
|
11
|
+
let score = 0;
|
|
12
|
+
for (const text of texts) {
|
|
13
|
+
if (!text)
|
|
14
|
+
continue;
|
|
15
|
+
const textLower = text.toLowerCase();
|
|
16
|
+
// Exact match gets highest score
|
|
17
|
+
if (textLower.includes(queryLower)) {
|
|
18
|
+
score += 10;
|
|
19
|
+
}
|
|
20
|
+
// Word boundary matches get good score
|
|
21
|
+
const words = queryLower.split(/\s+/);
|
|
22
|
+
for (const word of words) {
|
|
23
|
+
if (word.length > 2 && textLower.includes(word)) {
|
|
24
|
+
score += 5;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Partial matches get lower score
|
|
28
|
+
const queryChars = queryLower.split('');
|
|
29
|
+
let consecutiveMatches = 0;
|
|
30
|
+
for (const char of queryChars) {
|
|
31
|
+
if (textLower.includes(char)) {
|
|
32
|
+
consecutiveMatches++;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
consecutiveMatches = 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
score += consecutiveMatches * 0.1;
|
|
39
|
+
}
|
|
40
|
+
return score;
|
|
41
|
+
}
|
|
5
42
|
export const workspace = router({
|
|
6
43
|
// List current user's workspaces
|
|
7
44
|
list: authedProcedure
|
|
8
|
-
.
|
|
45
|
+
.input(z.object({
|
|
46
|
+
parentId: z.string().optional(),
|
|
47
|
+
}))
|
|
48
|
+
.query(async ({ ctx, input }) => {
|
|
9
49
|
const workspaces = await ctx.db.workspace.findMany({
|
|
10
50
|
where: {
|
|
11
51
|
ownerId: ctx.session.user.id,
|
|
52
|
+
folderId: input.parentId ?? null,
|
|
12
53
|
},
|
|
13
54
|
orderBy: { updatedAt: 'desc' },
|
|
14
55
|
});
|
|
15
|
-
|
|
56
|
+
const folders = await ctx.db.folder.findMany({
|
|
57
|
+
where: {
|
|
58
|
+
ownerId: ctx.session.user.id,
|
|
59
|
+
parentId: input.parentId ?? null,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return { workspaces, folders };
|
|
16
63
|
}),
|
|
17
64
|
create: authedProcedure
|
|
18
65
|
.input(z.object({
|
|
19
66
|
name: z.string().min(1).max(100),
|
|
20
67
|
description: z.string().max(500).optional(),
|
|
68
|
+
parentId: z.string().optional(),
|
|
21
69
|
}))
|
|
22
70
|
.mutation(async ({ ctx, input }) => {
|
|
23
71
|
const ws = await ctx.db.workspace.create({
|
|
@@ -25,25 +73,111 @@ export const workspace = router({
|
|
|
25
73
|
title: input.name,
|
|
26
74
|
description: input.description,
|
|
27
75
|
ownerId: ctx.session.user.id,
|
|
76
|
+
folderId: input.parentId ?? null,
|
|
77
|
+
artifacts: {
|
|
78
|
+
create: {
|
|
79
|
+
type: ArtifactType.FLASHCARD_SET,
|
|
80
|
+
title: "New Flashcard Set",
|
|
81
|
+
},
|
|
82
|
+
createMany: {
|
|
83
|
+
data: [
|
|
84
|
+
{ type: ArtifactType.WORKSHEET, title: "Worksheet 1" },
|
|
85
|
+
{ type: ArtifactType.WORKSHEET, title: "Worksheet 2" },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
28
89
|
},
|
|
29
90
|
});
|
|
30
91
|
return ws;
|
|
31
92
|
}),
|
|
93
|
+
createFolder: authedProcedure
|
|
94
|
+
.input(z.object({
|
|
95
|
+
name: z.string().min(1).max(100),
|
|
96
|
+
parentId: z.string().optional(),
|
|
97
|
+
}))
|
|
98
|
+
.mutation(async ({ ctx, input }) => {
|
|
99
|
+
const folder = await ctx.db.folder.create({
|
|
100
|
+
data: {
|
|
101
|
+
name: input.name,
|
|
102
|
+
ownerId: ctx.session.user.id,
|
|
103
|
+
parentId: input.parentId ?? null,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
return folder;
|
|
107
|
+
}),
|
|
32
108
|
get: authedProcedure
|
|
33
109
|
.input(z.object({
|
|
34
|
-
id: z.string()
|
|
110
|
+
id: z.string(),
|
|
35
111
|
}))
|
|
36
112
|
.query(async ({ ctx, input }) => {
|
|
37
113
|
const ws = await ctx.db.workspace.findFirst({
|
|
38
114
|
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
115
|
+
include: {
|
|
116
|
+
artifacts: true,
|
|
117
|
+
folder: true,
|
|
118
|
+
uploads: true,
|
|
119
|
+
},
|
|
39
120
|
});
|
|
40
121
|
if (!ws)
|
|
41
122
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
42
123
|
return ws;
|
|
43
124
|
}),
|
|
125
|
+
share: authedProcedure
|
|
126
|
+
.input(z.object({
|
|
127
|
+
id: z.string(),
|
|
128
|
+
}))
|
|
129
|
+
.query(async ({ ctx, input }) => {
|
|
130
|
+
const ws = await ctx.db.workspace.findFirst({
|
|
131
|
+
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
132
|
+
});
|
|
133
|
+
if (!ws)
|
|
134
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
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 },
|
|
152
|
+
});
|
|
153
|
+
if (!ws)
|
|
154
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' });
|
|
155
|
+
if (ws.ownerId === ctx.session.user.id) {
|
|
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 } } },
|
|
161
|
+
});
|
|
162
|
+
if (alreadyShared) {
|
|
163
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Already joined this workspace' });
|
|
164
|
+
}
|
|
165
|
+
await ctx.db.workspace.update({
|
|
166
|
+
where: { id: ws.id },
|
|
167
|
+
data: { sharedWith: { connect: { id: ctx.session.user.id } } },
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
id: ws.id,
|
|
171
|
+
title: ws.title,
|
|
172
|
+
description: ws.description,
|
|
173
|
+
ownerId: ws.ownerId,
|
|
174
|
+
createdAt: ws.createdAt,
|
|
175
|
+
updatedAt: ws.updatedAt,
|
|
176
|
+
};
|
|
177
|
+
}),
|
|
44
178
|
update: authedProcedure
|
|
45
179
|
.input(z.object({
|
|
46
|
-
id: z.string()
|
|
180
|
+
id: z.string(),
|
|
47
181
|
name: z.string().min(1).max(100).optional(),
|
|
48
182
|
description: z.string().max(500).optional(),
|
|
49
183
|
}))
|
|
@@ -64,7 +198,7 @@ export const workspace = router({
|
|
|
64
198
|
}),
|
|
65
199
|
delete: authedProcedure
|
|
66
200
|
.input(z.object({
|
|
67
|
-
id: z.string()
|
|
201
|
+
id: z.string(),
|
|
68
202
|
}))
|
|
69
203
|
.mutation(async ({ ctx, input }) => {
|
|
70
204
|
const deleted = await ctx.db.workspace.deleteMany({
|
|
@@ -74,9 +208,29 @@ export const workspace = router({
|
|
|
74
208
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
75
209
|
return true;
|
|
76
210
|
}),
|
|
211
|
+
getFolderInformation: authedProcedure
|
|
212
|
+
.input(z.object({
|
|
213
|
+
id: z.string(),
|
|
214
|
+
}))
|
|
215
|
+
.query(async ({ ctx, input }) => {
|
|
216
|
+
const folder = await ctx.db.folder.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
|
|
217
|
+
// find all of its parents
|
|
218
|
+
if (!folder)
|
|
219
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
220
|
+
const parents = [];
|
|
221
|
+
let current = folder;
|
|
222
|
+
while (current.parentId) {
|
|
223
|
+
const parent = await ctx.db.folder.findFirst({ where: { id: current.parentId, ownerId: ctx.session.user.id } });
|
|
224
|
+
if (!parent)
|
|
225
|
+
break;
|
|
226
|
+
parents.push(parent);
|
|
227
|
+
current = parent;
|
|
228
|
+
}
|
|
229
|
+
return { folder, parents };
|
|
230
|
+
}),
|
|
77
231
|
uploadFiles: authedProcedure
|
|
78
232
|
.input(z.object({
|
|
79
|
-
id: z.string()
|
|
233
|
+
id: z.string(),
|
|
80
234
|
files: z.array(z.object({
|
|
81
235
|
filename: z.string().min(1).max(255),
|
|
82
236
|
contentType: z.string().min(1).max(100),
|
|
@@ -125,8 +279,8 @@ export const workspace = router({
|
|
|
125
279
|
}),
|
|
126
280
|
deleteFiles: authedProcedure
|
|
127
281
|
.input(z.object({
|
|
128
|
-
fileId: z.array(z.string()
|
|
129
|
-
id: z.string()
|
|
282
|
+
fileId: z.array(z.string()),
|
|
283
|
+
id: z.string(),
|
|
130
284
|
}))
|
|
131
285
|
.mutation(async ({ ctx, input }) => {
|
|
132
286
|
// ensure files are in the user's workspace
|
|
@@ -155,4 +309,295 @@ export const workspace = router({
|
|
|
155
309
|
});
|
|
156
310
|
return true;
|
|
157
311
|
}),
|
|
312
|
+
uploadAndAnalyzeMedia: authedProcedure
|
|
313
|
+
.input(z.object({
|
|
314
|
+
workspaceId: z.string(),
|
|
315
|
+
file: z.object({
|
|
316
|
+
filename: z.string(),
|
|
317
|
+
contentType: z.string(),
|
|
318
|
+
size: z.number(),
|
|
319
|
+
content: z.string(), // Base64 encoded file content
|
|
320
|
+
}),
|
|
321
|
+
generateStudyGuide: z.boolean().default(true),
|
|
322
|
+
generateFlashcards: z.boolean().default(true),
|
|
323
|
+
generateWorksheet: z.boolean().default(true),
|
|
324
|
+
}))
|
|
325
|
+
.mutation(async ({ ctx, input }) => {
|
|
326
|
+
console.log('🚀 uploadAndAnalyzeMedia started', {
|
|
327
|
+
workspaceId: input.workspaceId,
|
|
328
|
+
filename: input.file.filename,
|
|
329
|
+
fileSize: input.file.size,
|
|
330
|
+
generateStudyGuide: input.generateStudyGuide,
|
|
331
|
+
generateFlashcards: input.generateFlashcards,
|
|
332
|
+
generateWorksheet: input.generateWorksheet
|
|
333
|
+
});
|
|
334
|
+
// Verify workspace ownership
|
|
335
|
+
const workspace = await ctx.db.workspace.findFirst({
|
|
336
|
+
where: { id: input.workspaceId, ownerId: ctx.session.user.id }
|
|
337
|
+
});
|
|
338
|
+
if (!workspace) {
|
|
339
|
+
console.error('❌ Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
|
|
340
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
341
|
+
}
|
|
342
|
+
console.log('✅ Workspace verified', { workspaceId: workspace.id, workspaceTitle: workspace.title });
|
|
343
|
+
// Convert base64 to buffer
|
|
344
|
+
console.log('📁 Converting base64 to buffer...');
|
|
345
|
+
const fileBuffer = Buffer.from(input.file.content, 'base64');
|
|
346
|
+
console.log('✅ File buffer created', { bufferSize: fileBuffer.length });
|
|
347
|
+
// // Check AI service health first
|
|
348
|
+
// console.log('🏥 Checking AI service health...');
|
|
349
|
+
// const isHealthy = await aiSessionService.checkHealth();
|
|
350
|
+
// if (!isHealthy) {
|
|
351
|
+
// console.error('❌ AI service is not available');
|
|
352
|
+
// await PusherService.emitError(input.workspaceId, 'AI service is currently unavailable');
|
|
353
|
+
// throw new TRPCError({
|
|
354
|
+
// code: 'SERVICE_UNAVAILABLE',
|
|
355
|
+
// message: 'AI service is currently unavailable. Please try again later.',
|
|
356
|
+
// });
|
|
357
|
+
// }
|
|
358
|
+
// 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
|
+
const fileObj = new File([fileBuffer], input.file.filename, { type: input.file.contentType });
|
|
364
|
+
const fileType = input.file.contentType.startsWith('image/') ? 'image' : 'pdf';
|
|
365
|
+
console.log('📤 Uploading file to AI service...', { filename: input.file.filename, fileType });
|
|
366
|
+
await aiSessionService.uploadFile(session.id, fileObj, fileType);
|
|
367
|
+
console.log('✅ File uploaded to AI service');
|
|
368
|
+
console.log('🚀 Starting LLM session...');
|
|
369
|
+
try {
|
|
370
|
+
await aiSessionService.startLLMSession(session.id);
|
|
371
|
+
console.log('✅ LLM session started');
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
console.error('❌ Failed to start LLM session:', error);
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
// Analyze the file first
|
|
378
|
+
console.log('🔍 Analyzing file...', { fileType });
|
|
379
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'file_analysis_start', { filename: input.file.filename, fileType });
|
|
380
|
+
try {
|
|
381
|
+
if (fileType === 'image') {
|
|
382
|
+
await aiSessionService.analyseImage(session.id);
|
|
383
|
+
console.log('✅ Image analysis completed');
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
await aiSessionService.analysePDF(session.id);
|
|
387
|
+
console.log('✅ PDF analysis completed');
|
|
388
|
+
}
|
|
389
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'file_analysis_complete', { filename: input.file.filename, fileType });
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
console.error('❌ Failed to analyze file:', error);
|
|
393
|
+
await PusherService.emitError(input.workspaceId, `Failed to analyze ${fileType}: ${error}`, 'file_analysis');
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
const results = {
|
|
397
|
+
filename: input.file.filename,
|
|
398
|
+
artifacts: {
|
|
399
|
+
studyGuide: null,
|
|
400
|
+
flashcards: null,
|
|
401
|
+
worksheet: null,
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
// Generate artifacts
|
|
405
|
+
if (input.generateStudyGuide) {
|
|
406
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'study_guide_load_start', { filename: input.file.filename });
|
|
407
|
+
const content = await aiSessionService.generateStudyGuide(session.id);
|
|
408
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'study_guide_info', { contentLength: content.length });
|
|
409
|
+
let artifact = await ctx.db.artifact.findFirst({
|
|
410
|
+
where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
|
|
411
|
+
});
|
|
412
|
+
if (!artifact) {
|
|
413
|
+
artifact = await ctx.db.artifact.create({
|
|
414
|
+
data: {
|
|
415
|
+
workspaceId: input.workspaceId,
|
|
416
|
+
type: ArtifactType.STUDY_GUIDE,
|
|
417
|
+
title: `Study Guide - ${input.file.filename}`,
|
|
418
|
+
createdById: ctx.session.user.id,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const lastVersion = await ctx.db.artifactVersion.findFirst({
|
|
423
|
+
where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
|
|
424
|
+
orderBy: { version: 'desc' },
|
|
425
|
+
});
|
|
426
|
+
await ctx.db.artifactVersion.create({
|
|
427
|
+
data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
|
|
428
|
+
});
|
|
429
|
+
results.artifacts.studyGuide = artifact;
|
|
430
|
+
// Emit Pusher notification
|
|
431
|
+
await PusherService.emitStudyGuideComplete(input.workspaceId, artifact);
|
|
432
|
+
}
|
|
433
|
+
if (input.generateFlashcards) {
|
|
434
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_load_start', { filename: input.file.filename });
|
|
435
|
+
const content = await aiSessionService.generateFlashcardQuestions(session.id, 10, 'medium');
|
|
436
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { contentLength: content.length });
|
|
437
|
+
const artifact = await ctx.db.artifact.create({
|
|
438
|
+
data: {
|
|
439
|
+
workspaceId: input.workspaceId,
|
|
440
|
+
type: ArtifactType.FLASHCARD_SET,
|
|
441
|
+
title: `Flashcards - ${input.file.filename}`,
|
|
442
|
+
createdById: ctx.session.user.id,
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
// Parse JSON flashcard content
|
|
446
|
+
try {
|
|
447
|
+
const flashcardData = JSON.parse(content);
|
|
448
|
+
let createdCards = 0;
|
|
449
|
+
for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
|
|
450
|
+
const card = flashcardData[i];
|
|
451
|
+
const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
|
|
452
|
+
const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
|
|
453
|
+
await ctx.db.flashcard.create({
|
|
454
|
+
data: {
|
|
455
|
+
artifactId: artifact.id,
|
|
456
|
+
front: front,
|
|
457
|
+
back: back,
|
|
458
|
+
order: i,
|
|
459
|
+
tags: ['ai-generated', 'medium'],
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
createdCards++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (parseError) {
|
|
466
|
+
// Fallback to text parsing if JSON fails
|
|
467
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
468
|
+
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
469
|
+
const line = lines[i];
|
|
470
|
+
if (line.includes(' - ')) {
|
|
471
|
+
const [front, back] = line.split(' - ');
|
|
472
|
+
await ctx.db.flashcard.create({
|
|
473
|
+
data: {
|
|
474
|
+
artifactId: artifact.id,
|
|
475
|
+
front: front.trim(),
|
|
476
|
+
back: back.trim(),
|
|
477
|
+
order: i,
|
|
478
|
+
tags: ['ai-generated', 'medium'],
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
results.artifacts.flashcards = artifact;
|
|
485
|
+
// Emit Pusher notification
|
|
486
|
+
await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
|
|
487
|
+
}
|
|
488
|
+
if (input.generateWorksheet) {
|
|
489
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_load_start', { filename: input.file.filename });
|
|
490
|
+
const content = await aiSessionService.generateWorksheetQuestions(session.id, 8, 'medium');
|
|
491
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: content.length });
|
|
492
|
+
const artifact = await ctx.db.artifact.create({
|
|
493
|
+
data: {
|
|
494
|
+
workspaceId: input.workspaceId,
|
|
495
|
+
type: ArtifactType.WORKSHEET,
|
|
496
|
+
title: `Worksheet - ${input.file.filename}`,
|
|
497
|
+
createdById: ctx.session.user.id,
|
|
498
|
+
},
|
|
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
|
+
}
|
|
560
|
+
results.artifacts.worksheet = artifact;
|
|
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);
|
|
569
|
+
return results;
|
|
570
|
+
}),
|
|
571
|
+
search: authedProcedure
|
|
572
|
+
.input(z.object({
|
|
573
|
+
query: z.string(),
|
|
574
|
+
limit: z.number().min(1).max(100).default(20),
|
|
575
|
+
}))
|
|
576
|
+
.query(async ({ ctx, input }) => {
|
|
577
|
+
const { query } = input;
|
|
578
|
+
const workspaces = await ctx.db.workspace.findMany({
|
|
579
|
+
where: {
|
|
580
|
+
ownerId: ctx.session.user.id,
|
|
581
|
+
OR: [
|
|
582
|
+
{
|
|
583
|
+
title: {
|
|
584
|
+
contains: query,
|
|
585
|
+
mode: 'insensitive',
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
description: {
|
|
590
|
+
contains: query,
|
|
591
|
+
mode: 'insensitive',
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
orderBy: {
|
|
597
|
+
updatedAt: 'desc',
|
|
598
|
+
},
|
|
599
|
+
take: input.limit,
|
|
600
|
+
});
|
|
601
|
+
return workspaces;
|
|
602
|
+
})
|
|
158
603
|
});
|
package/dist/server.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import 'dotenv/config';
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
1
2
|
import express from 'express';
|
|
2
3
|
import cors from 'cors';
|
|
3
4
|
import helmet from 'helmet';
|
|
@@ -12,12 +13,16 @@ async function main() {
|
|
|
12
13
|
// Middlewares
|
|
13
14
|
app.use(helmet());
|
|
14
15
|
app.use(cors({
|
|
15
|
-
origin: "http://localhost:3000",
|
|
16
|
+
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
|
16
17
|
credentials: true, // allow cookies
|
|
18
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
19
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'Set-Cookie'],
|
|
20
|
+
exposedHeaders: ['Set-Cookie'],
|
|
17
21
|
}));
|
|
18
22
|
app.use(morgan('dev'));
|
|
19
23
|
app.use(compression());
|
|
20
|
-
app.use(express.json());
|
|
24
|
+
app.use(express.json({ limit: '50mb' }));
|
|
25
|
+
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
|
21
26
|
// Health (plain Express)
|
|
22
27
|
app.get('/', (_req, res) => {
|
|
23
28
|
res.json({ ok: true, service: 'trpc-express', ts: Date.now() });
|
package/dist/trpc.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const router: import("@trpc/server").TRPCRouterBuilder<{
|
|
|
4
4
|
session: any;
|
|
5
5
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
6
6
|
res: import("express").Response<any, Record<string, any>>;
|
|
7
|
-
cookies:
|
|
7
|
+
cookies: Record<string, string | undefined>;
|
|
8
8
|
};
|
|
9
9
|
meta: object;
|
|
10
10
|
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
@@ -15,20 +15,20 @@ export declare const middleware: <$ContextOverrides>(fn: import("@trpc/server").
|
|
|
15
15
|
session: any;
|
|
16
16
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
17
17
|
res: import("express").Response<any, Record<string, any>>;
|
|
18
|
-
cookies:
|
|
18
|
+
cookies: Record<string, string | undefined>;
|
|
19
19
|
}, object, object, $ContextOverrides, unknown>) => import("@trpc/server").TRPCMiddlewareBuilder<{
|
|
20
20
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
21
21
|
session: any;
|
|
22
22
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
23
23
|
res: import("express").Response<any, Record<string, any>>;
|
|
24
|
-
cookies:
|
|
24
|
+
cookies: Record<string, string | undefined>;
|
|
25
25
|
}, object, $ContextOverrides, unknown>;
|
|
26
26
|
export declare const publicProcedure: import("@trpc/server").TRPCProcedureBuilder<{
|
|
27
27
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
28
28
|
session: any;
|
|
29
29
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
30
30
|
res: import("express").Response<any, Record<string, any>>;
|
|
31
|
-
cookies:
|
|
31
|
+
cookies: Record<string, string | undefined>;
|
|
32
32
|
}, object, object, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
33
33
|
/** Exported authed procedure */
|
|
34
34
|
export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilder<{
|
|
@@ -36,7 +36,7 @@ export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilde
|
|
|
36
36
|
session: any;
|
|
37
37
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
38
38
|
res: import("express").Response<any, Record<string, any>>;
|
|
39
|
-
cookies:
|
|
39
|
+
cookies: Record<string, string | undefined>;
|
|
40
40
|
}, object, {
|
|
41
41
|
session: any;
|
|
42
42
|
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goscribe/server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"dev": "
|
|
15
|
+
"dev": "tsx watch src/server.ts",
|
|
16
16
|
"build": "npx prisma generate && tsc -p .",
|
|
17
|
-
"start": "node --experimental-specifier-resolution=node dist/server.js"
|
|
17
|
+
"start": "node --experimental-specifier-resolution=node dist/server.js",
|
|
18
|
+
"seed": "node --experimental-specifier-resolution=node prisma/seed.mjs"
|
|
18
19
|
},
|
|
19
20
|
"author": "",
|
|
20
21
|
"license": "MIT",
|
|
@@ -22,8 +23,11 @@
|
|
|
22
23
|
"@auth/express": "^0.11.0",
|
|
23
24
|
"@auth/prisma-adapter": "^2.10.0",
|
|
24
25
|
"@google-cloud/storage": "^7.17.0",
|
|
26
|
+
"@goscribe/server": "^1.0.8",
|
|
25
27
|
"@prisma/client": "^6.14.0",
|
|
26
28
|
"@trpc/server": "^11.5.0",
|
|
29
|
+
"@types/uuid": "^10.0.0",
|
|
30
|
+
"@vingeray/editorjs-markdown-converter": "^0.1.2",
|
|
27
31
|
"bcryptjs": "^3.0.2",
|
|
28
32
|
"compression": "^1.8.1",
|
|
29
33
|
"cookie": "^1.0.2",
|
|
@@ -33,6 +37,9 @@
|
|
|
33
37
|
"helmet": "^8.1.0",
|
|
34
38
|
"morgan": "^1.10.1",
|
|
35
39
|
"prisma": "^6.14.0",
|
|
40
|
+
"pusher": "^5.2.0",
|
|
41
|
+
"pusher-js": "^8.4.0",
|
|
42
|
+
"socket.io": "^4.8.1",
|
|
36
43
|
"superjson": "^2.2.2",
|
|
37
44
|
"zod": "^4.1.1"
|
|
38
45
|
},
|
|
@@ -47,6 +54,7 @@
|
|
|
47
54
|
"ts-node-dev": "^2.0.0",
|
|
48
55
|
"tsc-alias": "^1.8.16",
|
|
49
56
|
"tsc-esm-fix": "^3.1.2",
|
|
57
|
+
"tsx": "^4.20.6",
|
|
50
58
|
"typescript": "^5.9.2",
|
|
51
59
|
"typescript-transform-paths": "^3.5.5"
|
|
52
60
|
}
|