@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
|
@@ -7,6 +7,7 @@ import { ai } from '../../lib/ai/index.js';
|
|
|
7
7
|
import { workspaceKbService } from '../workspace/workspace-kb.service.js';
|
|
8
8
|
import { getUserUsage, getUserPlanLimits } from '../billing/usage.service.js';
|
|
9
9
|
import { notifyArtifactFailed, notifyArtifactReady } from '../notifications/notification.service.js';
|
|
10
|
+
import { FlashcardService } from './flashcard.service.js';
|
|
10
11
|
const PIPELINE_STEPS = ['fileUpload', 'fileAnalysis', 'studyGuide', 'flashcards'];
|
|
11
12
|
const PROGRESS_FILENAME_MAX_CHARS = 20;
|
|
12
13
|
function truncateProgressFilename(value, maxChars = PROGRESS_FILENAME_MAX_CHARS) {
|
|
@@ -312,16 +313,27 @@ export class MediaAnalysisService extends BaseService {
|
|
|
312
313
|
}));
|
|
313
314
|
const flashcardRag = await workspaceKbService.retrieveContext(input.workspaceId, 'flashcard key concepts facts definitions questions', 8);
|
|
314
315
|
const content = await ai.backend.generateFlashcardQuestions(input.workspaceId, userId, 10, 'medium', undefined, { ragContext: flashcardRag });
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
},
|
|
316
|
+
const flashcardService = new FlashcardService(this.db);
|
|
317
|
+
const primarySet = await flashcardService.ensurePrimarySet(userId, input.workspaceId);
|
|
318
|
+
const orderOffset = primarySet.flashcards.reduce((max, card) => Math.max(max, card.order), -1);
|
|
319
|
+
const flashcardTitle = files.length === 1
|
|
320
|
+
? `Flashcards - ${primaryFile.name}`
|
|
321
|
+
: `Flashcards - ${files.length} files`;
|
|
322
|
+
await this.db.artifact.update({
|
|
323
|
+
where: { id: primarySet.id },
|
|
324
|
+
data: { title: flashcardTitle },
|
|
324
325
|
});
|
|
326
|
+
const appendCard = async (front, back, index) => {
|
|
327
|
+
await this.db.flashcard.create({
|
|
328
|
+
data: {
|
|
329
|
+
artifactId: primarySet.id,
|
|
330
|
+
front,
|
|
331
|
+
back,
|
|
332
|
+
order: orderOffset + 1 + index,
|
|
333
|
+
tags: ['ai-generated', 'medium'],
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
};
|
|
325
337
|
try {
|
|
326
338
|
const parsed = typeof content === 'string' ? JSON.parse(content) : content;
|
|
327
339
|
const flashcardData = Array.isArray(parsed) ? parsed : (parsed.flashcards || []);
|
|
@@ -329,15 +341,7 @@ export class MediaAnalysisService extends BaseService {
|
|
|
329
341
|
const card = flashcardData[i];
|
|
330
342
|
const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
|
|
331
343
|
const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
|
|
332
|
-
await
|
|
333
|
-
data: {
|
|
334
|
-
artifactId: artifact.id,
|
|
335
|
-
front: front,
|
|
336
|
-
back: back,
|
|
337
|
-
order: i,
|
|
338
|
-
tags: ['ai-generated', 'medium'],
|
|
339
|
-
},
|
|
340
|
-
});
|
|
344
|
+
await appendCard(front, back, i);
|
|
341
345
|
}
|
|
342
346
|
}
|
|
343
347
|
catch (parseError) {
|
|
@@ -347,18 +351,14 @@ export class MediaAnalysisService extends BaseService {
|
|
|
347
351
|
const line = lines[i];
|
|
348
352
|
if (line.includes(' - ')) {
|
|
349
353
|
const [front, back] = line.split(' - ');
|
|
350
|
-
await
|
|
351
|
-
data: {
|
|
352
|
-
artifactId: artifact.id,
|
|
353
|
-
front: front.trim(),
|
|
354
|
-
back: back.trim(),
|
|
355
|
-
order: i,
|
|
356
|
-
tags: ['ai-generated', 'medium'],
|
|
357
|
-
},
|
|
358
|
-
});
|
|
354
|
+
await appendCard(front.trim(), back.trim(), i);
|
|
359
355
|
}
|
|
360
356
|
}
|
|
361
357
|
}
|
|
358
|
+
const artifact = await this.db.artifact.findUniqueOrThrow({
|
|
359
|
+
where: { id: primarySet.id },
|
|
360
|
+
include: { flashcards: true },
|
|
361
|
+
});
|
|
362
362
|
results.artifacts.flashcards = artifact;
|
|
363
363
|
await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
|
|
364
364
|
await notifyArtifactReady(this.db, {
|
|
@@ -4,8 +4,8 @@ import { mergeWorksheetGenerationConfig, normalizeWorksheetProblemForDb } from '
|
|
|
4
4
|
test('mergeWorksheetGenerationConfig: override beats preset and legacy', () => {
|
|
5
5
|
const r = mergeWorksheetGenerationConfig({ mode: 'practice', numQuestions: 5, difficulty: 'easy' }, { mode: 'quiz', numQuestions: 10, difficulty: 'medium' }, { numQuestions: 3, difficulty: 'hard' });
|
|
6
6
|
assert.equal(r.mode, 'quiz');
|
|
7
|
-
assert.equal(r.numQuestions,
|
|
8
|
-
assert.equal(r.difficulty, '
|
|
7
|
+
assert.equal(r.numQuestions, 3);
|
|
8
|
+
assert.equal(r.difficulty, 'hard');
|
|
9
9
|
});
|
|
10
10
|
test('normalizeWorksheetProblemForDb coerces MCQ answer to option index', () => {
|
|
11
11
|
const row = normalizeWorksheetProblemForDb({
|
|
@@ -33,35 +33,26 @@ export declare class WorkspaceService extends BaseService {
|
|
|
33
33
|
folders: {
|
|
34
34
|
name: string;
|
|
35
35
|
id: string;
|
|
36
|
-
createdAt: Date;
|
|
37
36
|
updatedAt: Date;
|
|
38
|
-
ownerId: string;
|
|
39
37
|
parentId: string | null;
|
|
40
38
|
color: string;
|
|
41
39
|
markerColor: string | null;
|
|
42
40
|
}[];
|
|
43
|
-
workspaces:
|
|
44
|
-
uploads: {
|
|
45
|
-
name: string;
|
|
46
|
-
id: string;
|
|
47
|
-
createdAt: Date;
|
|
48
|
-
mimeType: string;
|
|
49
|
-
}[];
|
|
50
|
-
} & {
|
|
41
|
+
workspaces: {
|
|
51
42
|
id: string;
|
|
52
|
-
createdAt: Date;
|
|
53
43
|
updatedAt: Date;
|
|
54
44
|
title: string;
|
|
55
|
-
description: string | null;
|
|
56
|
-
ownerId: string;
|
|
57
45
|
color: string;
|
|
58
46
|
markerColor: string | null;
|
|
59
47
|
icon: string;
|
|
60
48
|
folderId: string | null;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
49
|
+
uploads: {
|
|
50
|
+
name: string;
|
|
51
|
+
id: string;
|
|
52
|
+
createdAt: Date;
|
|
53
|
+
mimeType: string;
|
|
54
|
+
}[];
|
|
55
|
+
}[];
|
|
65
56
|
}>;
|
|
66
57
|
create(userId: string, input: {
|
|
67
58
|
name: string;
|
|
@@ -122,70 +113,35 @@ export declare class WorkspaceService extends BaseService {
|
|
|
122
113
|
markerColor: string | null;
|
|
123
114
|
}>;
|
|
124
115
|
get(userId: string, id: string): Promise<{
|
|
116
|
+
id: string;
|
|
117
|
+
createdAt: Date;
|
|
118
|
+
updatedAt: Date;
|
|
119
|
+
title: string;
|
|
120
|
+
description: string | null;
|
|
125
121
|
folder: {
|
|
126
122
|
name: string;
|
|
127
123
|
id: string;
|
|
128
|
-
createdAt: Date;
|
|
129
|
-
updatedAt: Date;
|
|
130
|
-
ownerId: string;
|
|
131
|
-
parentId: string | null;
|
|
132
124
|
color: string;
|
|
133
|
-
markerColor: string | null;
|
|
134
125
|
} | null;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
difficulty: import("@prisma/client").$Enums.Difficulty | null;
|
|
144
|
-
estimatedTime: string | null;
|
|
145
|
-
createdById: string | null;
|
|
146
|
-
description: string | null;
|
|
147
|
-
generating: boolean;
|
|
148
|
-
generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
149
|
-
worksheetConfig: import("@prisma/client/runtime/library").JsonValue | null;
|
|
150
|
-
imageObjectKey: string | null;
|
|
151
|
-
}[];
|
|
126
|
+
ownerId: string;
|
|
127
|
+
color: string;
|
|
128
|
+
markerColor: string | null;
|
|
129
|
+
icon: string;
|
|
130
|
+
folderId: string | null;
|
|
131
|
+
fileBeingAnalyzed: boolean;
|
|
132
|
+
analysisProgress: import("@prisma/client/runtime/library").JsonValue;
|
|
133
|
+
needsAnalysis: boolean;
|
|
152
134
|
uploads: {
|
|
153
|
-
meta: import("@prisma/client/runtime/library").JsonValue | null;
|
|
154
135
|
name: string;
|
|
155
136
|
id: string;
|
|
156
137
|
createdAt: Date;
|
|
157
|
-
workspaceId: string | null;
|
|
158
|
-
userId: string | null;
|
|
159
138
|
mimeType: string;
|
|
160
139
|
size: number;
|
|
161
|
-
bucket: string | null;
|
|
162
140
|
objectKey: string | null;
|
|
163
|
-
url: string | null;
|
|
164
|
-
checksum: string | null;
|
|
165
|
-
aiTranscription: import("@prisma/client/runtime/library").JsonValue | null;
|
|
166
141
|
}[];
|
|
167
|
-
} & {
|
|
168
|
-
id: string;
|
|
169
|
-
createdAt: Date;
|
|
170
|
-
updatedAt: Date;
|
|
171
|
-
title: string;
|
|
172
|
-
description: string | null;
|
|
173
|
-
ownerId: string;
|
|
174
|
-
color: string;
|
|
175
|
-
markerColor: string | null;
|
|
176
|
-
icon: string;
|
|
177
|
-
folderId: string | null;
|
|
178
|
-
fileBeingAnalyzed: boolean;
|
|
179
|
-
analysisProgress: import("@prisma/client/runtime/library").JsonValue | null;
|
|
180
|
-
needsAnalysis: boolean;
|
|
181
|
-
}>;
|
|
182
|
-
getStats(userId: string): Promise<{
|
|
183
|
-
workspaces: number;
|
|
184
|
-
folders: number;
|
|
185
|
-
lastUpdated: Date | undefined;
|
|
186
|
-
spaceUsed: number;
|
|
187
|
-
spaceTotal: number;
|
|
188
142
|
}>;
|
|
143
|
+
getAccountSummary(userId: string): Promise<import("../billing/usage.service.js").AccountSummary>;
|
|
144
|
+
getStats(userId: string): Promise<import("../billing/usage.service.js").AccountStats>;
|
|
189
145
|
update(userId: string, input: {
|
|
190
146
|
id: string;
|
|
191
147
|
name?: string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { TRPCError } from '@trpc/server';
|
|
2
2
|
import { BaseService } from '../base.service.js';
|
|
3
|
-
import { ArtifactType } from '../../lib/constants.js';
|
|
4
3
|
import { supabaseClient } from '../../lib/storage.js';
|
|
5
4
|
import PusherService from '../../lib/pusher.js';
|
|
6
5
|
import { ai } from '../../lib/ai/index.js';
|
|
7
6
|
import { workspaceKbService } from './workspace-kb.service.js';
|
|
7
|
+
import { getAccountSummary, invalidateUserBillingCache, } from '../billing/usage.service.js';
|
|
8
8
|
import { getUserStorageLimit } from '../billing/subscription.service.js';
|
|
9
9
|
import { notifyWorkspaceDeleted } from '../notifications/notification.service.js';
|
|
10
10
|
export class WorkspaceService extends BaseService {
|
|
@@ -22,24 +22,41 @@ export class WorkspaceService extends BaseService {
|
|
|
22
22
|
return { workspaces, folders };
|
|
23
23
|
}
|
|
24
24
|
async getTree(userId) {
|
|
25
|
-
const allFolders = await
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
const [allFolders, allWorkspaces] = await Promise.all([
|
|
26
|
+
this.db.folder.findMany({
|
|
27
|
+
where: { ownerId: userId },
|
|
28
|
+
select: {
|
|
29
|
+
id: true,
|
|
30
|
+
name: true,
|
|
31
|
+
parentId: true,
|
|
32
|
+
color: true,
|
|
33
|
+
markerColor: true,
|
|
34
|
+
updatedAt: true,
|
|
35
|
+
},
|
|
36
|
+
orderBy: { updatedAt: 'desc' },
|
|
37
|
+
}),
|
|
38
|
+
this.db.workspace.findMany({
|
|
39
|
+
where: { ownerId: userId },
|
|
40
|
+
select: {
|
|
41
|
+
id: true,
|
|
42
|
+
title: true,
|
|
43
|
+
folderId: true,
|
|
44
|
+
icon: true,
|
|
45
|
+
color: true,
|
|
46
|
+
markerColor: true,
|
|
47
|
+
updatedAt: true,
|
|
48
|
+
uploads: {
|
|
49
|
+
select: {
|
|
50
|
+
id: true,
|
|
51
|
+
name: true,
|
|
52
|
+
mimeType: true,
|
|
53
|
+
createdAt: true,
|
|
54
|
+
},
|
|
38
55
|
},
|
|
39
56
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
orderBy: { updatedAt: 'desc' },
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
43
60
|
return { folders: allFolders, workspaces: allWorkspaces };
|
|
44
61
|
}
|
|
45
62
|
async create(userId, input) {
|
|
@@ -50,27 +67,16 @@ export class WorkspaceService extends BaseService {
|
|
|
50
67
|
ownerId: userId,
|
|
51
68
|
folderId: input.parentId ?? null,
|
|
52
69
|
...(input.markerColor !== undefined ? { markerColor: input.markerColor } : {}),
|
|
53
|
-
artifacts: {
|
|
54
|
-
create: {
|
|
55
|
-
type: ArtifactType.FLASHCARD_SET,
|
|
56
|
-
title: 'New Flashcard Set',
|
|
57
|
-
},
|
|
58
|
-
createMany: {
|
|
59
|
-
data: [
|
|
60
|
-
{ type: ArtifactType.WORKSHEET, title: 'Worksheet 1' },
|
|
61
|
-
{ type: ArtifactType.WORKSHEET, title: 'Worksheet 2' },
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
70
|
},
|
|
66
71
|
});
|
|
67
|
-
|
|
72
|
+
void ai.backend.initSession(ws.id, userId).catch((err) => {
|
|
68
73
|
this.logger.error('Failed to init AI session on workspace creation:', err);
|
|
69
74
|
});
|
|
70
|
-
|
|
75
|
+
void workspaceKbService.ensureWorkspaceKb(ws.id, userId, input.name).catch((err) => {
|
|
71
76
|
this.logger.error('Failed to create workspace knowledge base:', err);
|
|
72
77
|
});
|
|
73
|
-
|
|
78
|
+
invalidateUserBillingCache(userId);
|
|
79
|
+
void PusherService.emitLibraryUpdate(userId);
|
|
74
80
|
return ws;
|
|
75
81
|
}
|
|
76
82
|
async createFolder(userId, input) {
|
|
@@ -101,43 +107,44 @@ export class WorkspaceService extends BaseService {
|
|
|
101
107
|
async get(userId, id) {
|
|
102
108
|
const ws = await this.db.workspace.findFirst({
|
|
103
109
|
where: { id, ownerId: userId },
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
select: {
|
|
111
|
+
id: true,
|
|
112
|
+
title: true,
|
|
113
|
+
description: true,
|
|
114
|
+
icon: true,
|
|
115
|
+
color: true,
|
|
116
|
+
markerColor: true,
|
|
117
|
+
ownerId: true,
|
|
118
|
+
folderId: true,
|
|
119
|
+
fileBeingAnalyzed: true,
|
|
120
|
+
analysisProgress: true,
|
|
121
|
+
needsAnalysis: true,
|
|
122
|
+
createdAt: true,
|
|
123
|
+
updatedAt: true,
|
|
124
|
+
folder: { select: { id: true, name: true, color: true } },
|
|
125
|
+
uploads: {
|
|
126
|
+
select: {
|
|
127
|
+
id: true,
|
|
128
|
+
name: true,
|
|
129
|
+
mimeType: true,
|
|
130
|
+
size: true,
|
|
131
|
+
createdAt: true,
|
|
132
|
+
objectKey: true,
|
|
133
|
+
},
|
|
134
|
+
orderBy: { createdAt: 'desc' },
|
|
135
|
+
},
|
|
108
136
|
},
|
|
109
137
|
});
|
|
110
138
|
if (!ws)
|
|
111
139
|
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
112
140
|
return ws;
|
|
113
141
|
}
|
|
142
|
+
async getAccountSummary(userId) {
|
|
143
|
+
return getAccountSummary(userId);
|
|
144
|
+
}
|
|
114
145
|
async getStats(userId) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
const folders = await this.db.folder.findMany({
|
|
121
|
-
where: { OR: [{ ownerId: userId }] },
|
|
122
|
-
});
|
|
123
|
-
const lastUpdated = await this.db.workspace.findFirst({
|
|
124
|
-
where: {
|
|
125
|
-
OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
|
|
126
|
-
},
|
|
127
|
-
orderBy: { updatedAt: 'desc' },
|
|
128
|
-
});
|
|
129
|
-
const spaceLeft = await this.db.fileAsset.aggregate({
|
|
130
|
-
where: { workspaceId: { in: workspaces.map((ws) => ws.id) }, userId },
|
|
131
|
-
_sum: { size: true },
|
|
132
|
-
});
|
|
133
|
-
const storageLimit = await getUserStorageLimit(userId);
|
|
134
|
-
return {
|
|
135
|
-
workspaces: workspaces.length,
|
|
136
|
-
folders: folders.length,
|
|
137
|
-
lastUpdated: lastUpdated?.updatedAt,
|
|
138
|
-
spaceUsed: spaceLeft._sum?.size ?? 0,
|
|
139
|
-
spaceTotal: storageLimit,
|
|
140
|
-
};
|
|
146
|
+
const summary = await getAccountSummary(userId);
|
|
147
|
+
return summary.stats;
|
|
141
148
|
}
|
|
142
149
|
async update(userId, input) {
|
|
143
150
|
const existed = await this.db.workspace.findFirst({
|
package/dist/trpc.d.ts
CHANGED
|
@@ -20,7 +20,9 @@ export declare const publicProcedure: import("@trpc/server").TRPCProcedureBuilde
|
|
|
20
20
|
/** Exported procedures with middleware */
|
|
21
21
|
export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
22
22
|
userId: any;
|
|
23
|
-
session:
|
|
23
|
+
session: {
|
|
24
|
+
user: import("./context.js").SessionUser;
|
|
25
|
+
};
|
|
24
26
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
25
27
|
res: import("express").Response<any, Record<string, any>>;
|
|
26
28
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
@@ -28,7 +30,9 @@ export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilde
|
|
|
28
30
|
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
29
31
|
export declare const verifiedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
30
32
|
userId: any;
|
|
31
|
-
session:
|
|
33
|
+
session: {
|
|
34
|
+
user: import("./context.js").SessionUser;
|
|
35
|
+
};
|
|
32
36
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
33
37
|
res: import("express").Response<any, Record<string, any>>;
|
|
34
38
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
@@ -36,7 +40,9 @@ export declare const verifiedProcedure: import("@trpc/server").TRPCProcedureBuil
|
|
|
36
40
|
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
37
41
|
export declare const adminProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
38
42
|
userId: any;
|
|
39
|
-
session:
|
|
43
|
+
session: {
|
|
44
|
+
user: import("./context.js").SessionUser;
|
|
45
|
+
};
|
|
40
46
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
41
47
|
res: import("express").Response<any, Record<string, any>>;
|
|
42
48
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
@@ -44,7 +50,9 @@ export declare const adminProcedure: import("@trpc/server").TRPCProcedureBuilder
|
|
|
44
50
|
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
45
51
|
export declare const limitedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
46
52
|
userId: any;
|
|
47
|
-
session:
|
|
53
|
+
session: {
|
|
54
|
+
user: import("./context.js").SessionUser;
|
|
55
|
+
};
|
|
48
56
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
49
57
|
res: import("express").Response<any, Record<string, any>>;
|
|
50
58
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
package/dist/trpc.js
CHANGED
|
@@ -79,11 +79,8 @@ const errorHandler = middleware(async ({ next }) => {
|
|
|
79
79
|
/**
|
|
80
80
|
* Middleware that enforces email verification
|
|
81
81
|
*/
|
|
82
|
-
const isVerified = middleware(
|
|
83
|
-
const user =
|
|
84
|
-
where: { id: ctx.session.user.id },
|
|
85
|
-
select: { emailVerified: true },
|
|
86
|
-
});
|
|
82
|
+
const isVerified = middleware(({ ctx, next }) => {
|
|
83
|
+
const user = ctx.session?.user;
|
|
87
84
|
if (!user?.emailVerified) {
|
|
88
85
|
throw new TRPCError({
|
|
89
86
|
code: "FORBIDDEN",
|
|
@@ -139,12 +136,9 @@ const checkUsageLimit = middleware(async ({ ctx, next, path }) => {
|
|
|
139
136
|
/**
|
|
140
137
|
* Middleware that enforces system admin role
|
|
141
138
|
*/
|
|
142
|
-
const isAdmin = middleware(
|
|
143
|
-
const user =
|
|
144
|
-
|
|
145
|
-
include: { role: true },
|
|
146
|
-
});
|
|
147
|
-
if (user?.role?.name !== 'System Admin') {
|
|
139
|
+
const isAdmin = middleware(({ ctx, next }) => {
|
|
140
|
+
const user = ctx.session?.user;
|
|
141
|
+
if (!user?.isSystemAdmin) {
|
|
148
142
|
throw new TRPCError({
|
|
149
143
|
code: "FORBIDDEN",
|
|
150
144
|
message: "You do not have permission to access this resource",
|
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -4,14 +4,44 @@ import { prisma } from "./lib/prisma.js";
|
|
|
4
4
|
import { verifyCustomAuthCookie } from "./lib/auth.js";
|
|
5
5
|
import cookie from "cookie";
|
|
6
6
|
|
|
7
|
+
export type SessionUser = {
|
|
8
|
+
id: string;
|
|
9
|
+
emailVerified: boolean;
|
|
10
|
+
isSystemAdmin: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
7
13
|
export async function createContext({ req, res }: CreateExpressContextOptions) {
|
|
8
14
|
const cookies = cookie.parse(req.headers.cookie ?? "");
|
|
9
15
|
|
|
10
|
-
// Only use custom auth cookie
|
|
11
16
|
const custom = verifyCustomAuthCookie(cookies["auth_token"]);
|
|
12
|
-
|
|
17
|
+
|
|
13
18
|
if (custom) {
|
|
14
|
-
|
|
19
|
+
const user = await prisma.user.findUnique({
|
|
20
|
+
where: { id: custom.userId },
|
|
21
|
+
select: {
|
|
22
|
+
id: true,
|
|
23
|
+
emailVerified: true,
|
|
24
|
+
role: { select: { name: true } },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!user) {
|
|
29
|
+
return { db: prisma, session: null, req, res, cookies };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sessionUser: SessionUser = {
|
|
33
|
+
id: user.id,
|
|
34
|
+
emailVerified: !!user.emailVerified,
|
|
35
|
+
isSystemAdmin: user.role?.name === "System Admin",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
db: prisma,
|
|
40
|
+
session: { user: sessionUser } as { user: SessionUser },
|
|
41
|
+
req,
|
|
42
|
+
res,
|
|
43
|
+
cookies,
|
|
44
|
+
};
|
|
15
45
|
}
|
|
16
46
|
|
|
17
47
|
return { db: prisma, session: null, req, res, cookies };
|
package/src/lib/ai/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { aiConfig } from './config.js';
|
|
|
2
2
|
import {
|
|
3
3
|
complete,
|
|
4
4
|
completeText,
|
|
5
|
+
streamComplete,
|
|
5
6
|
} from './llm-client.js';
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_EMBEDDING_DIM,
|
|
@@ -21,6 +22,7 @@ export const ai = {
|
|
|
21
22
|
llm: {
|
|
22
23
|
complete,
|
|
23
24
|
completeText,
|
|
25
|
+
streamComplete,
|
|
24
26
|
},
|
|
25
27
|
embed: {
|
|
26
28
|
texts: embedTexts,
|
|
@@ -49,6 +51,7 @@ export {
|
|
|
49
51
|
aiConfig,
|
|
50
52
|
complete,
|
|
51
53
|
completeText,
|
|
54
|
+
streamComplete,
|
|
52
55
|
DEFAULT_EMBEDDING_DIM,
|
|
53
56
|
DEFAULT_EMBEDDING_MODEL,
|
|
54
57
|
embedQuery,
|
package/src/lib/ai/llm-client.ts
CHANGED
|
@@ -27,5 +27,28 @@ export async function completeText(
|
|
|
27
27
|
return response.choices?.[0]?.message?.content ?? '';
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export async function streamComplete(
|
|
31
|
+
messages: ChatMessage[],
|
|
32
|
+
onDelta: (delta: string) => void | Promise<void>,
|
|
33
|
+
options: CompleteOptions = {},
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const stream = await client.chat.completions.create({
|
|
36
|
+
model: options.model ?? aiConfig.llm.model,
|
|
37
|
+
messages,
|
|
38
|
+
stream: true,
|
|
39
|
+
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let content = '';
|
|
43
|
+
for await (const chunk of stream) {
|
|
44
|
+
const delta = chunk.choices?.[0]?.delta?.content ?? '';
|
|
45
|
+
if (!delta) continue;
|
|
46
|
+
content += delta;
|
|
47
|
+
await onDelta(delta);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return content;
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
/** @deprecated Use `complete()` from the ai module instead. */
|
|
31
54
|
export default complete;
|
package/src/routers/auth.ts
CHANGED
|
@@ -30,7 +30,7 @@ const passwordFieldSchema = z
|
|
|
30
30
|
.regex(/[^a-zA-Z0-9]/, 'Password must contain at least one special character');
|
|
31
31
|
|
|
32
32
|
export const auth = router({
|
|
33
|
-
updateProfile:
|
|
33
|
+
updateProfile: authedProcedure
|
|
34
34
|
.input(z.object({ name: z.string().min(1) }))
|
|
35
35
|
.mutation(({ ctx, input }) =>
|
|
36
36
|
new AuthService(ctx.db).updateProfile(ctx.session.user.id, input),
|
package/src/routers/workspace.ts
CHANGED
|
@@ -71,6 +71,10 @@ export const workspace = router({
|
|
|
71
71
|
new WorkspaceService(ctx.db).getStats(ctx.session.user.id),
|
|
72
72
|
),
|
|
73
73
|
|
|
74
|
+
getAccountSummary: authedProcedure.query(({ ctx }) =>
|
|
75
|
+
new WorkspaceService(ctx.db).getAccountSummary(ctx.session.user.id),
|
|
76
|
+
),
|
|
77
|
+
|
|
74
78
|
getStudyAnalytics: authedProcedure.query(({ ctx }) =>
|
|
75
79
|
new WorkspaceAnalyticsService(ctx.db).getStudyAnalytics(ctx.session.user.id),
|
|
76
80
|
),
|
|
@@ -5,7 +5,7 @@ import { Stripe } from 'stripe';
|
|
|
5
5
|
import { BaseService } from '../base.service.js';
|
|
6
6
|
import { stripe } from '../../lib/stripe.js';
|
|
7
7
|
import { env } from '../../lib/env.js';
|
|
8
|
-
import { getUserUsage, getUserPlanLimits } from './usage.service.js';
|
|
8
|
+
import { getAccountSummary, getUserUsage, getUserPlanLimits } from './usage.service.js';
|
|
9
9
|
import {
|
|
10
10
|
notifyPaymentSucceeded,
|
|
11
11
|
notifySubscriptionActivated,
|
|
@@ -423,11 +423,8 @@ export class PaymentService extends BaseService {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
async getUsageOverview(userId: string) {
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
getUserPlanLimits(userId),
|
|
429
|
-
]);
|
|
430
|
-
return { usage, limits, hasActivePlan: !!limits };
|
|
426
|
+
const { usage, limits, hasActivePlan } = await getAccountSummary(userId);
|
|
427
|
+
return { usage, limits, hasActivePlan };
|
|
431
428
|
}
|
|
432
429
|
|
|
433
430
|
getResourcePrices() {
|