@goscribe/server 1.5.0 → 1.6.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/dist/context.d.ts +14 -1
- package/dist/context.js +23 -2
- package/dist/lib/ai/index.d.ts +3 -2
- package/dist/lib/ai/index.js +3 -2
- package/dist/lib/ai/llm-client.d.ts +1 -0
- package/dist/lib/ai/llm-client.js +17 -0
- package/dist/routers/_app.d.ts +40 -80
- package/dist/routers/auth.js +1 -1
- package/dist/routers/flashcards.d.ts +12 -1
- package/dist/routers/payment.d.ts +1 -12
- package/dist/routers/workspace.d.ts +27 -67
- package/dist/routers/workspace.js +1 -0
- package/dist/services/billing/payment.service.d.ts +1 -12
- package/dist/services/billing/payment.service.js +3 -6
- package/dist/services/billing/usage.service.d.ts +30 -10
- package/dist/services/billing/usage.service.js +87 -15
- package/dist/services/content/copilot.service.js +15 -29
- package/dist/services/content/flashcard-progress.service.js +9 -9
- package/dist/services/content/flashcard.service.d.ts +45 -1
- package/dist/services/content/flashcard.service.js +81 -68
- package/dist/services/content/media-analysis.service.js +27 -27
- package/dist/services/content/worksheet-generation.service.test.js +2 -2
- package/dist/services/workspace/workspace.service.d.ts +23 -67
- package/dist/services/workspace/workspace.service.js +69 -62
- package/dist/trpc.d.ts +12 -4
- package/dist/trpc.js +5 -11
- package/package.json +1 -1
- package/src/context.ts +33 -3
- package/src/lib/ai/index.ts +3 -0
- package/src/lib/ai/llm-client.ts +23 -0
- package/src/routers/auth.ts +1 -1
- package/src/routers/workspace.ts +4 -0
- package/src/services/billing/payment.service.ts +3 -6
- package/src/services/billing/usage.service.ts +190 -77
- package/src/services/content/copilot.service.ts +23 -32
- package/src/services/content/flashcard-progress.service.ts +12 -9
- package/src/services/content/flashcard.service.ts +89 -66
- package/src/services/content/media-analysis.service.ts +34 -29
- package/src/services/content/worksheet-generation.service.test.ts +2 -2
- package/src/services/workspace/workspace.service.ts +73 -66
- package/src/trpc.ts +5 -13
|
@@ -8,6 +8,7 @@ import { ai } from '../../lib/ai/index.js';
|
|
|
8
8
|
import { workspaceKbService } from '../workspace/workspace-kb.service.js';
|
|
9
9
|
import { getUserUsage, getUserPlanLimits } from '../billing/usage.service.js';
|
|
10
10
|
import { notifyArtifactFailed, notifyArtifactReady } from '../notifications/notification.service.js';
|
|
11
|
+
import { FlashcardService } from './flashcard.service.js';
|
|
11
12
|
|
|
12
13
|
type StepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'error';
|
|
13
14
|
const PIPELINE_STEPS = ['fileUpload', 'fileAnalysis', 'studyGuide', 'flashcards'] as const;
|
|
@@ -449,18 +450,34 @@ export class MediaAnalysisService extends BaseService {
|
|
|
449
450
|
{ ragContext: flashcardRag },
|
|
450
451
|
);
|
|
451
452
|
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
453
|
+
const flashcardService = new FlashcardService(this.db);
|
|
454
|
+
const primarySet = await flashcardService.ensurePrimarySet(userId, input.workspaceId);
|
|
455
|
+
const orderOffset = primarySet.flashcards.reduce(
|
|
456
|
+
(max, card) => Math.max(max, card.order),
|
|
457
|
+
-1,
|
|
458
|
+
);
|
|
459
|
+
const flashcardTitle =
|
|
460
|
+
files.length === 1
|
|
461
|
+
? `Flashcards - ${primaryFile.name}`
|
|
462
|
+
: `Flashcards - ${files.length} files`;
|
|
463
|
+
|
|
464
|
+
await this.db.artifact.update({
|
|
465
|
+
where: { id: primarySet.id },
|
|
466
|
+
data: { title: flashcardTitle },
|
|
462
467
|
});
|
|
463
468
|
|
|
469
|
+
const appendCard = async (front: string, back: string, index: number) => {
|
|
470
|
+
await this.db.flashcard.create({
|
|
471
|
+
data: {
|
|
472
|
+
artifactId: primarySet.id,
|
|
473
|
+
front,
|
|
474
|
+
back,
|
|
475
|
+
order: orderOffset + 1 + index,
|
|
476
|
+
tags: ['ai-generated', 'medium'],
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
|
|
464
481
|
try {
|
|
465
482
|
const parsed = typeof content === 'string' ? JSON.parse(content) : content;
|
|
466
483
|
const flashcardData = Array.isArray(parsed) ? parsed : (parsed.flashcards || []);
|
|
@@ -469,16 +486,7 @@ export class MediaAnalysisService extends BaseService {
|
|
|
469
486
|
const card = flashcardData[i];
|
|
470
487
|
const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
|
|
471
488
|
const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
|
|
472
|
-
|
|
473
|
-
await this.db.flashcard.create({
|
|
474
|
-
data: {
|
|
475
|
-
artifactId: artifact.id,
|
|
476
|
-
front: front,
|
|
477
|
-
back: back,
|
|
478
|
-
order: i,
|
|
479
|
-
tags: ['ai-generated', 'medium'],
|
|
480
|
-
},
|
|
481
|
-
});
|
|
489
|
+
await appendCard(front, back, i);
|
|
482
490
|
}
|
|
483
491
|
} catch (parseError) {
|
|
484
492
|
console.error(
|
|
@@ -490,19 +498,16 @@ export class MediaAnalysisService extends BaseService {
|
|
|
490
498
|
const line = lines[i];
|
|
491
499
|
if (line.includes(' - ')) {
|
|
492
500
|
const [front, back] = line.split(' - ');
|
|
493
|
-
await
|
|
494
|
-
data: {
|
|
495
|
-
artifactId: artifact.id,
|
|
496
|
-
front: front.trim(),
|
|
497
|
-
back: back.trim(),
|
|
498
|
-
order: i,
|
|
499
|
-
tags: ['ai-generated', 'medium'],
|
|
500
|
-
},
|
|
501
|
-
});
|
|
501
|
+
await appendCard(front.trim(), back.trim(), i);
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
const artifact = await this.db.artifact.findUniqueOrThrow({
|
|
507
|
+
where: { id: primarySet.id },
|
|
508
|
+
include: { flashcards: true },
|
|
509
|
+
});
|
|
510
|
+
|
|
506
511
|
results.artifacts.flashcards = artifact;
|
|
507
512
|
await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
|
|
508
513
|
await notifyArtifactReady(this.db, {
|
|
@@ -9,8 +9,8 @@ test('mergeWorksheetGenerationConfig: override beats preset and legacy', () => {
|
|
|
9
9
|
{ numQuestions: 3, difficulty: 'hard' },
|
|
10
10
|
);
|
|
11
11
|
assert.equal(r.mode, 'quiz');
|
|
12
|
-
assert.equal(r.numQuestions,
|
|
13
|
-
assert.equal(r.difficulty, '
|
|
12
|
+
assert.equal(r.numQuestions, 3);
|
|
13
|
+
assert.equal(r.difficulty, 'hard');
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
test('normalizeWorksheetProblemForDb coerces MCQ answer to option index', () => {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { PrismaClient } from '@prisma/client';
|
|
2
2
|
import { TRPCError } from '@trpc/server';
|
|
3
3
|
import { BaseService } from '../base.service.js';
|
|
4
|
-
import { ArtifactType } from '../../lib/constants.js';
|
|
5
4
|
import { supabaseClient } from '../../lib/storage.js';
|
|
6
5
|
import PusherService from '../../lib/pusher.js';
|
|
7
6
|
import { ai } from '../../lib/ai/index.js';
|
|
8
7
|
import { workspaceKbService } from './workspace-kb.service.js';
|
|
8
|
+
import {
|
|
9
|
+
getAccountSummary,
|
|
10
|
+
invalidateUserBillingCache,
|
|
11
|
+
} from '../billing/usage.service.js';
|
|
9
12
|
import { getUserStorageLimit } from '../billing/subscription.service.js';
|
|
10
13
|
import { notifyWorkspaceDeleted } from '../notifications/notification.service.js';
|
|
11
14
|
|
|
@@ -26,25 +29,41 @@ export class WorkspaceService extends BaseService {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
async getTree(userId: string) {
|
|
29
|
-
const allFolders = await
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
const [allFolders, allWorkspaces] = await Promise.all([
|
|
33
|
+
this.db.folder.findMany({
|
|
34
|
+
where: { ownerId: userId },
|
|
35
|
+
select: {
|
|
36
|
+
id: true,
|
|
37
|
+
name: true,
|
|
38
|
+
parentId: true,
|
|
39
|
+
color: true,
|
|
40
|
+
markerColor: true,
|
|
41
|
+
updatedAt: true,
|
|
42
|
+
},
|
|
43
|
+
orderBy: { updatedAt: 'desc' },
|
|
44
|
+
}),
|
|
45
|
+
this.db.workspace.findMany({
|
|
46
|
+
where: { ownerId: userId },
|
|
47
|
+
select: {
|
|
48
|
+
id: true,
|
|
49
|
+
title: true,
|
|
50
|
+
folderId: true,
|
|
51
|
+
icon: true,
|
|
52
|
+
color: true,
|
|
53
|
+
markerColor: true,
|
|
54
|
+
updatedAt: true,
|
|
55
|
+
uploads: {
|
|
56
|
+
select: {
|
|
57
|
+
id: true,
|
|
58
|
+
name: true,
|
|
59
|
+
mimeType: true,
|
|
60
|
+
createdAt: true,
|
|
61
|
+
},
|
|
43
62
|
},
|
|
44
63
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
orderBy: { updatedAt: 'desc' },
|
|
65
|
+
}),
|
|
66
|
+
]);
|
|
48
67
|
|
|
49
68
|
return { folders: allFolders, workspaces: allWorkspaces };
|
|
50
69
|
}
|
|
@@ -65,30 +84,19 @@ export class WorkspaceService extends BaseService {
|
|
|
65
84
|
ownerId: userId,
|
|
66
85
|
folderId: input.parentId ?? null,
|
|
67
86
|
...(input.markerColor !== undefined ? { markerColor: input.markerColor } : {}),
|
|
68
|
-
artifacts: {
|
|
69
|
-
create: {
|
|
70
|
-
type: ArtifactType.FLASHCARD_SET,
|
|
71
|
-
title: 'New Flashcard Set',
|
|
72
|
-
},
|
|
73
|
-
createMany: {
|
|
74
|
-
data: [
|
|
75
|
-
{ type: ArtifactType.WORKSHEET, title: 'Worksheet 1' },
|
|
76
|
-
{ type: ArtifactType.WORKSHEET, title: 'Worksheet 2' },
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
87
|
},
|
|
81
88
|
});
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
void ai.backend.initSession(ws.id, userId).catch((err) => {
|
|
84
91
|
this.logger.error('Failed to init AI session on workspace creation:', err);
|
|
85
92
|
});
|
|
86
93
|
|
|
87
|
-
|
|
94
|
+
void workspaceKbService.ensureWorkspaceKb(ws.id, userId, input.name).catch((err) => {
|
|
88
95
|
this.logger.error('Failed to create workspace knowledge base:', err);
|
|
89
96
|
});
|
|
90
97
|
|
|
91
|
-
|
|
98
|
+
invalidateUserBillingCache(userId);
|
|
99
|
+
void PusherService.emitLibraryUpdate(userId);
|
|
92
100
|
|
|
93
101
|
return ws;
|
|
94
102
|
}
|
|
@@ -130,46 +138,45 @@ export class WorkspaceService extends BaseService {
|
|
|
130
138
|
async get(userId: string, id: string) {
|
|
131
139
|
const ws = await this.db.workspace.findFirst({
|
|
132
140
|
where: { id, ownerId: userId },
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
select: {
|
|
142
|
+
id: true,
|
|
143
|
+
title: true,
|
|
144
|
+
description: true,
|
|
145
|
+
icon: true,
|
|
146
|
+
color: true,
|
|
147
|
+
markerColor: true,
|
|
148
|
+
ownerId: true,
|
|
149
|
+
folderId: true,
|
|
150
|
+
fileBeingAnalyzed: true,
|
|
151
|
+
analysisProgress: true,
|
|
152
|
+
needsAnalysis: true,
|
|
153
|
+
createdAt: true,
|
|
154
|
+
updatedAt: true,
|
|
155
|
+
folder: { select: { id: true, name: true, color: true } },
|
|
156
|
+
uploads: {
|
|
157
|
+
select: {
|
|
158
|
+
id: true,
|
|
159
|
+
name: true,
|
|
160
|
+
mimeType: true,
|
|
161
|
+
size: true,
|
|
162
|
+
createdAt: true,
|
|
163
|
+
objectKey: true,
|
|
164
|
+
},
|
|
165
|
+
orderBy: { createdAt: 'desc' },
|
|
166
|
+
},
|
|
137
167
|
},
|
|
138
168
|
});
|
|
139
169
|
if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
140
170
|
return ws;
|
|
141
171
|
}
|
|
142
172
|
|
|
143
|
-
async
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
const folders = await this.db.folder.findMany({
|
|
150
|
-
where: { OR: [{ ownerId: userId }] },
|
|
151
|
-
});
|
|
152
|
-
const lastUpdated = await this.db.workspace.findFirst({
|
|
153
|
-
where: {
|
|
154
|
-
OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
|
|
155
|
-
},
|
|
156
|
-
orderBy: { updatedAt: 'desc' },
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const spaceLeft = await this.db.fileAsset.aggregate({
|
|
160
|
-
where: { workspaceId: { in: workspaces.map((ws: any) => ws.id) }, userId },
|
|
161
|
-
_sum: { size: true },
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const storageLimit = await getUserStorageLimit(userId);
|
|
173
|
+
async getAccountSummary(userId: string) {
|
|
174
|
+
return getAccountSummary(userId);
|
|
175
|
+
}
|
|
165
176
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
lastUpdated: lastUpdated?.updatedAt,
|
|
170
|
-
spaceUsed: spaceLeft._sum?.size ?? 0,
|
|
171
|
-
spaceTotal: storageLimit,
|
|
172
|
-
};
|
|
177
|
+
async getStats(userId: string) {
|
|
178
|
+
const summary = await getAccountSummary(userId);
|
|
179
|
+
return summary.stats;
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
async update(
|
package/src/trpc.ts
CHANGED
|
@@ -95,12 +95,8 @@ const errorHandler = middleware(async ({ next }) => {
|
|
|
95
95
|
/**
|
|
96
96
|
* Middleware that enforces email verification
|
|
97
97
|
*/
|
|
98
|
-
const isVerified = middleware(
|
|
99
|
-
const user =
|
|
100
|
-
where: { id: (ctx.session as any).user.id },
|
|
101
|
-
select: { emailVerified: true },
|
|
102
|
-
});
|
|
103
|
-
|
|
98
|
+
const isVerified = middleware(({ ctx, next }) => {
|
|
99
|
+
const user = (ctx.session as { user?: { emailVerified?: boolean } })?.user;
|
|
104
100
|
if (!user?.emailVerified) {
|
|
105
101
|
throw new TRPCError({
|
|
106
102
|
code: "FORBIDDEN",
|
|
@@ -168,13 +164,9 @@ const checkUsageLimit = middleware(async ({ ctx, next, path }) => {
|
|
|
168
164
|
/**
|
|
169
165
|
* Middleware that enforces system admin role
|
|
170
166
|
*/
|
|
171
|
-
const isAdmin = middleware(
|
|
172
|
-
const user =
|
|
173
|
-
|
|
174
|
-
include: { role: true },
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
if (user?.role?.name !== 'System Admin') {
|
|
167
|
+
const isAdmin = middleware(({ ctx, next }) => {
|
|
168
|
+
const user = (ctx.session as { user?: { isSystemAdmin?: boolean } })?.user;
|
|
169
|
+
if (!user?.isSystemAdmin) {
|
|
178
170
|
throw new TRPCError({
|
|
179
171
|
code: "FORBIDDEN",
|
|
180
172
|
message: "You do not have permission to access this resource",
|