@goscribe/server 1.1.2 → 1.1.3

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 (34) hide show
  1. package/dist/lib/ai-session.d.ts +13 -3
  2. package/dist/lib/ai-session.js +66 -146
  3. package/dist/lib/pusher.js +1 -1
  4. package/dist/routers/_app.d.ts +114 -7
  5. package/dist/routers/chat.js +2 -23
  6. package/dist/routers/flashcards.d.ts +25 -1
  7. package/dist/routers/flashcards.js +0 -14
  8. package/dist/routers/members.d.ts +18 -0
  9. package/dist/routers/members.js +14 -1
  10. package/dist/routers/worksheets.js +5 -4
  11. package/dist/routers/workspace.d.ts +89 -6
  12. package/dist/routers/workspace.js +389 -259
  13. package/dist/services/flashcard-progress.service.d.ts +25 -1
  14. package/dist/services/flashcard-progress.service.js +70 -31
  15. package/package.json +2 -2
  16. package/prisma/schema.prisma +12 -2
  17. package/src/lib/ai-session.ts +3 -21
  18. package/src/routers/flashcards.ts +0 -16
  19. package/src/routers/members.ts +13 -2
  20. package/src/routers/workspace.ts +468 -439
  21. package/ANALYSIS_PROGRESS_SPEC.md +0 -463
  22. package/PROGRESS_QUICK_REFERENCE.md +0 -239
  23. package/dist/lib/podcast-prompts.d.ts +0 -43
  24. package/dist/lib/podcast-prompts.js +0 -135
  25. package/dist/routers/ai-session.d.ts +0 -0
  26. package/dist/routers/ai-session.js +0 -1
  27. package/dist/services/flashcard.service.d.ts +0 -183
  28. package/dist/services/flashcard.service.js +0 -224
  29. package/dist/services/podcast-segment-reorder.d.ts +0 -0
  30. package/dist/services/podcast-segment-reorder.js +0 -107
  31. package/dist/services/podcast.service.d.ts +0 -0
  32. package/dist/services/podcast.service.js +0 -326
  33. package/dist/services/worksheet.service.d.ts +0 -0
  34. package/dist/services/worksheet.service.js +0 -295
@@ -94,7 +94,7 @@ export declare class FlashcardProgressService {
94
94
  /**
95
95
  * Get flashcards due for review, non-studied flashcards, and flashcards with low mastery
96
96
  */
97
- getDueFlashcards(userId: string, workspaceId: string): Promise<({
97
+ getDueFlashcards(userId: string, workspaceId: string): Promise<(({
98
98
  artifact: {
99
99
  id: string;
100
100
  createdAt: Date;
@@ -119,6 +119,30 @@ export declare class FlashcardProgressService {
119
119
  front: string;
120
120
  back: string;
121
121
  tags: string[];
122
+ }) | {
123
+ artifact: {
124
+ id: string;
125
+ createdAt: Date;
126
+ updatedAt: Date;
127
+ title: string;
128
+ description: string | null;
129
+ workspaceId: string;
130
+ type: import("@prisma/client").$Enums.ArtifactType;
131
+ isArchived: boolean;
132
+ generating: boolean;
133
+ generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
134
+ difficulty: import("@prisma/client").$Enums.Difficulty | null;
135
+ estimatedTime: string | null;
136
+ imageObjectKey: string | null;
137
+ createdById: string | null;
138
+ };
139
+ id: string;
140
+ createdAt: Date;
141
+ artifactId: string;
142
+ order: number;
143
+ front: string;
144
+ back: string;
145
+ tags: string[];
122
146
  })[]>;
123
147
  /**
124
148
  * Get user statistics for a flashcard set
@@ -226,12 +226,23 @@ export class FlashcardProgressService {
226
226
  async getDueFlashcards(userId, workspaceId) {
227
227
  const now = new Date();
228
228
  const LOW_MASTERY_THRESHOLD = 50; // Consider mastery < 50 as low
229
- // Get all flashcards in the workspace
229
+ // Get the latest artifact in the workspace
230
+ const latestArtifact = await this.db.artifact.findFirst({
231
+ where: {
232
+ workspaceId,
233
+ type: 'FLASHCARD_SET',
234
+ },
235
+ orderBy: {
236
+ updatedAt: 'desc',
237
+ },
238
+ });
239
+ if (!latestArtifact) {
240
+ return [];
241
+ }
242
+ // Get all flashcards from the latest artifact
230
243
  const allFlashcards = await this.db.flashcard.findMany({
231
244
  where: {
232
- artifact: {
233
- workspaceId,
234
- },
245
+ artifactId: latestArtifact.id,
235
246
  },
236
247
  include: {
237
248
  artifact: true,
@@ -242,6 +253,7 @@ export class FlashcardProgressService {
242
253
  },
243
254
  },
244
255
  });
256
+ console.log('allFlashcards', allFlashcards.length);
245
257
  const TAKE_NUMBER = (allFlashcards.length > 10) ? 10 : allFlashcards.length;
246
258
  // Get progress records for flashcards that are due or have low mastery
247
259
  const progressRecords = await this.db.flashcardProgress.findMany({
@@ -265,9 +277,7 @@ export class FlashcardProgressService {
265
277
  }
266
278
  ],
267
279
  flashcard: {
268
- artifact: {
269
- workspaceId,
270
- },
280
+ artifactId: latestArtifact.id,
271
281
  },
272
282
  },
273
283
  include: {
@@ -279,6 +289,38 @@ export class FlashcardProgressService {
279
289
  },
280
290
  take: TAKE_NUMBER,
281
291
  });
292
+ console.log('TAKE_NUMBER', TAKE_NUMBER);
293
+ console.log('TAKE_NUMBER - progressRecords.length', TAKE_NUMBER - progressRecords.length);
294
+ console.log('progressRecords', progressRecords.map((progress) => progress.flashcard.id));
295
+ // Get flashcard IDs that already have progress records
296
+ const flashcardIdsWithProgress = new Set(progressRecords.map((progress) => progress.flashcard.id));
297
+ // Find flashcards without progress records (non-studied) to pad the results
298
+ const nonStudiedFlashcards = allFlashcards
299
+ .filter((flashcard) => !flashcardIdsWithProgress.has(flashcard.id))
300
+ .slice(0, TAKE_NUMBER - progressRecords.length);
301
+ // Create progress-like structures for non-studied flashcards
302
+ const progressRecordsPadding = nonStudiedFlashcards.map((flashcard) => {
303
+ const { progress, ...flashcardWithoutProgress } = flashcard;
304
+ return {
305
+ id: `temp-${flashcard.id}`,
306
+ userId,
307
+ flashcardId: flashcard.id,
308
+ timesStudied: 0,
309
+ timesCorrect: 0,
310
+ timesIncorrect: 0,
311
+ timesIncorrectConsecutive: 0,
312
+ easeFactor: 2.5,
313
+ interval: 0,
314
+ repetitions: 0,
315
+ masteryLevel: 0,
316
+ lastStudiedAt: null,
317
+ nextReviewAt: null,
318
+ flashcard: flashcardWithoutProgress,
319
+ };
320
+ });
321
+ console.log('progressRecordsPadding', progressRecordsPadding.length);
322
+ console.log('progressRecords', progressRecords.length);
323
+ const selectedCards = [...progressRecords, ...progressRecordsPadding];
282
324
  // Build result array: include progress records and non-studied flashcards
283
325
  const results = [];
284
326
  // Add flashcards with progress (due or low mastery)
@@ -286,30 +328,27 @@ export class FlashcardProgressService {
286
328
  results.push(progress);
287
329
  }
288
330
  // Sort by priority: due first (by nextReviewAt), then low mastery, then non-studied
289
- results.sort((a, b) => {
290
- // Due flashcards first (nextReviewAt <= now)
291
- const aIsDue = a.nextReviewAt && a.nextReviewAt <= now;
292
- const bIsDue = b.nextReviewAt && b.nextReviewAt <= now;
293
- if (aIsDue && !bIsDue)
294
- return -1;
295
- if (!aIsDue && bIsDue)
296
- return 1;
297
- // Among due flashcards, sort by nextReviewAt
298
- if (aIsDue && bIsDue && a.nextReviewAt && b.nextReviewAt) {
299
- return a.nextReviewAt.getTime() - b.nextReviewAt.getTime();
300
- }
301
- // Then low mastery (lower mastery first)
302
- if (a.masteryLevel !== b.masteryLevel) {
303
- return a.masteryLevel - b.masteryLevel;
304
- }
305
- // Finally, non-studied (timesStudied === 0)
306
- if (a.timesStudied === 0 && b.timesStudied !== 0)
307
- return -1;
308
- if (a.timesStudied !== 0 && b.timesStudied === 0)
309
- return 1;
310
- return 0;
311
- });
312
- return results.map((progress) => progress.flashcard);
331
+ // @todo: make an actual algorithm. research
332
+ // results.sort((a, b) => {
333
+ // // Due flashcards first (nextReviewAt <= now)
334
+ // const aIsDue = a.nextReviewAt && a.nextReviewAt <= now;
335
+ // const bIsDue = b.nextReviewAt && b.nextReviewAt <= now;
336
+ // // if (aIsDue && !bIsDue) return -1;
337
+ // // if (!aIsDue && bIsDue) return 1;
338
+ // // Among due flashcards, sort by nextReviewAt
339
+ // if (aIsDue && bIsDue && a.nextReviewAt && b.nextReviewAt) {
340
+ // return a.nextReviewAt.getTime() - b.nextReviewAt.getTime();
341
+ // }
342
+ // // Then low mastery (lower mastery first)
343
+ // if (a.masteryLevel !== b.masteryLevel) {
344
+ // return a.masteryLevel - b.masteryLevel;
345
+ // }
346
+ // // Finally, non-studied (timesStudied === 0)
347
+ // if (a.timesStudied === 0 && b.timesStudied !== 0) return -1;
348
+ // if (a.timesStudied !== 0 && b.timesStudied === 0) return 1;
349
+ // return 0;
350
+ // });
351
+ return selectedCards.map((progress) => progress.flashcard);
313
352
  }
314
353
  /**
315
354
  * Get user statistics for a flashcard set
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goscribe/server",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@auth/express": "^0.11.0",
24
24
  "@auth/prisma-adapter": "^2.10.0",
25
- "@goscribe/server": "^1.0.8",
25
+ "@goscribe/server": "^1.1.2",
26
26
  "@prisma/client": "^6.14.0",
27
27
  "@supabase/supabase-js": "^2.76.1",
28
28
  "@trpc/server": "^11.5.0",
@@ -63,11 +63,21 @@ model User {
63
63
  // Invitations
64
64
  sentInvitations WorkspaceInvitation[] @relation("UserInvitations")
65
65
 
66
+ notifications Notification[]
66
67
  chats Chat[]
67
68
  createdAt DateTime @default(now())
68
- updatedAt DateTime @updatedAt
69
+ updatedAt DateTime @updatedAt
69
70
  }
70
71
 
72
+ model Notification {
73
+ id String @id @default(cuid())
74
+ userId String
75
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
76
+ content String
77
+ read Boolean @default(false)
78
+ createdAt DateTime @default(now())
79
+ updatedAt DateTime @updatedAt
80
+ }
71
81
  model Session {
72
82
  id String @id @default(cuid())
73
83
  userId String
@@ -193,7 +203,7 @@ model FileAsset {
193
203
  objectKey String?
194
204
  url String? // optional if serving via signed GET per-view
195
205
  checksum String? // optional server-side integrity
196
- aiTranscription String?
206
+ aiTranscription Json? @default("{}")
197
207
 
198
208
  meta Json? // arbitrary metadata
199
209
 
@@ -136,11 +136,6 @@ export class AISessionService {
136
136
  fileType: 'image' | 'pdf',
137
137
  maxPages?: number
138
138
  ): Promise<ProcessFileResult> {
139
- const session = this.sessions.get(sessionId);
140
- if (!session) {
141
- throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
142
- }
143
-
144
139
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
145
140
 
146
141
  // Mock mode - return fake processing result
@@ -168,6 +163,8 @@ export class AISessionService {
168
163
  formData.append('maxPages', maxPages.toString());
169
164
  }
170
165
 
166
+ console.log('formData', formData);
167
+
171
168
  // Retry logic for file processing
172
169
  const maxRetries = 3;
173
170
  let lastError: Error | null = null;
@@ -183,7 +180,7 @@ export class AISessionService {
183
180
  const response = await fetch(AI_SERVICE_URL, {
184
181
  method: 'POST',
185
182
  body: formData,
186
- signal: controller.signal,
183
+ // signal: controller.signal,
187
184
  });
188
185
 
189
186
  clearTimeout(timeoutId);
@@ -201,11 +198,6 @@ export class AISessionService {
201
198
  throw new Error(result.error || 'File processing failed');
202
199
  }
203
200
 
204
- // Update session
205
- session.files.push(fileUrl);
206
- session.updatedAt = new Date();
207
- this.sessions.set(sessionId, session);
208
-
209
201
  return result as ProcessFileResult;
210
202
 
211
203
  } catch (error) {
@@ -235,11 +227,6 @@ export class AISessionService {
235
227
 
236
228
  // Generate study guide
237
229
  async generateStudyGuide(sessionId: string, user: string): Promise<string> {
238
- const session = this.sessions.get(sessionId);
239
- if (!session) {
240
- throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
241
- }
242
-
243
230
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
244
231
  // Mock mode - return fake study guide
245
232
  if (MOCK_MODE) {
@@ -291,11 +278,6 @@ This mock study guide demonstrates the structure and format that would be genera
291
278
 
292
279
  // Generate flashcard questions
293
280
  async generateFlashcardQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string> {
294
- // const session = this.sessions.get(sessionId);
295
- // if (!session) {
296
- // throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
297
- // }
298
-
299
281
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
300
282
  // Mock mode - return fake flashcard questions
301
283
  if (MOCK_MODE) {
@@ -177,22 +177,6 @@ export const flashcards = router({
177
177
 
178
178
  await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { status: 'generating', numCards: input.numCards, difficulty: input.difficulty });
179
179
 
180
- const formattedPreviousCards = flashcardCurrent?.flashcards.map((card) => ({
181
- front: card.front,
182
- back: card.back,
183
- }));
184
-
185
-
186
- const partialPrompt = `
187
- This is the users previous flashcards, avoid repeating any existing cards.
188
- Please generate ${input.numCards} new cards,
189
- Of a ${input.difficulty} difficulty,
190
- Of a ${input.tags?.join(', ')} tag,
191
- Of a ${input.title} title.
192
- ${formattedPreviousCards?.map((card) => `Front: ${card.front}\nBack: ${card.back}`).join('\n')}
193
-
194
- The user has also left you this prompt: ${input.prompt}
195
- `
196
180
  const artifact = await ctx.db.artifact.create({
197
181
  data: {
198
182
  workspaceId: input.workspaceId,
@@ -230,7 +230,16 @@ export const members = router({
230
230
  invitedByName: invitation.workspace.owner.name || invitation.workspace.owner.email,
231
231
  };
232
232
  }),
233
-
233
+ getInvitations: authedProcedure
234
+ .input(z.object({
235
+ workspaceId: z.string(),
236
+ }))
237
+ .query(async ({ ctx, input }) => {
238
+ const invitations = await ctx.db.workspaceInvitation.findMany({
239
+ where: { workspaceId: input.workspaceId },
240
+ });
241
+ return invitations;
242
+ }),
234
243
  /**
235
244
  * Accept an invitation (public endpoint)
236
245
  */
@@ -277,8 +286,10 @@ export const members = router({
277
286
  });
278
287
  }
279
288
 
289
+ const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
290
+ if (!user || !user.email) throw new TRPCError({ code: 'NOT_FOUND' });
280
291
  // Check if the email matches the user's email
281
- if (ctx.session.user.email !== invitation.email) {
292
+ if (user.email !== invitation.email) {
282
293
  throw new TRPCError({
283
294
  code: 'BAD_REQUEST',
284
295
  message: 'This invitation was sent to a different email address'