@goscribe/server 1.2.0 → 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/check-difficulty.cjs +14 -0
- package/check-questions.cjs +14 -0
- package/db-summary.cjs +22 -0
- package/mcq-test.cjs +36 -0
- package/package.json +9 -2
- package/prisma/migrations/20260413143206_init/migration.sql +873 -0
- package/prisma/schema.prisma +471 -324
- 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 +79 -51
- package/src/lib/email.ts +213 -29
- package/src/lib/env.ts +23 -6
- package/src/lib/inference.ts +2 -2
- 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 +86 -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/routers/_app.ts +9 -0
- package/src/routers/admin.ts +710 -0
- package/src/routers/annotations.ts +41 -0
- package/src/routers/auth.ts +338 -28
- package/src/routers/copilot.ts +719 -0
- package/src/routers/flashcards.ts +201 -68
- 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 +112 -83
- package/src/routers/studyguide.ts +12 -0
- package/src/routers/worksheets.ts +289 -66
- package/src/routers/workspace.ts +329 -122
- package/src/scripts/purge-deleted-users.ts +167 -0
- package/src/server.ts +137 -11
- package/src/services/flashcard-progress.service.ts +49 -37
- 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
|
@@ -1,11 +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
|
import { ArtifactType } from '../lib/constants.js';
|
|
8
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';
|
|
9
17
|
|
|
10
18
|
const Difficulty = {
|
|
11
19
|
EASY: 'EASY',
|
|
@@ -72,10 +80,91 @@ export const worksheets = router({
|
|
|
72
80
|
return merged;
|
|
73
81
|
}),
|
|
74
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
|
+
|
|
75
164
|
// Create a worksheet set
|
|
76
|
-
create:
|
|
77
|
-
.input(z.object({
|
|
78
|
-
workspaceId: z.string(),
|
|
165
|
+
create: limitedProcedure
|
|
166
|
+
.input(z.object({
|
|
167
|
+
workspaceId: z.string(),
|
|
79
168
|
title: z.string().min(1).max(120),
|
|
80
169
|
description: z.string().optional(),
|
|
81
170
|
difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
|
|
@@ -166,7 +255,7 @@ export const worksheets = router({
|
|
|
166
255
|
}),
|
|
167
256
|
|
|
168
257
|
// Add a question to a worksheet
|
|
169
|
-
createWorksheetQuestion:
|
|
258
|
+
createWorksheetQuestion: limitedProcedure
|
|
170
259
|
.input(z.object({
|
|
171
260
|
worksheetId: z.string(),
|
|
172
261
|
prompt: z.string().min(1),
|
|
@@ -333,7 +422,7 @@ export const worksheets = router({
|
|
|
333
422
|
}))
|
|
334
423
|
.mutation(async ({ ctx, input }) => {
|
|
335
424
|
const { id, problems, ...updateData } = input;
|
|
336
|
-
|
|
425
|
+
|
|
337
426
|
// Verify worksheet ownership
|
|
338
427
|
const existingWorksheet = await ctx.db.artifact.findFirst({
|
|
339
428
|
where: {
|
|
@@ -392,89 +481,223 @@ export const worksheets = router({
|
|
|
392
481
|
}),
|
|
393
482
|
|
|
394
483
|
// Generate a worksheet from a user prompt
|
|
395
|
-
generateFromPrompt:
|
|
484
|
+
generateFromPrompt: limitedProcedure
|
|
396
485
|
.input(z.object({
|
|
397
486
|
workspaceId: z.string(),
|
|
398
487
|
prompt: z.string().min(1),
|
|
399
488
|
numQuestions: z.number().int().min(1).max(20).default(8),
|
|
400
489
|
difficulty: z.enum(['easy', 'medium', 'hard']).default('medium'),
|
|
490
|
+
mode: worksheetModeSchema.optional(),
|
|
491
|
+
presetId: z.string().optional(),
|
|
492
|
+
configOverride: worksheetPresetConfigPartialSchema.optional(),
|
|
401
493
|
title: z.string().optional(),
|
|
402
494
|
estimatedTime: z.string().optional(),
|
|
403
495
|
}))
|
|
404
496
|
.mutation(async ({ ctx, input }) => {
|
|
405
|
-
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
|
+
});
|
|
406
500
|
if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
407
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
|
+
|
|
408
552
|
const artifact = await ctx.db.artifact.create({
|
|
409
553
|
data: {
|
|
410
554
|
workspaceId: input.workspaceId,
|
|
411
555
|
type: ArtifactType.WORKSHEET,
|
|
412
|
-
title: input.title || `Worksheet - ${new Date().toLocaleString()}
|
|
556
|
+
title: input.title || (resolved.mode === 'quiz' ? `Quiz - ${new Date().toLocaleString()}` : `Worksheet - ${new Date().toLocaleString()}`),
|
|
413
557
|
createdById: ctx.session.user.id,
|
|
414
|
-
difficulty:
|
|
558
|
+
difficulty: difficultyUpper as any,
|
|
415
559
|
estimatedTime: input.estimatedTime,
|
|
416
560
|
generating: true,
|
|
417
|
-
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,
|
|
418
568
|
},
|
|
419
569
|
});
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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({
|
|
440
619
|
artifactId: artifact.id,
|
|
441
|
-
prompt,
|
|
442
|
-
answer,
|
|
443
|
-
difficulty:
|
|
444
|
-
order:
|
|
445
|
-
type,
|
|
446
|
-
meta:
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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,
|
|
450
657
|
},
|
|
451
658
|
});
|
|
452
|
-
}
|
|
453
|
-
} catch {
|
|
454
|
-
logger.error('Failed to parse worksheet JSON,');
|
|
455
|
-
await ctx.db.artifact.delete({
|
|
456
|
-
where: { id: artifact.id },
|
|
457
|
-
});
|
|
458
|
-
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to parse worksheet JSON' });
|
|
459
|
-
}
|
|
460
659
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
660
|
+
const updatedWorksheet = await ctx.db.artifact.findFirst({
|
|
661
|
+
where: { id: artifact.id },
|
|
662
|
+
include: { questions: true }
|
|
663
|
+
});
|
|
465
664
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
+
})();
|
|
474
697
|
|
|
475
698
|
return { artifact };
|
|
476
699
|
}),
|
|
477
|
-
|
|
700
|
+
checkAnswer: authedProcedure
|
|
478
701
|
.input(z.object({
|
|
479
702
|
worksheetId: z.string(),
|
|
480
703
|
questionId: z.string(),
|
|
@@ -485,7 +708,7 @@ export const worksheets = router({
|
|
|
485
708
|
if (!worksheet) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
486
709
|
const question = await ctx.db.worksheetQuestion.findFirst({ where: { id: input.questionId, artifactId: input.worksheetId } });
|
|
487
710
|
if (!question) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
488
|
-
|
|
711
|
+
|
|
489
712
|
// Parse question meta to get mark_scheme
|
|
490
713
|
const questionMeta = question.meta ? (typeof question.meta === 'object' ? question.meta : JSON.parse(question.meta as any)) : {} as any;
|
|
491
714
|
const markScheme = questionMeta.markScheme;
|
|
@@ -503,11 +726,11 @@ export const worksheets = router({
|
|
|
503
726
|
input.answer,
|
|
504
727
|
markScheme
|
|
505
728
|
);
|
|
506
|
-
|
|
729
|
+
|
|
507
730
|
// Determine if correct by comparing achieved points vs total points
|
|
508
731
|
const achievedTotal = userMarkScheme.points.reduce((sum: number, p: any) => sum + (p.achievedPoints || 0), 0);
|
|
509
732
|
isCorrect = achievedTotal === markScheme.totalPoints;
|
|
510
|
-
|
|
733
|
+
|
|
511
734
|
} catch (error) {
|
|
512
735
|
logger.error('Failed to check answer with AI', error instanceof Error ? error.message : 'Unknown error');
|
|
513
736
|
// Fallback to simple string comparison
|
|
@@ -519,7 +742,7 @@ export const worksheets = router({
|
|
|
519
742
|
}
|
|
520
743
|
|
|
521
744
|
|
|
522
|
-
|
|
745
|
+
|
|
523
746
|
// @todo: figure out this wierd fix
|
|
524
747
|
const progress = await ctx.db.worksheetQuestionProgress.upsert({
|
|
525
748
|
where: {
|
|
@@ -552,6 +775,6 @@ export const worksheets = router({
|
|
|
552
775
|
|
|
553
776
|
return { isCorrect, userMarkScheme, progress };
|
|
554
777
|
}),
|
|
555
|
-
|
|
778
|
+
});
|
|
556
779
|
|
|
557
780
|
|