@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.
Files changed (74) 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/context.d.ts +1 -1
  9. package/dist/lib/ai-session.d.ts +26 -0
  10. package/dist/lib/ai-session.js +343 -0
  11. package/dist/lib/auth.js +10 -6
  12. package/dist/lib/inference.d.ts +2 -0
  13. package/dist/lib/inference.js +21 -0
  14. package/dist/lib/pusher.d.ts +14 -0
  15. package/dist/lib/pusher.js +94 -0
  16. package/dist/lib/storage.d.ts +10 -2
  17. package/dist/lib/storage.js +63 -6
  18. package/dist/routers/_app.d.ts +878 -100
  19. package/dist/routers/_app.js +8 -2
  20. package/dist/routers/ai-session.d.ts +0 -0
  21. package/dist/routers/ai-session.js +1 -0
  22. package/dist/routers/auth.d.ts +13 -11
  23. package/dist/routers/auth.js +50 -21
  24. package/dist/routers/chat.d.ts +171 -0
  25. package/dist/routers/chat.js +270 -0
  26. package/dist/routers/flashcards.d.ts +51 -39
  27. package/dist/routers/flashcards.js +143 -31
  28. package/dist/routers/meetingsummary.d.ts +0 -0
  29. package/dist/routers/meetingsummary.js +377 -0
  30. package/dist/routers/podcast.d.ts +277 -0
  31. package/dist/routers/podcast.js +847 -0
  32. package/dist/routers/studyguide.d.ts +54 -0
  33. package/dist/routers/studyguide.js +125 -0
  34. package/dist/routers/worksheets.d.ts +147 -40
  35. package/dist/routers/worksheets.js +348 -33
  36. package/dist/routers/workspace.d.ts +163 -8
  37. package/dist/routers/workspace.js +453 -8
  38. package/dist/server.d.ts +1 -1
  39. package/dist/server.js +7 -2
  40. package/dist/trpc.d.ts +5 -5
  41. package/package.json +11 -3
  42. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
  43. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
  44. package/prisma/migrations/migration_lock.toml +3 -0
  45. package/prisma/schema.prisma +87 -6
  46. package/prisma/seed.mjs +135 -0
  47. package/src/lib/ai-session.ts +411 -0
  48. package/src/lib/auth.ts +1 -1
  49. package/src/lib/inference.ts +21 -0
  50. package/src/lib/pusher.ts +104 -0
  51. package/src/lib/storage.ts +89 -6
  52. package/src/routers/_app.ts +6 -0
  53. package/src/routers/auth.ts +8 -4
  54. package/src/routers/chat.ts +275 -0
  55. package/src/routers/flashcards.ts +151 -33
  56. package/src/routers/meetingsummary.ts +416 -0
  57. package/src/routers/podcast.ts +934 -0
  58. package/src/routers/studyguide.ts +144 -0
  59. package/src/routers/worksheets.ts +346 -18
  60. package/src/routers/workspace.ts +500 -8
  61. package/src/server.ts +7 -2
  62. package/test-ai-integration.js +134 -0
  63. package/dist/context.d.ts.map +0 -1
  64. package/dist/index.d.ts.map +0 -1
  65. package/dist/lib/auth.d.ts.map +0 -1
  66. package/dist/lib/file.d.ts.map +0 -1
  67. package/dist/lib/prisma.d.ts.map +0 -1
  68. package/dist/lib/storage.d.ts.map +0 -1
  69. package/dist/routers/_app.d.ts.map +0 -1
  70. package/dist/routers/auth.d.ts.map +0 -1
  71. package/dist/routers/sample.js +0 -21
  72. package/dist/routers/workspace.d.ts.map +0 -1
  73. package/dist/server.d.ts.map +0 -1
  74. package/dist/trpc.d.ts.map +0 -1
@@ -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,48 +15,113 @@ 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
- listSets: authedProcedure
19
- .input(z.object({ workspaceId: z.string().uuid() }))
29
+ list: authedProcedure
30
+ .input(z.object({ workspaceId: z.string() }))
20
31
  .query(async ({ ctx, input }) => {
21
- const workspace = await ctx.db.workspace.findFirst({
22
- where: { id: input.workspaceId, ownerId: ctx.session.user.id },
23
- });
24
- if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
25
- return ctx.db.artifact.findMany({
32
+ const worksheets = await ctx.db.artifact.findMany({
26
33
  where: { workspaceId: input.workspaceId, type: ArtifactType.WORKSHEET },
27
34
  include: {
28
35
  versions: {
29
36
  orderBy: { version: 'desc' },
30
37
  take: 1, // Get only the latest version
31
38
  },
39
+ questions: true,
32
40
  },
33
41
  orderBy: { updatedAt: 'desc' },
34
42
  });
43
+ if (!worksheets) throw new TRPCError({ code: 'NOT_FOUND' });
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;
35
73
  }),
36
74
 
37
75
  // Create a worksheet set
38
- createWorksheet: authedProcedure
39
- .input(z.object({ workspaceId: z.string().uuid(), 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
+ }))
40
90
  .mutation(async ({ ctx, input }) => {
41
91
  const workspace = await ctx.db.workspace.findFirst({
42
92
  where: { id: input.workspaceId, ownerId: ctx.session.user.id },
43
93
  });
44
94
  if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
95
+
96
+ const { problems, ...worksheetData } = input;
97
+
45
98
  return ctx.db.artifact.create({
46
99
  data: {
47
100
  workspaceId: input.workspaceId,
48
101
  type: ArtifactType.WORKSHEET,
49
102
  title: input.title,
103
+ difficulty: input.difficulty as any,
104
+ estimatedTime: input.estimatedTime,
50
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,
51
118
  },
52
119
  });
53
120
  }),
54
121
 
55
122
  // Get a worksheet with its questions
56
- getWorksheet: authedProcedure
57
- .input(z.object({ worksheetId: z.string().uuid() }))
123
+ get: authedProcedure
124
+ .input(z.object({ worksheetId: z.string() }))
58
125
  .query(async ({ ctx, input }) => {
59
126
  const worksheet = await ctx.db.artifact.findFirst({
60
127
  where: {
@@ -63,17 +130,46 @@ export const worksheets = router({
63
130
  workspace: { ownerId: ctx.session.user.id },
64
131
  },
65
132
  include: { questions: true },
133
+ orderBy: { updatedAt: 'desc' },
66
134
  });
67
135
  if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
68
- 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;
69
164
  }),
70
165
 
71
166
  // Add a question to a worksheet
72
167
  createWorksheetQuestion: authedProcedure
73
168
  .input(z.object({
74
- worksheetId: z.string().uuid(),
169
+ worksheetId: z.string(),
75
170
  prompt: z.string().min(1),
76
171
  answer: z.string().optional(),
172
+ type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).optional(),
77
173
  difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
78
174
  order: z.number().int().optional(),
79
175
  meta: z.record(z.string(), z.unknown()).optional(),
@@ -88,6 +184,7 @@ export const worksheets = router({
88
184
  artifactId: input.worksheetId,
89
185
  prompt: input.prompt,
90
186
  answer: input.answer,
187
+ type: (input.type ?? QuestionType.TEXT) as any,
91
188
  difficulty: (input.difficulty ?? Difficulty.MEDIUM) as any,
92
189
  order: input.order ?? 0,
93
190
  meta: input.meta as any,
@@ -98,9 +195,10 @@ export const worksheets = router({
98
195
  // Update a question
99
196
  updateWorksheetQuestion: authedProcedure
100
197
  .input(z.object({
101
- worksheetQuestionId: z.string().uuid(),
198
+ worksheetQuestionId: z.string(),
102
199
  prompt: z.string().optional(),
103
200
  answer: z.string().optional(),
201
+ type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).optional(),
104
202
  difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
105
203
  order: z.number().int().optional(),
106
204
  meta: z.record(z.string(), z.unknown()).optional(),
@@ -115,6 +213,7 @@ export const worksheets = router({
115
213
  data: {
116
214
  prompt: input.prompt ?? q.prompt,
117
215
  answer: input.answer ?? q.answer,
216
+ type: (input.type ?? q.type) as any,
118
217
  difficulty: (input.difficulty ?? q.difficulty) as any,
119
218
  order: input.order ?? q.order,
120
219
  meta: (input.meta ?? q.meta) as any,
@@ -124,7 +223,7 @@ export const worksheets = router({
124
223
 
125
224
  // Delete a question
126
225
  deleteWorksheetQuestion: authedProcedure
127
- .input(z.object({ worksheetQuestionId: z.string().uuid() }))
226
+ .input(z.object({ worksheetQuestionId: z.string() }))
128
227
  .mutation(async ({ ctx, input }) => {
129
228
  const q = await ctx.db.worksheetQuestion.findFirst({
130
229
  where: { id: input.worksheetQuestionId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
@@ -134,16 +233,245 @@ export const worksheets = router({
134
233
  return true;
135
234
  }),
136
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
+
137
378
  // Delete a worksheet set and its questions
138
- deleteWorksheet: authedProcedure
139
- .input(z.object({ worksheetId: z.string().uuid() }))
379
+ delete: authedProcedure
380
+ .input(z.object({ id: z.string() }))
140
381
  .mutation(async ({ ctx, input }) => {
141
382
  const deleted = await ctx.db.artifact.deleteMany({
142
- 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 } },
143
384
  });
144
385
  if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
145
386
  return true;
146
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
+ }),
147
475
  });
148
476
 
149
477