@goscribe/server 1.1.7 → 1.3.0
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/.env.example +43 -0
- package/check-difficulty.cjs +14 -0
- package/check-questions.cjs +14 -0
- package/db-summary.cjs +22 -0
- package/dist/routers/auth.js +1 -1
- package/mcq-test.cjs +36 -0
- package/package.json +10 -2
- package/prisma/migrations/20260413143206_init/migration.sql +873 -0
- package/prisma/schema.prisma +485 -292
- package/src/context.ts +4 -1
- package/src/lib/activity_human_description.test.ts +28 -0
- package/src/lib/activity_human_description.ts +239 -0
- package/src/lib/activity_log_service.test.ts +37 -0
- package/src/lib/activity_log_service.ts +353 -0
- package/src/lib/ai-session.ts +194 -112
- package/src/lib/constants.ts +14 -0
- package/src/lib/email.ts +230 -0
- package/src/lib/env.ts +23 -6
- package/src/lib/inference.ts +3 -3
- package/src/lib/logger.ts +26 -9
- package/src/lib/notification-service.test.ts +106 -0
- package/src/lib/notification-service.ts +677 -0
- package/src/lib/prisma.ts +6 -1
- package/src/lib/pusher.ts +90 -6
- package/src/lib/retry.ts +61 -0
- package/src/lib/storage.ts +2 -2
- package/src/lib/stripe.ts +39 -0
- package/src/lib/subscription_service.ts +722 -0
- package/src/lib/usage_service.ts +74 -0
- package/src/lib/worksheet-generation.test.ts +31 -0
- package/src/lib/worksheet-generation.ts +139 -0
- package/src/lib/workspace-access.ts +13 -0
- package/src/routers/_app.ts +11 -0
- package/src/routers/admin.ts +710 -0
- package/src/routers/annotations.ts +227 -0
- package/src/routers/auth.ts +432 -33
- package/src/routers/copilot.ts +719 -0
- package/src/routers/flashcards.ts +207 -80
- package/src/routers/members.ts +280 -80
- package/src/routers/notifications.ts +142 -0
- package/src/routers/payment.ts +448 -0
- package/src/routers/podcast.ts +133 -108
- package/src/routers/studyguide.ts +80 -74
- package/src/routers/worksheets.ts +300 -80
- package/src/routers/workspace.ts +538 -328
- package/src/scripts/purge-deleted-users.ts +167 -0
- package/src/server.ts +140 -12
- package/src/services/flashcard-progress.service.ts +52 -43
- package/src/trpc.ts +184 -5
- package/test-generate.js +30 -0
- package/test-ratio.cjs +9 -0
- package/zod-test.cjs +22 -0
- package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
- package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
- package/prisma/seed.mjs +0 -135
- package/src/routers/meetingsummary.ts +0 -416
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { TRPCError } from '@trpc/server';
|
|
3
|
-
import { router, authedProcedure } from '../trpc.js';
|
|
3
|
+
import { router, authedProcedure, verifiedProcedure, limitedProcedure } from '../trpc.js';
|
|
4
4
|
import { aiSessionService } from '../lib/ai-session.js';
|
|
5
5
|
import PusherService from '../lib/pusher.js';
|
|
6
|
+
import { notifyArtifactFailed, notifyArtifactReady } from '../lib/notification-service.js';
|
|
6
7
|
import { logger } from '../lib/logger.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import { ArtifactType } from '../lib/constants.js';
|
|
9
|
+
import { workspaceAccessFilter } from '../lib/workspace-access.js';
|
|
10
|
+
import {
|
|
11
|
+
mergeWorksheetGenerationConfig,
|
|
12
|
+
normalizeWorksheetProblemForDb,
|
|
13
|
+
parsePresetConfig,
|
|
14
|
+
worksheetModeSchema,
|
|
15
|
+
worksheetPresetConfigPartialSchema,
|
|
16
|
+
} from '../lib/worksheet-generation.js';
|
|
12
17
|
|
|
13
18
|
const Difficulty = {
|
|
14
19
|
EASY: 'EASY',
|
|
@@ -75,10 +80,91 @@ export const worksheets = router({
|
|
|
75
80
|
return merged;
|
|
76
81
|
}),
|
|
77
82
|
|
|
83
|
+
listPresets: authedProcedure
|
|
84
|
+
.input(z.object({ workspaceId: z.string() }))
|
|
85
|
+
.query(async ({ ctx, input }) => {
|
|
86
|
+
const ws = await ctx.db.workspace.findFirst({
|
|
87
|
+
where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
|
|
88
|
+
});
|
|
89
|
+
if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
90
|
+
|
|
91
|
+
return ctx.db.worksheetPreset.findMany({
|
|
92
|
+
where: {
|
|
93
|
+
OR: [
|
|
94
|
+
{ isSystem: true },
|
|
95
|
+
{
|
|
96
|
+
userId: ctx.session.user.id,
|
|
97
|
+
OR: [{ workspaceId: input.workspaceId }, { workspaceId: null }],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
orderBy: [{ isSystem: 'desc' }, { name: 'asc' }],
|
|
102
|
+
});
|
|
103
|
+
}),
|
|
104
|
+
|
|
105
|
+
createPreset: authedProcedure
|
|
106
|
+
.input(z.object({
|
|
107
|
+
workspaceId: z.string().optional(),
|
|
108
|
+
name: z.string().min(1).max(120),
|
|
109
|
+
config: z.record(z.string(), z.unknown()),
|
|
110
|
+
}))
|
|
111
|
+
.mutation(async ({ ctx, input }) => {
|
|
112
|
+
if (input.workspaceId) {
|
|
113
|
+
const ws = await ctx.db.workspace.findFirst({
|
|
114
|
+
where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
|
|
115
|
+
});
|
|
116
|
+
if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
117
|
+
}
|
|
118
|
+
const config = parsePresetConfig(input.config);
|
|
119
|
+
return ctx.db.worksheetPreset.create({
|
|
120
|
+
data: {
|
|
121
|
+
userId: ctx.session.user.id,
|
|
122
|
+
workspaceId: input.workspaceId ?? null,
|
|
123
|
+
name: input.name,
|
|
124
|
+
isSystem: false,
|
|
125
|
+
config: config as object,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}),
|
|
129
|
+
|
|
130
|
+
updatePreset: authedProcedure
|
|
131
|
+
.input(z.object({
|
|
132
|
+
id: z.string(),
|
|
133
|
+
name: z.string().min(1).max(120).optional(),
|
|
134
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
135
|
+
}).refine(d => d.name !== undefined || d.config !== undefined, { message: 'Provide name and/or config' }))
|
|
136
|
+
.mutation(async ({ ctx, input }) => {
|
|
137
|
+
const existing = await ctx.db.worksheetPreset.findFirst({
|
|
138
|
+
where: { id: input.id, userId: ctx.session.user.id, isSystem: false },
|
|
139
|
+
});
|
|
140
|
+
if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found or read-only' });
|
|
141
|
+
|
|
142
|
+
const data: { name?: string; config?: object } = {};
|
|
143
|
+
if (input.name !== undefined) data.name = input.name;
|
|
144
|
+
if (input.config !== undefined) data.config = parsePresetConfig(input.config) as object;
|
|
145
|
+
|
|
146
|
+
return ctx.db.worksheetPreset.update({
|
|
147
|
+
where: { id: input.id },
|
|
148
|
+
data,
|
|
149
|
+
});
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
deletePreset: authedProcedure
|
|
153
|
+
.input(z.object({ id: z.string() }))
|
|
154
|
+
.mutation(async ({ ctx, input }) => {
|
|
155
|
+
const existing = await ctx.db.worksheetPreset.findFirst({
|
|
156
|
+
where: { id: input.id, userId: ctx.session.user.id, isSystem: false },
|
|
157
|
+
});
|
|
158
|
+
if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found or read-only' });
|
|
159
|
+
|
|
160
|
+
await ctx.db.worksheetPreset.delete({ where: { id: input.id } });
|
|
161
|
+
return true;
|
|
162
|
+
}),
|
|
163
|
+
|
|
78
164
|
// Create a worksheet set
|
|
79
|
-
create:
|
|
80
|
-
.input(z.object({
|
|
81
|
-
workspaceId: z.string(),
|
|
165
|
+
create: limitedProcedure
|
|
166
|
+
.input(z.object({
|
|
167
|
+
workspaceId: z.string(),
|
|
82
168
|
title: z.string().min(1).max(120),
|
|
83
169
|
description: z.string().optional(),
|
|
84
170
|
difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
|
|
@@ -130,7 +216,7 @@ export const worksheets = router({
|
|
|
130
216
|
where: {
|
|
131
217
|
id: input.worksheetId,
|
|
132
218
|
type: ArtifactType.WORKSHEET,
|
|
133
|
-
workspace:
|
|
219
|
+
workspace: workspaceAccessFilter(ctx.session.user.id),
|
|
134
220
|
},
|
|
135
221
|
include: { questions: true },
|
|
136
222
|
orderBy: { updatedAt: 'desc' },
|
|
@@ -169,7 +255,7 @@ export const worksheets = router({
|
|
|
169
255
|
}),
|
|
170
256
|
|
|
171
257
|
// Add a question to a worksheet
|
|
172
|
-
createWorksheetQuestion:
|
|
258
|
+
createWorksheetQuestion: limitedProcedure
|
|
173
259
|
.input(z.object({
|
|
174
260
|
worksheetId: z.string(),
|
|
175
261
|
prompt: z.string().min(1),
|
|
@@ -181,7 +267,7 @@ export const worksheets = router({
|
|
|
181
267
|
}))
|
|
182
268
|
.mutation(async ({ ctx, input }) => {
|
|
183
269
|
const worksheet = await ctx.db.artifact.findFirst({
|
|
184
|
-
where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace:
|
|
270
|
+
where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) },
|
|
185
271
|
});
|
|
186
272
|
if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
187
273
|
return ctx.db.worksheetQuestion.create({
|
|
@@ -210,7 +296,7 @@ export const worksheets = router({
|
|
|
210
296
|
}))
|
|
211
297
|
.mutation(async ({ ctx, input }) => {
|
|
212
298
|
const q = await ctx.db.worksheetQuestion.findFirst({
|
|
213
|
-
where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace:
|
|
299
|
+
where: { id: input.worksheetQuestionId, artifact: { type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) } },
|
|
214
300
|
});
|
|
215
301
|
if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
216
302
|
return ctx.db.worksheetQuestion.update({
|
|
@@ -231,7 +317,7 @@ export const worksheets = router({
|
|
|
231
317
|
.input(z.object({ worksheetQuestionId: z.string() }))
|
|
232
318
|
.mutation(async ({ ctx, input }) => {
|
|
233
319
|
const q = await ctx.db.worksheetQuestion.findFirst({
|
|
234
|
-
where: { id: input.worksheetQuestionId, artifact: { workspace:
|
|
320
|
+
where: { id: input.worksheetQuestionId, artifact: { workspace: workspaceAccessFilter(ctx.session.user.id) } },
|
|
235
321
|
});
|
|
236
322
|
if (!q) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
237
323
|
await ctx.db.worksheetQuestion.delete({ where: { id: input.worksheetQuestionId } });
|
|
@@ -253,7 +339,7 @@ export const worksheets = router({
|
|
|
253
339
|
id: input.problemId,
|
|
254
340
|
artifact: {
|
|
255
341
|
type: ArtifactType.WORKSHEET,
|
|
256
|
-
workspace:
|
|
342
|
+
workspace: workspaceAccessFilter(ctx.session.user.id),
|
|
257
343
|
},
|
|
258
344
|
},
|
|
259
345
|
});
|
|
@@ -297,7 +383,7 @@ export const worksheets = router({
|
|
|
297
383
|
where: {
|
|
298
384
|
id: input.worksheetId,
|
|
299
385
|
type: ArtifactType.WORKSHEET,
|
|
300
|
-
workspace:
|
|
386
|
+
workspace: workspaceAccessFilter(ctx.session.user.id),
|
|
301
387
|
},
|
|
302
388
|
});
|
|
303
389
|
if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
@@ -336,13 +422,13 @@ export const worksheets = router({
|
|
|
336
422
|
}))
|
|
337
423
|
.mutation(async ({ ctx, input }) => {
|
|
338
424
|
const { id, problems, ...updateData } = input;
|
|
339
|
-
|
|
425
|
+
|
|
340
426
|
// Verify worksheet ownership
|
|
341
427
|
const existingWorksheet = await ctx.db.artifact.findFirst({
|
|
342
428
|
where: {
|
|
343
429
|
id,
|
|
344
430
|
type: ArtifactType.WORKSHEET,
|
|
345
|
-
workspace:
|
|
431
|
+
workspace: workspaceAccessFilter(ctx.session.user.id),
|
|
346
432
|
},
|
|
347
433
|
});
|
|
348
434
|
if (!existingWorksheet) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
@@ -388,107 +474,241 @@ export const worksheets = router({
|
|
|
388
474
|
.input(z.object({ id: z.string() }))
|
|
389
475
|
.mutation(async ({ ctx, input }) => {
|
|
390
476
|
const deleted = await ctx.db.artifact.deleteMany({
|
|
391
|
-
where: { id: input.id, type: ArtifactType.WORKSHEET, workspace:
|
|
477
|
+
where: { id: input.id, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) },
|
|
392
478
|
});
|
|
393
479
|
if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
394
480
|
return true;
|
|
395
481
|
}),
|
|
396
482
|
|
|
397
483
|
// Generate a worksheet from a user prompt
|
|
398
|
-
generateFromPrompt:
|
|
484
|
+
generateFromPrompt: limitedProcedure
|
|
399
485
|
.input(z.object({
|
|
400
486
|
workspaceId: z.string(),
|
|
401
487
|
prompt: z.string().min(1),
|
|
402
488
|
numQuestions: z.number().int().min(1).max(20).default(8),
|
|
403
489
|
difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
|
|
490
|
+
mode: worksheetModeSchema.optional(),
|
|
491
|
+
presetId: z.string().optional(),
|
|
492
|
+
configOverride: worksheetPresetConfigPartialSchema.optional(),
|
|
404
493
|
title: z.string().optional(),
|
|
405
494
|
estimatedTime: z.string().optional(),
|
|
406
495
|
}))
|
|
407
496
|
.mutation(async ({ ctx, input }) => {
|
|
408
|
-
const workspace = await ctx.db.workspace.findFirst({
|
|
497
|
+
const workspace = await ctx.db.workspace.findFirst({
|
|
498
|
+
where: { id: input.workspaceId, ...workspaceAccessFilter(ctx.session.user.id) },
|
|
499
|
+
});
|
|
409
500
|
if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
410
501
|
|
|
502
|
+
let presetRow: Awaited<ReturnType<typeof ctx.db.worksheetPreset.findFirst>> = null;
|
|
503
|
+
if (input.presetId) {
|
|
504
|
+
presetRow = await ctx.db.worksheetPreset.findFirst({
|
|
505
|
+
where: {
|
|
506
|
+
id: input.presetId,
|
|
507
|
+
OR: [
|
|
508
|
+
{ isSystem: true },
|
|
509
|
+
{
|
|
510
|
+
userId: ctx.session.user.id,
|
|
511
|
+
OR: [{ workspaceId: input.workspaceId }, { workspaceId: null }],
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
if (!presetRow) throw new TRPCError({ code: 'NOT_FOUND', message: 'Preset not found' });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const presetConfig = presetRow?.config
|
|
520
|
+
? parsePresetConfig(presetRow.config)
|
|
521
|
+
: undefined;
|
|
522
|
+
|
|
523
|
+
const resolved = mergeWorksheetGenerationConfig(
|
|
524
|
+
presetConfig,
|
|
525
|
+
input.configOverride ?? undefined,
|
|
526
|
+
{
|
|
527
|
+
numQuestions: input.numQuestions,
|
|
528
|
+
difficulty: input.difficulty,
|
|
529
|
+
mode: input.mode,
|
|
530
|
+
},
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const difficultyUpper = resolved.difficulty.toUpperCase() as 'EASY' | 'MEDIUM' | 'HARD';
|
|
534
|
+
|
|
535
|
+
console.log("[DEBUG TRPC PAYLOAD] input =", input);
|
|
536
|
+
console.log("[DEBUG TRPC PAYLOAD] presetConfig =", presetConfig);
|
|
537
|
+
console.log("[DEBUG TRPC PAYLOAD] legacy merged legacy values:", { numQuestions: input.numQuestions, difficulty: input.difficulty, mode: input.mode });
|
|
538
|
+
console.log("[DEBUG TRPC PAYLOAD] RESOLVED =", resolved);
|
|
539
|
+
|
|
540
|
+
const worksheetConfigSnapshot = {
|
|
541
|
+
mode: resolved.mode,
|
|
542
|
+
presetId: input.presetId ?? null,
|
|
543
|
+
presetName: presetRow?.name ?? null,
|
|
544
|
+
numQuestions: resolved.numQuestions,
|
|
545
|
+
difficulty: resolved.difficulty,
|
|
546
|
+
mcqRatio: resolved.mcqRatio ?? null,
|
|
547
|
+
questionTypes: resolved.questionTypes ?? null,
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
await PusherService.emitWorksheetGenerationStart(input.workspaceId);
|
|
551
|
+
|
|
411
552
|
const artifact = await ctx.db.artifact.create({
|
|
412
553
|
data: {
|
|
413
554
|
workspaceId: input.workspaceId,
|
|
414
555
|
type: ArtifactType.WORKSHEET,
|
|
415
|
-
title: input.title || `Worksheet - ${new Date().toLocaleString()}
|
|
556
|
+
title: input.title || (resolved.mode === 'quiz' ? `Quiz - ${new Date().toLocaleString()}` : `Worksheet - ${new Date().toLocaleString()}`),
|
|
416
557
|
createdById: ctx.session.user.id,
|
|
417
|
-
difficulty:
|
|
558
|
+
difficulty: difficultyUpper as any,
|
|
418
559
|
estimatedTime: input.estimatedTime,
|
|
419
560
|
generating: true,
|
|
420
|
-
generatingMetadata: {
|
|
561
|
+
generatingMetadata: {
|
|
562
|
+
quantity: resolved.numQuestions,
|
|
563
|
+
difficulty: resolved.difficulty,
|
|
564
|
+
mode: resolved.mode,
|
|
565
|
+
presetId: input.presetId ?? null,
|
|
566
|
+
},
|
|
567
|
+
worksheetConfig: worksheetConfigSnapshot as object,
|
|
421
568
|
},
|
|
422
569
|
});
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
570
|
+
|
|
571
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: resolved.numQuestions });
|
|
572
|
+
await PusherService.emitWorksheetNew(input.workspaceId, artifact);
|
|
573
|
+
|
|
574
|
+
const userId = ctx.session.user.id;
|
|
575
|
+
const workspaceId = input.workspaceId;
|
|
576
|
+
const promptText = input.prompt;
|
|
577
|
+
const estTime = input.estimatedTime;
|
|
578
|
+
|
|
579
|
+
// Launch generation in the background to free up the connection pool immediately
|
|
580
|
+
(async () => {
|
|
581
|
+
try {
|
|
582
|
+
const content = await aiSessionService.generateWorksheetQuestions(
|
|
583
|
+
workspaceId,
|
|
584
|
+
userId,
|
|
585
|
+
resolved.numQuestions,
|
|
586
|
+
difficultyUpper,
|
|
587
|
+
{
|
|
588
|
+
mode: resolved.mode,
|
|
589
|
+
mcqRatio: resolved.mcqRatio,
|
|
590
|
+
questionTypes: resolved.questionTypes,
|
|
591
|
+
prompt: promptText,
|
|
592
|
+
},
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
let problems: any[] = [];
|
|
596
|
+
try {
|
|
597
|
+
const worksheetData = JSON.parse(content);
|
|
598
|
+
let actualWorksheetData: any = worksheetData;
|
|
599
|
+
if (worksheetData.last_response) {
|
|
600
|
+
try { actualWorksheetData = JSON.parse(worksheetData.last_response); } catch { /* noop */ }
|
|
601
|
+
}
|
|
602
|
+
problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
|
|
603
|
+
if (!Array.isArray(problems)) problems = [];
|
|
604
|
+
} catch (parseError) {
|
|
605
|
+
logger.error('Failed to parse worksheet JSON', parseError);
|
|
606
|
+
throw new Error('Failed to parse worksheet JSON');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const forceMcq = resolved.mode === 'quiz';
|
|
610
|
+
const questionsToCreate = [];
|
|
611
|
+
for (let i = 0; i < Math.min(problems.length, resolved.numQuestions); i++) {
|
|
612
|
+
const problem = problems[i] && typeof problems[i] === 'object' ? problems[i] as Record<string, unknown> : {};
|
|
613
|
+
const row = normalizeWorksheetProblemForDb(problem, i, difficultyUpper, forceMcq);
|
|
614
|
+
const metaPayload: Record<string, unknown> = {};
|
|
615
|
+
if (row.meta.options?.length) metaPayload.options = row.meta.options;
|
|
616
|
+
if (row.meta.markScheme != null) metaPayload.markScheme = row.meta.markScheme;
|
|
617
|
+
|
|
618
|
+
questionsToCreate.push({
|
|
443
619
|
artifactId: artifact.id,
|
|
444
|
-
prompt,
|
|
445
|
-
answer,
|
|
446
|
-
difficulty:
|
|
447
|
-
order:
|
|
448
|
-
type,
|
|
449
|
-
meta:
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
620
|
+
prompt: row.prompt,
|
|
621
|
+
answer: row.answer ?? '',
|
|
622
|
+
difficulty: row.difficulty as any,
|
|
623
|
+
order: row.order,
|
|
624
|
+
type: row.type as any,
|
|
625
|
+
meta: Object.keys(metaPayload).length ? (metaPayload as object) : undefined,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (questionsToCreate.length > 0) {
|
|
630
|
+
await ctx.db.worksheetQuestion.createMany({
|
|
631
|
+
data: questionsToCreate,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
let parsedTitle: string | undefined;
|
|
636
|
+
let parsedDescription: string | undefined;
|
|
637
|
+
let parsedEstimated: string | undefined;
|
|
638
|
+
try {
|
|
639
|
+
const worksheetData = JSON.parse(content);
|
|
640
|
+
let actualWorksheetData: any = worksheetData;
|
|
641
|
+
if (worksheetData.last_response) {
|
|
642
|
+
try { actualWorksheetData = JSON.parse(worksheetData.last_response); } catch { /* noop */ }
|
|
643
|
+
}
|
|
644
|
+
parsedTitle = typeof actualWorksheetData.title === 'string' ? actualWorksheetData.title : undefined;
|
|
645
|
+
parsedDescription = typeof actualWorksheetData.description === 'string' ? actualWorksheetData.description : undefined;
|
|
646
|
+
parsedEstimated = typeof actualWorksheetData.estimatedTime === 'string' ? actualWorksheetData.estimatedTime : undefined;
|
|
647
|
+
} catch { /* noop */ }
|
|
648
|
+
|
|
649
|
+
await ctx.db.artifact.update({
|
|
650
|
+
where: { id: artifact.id },
|
|
651
|
+
data: {
|
|
652
|
+
generating: false,
|
|
653
|
+
title: parsedTitle || artifact.title,
|
|
654
|
+
description: parsedDescription ?? undefined,
|
|
655
|
+
estimatedTime: estTime || parsedEstimated || undefined,
|
|
656
|
+
worksheetConfig: worksheetConfigSnapshot as object,
|
|
453
657
|
},
|
|
454
658
|
});
|
|
455
|
-
}
|
|
456
|
-
} catch {
|
|
457
|
-
logger.error('Failed to parse worksheet JSON,');
|
|
458
|
-
await ctx.db.artifact.delete({
|
|
459
|
-
where: { id: artifact.id },
|
|
460
|
-
});
|
|
461
|
-
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to parse worksheet JSON' });
|
|
462
|
-
}
|
|
463
659
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
660
|
+
const updatedWorksheet = await ctx.db.artifact.findFirst({
|
|
661
|
+
where: { id: artifact.id },
|
|
662
|
+
include: { questions: true }
|
|
663
|
+
});
|
|
468
664
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
665
|
+
await PusherService.emitWorksheetGenerationComplete(workspaceId, updatedWorksheet);
|
|
666
|
+
await PusherService.emitWorksheetComplete(workspaceId, artifact);
|
|
667
|
+
|
|
668
|
+
await notifyArtifactReady(ctx.db, {
|
|
669
|
+
userId,
|
|
670
|
+
workspaceId,
|
|
671
|
+
artifactId: artifact.id,
|
|
672
|
+
artifactType: ArtifactType.WORKSHEET,
|
|
673
|
+
title: updatedWorksheet?.title ?? artifact.title,
|
|
674
|
+
}).catch(() => {});
|
|
675
|
+
} catch (error) {
|
|
676
|
+
logger.error(`Background generation failed for artifact ${artifact.id}:`, error);
|
|
677
|
+
await notifyArtifactFailed(ctx.db, {
|
|
678
|
+
userId,
|
|
679
|
+
workspaceId,
|
|
680
|
+
artifactId: artifact.id,
|
|
681
|
+
artifactType: ArtifactType.WORKSHEET,
|
|
682
|
+
title: artifact.title,
|
|
683
|
+
message: `Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
684
|
+
}).catch(() => {});
|
|
685
|
+
|
|
686
|
+
await ctx.db.artifact.deleteMany({
|
|
687
|
+
where: { id: artifact.id },
|
|
688
|
+
}).catch(() => { }); // Ignore delete errors if already gone
|
|
689
|
+
|
|
690
|
+
await PusherService.emitError(
|
|
691
|
+
workspaceId,
|
|
692
|
+
`Failed to generate worksheet: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
693
|
+
'worksheet_generation'
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
})();
|
|
477
697
|
|
|
478
698
|
return { artifact };
|
|
479
699
|
}),
|
|
480
|
-
|
|
700
|
+
checkAnswer: authedProcedure
|
|
481
701
|
.input(z.object({
|
|
482
702
|
worksheetId: z.string(),
|
|
483
703
|
questionId: z.string(),
|
|
484
704
|
answer: z.string().min(1),
|
|
485
705
|
}))
|
|
486
706
|
.mutation(async ({ ctx, input }) => {
|
|
487
|
-
const worksheet = await ctx.db.artifact.findFirst({ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace:
|
|
707
|
+
const worksheet = await ctx.db.artifact.findFirst({ where: { id: input.worksheetId, type: ArtifactType.WORKSHEET, workspace: workspaceAccessFilter(ctx.session.user.id) }, include: { workspace: true } });
|
|
488
708
|
if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
489
709
|
const question = await ctx.db.worksheetQuestion.findFirst({ where: { id: input.questionId, artifactId: input.worksheetId } });
|
|
490
710
|
if (!question) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
491
|
-
|
|
711
|
+
|
|
492
712
|
// Parse question meta to get mark_scheme
|
|
493
713
|
const questionMeta = question.meta ? (typeof question.meta === 'object' ? question.meta : JSON.parse(question.meta as any)) : {} as any;
|
|
494
714
|
const markScheme = questionMeta.markScheme;
|
|
@@ -506,11 +726,11 @@ export const worksheets = router({
|
|
|
506
726
|
input.answer,
|
|
507
727
|
markScheme
|
|
508
728
|
);
|
|
509
|
-
|
|
729
|
+
|
|
510
730
|
// Determine if correct by comparing achieved points vs total points
|
|
511
731
|
const achievedTotal = userMarkScheme.points.reduce((sum: number, p: any) => sum + (p.achievedPoints || 0), 0);
|
|
512
732
|
isCorrect = achievedTotal === markScheme.totalPoints;
|
|
513
|
-
|
|
733
|
+
|
|
514
734
|
} catch (error) {
|
|
515
735
|
logger.error('Failed to check answer with AI', error instanceof Error ? error.message : 'Unknown error');
|
|
516
736
|
// Fallback to simple string comparison
|
|
@@ -522,7 +742,7 @@ export const worksheets = router({
|
|
|
522
742
|
}
|
|
523
743
|
|
|
524
744
|
|
|
525
|
-
|
|
745
|
+
|
|
526
746
|
// @todo: figure out this wierd fix
|
|
527
747
|
const progress = await ctx.db.worksheetQuestionProgress.upsert({
|
|
528
748
|
where: {
|
|
@@ -555,6 +775,6 @@ export const worksheets = router({
|
|
|
555
775
|
|
|
556
776
|
return { isCorrect, userMarkScheme, progress };
|
|
557
777
|
}),
|
|
558
|
-
|
|
778
|
+
});
|
|
559
779
|
|
|
560
780
|
|