@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
|
@@ -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
|
// Avoid importing Prisma enums directly; mirror values as string literals
|
|
5
7
|
const ArtifactType = {
|
|
6
8
|
WORKSHEET: 'WORKSHEET',
|
|
@@ -10,76 +12,171 @@ const Difficulty = {
|
|
|
10
12
|
MEDIUM: 'MEDIUM',
|
|
11
13
|
HARD: 'HARD',
|
|
12
14
|
};
|
|
15
|
+
const QuestionType = {
|
|
16
|
+
MULTIPLE_CHOICE: 'MULTIPLE_CHOICE',
|
|
17
|
+
TEXT: 'TEXT',
|
|
18
|
+
NUMERIC: 'NUMERIC',
|
|
19
|
+
TRUE_FALSE: 'TRUE_FALSE',
|
|
20
|
+
MATCHING: 'MATCHING',
|
|
21
|
+
FILL_IN_THE_BLANK: 'FILL_IN_THE_BLANK',
|
|
22
|
+
};
|
|
13
23
|
export const worksheets = router({
|
|
14
24
|
// List all worksheet artifacts for a workspace
|
|
15
|
-
|
|
16
|
-
.input(z.object({ workspaceId: z.string()
|
|
25
|
+
list: authedProcedure
|
|
26
|
+
.input(z.object({ workspaceId: z.string() }))
|
|
17
27
|
.query(async ({ ctx, input }) => {
|
|
18
|
-
const
|
|
19
|
-
where: { id: input.workspaceId, ownerId: ctx.session.user.id },
|
|
20
|
-
});
|
|
21
|
-
if (!workspace)
|
|
22
|
-
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
23
|
-
return ctx.db.artifact.findMany({
|
|
28
|
+
const worksheets = await ctx.db.artifact.findMany({
|
|
24
29
|
where: { workspaceId: input.workspaceId, type: ArtifactType.WORKSHEET },
|
|
30
|
+
include: {
|
|
31
|
+
versions: {
|
|
32
|
+
orderBy: { version: 'desc' },
|
|
33
|
+
take: 1, // Get only the latest version
|
|
34
|
+
},
|
|
35
|
+
questions: true,
|
|
36
|
+
},
|
|
25
37
|
orderBy: { updatedAt: 'desc' },
|
|
26
38
|
});
|
|
39
|
+
if (!worksheets)
|
|
40
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
41
|
+
// Merge per-user progress into question.meta for compatibility with UI
|
|
42
|
+
const allQuestionIds = worksheets.flatMap(w => w.questions.map(q => q.id));
|
|
43
|
+
if (allQuestionIds.length === 0)
|
|
44
|
+
return worksheets;
|
|
45
|
+
const progress = await ctx.db.worksheetQuestionProgress.findMany({
|
|
46
|
+
where: { userId: ctx.session.user.id, worksheetQuestionId: { in: allQuestionIds } },
|
|
47
|
+
});
|
|
48
|
+
const progressByQuestionId = new Map(progress.map(p => [p.worksheetQuestionId, p]));
|
|
49
|
+
const merged = worksheets.map(w => ({
|
|
50
|
+
...w,
|
|
51
|
+
questions: w.questions.map(q => {
|
|
52
|
+
const p = progressByQuestionId.get(q.id);
|
|
53
|
+
if (!p)
|
|
54
|
+
return q;
|
|
55
|
+
const existingMeta = q.meta ? (typeof q.meta === 'object' ? q.meta : JSON.parse(q.meta)) : {};
|
|
56
|
+
return {
|
|
57
|
+
...q,
|
|
58
|
+
meta: {
|
|
59
|
+
...existingMeta,
|
|
60
|
+
completed: p.completed,
|
|
61
|
+
userAnswer: p.userAnswer,
|
|
62
|
+
completedAt: p.completedAt,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}),
|
|
66
|
+
}));
|
|
67
|
+
return merged;
|
|
27
68
|
}),
|
|
28
69
|
// Create a worksheet set
|
|
29
|
-
|
|
30
|
-
.input(z.object({
|
|
70
|
+
create: authedProcedure
|
|
71
|
+
.input(z.object({
|
|
72
|
+
workspaceId: z.string(),
|
|
73
|
+
title: z.string().min(1).max(120),
|
|
74
|
+
description: z.string().optional(),
|
|
75
|
+
difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
|
|
76
|
+
estimatedTime: z.string().optional(),
|
|
77
|
+
problems: z.array(z.object({
|
|
78
|
+
question: z.string().min(1),
|
|
79
|
+
answer: z.string().min(1),
|
|
80
|
+
type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).default('TEXT'),
|
|
81
|
+
options: z.array(z.string()).optional(),
|
|
82
|
+
})).optional(),
|
|
83
|
+
}))
|
|
31
84
|
.mutation(async ({ ctx, input }) => {
|
|
32
85
|
const workspace = await ctx.db.workspace.findFirst({
|
|
33
86
|
where: { id: input.workspaceId, ownerId: ctx.session.user.id },
|
|
34
87
|
});
|
|
35
88
|
if (!workspace)
|
|
36
89
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
90
|
+
const { problems, ...worksheetData } = input;
|
|
37
91
|
return ctx.db.artifact.create({
|
|
38
92
|
data: {
|
|
39
93
|
workspaceId: input.workspaceId,
|
|
40
94
|
type: ArtifactType.WORKSHEET,
|
|
41
95
|
title: input.title,
|
|
96
|
+
difficulty: input.difficulty,
|
|
97
|
+
estimatedTime: input.estimatedTime,
|
|
42
98
|
createdById: ctx.session.user.id,
|
|
99
|
+
questions: problems ? {
|
|
100
|
+
create: problems.map((problem, index) => ({
|
|
101
|
+
prompt: problem.question,
|
|
102
|
+
answer: problem.answer,
|
|
103
|
+
type: problem.type,
|
|
104
|
+
order: index,
|
|
105
|
+
meta: problem.options ? { options: problem.options } : undefined,
|
|
106
|
+
})),
|
|
107
|
+
} : undefined,
|
|
108
|
+
},
|
|
109
|
+
include: {
|
|
110
|
+
questions: true,
|
|
43
111
|
},
|
|
44
112
|
});
|
|
45
113
|
}),
|
|
46
114
|
// Get a worksheet with its questions
|
|
47
|
-
|
|
48
|
-
.input(z.object({
|
|
115
|
+
get: authedProcedure
|
|
116
|
+
.input(z.object({ worksheetId: z.string() }))
|
|
49
117
|
.query(async ({ ctx, input }) => {
|
|
50
|
-
const
|
|
118
|
+
const worksheet = await ctx.db.artifact.findFirst({
|
|
51
119
|
where: {
|
|
52
|
-
id: input.
|
|
120
|
+
id: input.worksheetId,
|
|
53
121
|
type: ArtifactType.WORKSHEET,
|
|
54
122
|
workspace: { ownerId: ctx.session.user.id },
|
|
55
123
|
},
|
|
56
124
|
include: { questions: true },
|
|
125
|
+
orderBy: { updatedAt: 'desc' },
|
|
57
126
|
});
|
|
58
|
-
if (!
|
|
127
|
+
if (!worksheet)
|
|
59
128
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
60
|
-
|
|
129
|
+
// Merge per-user progress into question.meta for compatibility with UI
|
|
130
|
+
const questionIds = worksheet.questions.map(q => q.id);
|
|
131
|
+
if (questionIds.length === 0)
|
|
132
|
+
return worksheet;
|
|
133
|
+
const progress = await ctx.db.worksheetQuestionProgress.findMany({
|
|
134
|
+
where: { userId: ctx.session.user.id, worksheetQuestionId: { in: questionIds } },
|
|
135
|
+
});
|
|
136
|
+
const progressByQuestionId = new Map(progress.map(p => [p.worksheetQuestionId, p]));
|
|
137
|
+
const merged = {
|
|
138
|
+
...worksheet,
|
|
139
|
+
questions: worksheet.questions.map(q => {
|
|
140
|
+
const p = progressByQuestionId.get(q.id);
|
|
141
|
+
if (!p)
|
|
142
|
+
return q;
|
|
143
|
+
const existingMeta = q.meta ? (typeof q.meta === 'object' ? q.meta : JSON.parse(q.meta)) : {};
|
|
144
|
+
return {
|
|
145
|
+
...q,
|
|
146
|
+
meta: {
|
|
147
|
+
...existingMeta,
|
|
148
|
+
completed: p.completed,
|
|
149
|
+
userAnswer: p.userAnswer,
|
|
150
|
+
completedAt: p.completedAt,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
return merged;
|
|
61
156
|
}),
|
|
62
157
|
// Add a question to a worksheet
|
|
63
|
-
|
|
158
|
+
createWorksheetQuestion: authedProcedure
|
|
64
159
|
.input(z.object({
|
|
65
|
-
|
|
160
|
+
worksheetId: z.string(),
|
|
66
161
|
prompt: z.string().min(1),
|
|
67
162
|
answer: z.string().optional(),
|
|
163
|
+
type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).optional(),
|
|
68
164
|
difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
|
|
69
165
|
order: z.number().int().optional(),
|
|
70
166
|
meta: z.record(z.string(), z.unknown()).optional(),
|
|
71
167
|
}))
|
|
72
168
|
.mutation(async ({ ctx, input }) => {
|
|
73
|
-
const
|
|
74
|
-
where: { id: input.
|
|
169
|
+
const worksheet = await ctx.db.artifact.findFirst({
|
|
170
|
+
where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
|
|
75
171
|
});
|
|
76
|
-
if (!
|
|
172
|
+
if (!worksheet)
|
|
77
173
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
78
174
|
return ctx.db.worksheetQuestion.create({
|
|
79
175
|
data: {
|
|
80
|
-
artifactId: input.
|
|
176
|
+
artifactId: input.worksheetId,
|
|
81
177
|
prompt: input.prompt,
|
|
82
178
|
answer: input.answer,
|
|
179
|
+
type: (input.type ?? QuestionType.TEXT),
|
|
83
180
|
difficulty: (input.difficulty ?? Difficulty.MEDIUM),
|
|
84
181
|
order: input.order ?? 0,
|
|
85
182
|
meta: input.meta,
|
|
@@ -87,26 +184,28 @@ export const worksheets = router({
|
|
|
87
184
|
});
|
|
88
185
|
}),
|
|
89
186
|
// Update a question
|
|
90
|
-
|
|
187
|
+
updateWorksheetQuestion: authedProcedure
|
|
91
188
|
.input(z.object({
|
|
92
|
-
|
|
189
|
+
worksheetQuestionId: z.string(),
|
|
93
190
|
prompt: z.string().optional(),
|
|
94
191
|
answer: z.string().optional(),
|
|
192
|
+
type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).optional(),
|
|
95
193
|
difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
|
|
96
194
|
order: z.number().int().optional(),
|
|
97
195
|
meta: z.record(z.string(), z.unknown()).optional(),
|
|
98
196
|
}))
|
|
99
197
|
.mutation(async ({ ctx, input }) => {
|
|
100
198
|
const q = await ctx.db.worksheetQuestion.findFirst({
|
|
101
|
-
where: { id: input.
|
|
199
|
+
where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } } },
|
|
102
200
|
});
|
|
103
201
|
if (!q)
|
|
104
202
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
105
203
|
return ctx.db.worksheetQuestion.update({
|
|
106
|
-
where: { id: input.
|
|
204
|
+
where: { id: input.worksheetQuestionId },
|
|
107
205
|
data: {
|
|
108
206
|
prompt: input.prompt ?? q.prompt,
|
|
109
207
|
answer: input.answer ?? q.answer,
|
|
208
|
+
type: (input.type ?? q.type),
|
|
110
209
|
difficulty: (input.difficulty ?? q.difficulty),
|
|
111
210
|
order: input.order ?? q.order,
|
|
112
211
|
meta: (input.meta ?? q.meta),
|
|
@@ -114,26 +213,242 @@ export const worksheets = router({
|
|
|
114
213
|
});
|
|
115
214
|
}),
|
|
116
215
|
// Delete a question
|
|
117
|
-
|
|
118
|
-
.input(z.object({
|
|
216
|
+
deleteWorksheetQuestion: authedProcedure
|
|
217
|
+
.input(z.object({ worksheetQuestionId: z.string() }))
|
|
119
218
|
.mutation(async ({ ctx, input }) => {
|
|
120
219
|
const q = await ctx.db.worksheetQuestion.findFirst({
|
|
121
|
-
where: { id: input.
|
|
220
|
+
where: { id: input.worksheetQuestionId, artifact: { workspace: { ownerId: ctx.session.user.id } } },
|
|
122
221
|
});
|
|
123
222
|
if (!q)
|
|
124
223
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
125
|
-
await ctx.db.worksheetQuestion.delete({ where: { id: input.
|
|
224
|
+
await ctx.db.worksheetQuestion.delete({ where: { id: input.worksheetQuestionId } });
|
|
126
225
|
return true;
|
|
127
226
|
}),
|
|
227
|
+
// Update problem completion status
|
|
228
|
+
updateProblemStatus: authedProcedure
|
|
229
|
+
.input(z.object({
|
|
230
|
+
problemId: z.string(),
|
|
231
|
+
completed: z.boolean(),
|
|
232
|
+
answer: z.string().optional(),
|
|
233
|
+
}))
|
|
234
|
+
.mutation(async ({ ctx, input }) => {
|
|
235
|
+
// Verify question ownership through worksheet
|
|
236
|
+
const question = await ctx.db.worksheetQuestion.findFirst({
|
|
237
|
+
where: {
|
|
238
|
+
id: input.problemId,
|
|
239
|
+
artifact: {
|
|
240
|
+
type: ArtifactType.WORKSHEET,
|
|
241
|
+
workspace: { ownerId: ctx.session.user.id },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
if (!question)
|
|
246
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
247
|
+
// Upsert per-user progress row
|
|
248
|
+
const progress = await ctx.db.worksheetQuestionProgress.upsert({
|
|
249
|
+
where: {
|
|
250
|
+
worksheetQuestionId_userId: {
|
|
251
|
+
worksheetQuestionId: input.problemId,
|
|
252
|
+
userId: ctx.session.user.id,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
create: {
|
|
256
|
+
worksheetQuestionId: input.problemId,
|
|
257
|
+
userId: ctx.session.user.id,
|
|
258
|
+
completed: input.completed,
|
|
259
|
+
userAnswer: input.answer,
|
|
260
|
+
completedAt: input.completed ? new Date() : null,
|
|
261
|
+
attempts: 1,
|
|
262
|
+
},
|
|
263
|
+
update: {
|
|
264
|
+
completed: input.completed,
|
|
265
|
+
userAnswer: input.answer,
|
|
266
|
+
completedAt: input.completed ? new Date() : null,
|
|
267
|
+
attempts: { increment: 1 },
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
return progress;
|
|
271
|
+
}),
|
|
272
|
+
// Get current user's progress for all questions in a worksheet
|
|
273
|
+
getProgress: authedProcedure
|
|
274
|
+
.input(z.object({ worksheetId: z.string() }))
|
|
275
|
+
.query(async ({ ctx, input }) => {
|
|
276
|
+
// Verify worksheet ownership
|
|
277
|
+
const worksheet = await ctx.db.artifact.findFirst({
|
|
278
|
+
where: {
|
|
279
|
+
id: input.worksheetId,
|
|
280
|
+
type: ArtifactType.WORKSHEET,
|
|
281
|
+
workspace: { ownerId: ctx.session.user.id },
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
if (!worksheet)
|
|
285
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
286
|
+
const questions = await ctx.db.worksheetQuestion.findMany({
|
|
287
|
+
where: { artifactId: input.worksheetId },
|
|
288
|
+
select: { id: true },
|
|
289
|
+
});
|
|
290
|
+
const questionIds = questions.map(q => q.id);
|
|
291
|
+
const progress = await ctx.db.worksheetQuestionProgress.findMany({
|
|
292
|
+
where: {
|
|
293
|
+
userId: ctx.session.user.id,
|
|
294
|
+
worksheetQuestionId: { in: questionIds },
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
return progress;
|
|
298
|
+
}),
|
|
299
|
+
// Update a worksheet
|
|
300
|
+
update: authedProcedure
|
|
301
|
+
.input(z.object({
|
|
302
|
+
id: z.string(),
|
|
303
|
+
title: z.string().min(1).max(120).optional(),
|
|
304
|
+
description: z.string().optional(),
|
|
305
|
+
difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
|
|
306
|
+
estimatedTime: z.string().optional(),
|
|
307
|
+
problems: z.array(z.object({
|
|
308
|
+
id: z.string().optional(),
|
|
309
|
+
question: z.string().min(1),
|
|
310
|
+
answer: z.string().min(1),
|
|
311
|
+
type: z.enum(['MULTIPLE_CHOICE', 'TEXT', 'NUMERIC', 'TRUE_FALSE', 'MATCHING', 'FILL_IN_THE_BLANK']).default('TEXT'),
|
|
312
|
+
options: z.array(z.string()).optional(),
|
|
313
|
+
})).optional(),
|
|
314
|
+
}))
|
|
315
|
+
.mutation(async ({ ctx, input }) => {
|
|
316
|
+
const { id, problems, ...updateData } = input;
|
|
317
|
+
// Verify worksheet ownership
|
|
318
|
+
const existingWorksheet = await ctx.db.artifact.findFirst({
|
|
319
|
+
where: {
|
|
320
|
+
id,
|
|
321
|
+
type: ArtifactType.WORKSHEET,
|
|
322
|
+
workspace: { ownerId: ctx.session.user.id },
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
if (!existingWorksheet)
|
|
326
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
327
|
+
// Handle questions update if provided
|
|
328
|
+
if (problems) {
|
|
329
|
+
// Delete existing questions and create new ones
|
|
330
|
+
await ctx.db.worksheetQuestion.deleteMany({
|
|
331
|
+
where: { artifactId: id },
|
|
332
|
+
});
|
|
333
|
+
await ctx.db.worksheetQuestion.createMany({
|
|
334
|
+
data: problems.map((problem, index) => ({
|
|
335
|
+
artifactId: id,
|
|
336
|
+
prompt: problem.question,
|
|
337
|
+
answer: problem.answer,
|
|
338
|
+
type: problem.type,
|
|
339
|
+
order: index,
|
|
340
|
+
meta: problem.options ? { options: problem.options } : undefined,
|
|
341
|
+
})),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Process update data
|
|
345
|
+
const processedUpdateData = {
|
|
346
|
+
...updateData,
|
|
347
|
+
difficulty: updateData.difficulty,
|
|
348
|
+
};
|
|
349
|
+
return ctx.db.artifact.update({
|
|
350
|
+
where: { id },
|
|
351
|
+
data: processedUpdateData,
|
|
352
|
+
include: {
|
|
353
|
+
questions: {
|
|
354
|
+
orderBy: { order: 'asc' },
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
}),
|
|
128
359
|
// Delete a worksheet set and its questions
|
|
129
|
-
|
|
130
|
-
.input(z.object({
|
|
360
|
+
delete: authedProcedure
|
|
361
|
+
.input(z.object({ id: z.string() }))
|
|
131
362
|
.mutation(async ({ ctx, input }) => {
|
|
132
363
|
const deleted = await ctx.db.artifact.deleteMany({
|
|
133
|
-
where: { id: input.
|
|
364
|
+
where: { id: input.id, type: ArtifactType.WORKSHEET, workspace: { ownerId: ctx.session.user.id } },
|
|
134
365
|
});
|
|
135
366
|
if (deleted.count === 0)
|
|
136
367
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
137
368
|
return true;
|
|
138
369
|
}),
|
|
370
|
+
// Generate a worksheet from a user prompt
|
|
371
|
+
generateFromPrompt: authedProcedure
|
|
372
|
+
.input(z.object({
|
|
373
|
+
workspaceId: z.string(),
|
|
374
|
+
prompt: z.string().min(1),
|
|
375
|
+
numQuestions: z.number().int().min(1).max(20).default(8),
|
|
376
|
+
difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
|
|
377
|
+
title: z.string().optional(),
|
|
378
|
+
estimatedTime: z.string().optional(),
|
|
379
|
+
}))
|
|
380
|
+
.mutation(async ({ ctx, input }) => {
|
|
381
|
+
const workspace = await ctx.db.workspace.findFirst({ where: { id: input.workspaceId, ownerId: ctx.session.user.id } });
|
|
382
|
+
if (!workspace)
|
|
383
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
384
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_load_start', { source: 'prompt' });
|
|
385
|
+
const session = await aiSessionService.initSession(input.workspaceId);
|
|
386
|
+
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}`);
|
|
387
|
+
await aiSessionService.startLLMSession(session.id);
|
|
388
|
+
const content = await aiSessionService.generateWorksheetQuestions(session.id, input.numQuestions, input.difficulty);
|
|
389
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: content.length });
|
|
390
|
+
const artifact = await ctx.db.artifact.create({
|
|
391
|
+
data: {
|
|
392
|
+
workspaceId: input.workspaceId,
|
|
393
|
+
type: ArtifactType.WORKSHEET,
|
|
394
|
+
title: input.title || `Worksheet - ${new Date().toLocaleString()}`,
|
|
395
|
+
createdById: ctx.session.user.id,
|
|
396
|
+
difficulty: (input.difficulty.toUpperCase()),
|
|
397
|
+
estimatedTime: input.estimatedTime,
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
try {
|
|
401
|
+
const worksheetData = JSON.parse(content);
|
|
402
|
+
let actualWorksheetData = worksheetData;
|
|
403
|
+
if (worksheetData.last_response) {
|
|
404
|
+
try {
|
|
405
|
+
actualWorksheetData = JSON.parse(worksheetData.last_response);
|
|
406
|
+
}
|
|
407
|
+
catch { }
|
|
408
|
+
}
|
|
409
|
+
const problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
|
|
410
|
+
for (let i = 0; i < Math.min(problems.length, input.numQuestions); i++) {
|
|
411
|
+
const problem = problems[i];
|
|
412
|
+
const prompt = problem.question || problem.prompt || `Question ${i + 1}`;
|
|
413
|
+
const answer = problem.answer || problem.solution || `Answer ${i + 1}`;
|
|
414
|
+
const type = problem.type || 'TEXT';
|
|
415
|
+
const options = problem.options || [];
|
|
416
|
+
await ctx.db.worksheetQuestion.create({
|
|
417
|
+
data: {
|
|
418
|
+
artifactId: artifact.id,
|
|
419
|
+
prompt,
|
|
420
|
+
answer,
|
|
421
|
+
difficulty: (input.difficulty.toUpperCase()),
|
|
422
|
+
order: i,
|
|
423
|
+
meta: {
|
|
424
|
+
type,
|
|
425
|
+
options: options.length > 0 ? options : undefined,
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
433
|
+
for (let i = 0; i < Math.min(lines.length, input.numQuestions); i++) {
|
|
434
|
+
const line = lines[i];
|
|
435
|
+
if (line.includes(' - ')) {
|
|
436
|
+
const [q, a] = line.split(' - ');
|
|
437
|
+
await ctx.db.worksheetQuestion.create({
|
|
438
|
+
data: {
|
|
439
|
+
artifactId: artifact.id,
|
|
440
|
+
prompt: q.trim(),
|
|
441
|
+
answer: a.trim(),
|
|
442
|
+
difficulty: (input.difficulty.toUpperCase()),
|
|
443
|
+
order: i,
|
|
444
|
+
meta: { type: 'TEXT' },
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
await PusherService.emitWorksheetComplete(input.workspaceId, artifact);
|
|
451
|
+
aiSessionService.deleteSession(session.id);
|
|
452
|
+
return { artifact };
|
|
453
|
+
}),
|
|
139
454
|
});
|