@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.
Files changed (41) hide show
  1. package/dist/context.d.ts +14 -1
  2. package/dist/context.js +23 -2
  3. package/dist/lib/ai/index.d.ts +3 -2
  4. package/dist/lib/ai/index.js +3 -2
  5. package/dist/lib/ai/llm-client.d.ts +1 -0
  6. package/dist/lib/ai/llm-client.js +17 -0
  7. package/dist/routers/_app.d.ts +40 -80
  8. package/dist/routers/auth.js +1 -1
  9. package/dist/routers/flashcards.d.ts +12 -1
  10. package/dist/routers/payment.d.ts +1 -12
  11. package/dist/routers/workspace.d.ts +27 -67
  12. package/dist/routers/workspace.js +1 -0
  13. package/dist/services/billing/payment.service.d.ts +1 -12
  14. package/dist/services/billing/payment.service.js +3 -6
  15. package/dist/services/billing/usage.service.d.ts +30 -10
  16. package/dist/services/billing/usage.service.js +87 -15
  17. package/dist/services/content/copilot.service.js +15 -29
  18. package/dist/services/content/flashcard-progress.service.js +9 -9
  19. package/dist/services/content/flashcard.service.d.ts +45 -1
  20. package/dist/services/content/flashcard.service.js +81 -68
  21. package/dist/services/content/media-analysis.service.js +27 -27
  22. package/dist/services/content/worksheet-generation.service.test.js +2 -2
  23. package/dist/services/workspace/workspace.service.d.ts +23 -67
  24. package/dist/services/workspace/workspace.service.js +69 -62
  25. package/dist/trpc.d.ts +12 -4
  26. package/dist/trpc.js +5 -11
  27. package/package.json +1 -1
  28. package/src/context.ts +33 -3
  29. package/src/lib/ai/index.ts +3 -0
  30. package/src/lib/ai/llm-client.ts +23 -0
  31. package/src/routers/auth.ts +1 -1
  32. package/src/routers/workspace.ts +4 -0
  33. package/src/services/billing/payment.service.ts +3 -6
  34. package/src/services/billing/usage.service.ts +190 -77
  35. package/src/services/content/copilot.service.ts +23 -32
  36. package/src/services/content/flashcard-progress.service.ts +12 -9
  37. package/src/services/content/flashcard.service.ts +89 -66
  38. package/src/services/content/media-analysis.service.ts +34 -29
  39. package/src/services/content/worksheet-generation.service.test.ts +2 -2
  40. package/src/services/workspace/workspace.service.ts +73 -66
  41. package/src/trpc.ts +5 -13
@@ -1,4 +1,5 @@
1
1
  import { prisma } from '../../lib/prisma.js';
2
+ import { workspaceAccessWhere } from '../../repositories/workspace.repository.js';
2
3
  const FALLBACK_PLAN_LIMITS = {
3
4
  id: 'fallback-free',
4
5
  planId: 'fallback-free',
@@ -8,10 +9,37 @@ const FALLBACK_PLAN_LIMITS = {
8
9
  maxPodcasts: 0,
9
10
  maxStudyGuides: 1,
10
11
  };
12
+ const CACHE_TTL_MS = 30000;
13
+ const usageCache = new Map();
14
+ const limitsCache = new Map();
15
+ const accountSummaryCache = new Map();
16
+ function readCache(map, userId) {
17
+ const entry = map.get(userId);
18
+ if (!entry || entry.expiresAt <= Date.now()) {
19
+ map.delete(userId);
20
+ return null;
21
+ }
22
+ return entry.value;
23
+ }
24
+ function writeCache(map, userId, value) {
25
+ map.set(userId, { value, expiresAt: Date.now() + CACHE_TTL_MS });
26
+ }
27
+ /** Bust cached usage/limit reads after creates, deletes, or billing changes. */
28
+ export function invalidateUserBillingCache(userId) {
29
+ usageCache.delete(userId);
30
+ limitsCache.delete(userId);
31
+ accountSummaryCache.delete(userId);
32
+ }
33
+ function workspaceAccessFilter(userId) {
34
+ return workspaceAccessWhere(userId);
35
+ }
11
36
  /**
12
37
  * Counts all resources consumed by a user across the platform.
13
38
  */
14
39
  export async function getUserUsage(userId) {
40
+ const cached = readCache(usageCache, userId);
41
+ if (cached)
42
+ return cached;
15
43
  const [flashcards, worksheets, studyGuides, podcasts, storageResult] = await Promise.all([
16
44
  prisma.artifact.count({ where: { createdById: userId, type: 'FLASHCARD_SET' } }),
17
45
  prisma.artifact.count({ where: { createdById: userId, type: 'WORKSHEET' } }),
@@ -19,53 +47,55 @@ export async function getUserUsage(userId) {
19
47
  prisma.artifact.count({ where: { createdById: userId, type: 'PODCAST_EPISODE' } }),
20
48
  prisma.fileAsset.aggregate({
21
49
  _sum: { size: true },
22
- where: { userId }
23
- })
50
+ where: { userId },
51
+ }),
24
52
  ]);
25
- return {
53
+ const usage = {
26
54
  flashcards,
27
55
  worksheets,
28
56
  studyGuides,
29
57
  podcasts,
30
- storageBytes: Number(storageResult._sum.size || 0)
58
+ storageBytes: Number(storageResult._sum.size || 0),
31
59
  };
60
+ writeCache(usageCache, userId, usage);
61
+ return usage;
32
62
  }
33
63
  /**
34
64
  * Retrieves the specific plan limits for a user based on their active subscription
35
65
  * PLUS any extra credits purchased.
36
66
  */
37
67
  export async function getUserPlanLimits(userId) {
68
+ const cached = readCache(limitsCache, userId);
69
+ if (cached)
70
+ return cached;
38
71
  const [activeSub, freePlan, userCredits] = await Promise.all([
39
72
  prisma.subscription.findFirst({
40
73
  where: { userId, status: 'active' },
41
74
  include: { plan: { include: { limit: true } } },
42
- orderBy: { createdAt: 'desc' }
75
+ orderBy: { createdAt: 'desc' },
43
76
  }),
44
77
  prisma.plan.findFirst({
45
78
  where: {
46
79
  active: true,
47
80
  price: 0,
48
- limit: { isNot: null }
81
+ limit: { isNot: null },
49
82
  },
50
83
  include: { limit: true },
51
- orderBy: { createdAt: 'asc' }
84
+ orderBy: { createdAt: 'asc' },
52
85
  }),
53
86
  prisma.userCredit.groupBy({
54
87
  by: ['resourceType'],
55
88
  where: { userId },
56
- _sum: { amount: true }
57
- })
89
+ _sum: { amount: true },
90
+ }),
58
91
  ]);
59
92
  const baseLimit = activeSub?.plan?.limit ?? freePlan?.limit;
60
- // Prefer subscription limits, then DB free-plan limits, then hardcoded safety fallback.
61
93
  const base = baseLimit ?? FALLBACK_PLAN_LIMITS;
62
- // Fast lookup for credits
63
- const getCreditSum = (type) => userCredits.find(c => c.resourceType === type)?._sum.amount || 0;
64
- // Return the merged total limit
65
- return {
94
+ const getCreditSum = (type) => userCredits.find((c) => c.resourceType === type)?._sum.amount || 0;
95
+ const limits = {
66
96
  id: base.id,
67
97
  planId: base.planId,
68
- maxStorageBytes: BigInt(Number(base.maxStorageBytes) + (getCreditSum('STORAGE') * 1024 * 1024)),
98
+ maxStorageBytes: BigInt(Number(base.maxStorageBytes) + getCreditSum('STORAGE') * 1024 * 1024),
69
99
  maxWorksheets: base.maxWorksheets + getCreditSum('WORKSHEET'),
70
100
  maxFlashcards: base.maxFlashcards + getCreditSum('FLASHCARD_SET'),
71
101
  maxPodcasts: base.maxPodcasts + getCreditSum('PODCAST_EPISODE'),
@@ -74,4 +104,46 @@ export async function getUserPlanLimits(userId) {
74
104
  updatedAt: baseLimit?.updatedAt || new Date(),
75
105
  isFallbackPlan: !baseLimit,
76
106
  };
107
+ writeCache(limitsCache, userId, limits);
108
+ return limits;
109
+ }
110
+ /**
111
+ * Sidebar/dashboard summary: stats + usage + limits in one parallel DB round-trip batch.
112
+ */
113
+ export async function getAccountSummary(userId) {
114
+ const cached = readCache(accountSummaryCache, userId);
115
+ if (cached)
116
+ return cached;
117
+ const workspaceWhere = workspaceAccessFilter(userId);
118
+ const [workspaceMeta, folderCount, usage, limits, storageUsed] = await Promise.all([
119
+ prisma.workspace.aggregate({
120
+ where: workspaceWhere,
121
+ _count: { _all: true },
122
+ _max: { updatedAt: true },
123
+ }),
124
+ prisma.folder.count({ where: { ownerId: userId } }),
125
+ getUserUsage(userId),
126
+ getUserPlanLimits(userId),
127
+ prisma.fileAsset.aggregate({
128
+ where: {
129
+ userId,
130
+ workspace: workspaceWhere,
131
+ },
132
+ _sum: { size: true },
133
+ }),
134
+ ]);
135
+ const summary = {
136
+ stats: {
137
+ workspaces: workspaceMeta._count._all,
138
+ folders: folderCount,
139
+ lastUpdated: workspaceMeta._max.updatedAt,
140
+ spaceUsed: Number(storageUsed._sum.size ?? 0),
141
+ spaceTotal: Number(limits.maxStorageBytes),
142
+ },
143
+ usage,
144
+ limits,
145
+ hasActivePlan: !limits.isFallbackPlan,
146
+ };
147
+ writeCache(accountSummaryCache, userId, summary);
148
+ return summary;
77
149
  }
@@ -6,7 +6,7 @@ import { workspaceKbService } from '../workspace/workspace-kb.service.js';
6
6
  import { sanitizeString } from '../../lib/validation.js';
7
7
  import { workspaceAccessWhere } from '../../repositories/workspace.repository.js';
8
8
  import PusherService from '../../lib/pusher.js';
9
- import { ArtifactType } from '../../lib/constants.js';
9
+ import { FlashcardService } from './flashcard.service.js';
10
10
  export const copilotArtifactType = z.enum([
11
11
  'study-guide',
12
12
  'worksheet',
@@ -304,9 +304,6 @@ export class CopilotService extends BaseService {
304
304
  async ask(userId, input) {
305
305
  enforceRateLimit(userId, input.context.workspaceId);
306
306
  await this.assertWorkspaceAccess(userId, input.context.workspaceId);
307
- await PusherService.emitTaskComplete(input.context.workspaceId, 'copilot:thinking', {
308
- status: 'started',
309
- });
310
307
  const history = await this.loadHistory(input.conversationId);
311
308
  const model = await callCopilotModel({
312
309
  mode: 'ask',
@@ -318,9 +315,6 @@ export class CopilotService extends BaseService {
318
315
  model.rawContent ||
319
316
  'I could not generate a response.';
320
317
  const safeHighlights = normalizeHighlightInstructions(model.parsed?.highlights, input.context.documentPlainText ?? input.context.documentContent);
321
- await PusherService.emitTaskComplete(input.context.workspaceId, 'copilot:response', {
322
- status: 'completed',
323
- });
324
318
  await this.persistConversationExchange({
325
319
  userId,
326
320
  workspaceId: input.context.workspaceId,
@@ -333,9 +327,6 @@ export class CopilotService extends BaseService {
333
327
  async explainSelection(userId, input) {
334
328
  enforceRateLimit(userId, input.context.workspaceId);
335
329
  await this.assertWorkspaceAccess(userId, input.context.workspaceId);
336
- await PusherService.emitTaskComplete(input.context.workspaceId, 'copilot:thinking', {
337
- status: 'started',
338
- });
339
330
  const question = input.message && input.message.trim().length > 0
340
331
  ? input.message
341
332
  : 'Explain the selected text in simple study-friendly terms and include one practical example.';
@@ -349,9 +340,6 @@ export class CopilotService extends BaseService {
349
340
  const answer = getAnswerFromParsedJSON(model.parsed) ||
350
341
  model.rawContent ||
351
342
  'I could not explain this selection.';
352
- await PusherService.emitTaskComplete(input.context.workspaceId, 'copilot:response', {
353
- status: 'completed',
354
- });
355
343
  await this.persistConversationExchange({
356
344
  userId,
357
345
  workspaceId: input.context.workspaceId,
@@ -413,22 +401,20 @@ export class CopilotService extends BaseService {
413
401
  message: 'Copilot did not return valid flashcards.',
414
402
  });
415
403
  }
416
- const artifact = await this.db.artifact.create({
417
- data: {
418
- workspaceId: input.context.workspaceId,
419
- type: ArtifactType.FLASHCARD_SET,
420
- title: `Copilot Flashcards - ${new Date().toLocaleString()}`,
421
- createdById: userId,
422
- generating: false,
423
- flashcards: {
424
- create: cards.map((card, index) => ({
425
- front: sanitizeString(card.front, 200),
426
- back: sanitizeString(card.back, 500),
427
- order: index,
428
- tags: ['copilot-generated'],
429
- })),
430
- },
431
- },
404
+ const flashcardService = new FlashcardService(this.db);
405
+ const primarySet = await flashcardService.ensurePrimarySet(userId, input.context.workspaceId);
406
+ const orderOffset = primarySet.flashcards.reduce((max, card) => Math.max(max, card.order), -1);
407
+ await this.db.flashcard.createMany({
408
+ data: cards.map((card, index) => ({
409
+ artifactId: primarySet.id,
410
+ front: sanitizeString(card.front, 200),
411
+ back: sanitizeString(card.back, 500),
412
+ order: orderOffset + 1 + index,
413
+ tags: ['copilot-generated'],
414
+ })),
415
+ });
416
+ const artifact = await this.db.artifact.findUniqueOrThrow({
417
+ where: { id: primarySet.id },
432
418
  include: { flashcards: true },
433
419
  });
434
420
  await PusherService.emitTaskComplete(input.context.workspaceId, 'copilot:response', {
@@ -239,23 +239,23 @@ export class FlashcardProgressService extends BaseService {
239
239
  async getDueFlashcards(userId, workspaceId) {
240
240
  const now = new Date();
241
241
  const LOW_MASTERY_THRESHOLD = 50; // Consider mastery < 50 as low
242
- // Get the latest artifact in the workspace
243
- const latestArtifact = await this.db.artifact.findFirst({
242
+ const flashcardSets = await this.db.artifact.findMany({
244
243
  where: {
245
244
  workspaceId,
246
245
  type: 'FLASHCARD_SET',
247
246
  },
248
- orderBy: {
249
- updatedAt: 'desc',
250
- },
247
+ include: { _count: { select: { flashcards: true } } },
248
+ orderBy: { createdAt: 'asc' },
251
249
  });
252
- if (!latestArtifact) {
250
+ if (flashcardSets.length === 0) {
253
251
  return [];
254
252
  }
255
- // Get all flashcards from the latest artifact
253
+ const withCards = flashcardSets.filter((set) => set._count.flashcards > 0);
254
+ const candidates = withCards.length > 0 ? withCards : flashcardSets;
255
+ const primaryArtifact = candidates.reduce((best, current) => current._count.flashcards > best._count.flashcards ? current : best);
256
256
  const allFlashcards = await this.db.flashcard.findMany({
257
257
  where: {
258
- artifactId: latestArtifact.id,
258
+ artifactId: primaryArtifact.id,
259
259
  },
260
260
  include: {
261
261
  artifact: true,
@@ -290,7 +290,7 @@ export class FlashcardProgressService extends BaseService {
290
290
  }
291
291
  ],
292
292
  flashcard: {
293
- artifactId: latestArtifact.id,
293
+ artifactId: primaryArtifact.id,
294
294
  },
295
295
  },
296
296
  include: {
@@ -9,6 +9,39 @@ export declare const typedAnswerGradeSchema: z.ZodObject<{
9
9
  }, z.core.$strip>;
10
10
  export declare class FlashcardService extends BaseService {
11
11
  constructor(db: PrismaClient);
12
+ private findPrimarySet;
13
+ /** Returns the primary set, creating one only when flashcard content is first added. */
14
+ ensurePrimarySet(userId: string, workspaceId: string): Promise<{
15
+ flashcards: {
16
+ id: string;
17
+ createdAt: Date;
18
+ artifactId: string;
19
+ tags: string[];
20
+ order: number;
21
+ front: string;
22
+ back: string;
23
+ acceptedAnswers: string[];
24
+ }[];
25
+ _count: {
26
+ flashcards: number;
27
+ };
28
+ } & {
29
+ id: string;
30
+ createdAt: Date;
31
+ updatedAt: Date;
32
+ workspaceId: string;
33
+ type: import("@prisma/client").$Enums.ArtifactType;
34
+ title: string;
35
+ isArchived: boolean;
36
+ difficulty: import("@prisma/client").$Enums.Difficulty | null;
37
+ estimatedTime: string | null;
38
+ createdById: string | null;
39
+ description: string | null;
40
+ generating: boolean;
41
+ generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
42
+ worksheetConfig: import("@prisma/client/runtime/library").JsonValue | null;
43
+ imageObjectKey: string | null;
44
+ }>;
12
45
  listSets(userId: string, workspaceId: string): Promise<({
13
46
  versions: {
14
47
  id: string;
@@ -64,7 +97,7 @@ export declare class FlashcardService extends BaseService {
64
97
  back: string;
65
98
  acceptedAnswers: string[];
66
99
  })[]>;
67
- isGenerating(userId: string, workspaceId: string): Promise<boolean | undefined>;
100
+ isGenerating(userId: string, workspaceId: string): Promise<boolean>;
68
101
  createCard(userId: string, input: {
69
102
  workspaceId: string;
70
103
  front: string;
@@ -119,6 +152,17 @@ export declare class FlashcardService extends BaseService {
119
152
  tags?: string[];
120
153
  }): Promise<{
121
154
  artifact: {
155
+ flashcards: {
156
+ id: string;
157
+ createdAt: Date;
158
+ artifactId: string;
159
+ tags: string[];
160
+ order: number;
161
+ front: string;
162
+ back: string;
163
+ acceptedAnswers: string[];
164
+ }[];
165
+ } & {
122
166
  id: string;
123
167
  createdAt: Date;
124
168
  updatedAt: Date;
@@ -7,6 +7,7 @@ import { ai } from '../../lib/ai/index.js';
7
7
  import { workspaceKbService } from '../workspace/workspace-kb.service.js';
8
8
  import PusherService from '../../lib/pusher.js';
9
9
  import { notifyArtifactFailed, notifyArtifactReady } from '../notifications/notification.service.js';
10
+ import { invalidateUserBillingCache } from '../billing/usage.service.js';
10
11
  export const typedAnswerGradeSchema = z.object({
11
12
  isCorrect: z.boolean(),
12
13
  confidence: z.number().min(0).max(1),
@@ -41,6 +42,50 @@ export class FlashcardService extends BaseService {
41
42
  constructor(db) {
42
43
  super(db);
43
44
  }
45
+ async findPrimarySet(userId, workspaceId) {
46
+ const sets = await this.db.artifact.findMany({
47
+ where: {
48
+ workspaceId,
49
+ type: ArtifactType.FLASHCARD_SET,
50
+ workspace: workspaceAccessWhere(userId),
51
+ },
52
+ include: {
53
+ flashcards: { orderBy: { order: 'asc' } },
54
+ _count: { select: { flashcards: true } },
55
+ },
56
+ orderBy: { createdAt: 'asc' },
57
+ });
58
+ if (sets.length === 0)
59
+ return null;
60
+ const withCards = sets.filter((set) => set._count.flashcards > 0);
61
+ const candidates = withCards.length > 0 ? withCards : sets;
62
+ return candidates.reduce((best, current) => current._count.flashcards > best._count.flashcards ? current : best);
63
+ }
64
+ /** Returns the primary set, creating one only when flashcard content is first added. */
65
+ async ensurePrimarySet(userId, workspaceId) {
66
+ const existing = await this.findPrimarySet(userId, workspaceId);
67
+ if (existing)
68
+ return existing;
69
+ const workspace = await this.db.workspace.findFirst({
70
+ where: { id: workspaceId, ...workspaceAccessWhere(userId) },
71
+ select: { id: true, title: true },
72
+ });
73
+ if (!workspace) {
74
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' });
75
+ }
76
+ return this.db.artifact.create({
77
+ data: {
78
+ workspaceId,
79
+ type: ArtifactType.FLASHCARD_SET,
80
+ title: 'Flashcards',
81
+ createdById: userId,
82
+ },
83
+ include: {
84
+ flashcards: { orderBy: { order: 'asc' } },
85
+ _count: { select: { flashcards: true } },
86
+ },
87
+ });
88
+ }
44
89
  async listSets(userId, workspaceId) {
45
90
  const workspace = await this.db.workspace.findFirst({
46
91
  where: { id: workspaceId, ownerId: userId },
@@ -56,57 +101,37 @@ export class FlashcardService extends BaseService {
56
101
  });
57
102
  }
58
103
  async listCards(userId, workspaceId) {
59
- const set = await this.db.artifact.findFirst({
60
- where: {
61
- workspaceId,
62
- type: ArtifactType.FLASHCARD_SET,
63
- workspace: workspaceAccessWhere(userId),
64
- },
104
+ const set = await this.findPrimarySet(userId, workspaceId);
105
+ if (!set)
106
+ return [];
107
+ return this.db.flashcard.findMany({
108
+ where: { artifactId: set.id },
65
109
  include: {
66
- flashcards: {
67
- include: {
68
- progress: { where: { userId } },
69
- },
70
- },
110
+ progress: { where: { userId } },
71
111
  },
72
- orderBy: { createdAt: 'desc' },
112
+ orderBy: { order: 'asc' },
73
113
  });
74
- if (!set)
75
- throw new TRPCError({ code: 'NOT_FOUND' });
76
- return set.flashcards;
77
114
  }
78
115
  async isGenerating(userId, workspaceId) {
79
- const artifact = await this.db.artifact.findFirst({
80
- where: {
81
- workspaceId,
82
- type: ArtifactType.FLASHCARD_SET,
83
- workspace: workspaceAccessWhere(userId),
84
- },
85
- orderBy: { createdAt: 'desc' },
86
- });
87
- return artifact?.generating;
116
+ const set = await this.findPrimarySet(userId, workspaceId);
117
+ return set?.generating ?? false;
88
118
  }
89
119
  async createCard(userId, input) {
90
- const set = await this.db.artifact.findFirst({
91
- where: {
92
- type: ArtifactType.FLASHCARD_SET,
93
- workspace: { id: input.workspaceId },
94
- },
95
- include: { flashcards: true },
96
- orderBy: { updatedAt: 'desc' },
97
- });
98
- if (!set)
99
- throw new TRPCError({ code: 'NOT_FOUND' });
100
- return this.db.flashcard.create({
120
+ const set = await this.ensurePrimarySet(userId, input.workspaceId);
121
+ const nextOrder = input.order ??
122
+ (set.flashcards.reduce((max, card) => Math.max(max, card.order), -1) + 1);
123
+ const card = await this.db.flashcard.create({
101
124
  data: {
102
125
  artifactId: set.id,
103
126
  front: input.front,
104
127
  back: input.back,
105
128
  acceptedAnswers: normalizeAcceptedAnswers(input.acceptedAnswers),
106
129
  tags: input.tags ?? [],
107
- order: input.order ?? 0,
130
+ order: nextOrder,
108
131
  },
109
132
  });
133
+ invalidateUserBillingCache(userId);
134
+ return card;
110
135
  }
111
136
  async updateCard(userId, input) {
112
137
  const card = await this.db.flashcard.findFirst({
@@ -217,37 +242,23 @@ export class FlashcardService extends BaseService {
217
242
  });
218
243
  if (!workspace)
219
244
  throw new TRPCError({ code: 'NOT_FOUND' });
220
- const flashcardCurrent = await this.db.artifact.findFirst({
221
- where: {
222
- workspaceId: input.workspaceId,
223
- type: ArtifactType.FLASHCARD_SET,
224
- },
225
- select: { id: true, flashcards: true },
226
- orderBy: { updatedAt: 'desc' },
227
- });
245
+ const primarySet = await this.ensurePrimarySet(userId, input.workspaceId);
246
+ const orderOffset = primarySet.flashcards.reduce((max, card) => Math.max(max, card.order), -1);
228
247
  try {
229
248
  await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', {
230
249
  status: 'generating',
231
250
  numCards: input.numCards,
232
251
  difficulty: input.difficulty,
233
252
  });
234
- const artifact = await this.db.artifact.create({
253
+ await this.db.artifact.update({
254
+ where: { id: primarySet.id },
235
255
  data: {
236
- workspaceId: input.workspaceId,
237
- type: ArtifactType.FLASHCARD_SET,
238
- title: input.title || `Flashcards - ${new Date().toLocaleString()}`,
239
- createdById: userId,
240
256
  generating: true,
241
257
  generatingMetadata: {
242
258
  quantity: input.numCards,
243
259
  difficulty: input.difficulty.toLowerCase(),
244
260
  },
245
- flashcards: {
246
- create: flashcardCurrent?.flashcards.map((card) => ({
247
- front: card.front,
248
- back: card.back,
249
- })),
250
- },
261
+ ...(input.title ? { title: input.title } : {}),
251
262
  },
252
263
  });
253
264
  const ragContext = await workspaceKbService.retrieveContext(input.workspaceId, input.prompt ?? 'flashcard key concepts facts definitions', 8);
@@ -262,10 +273,10 @@ export class FlashcardService extends BaseService {
262
273
  const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
263
274
  await this.db.flashcard.create({
264
275
  data: {
265
- artifactId: artifact.id,
276
+ artifactId: primarySet.id,
266
277
  front,
267
278
  back,
268
- order: i,
279
+ order: orderOffset + 1 + i,
269
280
  tags: input.tags ?? ['ai-generated', input.difficulty],
270
281
  },
271
282
  });
@@ -281,10 +292,10 @@ export class FlashcardService extends BaseService {
281
292
  const [front, back] = line.split(' - ');
282
293
  await this.db.flashcard.create({
283
294
  data: {
284
- artifactId: artifact.id,
295
+ artifactId: primarySet.id,
285
296
  front: front.trim(),
286
297
  back: back.trim(),
287
- order: i,
298
+ order: orderOffset + 1 + i,
288
299
  tags: input.tags ?? ['ai-generated', input.difficulty],
289
300
  },
290
301
  });
@@ -292,11 +303,12 @@ export class FlashcardService extends BaseService {
292
303
  }
293
304
  }
294
305
  }
295
- await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
296
- await this.db.artifact.update({
297
- where: { id: artifact.id },
306
+ const artifact = await this.db.artifact.update({
307
+ where: { id: primarySet.id },
298
308
  data: { generating: false },
309
+ include: { flashcards: true },
299
310
  });
311
+ await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
300
312
  await notifyArtifactReady(this.db, {
301
313
  userId,
302
314
  workspaceId: input.workspaceId,
@@ -304,15 +316,16 @@ export class FlashcardService extends BaseService {
304
316
  artifactType: ArtifactType.FLASHCARD_SET,
305
317
  title: artifact.title,
306
318
  }).catch(() => { });
319
+ invalidateUserBillingCache(userId);
307
320
  return { artifact, createdCards };
308
321
  }
309
322
  catch (error) {
310
- if (flashcardCurrent?.id) {
311
- await this.db.artifact.update({
312
- where: { id: flashcardCurrent.id },
313
- data: { generating: false },
314
- });
315
- }
323
+ await this.db.artifact
324
+ .update({
325
+ where: { id: primarySet.id },
326
+ data: { generating: false },
327
+ })
328
+ .catch(() => { });
316
329
  await PusherService.emitError(input.workspaceId, `Failed to generate flashcards: ${error}`, 'flash_card_generation');
317
330
  await notifyArtifactFailed(this.db, {
318
331
  userId,