@goscribe/server 1.0.8 → 1.0.10
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/DATABASE_SETUP.md +165 -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/lib/ai-session.d.ts +26 -0
- package/dist/lib/ai-session.js +343 -0
- 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 +840 -58
- package/dist/routers/_app.js +6 -0
- package/dist/routers/ai-session.d.ts +0 -0
- package/dist/routers/ai-session.js +1 -0
- package/dist/routers/auth.d.ts +1 -0
- package/dist/routers/auth.js +6 -4
- package/dist/routers/chat.d.ts +171 -0
- package/dist/routers/chat.js +270 -0
- package/dist/routers/flashcards.d.ts +37 -0
- package/dist/routers/flashcards.js +128 -0
- 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 +138 -51
- package/dist/routers/worksheets.js +317 -7
- package/dist/routers/workspace.d.ts +162 -7
- package/dist/routers/workspace.js +440 -8
- package/dist/server.js +6 -2
- package/package.json +11 -4
- 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 +412 -0
- 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 +142 -0
- 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 +336 -7
- package/src/routers/workspace.ts +487 -8
- package/src/server.ts +7 -2
- 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
|
-
|
|
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
|
-
|
|
38
|
-
.input(z.object({
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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.
|
|
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, ctx.session.user.id);
|
|
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
|
|