@goscribe/server 1.0.8 → 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.
Files changed (57) hide show
  1. package/AUTH_FRONTEND_SPEC.md +21 -0
  2. package/CHAT_FRONTEND_SPEC.md +474 -0
  3. package/MEETINGSUMMARY_FRONTEND_SPEC.md +28 -0
  4. package/PODCAST_FRONTEND_SPEC.md +595 -0
  5. package/STUDYGUIDE_FRONTEND_SPEC.md +18 -0
  6. package/WORKSHEETS_FRONTEND_SPEC.md +26 -0
  7. package/WORKSPACE_FRONTEND_SPEC.md +47 -0
  8. package/dist/lib/ai-session.d.ts +26 -0
  9. package/dist/lib/ai-session.js +343 -0
  10. package/dist/lib/inference.d.ts +2 -0
  11. package/dist/lib/inference.js +21 -0
  12. package/dist/lib/pusher.d.ts +14 -0
  13. package/dist/lib/pusher.js +94 -0
  14. package/dist/lib/storage.d.ts +10 -2
  15. package/dist/lib/storage.js +63 -6
  16. package/dist/routers/_app.d.ts +840 -58
  17. package/dist/routers/_app.js +6 -0
  18. package/dist/routers/ai-session.d.ts +0 -0
  19. package/dist/routers/ai-session.js +1 -0
  20. package/dist/routers/auth.d.ts +1 -0
  21. package/dist/routers/auth.js +6 -4
  22. package/dist/routers/chat.d.ts +171 -0
  23. package/dist/routers/chat.js +270 -0
  24. package/dist/routers/flashcards.d.ts +37 -0
  25. package/dist/routers/flashcards.js +128 -0
  26. package/dist/routers/meetingsummary.d.ts +0 -0
  27. package/dist/routers/meetingsummary.js +377 -0
  28. package/dist/routers/podcast.d.ts +277 -0
  29. package/dist/routers/podcast.js +847 -0
  30. package/dist/routers/studyguide.d.ts +54 -0
  31. package/dist/routers/studyguide.js +125 -0
  32. package/dist/routers/worksheets.d.ts +138 -51
  33. package/dist/routers/worksheets.js +317 -7
  34. package/dist/routers/workspace.d.ts +162 -7
  35. package/dist/routers/workspace.js +440 -8
  36. package/dist/server.js +6 -2
  37. package/package.json +11 -4
  38. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
  39. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
  40. package/prisma/migrations/migration_lock.toml +3 -0
  41. package/prisma/schema.prisma +87 -6
  42. package/prisma/seed.mjs +135 -0
  43. package/src/lib/ai-session.ts +411 -0
  44. package/src/lib/inference.ts +21 -0
  45. package/src/lib/pusher.ts +104 -0
  46. package/src/lib/storage.ts +89 -6
  47. package/src/routers/_app.ts +6 -0
  48. package/src/routers/auth.ts +8 -4
  49. package/src/routers/chat.ts +275 -0
  50. package/src/routers/flashcards.ts +142 -0
  51. package/src/routers/meetingsummary.ts +416 -0
  52. package/src/routers/podcast.ts +934 -0
  53. package/src/routers/studyguide.ts +144 -0
  54. package/src/routers/worksheets.ts +336 -7
  55. package/src/routers/workspace.ts +487 -8
  56. package/src/server.ts +7 -2
  57. package/test-ai-integration.js +134 -0
@@ -0,0 +1,144 @@
1
+ import { z } from 'zod';
2
+ import { TRPCError } from '@trpc/server';
3
+ import { router, authedProcedure } from '../trpc.js';
4
+ import { title } from 'node:process';
5
+
6
+ // Mirror Prisma enum to avoid direct type import
7
+ const ArtifactType = {
8
+ STUDY_GUIDE: 'STUDY_GUIDE',
9
+ } as const;
10
+
11
+ const initializeEditorJsEmptyBlock = () => ({
12
+ time: Date.now(),
13
+ blocks: [
14
+ {
15
+ id: 'initial',
16
+ type: 'paragraph',
17
+ data: { text: 'Start writing your study guide...' },
18
+ },
19
+ ],
20
+ version: '2.27.0',
21
+ });
22
+
23
+ export const studyguide = router({
24
+ // Get latest study guide for a workspace or a specific study guide by ID
25
+ get: authedProcedure
26
+ .input(
27
+ z.object({
28
+ workspaceId: z.string(),
29
+ })
30
+ )
31
+ .query(async ({ ctx, input }) => {
32
+ // by studyGuideId (artifact id)
33
+ let artifact = await ctx.db.artifact.findFirst({
34
+ where: {
35
+ workspaceId: input.workspaceId!,
36
+ type: ArtifactType.STUDY_GUIDE,
37
+ workspace: { ownerId: ctx.session.user.id },
38
+ },
39
+ include: {
40
+ versions: { orderBy: { version: 'desc' }, take: 1 },
41
+ },
42
+ });
43
+
44
+ console.log('artifact', artifact);
45
+ if (!artifact) {
46
+ artifact = await ctx.db.artifact.create({
47
+ data: {
48
+ workspaceId: input.workspaceId,
49
+ type: ArtifactType.STUDY_GUIDE,
50
+ title: 'Study Guide',
51
+ createdById: ctx.session.user.id,
52
+ versions: {
53
+ create: {
54
+ content: `${JSON.stringify(initializeEditorJsEmptyBlock())}`,
55
+ version: 1,
56
+ createdById: ctx.session.user.id,
57
+ }
58
+ }
59
+ },
60
+ include: {
61
+ versions: { orderBy: { version: 'desc' }, take: 1 },
62
+ }
63
+ });
64
+ }
65
+ const latestVersion = artifact.versions[0] ?? null;
66
+ return { artifactId: artifact.id, title: artifact.title, latestVersion };
67
+ }),
68
+
69
+ // Edit study guide content by creating a new version, or create if doesn't exist
70
+ edit: authedProcedure
71
+ .input(
72
+ z.object({
73
+ workspaceId: z.string(),
74
+ studyGuideId: z.string().optional(),
75
+ content: z.string().min(1),
76
+ data: z.record(z.string(), z.unknown()).optional(),
77
+ title: z.string().min(1).optional(),
78
+ })
79
+ )
80
+ .mutation(async ({ ctx, input }) => {
81
+ let artifact;
82
+
83
+ if (input.studyGuideId) {
84
+ // Try to find existing study guide
85
+ artifact = await ctx.db.artifact.findFirst({
86
+ where: {
87
+ id: input.studyGuideId,
88
+ type: ArtifactType.STUDY_GUIDE,
89
+ workspace: { ownerId: ctx.session.user.id },
90
+ },
91
+ });
92
+ } else {
93
+ // Find by workspace if no specific studyGuideId provided
94
+ artifact = await ctx.db.artifact.findFirst({
95
+ where: {
96
+ workspaceId: input.workspaceId,
97
+ type: ArtifactType.STUDY_GUIDE,
98
+ workspace: { ownerId: ctx.session.user.id },
99
+ },
100
+ });
101
+ }
102
+
103
+ // If no study guide found, create a new one
104
+ if (!artifact) {
105
+ artifact = await ctx.db.artifact.create({
106
+ data: {
107
+ workspaceId: input.workspaceId,
108
+ type: ArtifactType.STUDY_GUIDE,
109
+ title: 'Study Guide',
110
+ createdById: ctx.session.user.id,
111
+ },
112
+ });
113
+ }
114
+
115
+ const last = await ctx.db.artifactVersion.findFirst({
116
+ where: { artifactId: artifact.id },
117
+ orderBy: { version: 'desc' },
118
+ });
119
+
120
+ if (input.title && input.title !== artifact.title) {
121
+ console.log('rename')
122
+ await ctx.db.artifact.update({
123
+ where: { id: artifact.id },
124
+ data: { title: input.title },
125
+ });
126
+ }
127
+
128
+ const nextVersion = (last?.version ?? 0) + 1;
129
+
130
+ const version = await ctx.db.artifactVersion.create({
131
+ data: {
132
+ artifactId: artifact.id,
133
+ content: input.content,
134
+ data: input.data as any,
135
+ version: nextVersion,
136
+ createdById: ctx.session.user.id,
137
+ },
138
+ });
139
+
140
+ return { artifactId: artifact.id, version };
141
+ }),
142
+ });
143
+
144
+
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import { router, authedProcedure } from '../trpc.js';
4
+ import { aiSessionService } from '../lib/ai-session.js';
5
+ import PusherService from '../lib/pusher.js';
4
6
 
5
7
  // Avoid importing Prisma enums directly; mirror values as string literals
6
8
  const ArtifactType = {
@@ -13,6 +15,15 @@ const Difficulty = {
13
15
  HARD: 'HARD',
14
16
  } as const;
15
17
 
18
+ const QuestionType = {
19
+ MULTIPLE_CHOICE: 'MULTIPLE_CHOICE',
20
+ TEXT: 'TEXT',
21
+ NUMERIC: 'NUMERIC',
22
+ TRUE_FALSE: 'TRUE_FALSE',
23
+ MATCHING: 'MATCHING',
24
+ FILL_IN_THE_BLANK: 'FILL_IN_THE_BLANK',
25
+ } as const;
26
+
16
27
  export const worksheets = router({
17
28
  // List all worksheet artifacts for a workspace
18
29
  list: authedProcedure
@@ -30,23 +41,80 @@ export const worksheets = router({
30
41
  orderBy: { updatedAt: 'desc' },
31
42
  });
32
43
  if (!worksheets) throw new TRPCError({ code: 'NOT_FOUND' });
33
- return worksheets;
44
+
45
+ // Merge per-user progress into question.meta for compatibility with UI
46
+ const allQuestionIds = worksheets.flatMap(w => w.questions.map(q => q.id));
47
+ if (allQuestionIds.length === 0) return worksheets;
48
+
49
+ const progress = await ctx.db.worksheetQuestionProgress.findMany({
50
+ where: { userId: ctx.session.user.id, worksheetQuestionId: { in: allQuestionIds } },
51
+ });
52
+ const progressByQuestionId = new Map(progress.map(p => [p.worksheetQuestionId, p]));
53
+
54
+ const merged = worksheets.map(w => ({
55
+ ...w,
56
+ questions: w.questions.map(q => {
57
+ const p = progressByQuestionId.get(q.id);
58
+ if (!p) return q;
59
+ const existingMeta = q.meta ? (typeof q.meta === 'object' ? q.meta : JSON.parse(q.meta as any)) : {} as any;
60
+ return {
61
+ ...q,
62
+ meta: {
63
+ ...existingMeta,
64
+ completed: p.completed,
65
+ userAnswer: p.userAnswer,
66
+ completedAt: p.completedAt,
67
+ },
68
+ } as typeof q;
69
+ }),
70
+ }));
71
+
72
+ return merged as any;
34
73
  }),
35
74
 
36
75
  // Create a worksheet set
37
- createWorksheet: authedProcedure
38
- .input(z.object({ workspaceId: z.string(), title: z.string().min(1).max(120) }))
76
+ create: authedProcedure
77
+ .input(z.object({
78
+ workspaceId: z.string(),
79
+ title: z.string().min(1).max(120),
80
+ description: z.string().optional(),
81
+ difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
82
+ estimatedTime: z.string().optional(),
83
+ problems: z.array(z.object({
84
+ question: z.string().min(1),
85
+ answer: z.string().min(1),
86
+ type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).default('TEXT'),
87
+ options: z.array(z.string()).optional(),
88
+ })).optional(),
89
+ }))
39
90
  .mutation(async ({ ctx, input }) => {
40
91
  const workspace = await ctx.db.workspace.findFirst({
41
92
  where: { id: input.workspaceId, ownerId: ctx.session.user.id },
42
93
  });
43
94
  if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
95
+
96
+ const { problems, ...worksheetData } = input;
97
+
44
98
  return ctx.db.artifact.create({
45
99
  data: {
46
100
  workspaceId: input.workspaceId,
47
101
  type: ArtifactType.WORKSHEET,
48
102
  title: input.title,
103
+ difficulty: input.difficulty as any,
104
+ estimatedTime: input.estimatedTime,
49
105
  createdById: ctx.session.user.id,
106
+ questions: problems ? {
107
+ create: problems.map((problem, index) => ({
108
+ prompt: problem.question,
109
+ answer: problem.answer,
110
+ type: problem.type as any,
111
+ order: index,
112
+ meta: problem.options ? { options: problem.options } : undefined,
113
+ })),
114
+ } : undefined,
115
+ },
116
+ include: {
117
+ questions: true,
50
118
  },
51
119
  });
52
120
  }),
@@ -62,9 +130,37 @@ export const worksheets = router({
62
130
  workspace: { ownerId: ctx.session.user.id },
63
131
  },
64
132
  include: { questions: true },
133
+ orderBy: { updatedAt: 'desc' },
65
134
  });
66
135
  if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
67
- return worksheet;
136
+
137
+ // Merge per-user progress into question.meta for compatibility with UI
138
+ const questionIds = worksheet.questions.map(q => q.id);
139
+ if (questionIds.length === 0) return worksheet;
140
+ const progress = await ctx.db.worksheetQuestionProgress.findMany({
141
+ where: { userId: ctx.session.user.id, worksheetQuestionId: { in: questionIds } },
142
+ });
143
+ const progressByQuestionId = new Map(progress.map(p => [p.worksheetQuestionId, p]));
144
+
145
+ const merged = {
146
+ ...worksheet,
147
+ questions: worksheet.questions.map(q => {
148
+ const p = progressByQuestionId.get(q.id);
149
+ if (!p) return q;
150
+ const existingMeta = q.meta ? (typeof q.meta === 'object' ? q.meta : JSON.parse(q.meta as any)) : {} as any;
151
+ return {
152
+ ...q,
153
+ meta: {
154
+ ...existingMeta,
155
+ completed: p.completed,
156
+ userAnswer: p.userAnswer,
157
+ completedAt: p.completedAt,
158
+ },
159
+ } as typeof q;
160
+ }),
161
+ };
162
+
163
+ return merged as any;
68
164
  }),
69
165
 
70
166
  // Add a question to a worksheet
@@ -73,6 +169,7 @@ export const worksheets = router({
73
169
  worksheetId: z.string(),
74
170
  prompt: z.string().min(1),
75
171
  answer: z.string().optional(),
172
+ type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).optional(),
76
173
  difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
77
174
  order: z.number().int().optional(),
78
175
  meta: z.record(z.string(), z.unknown()).optional(),
@@ -87,6 +184,7 @@ export const worksheets = router({
87
184
  artifactId: input.worksheetId,
88
185
  prompt: input.prompt,
89
186
  answer: input.answer,
187
+ type: (input.type ?? QuestionType.TEXT) as any,
90
188
  difficulty: (input.difficulty ?? Difficulty.MEDIUM) as any,
91
189
  order: input.order ?? 0,
92
190
  meta: input.meta as any,
@@ -100,6 +198,7 @@ export const worksheets = router({
100
198
  worksheetQuestionId: z.string(),
101
199
  prompt: z.string().optional(),
102
200
  answer: z.string().optional(),
201
+ type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).optional(),
103
202
  difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
104
203
  order: z.number().int().optional(),
105
204
  meta: z.record(z.string(), z.unknown()).optional(),
@@ -114,6 +213,7 @@ export const worksheets = router({
114
213
  data: {
115
214
  prompt: input.prompt ?? q.prompt,
116
215
  answer: input.answer ?? q.answer,
216
+ type: (input.type ?? q.type) as any,
117
217
  difficulty: (input.difficulty ?? q.difficulty) as any,
118
218
  order: input.order ?? q.order,
119
219
  meta: (input.meta ?? q.meta) as any,
@@ -133,16 +233,245 @@ export const worksheets = router({
133
233
  return true;
134
234
  }),
135
235
 
236
+ // Update problem completion status
237
+ updateProblemStatus: authedProcedure
238
+ .input(z.object({
239
+ problemId: z.string(),
240
+ completed: z.boolean(),
241
+ answer: z.string().optional(),
242
+ }))
243
+ .mutation(async ({ ctx, input }) => {
244
+ // Verify question ownership through worksheet
245
+ const question = await ctx.db.worksheetQuestion.findFirst({
246
+ where: {
247
+ id: input.problemId,
248
+ artifact: {
249
+ type: ArtifactType.WORKSHEET,
250
+ workspace: { ownerId: ctx.session.user.id },
251
+ },
252
+ },
253
+ });
254
+ if (!question) throw new TRPCError({ code: 'NOT_FOUND' });
255
+
256
+ // Upsert per-user progress row
257
+ const progress = await ctx.db.worksheetQuestionProgress.upsert({
258
+ where: {
259
+ worksheetQuestionId_userId: {
260
+ worksheetQuestionId: input.problemId,
261
+ userId: ctx.session.user.id,
262
+ },
263
+ },
264
+ create: {
265
+ worksheetQuestionId: input.problemId,
266
+ userId: ctx.session.user.id,
267
+ completed: input.completed,
268
+ userAnswer: input.answer,
269
+ completedAt: input.completed ? new Date() : null,
270
+ attempts: 1,
271
+ },
272
+ update: {
273
+ completed: input.completed,
274
+ userAnswer: input.answer,
275
+ completedAt: input.completed ? new Date() : null,
276
+ attempts: { increment: 1 },
277
+ },
278
+ });
279
+
280
+ return progress;
281
+ }),
282
+
283
+ // Get current user's progress for all questions in a worksheet
284
+ getProgress: authedProcedure
285
+ .input(z.object({ worksheetId: z.string() }))
286
+ .query(async ({ ctx, input }) => {
287
+ // Verify worksheet ownership
288
+ const worksheet = await ctx.db.artifact.findFirst({
289
+ where: {
290
+ id: input.worksheetId,
291
+ type: ArtifactType.WORKSHEET,
292
+ workspace: { ownerId: ctx.session.user.id },
293
+ },
294
+ });
295
+ if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
296
+
297
+ const questions = await ctx.db.worksheetQuestion.findMany({
298
+ where: { artifactId: input.worksheetId },
299
+ select: { id: true },
300
+ });
301
+ const questionIds = questions.map(q => q.id);
302
+
303
+ const progress = await ctx.db.worksheetQuestionProgress.findMany({
304
+ where: {
305
+ userId: ctx.session.user.id,
306
+ worksheetQuestionId: { in: questionIds },
307
+ },
308
+ });
309
+
310
+ return progress;
311
+ }),
312
+
313
+ // Update a worksheet
314
+ update: authedProcedure
315
+ .input(z.object({
316
+ id: z.string(),
317
+ title: z.string().min(1).max(120).optional(),
318
+ description: z.string().optional(),
319
+ difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
320
+ estimatedTime: z.string().optional(),
321
+ problems: z.array(z.object({
322
+ id: z.string().optional(),
323
+ question: z.string().min(1),
324
+ answer: z.string().min(1),
325
+ type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).default('TEXT'),
326
+ options: z.array(z.string()).optional(),
327
+ })).optional(),
328
+ }))
329
+ .mutation(async ({ ctx, input }) => {
330
+ const { id, problems, ...updateData } = input;
331
+
332
+ // Verify worksheet ownership
333
+ const existingWorksheet = await ctx.db.artifact.findFirst({
334
+ where: {
335
+ id,
336
+ type: ArtifactType.WORKSHEET,
337
+ workspace: { ownerId: ctx.session.user.id },
338
+ },
339
+ });
340
+ if (!existingWorksheet) throw new TRPCError({ code: 'NOT_FOUND' });
341
+
342
+ // Handle questions update if provided
343
+ if (problems) {
344
+ // Delete existing questions and create new ones
345
+ await ctx.db.worksheetQuestion.deleteMany({
346
+ where: { artifactId: id },
347
+ });
348
+
349
+ await ctx.db.worksheetQuestion.createMany({
350
+ data: problems.map((problem, index) => ({
351
+ artifactId: id,
352
+ prompt: problem.question,
353
+ answer: problem.answer,
354
+ type: problem.type as any,
355
+ order: index,
356
+ meta: problem.options ? { options: problem.options } : undefined,
357
+ })),
358
+ });
359
+ }
360
+
361
+ // Process update data
362
+ const processedUpdateData = {
363
+ ...updateData,
364
+ difficulty: updateData.difficulty as any,
365
+ };
366
+
367
+ return ctx.db.artifact.update({
368
+ where: { id },
369
+ data: processedUpdateData,
370
+ include: {
371
+ questions: {
372
+ orderBy: { order: 'asc' },
373
+ },
374
+ },
375
+ });
376
+ }),
377
+
136
378
  // Delete a worksheet set and its questions
137
- deleteWorksheet: authedProcedure
138
- .input(z.object({ worksheetId: z.string() }))
379
+ delete: authedProcedure
380
+ .input(z.object({ id: z.string() }))
139
381
  .mutation(async ({ ctx, input }) => {
140
382
  const deleted = await ctx.db.artifact.deleteMany({
141
- where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
383
+ where: { id: input.id, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
142
384
  });
143
385
  if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
144
386
  return true;
145
387
  }),
388
+
389
+ // Generate a worksheet from a user prompt
390
+ generateFromPrompt: authedProcedure
391
+ .input(z.object({
392
+ workspaceId: z.string(),
393
+ prompt: z.string().min(1),
394
+ numQuestions: z.number().int().min(1).max(20).default(8),
395
+ difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
396
+ title: z.string().optional(),
397
+ estimatedTime: z.string().optional(),
398
+ }))
399
+ .mutation(async ({ ctx, input }) => {
400
+ const workspace = await ctx.db.workspace.findFirst({ where: { id: input.workspaceId, ownerId: ctx.session.user.id } });
401
+ if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
402
+
403
+ await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_load_start', { source: 'prompt' });
404
+
405
+ const session = await aiSessionService.initSession(input.workspaceId);
406
+ await aiSessionService.setInstruction(session.id, `Create a worksheet with ${input.numQuestions} questions at ${input.difficulty} difficulty from this prompt. Return JSON.\n\nPrompt:\n${input.prompt}`);
407
+ await aiSessionService.startLLMSession(session.id);
408
+
409
+ const content = await aiSessionService.generateWorksheetQuestions(session.id, input.numQuestions, input.difficulty);
410
+ await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: content.length });
411
+
412
+ const artifact = await ctx.db.artifact.create({
413
+ data: {
414
+ workspaceId: input.workspaceId,
415
+ type: ArtifactType.WORKSHEET,
416
+ title: input.title || `Worksheet - ${new Date().toLocaleString()}`,
417
+ createdById: ctx.session.user.id,
418
+ difficulty: (input.difficulty.toUpperCase()) as any,
419
+ estimatedTime: input.estimatedTime,
420
+ },
421
+ });
422
+
423
+ try {
424
+ const worksheetData = JSON.parse(content);
425
+ let actualWorksheetData = worksheetData;
426
+ if (worksheetData.last_response) {
427
+ try { actualWorksheetData = JSON.parse(worksheetData.last_response); } catch {}
428
+ }
429
+ const problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
430
+ for (let i = 0; i < Math.min(problems.length, input.numQuestions); i++) {
431
+ const problem = problems[i];
432
+ const prompt = problem.question || problem.prompt || `Question ${i + 1}`;
433
+ const answer = problem.answer || problem.solution || `Answer ${i + 1}`;
434
+ const type = problem.type || 'TEXT';
435
+ const options = problem.options || [];
436
+ await ctx.db.worksheetQuestion.create({
437
+ data: {
438
+ artifactId: artifact.id,
439
+ prompt,
440
+ answer,
441
+ difficulty: (input.difficulty.toUpperCase()) as any,
442
+ order: i,
443
+ meta: {
444
+ type,
445
+ options: options.length > 0 ? options : undefined,
446
+ },
447
+ },
448
+ });
449
+ }
450
+ } catch {
451
+ const lines = content.split('\n').filter(line => line.trim());
452
+ for (let i = 0; i < Math.min(lines.length, input.numQuestions); i++) {
453
+ const line = lines[i];
454
+ if (line.includes(' - ')) {
455
+ const [q, a] = line.split(' - ');
456
+ await ctx.db.worksheetQuestion.create({
457
+ data: {
458
+ artifactId: artifact.id,
459
+ prompt: q.trim(),
460
+ answer: a.trim(),
461
+ difficulty: (input.difficulty.toUpperCase()) as any,
462
+ order: i,
463
+ meta: { type: 'TEXT' },
464
+ },
465
+ });
466
+ }
467
+ }
468
+ }
469
+
470
+ await PusherService.emitWorksheetComplete(input.workspaceId, artifact);
471
+ aiSessionService.deleteSession(session.id);
472
+
473
+ return { artifact };
474
+ }),
146
475
  });
147
476
 
148
477