@goscribe/server 1.1.1 → 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 (36) 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 +14 -1
  17. package/src/lib/ai-session.ts +97 -158
  18. package/src/routers/flashcards.ts +0 -16
  19. package/src/routers/members.ts +13 -2
  20. package/src/routers/podcast.ts +0 -1
  21. package/src/routers/worksheets.ts +3 -2
  22. package/src/routers/workspace.ts +516 -399
  23. package/ANALYSIS_PROGRESS_SPEC.md +0 -463
  24. package/PROGRESS_QUICK_REFERENCE.md +0 -239
  25. package/dist/lib/podcast-prompts.d.ts +0 -43
  26. package/dist/lib/podcast-prompts.js +0 -135
  27. package/dist/routers/ai-session.d.ts +0 -0
  28. package/dist/routers/ai-session.js +0 -1
  29. package/dist/services/flashcard.service.d.ts +0 -183
  30. package/dist/services/flashcard.service.js +0 -224
  31. package/dist/services/podcast-segment-reorder.d.ts +0 -0
  32. package/dist/services/podcast-segment-reorder.js +0 -107
  33. package/dist/services/podcast.service.d.ts +0 -0
  34. package/dist/services/podcast.service.js +0 -326
  35. package/dist/services/worksheet.service.d.ts +0 -0
  36. 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.1",
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
@@ -130,6 +140,8 @@ model Workspace {
130
140
 
131
141
  analysisProgress Json?
132
142
 
143
+ needsAnalysis Boolean @default(false)
144
+
133
145
  // Raw uploads attached to this workspace
134
146
  uploads FileAsset[]
135
147
 
@@ -191,6 +203,7 @@ model FileAsset {
191
203
  objectKey String?
192
204
  url String? // optional if serving via signed GET per-view
193
205
  checksum String? // optional server-side integrity
206
+ aiTranscription Json? @default("{}")
194
207
 
195
208
  meta Json? // arbitrary metadata
196
209
 
@@ -26,6 +26,19 @@ export interface AISession {
26
26
 
27
27
  const IMITATE_WAIT_TIME_MS = MOCK_MODE ? 1000 * 10 : 0;
28
28
 
29
+ export interface ProcessFileResult {
30
+ status: 'success' | 'error';
31
+ textContent: string | null;
32
+ imageDescriptions: Array<{
33
+ page: number;
34
+ description: string;
35
+ hasVisualContent: boolean;
36
+ }>;
37
+ comprehensiveDescription: string | null;
38
+ pageCount: number;
39
+ error?: string;
40
+ }
41
+
29
42
  export class AISessionService {
30
43
  private sessions = new Map<string, AISession>();
31
44
 
@@ -115,67 +128,105 @@ export class AISessionService {
115
128
  });
116
129
  }
117
130
 
118
- // Upload file to AI session
119
- async uploadFile(sessionId: string, user: string, file: File, fileType: 'image' | 'pdf'): Promise<void> {
120
- const session = this.sessions.get(sessionId);
121
- if (!session) {
122
- throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
123
- }
124
-
131
+ // Process file (PDF/image) and return comprehensive text descriptions
132
+ async processFile(
133
+ sessionId: string,
134
+ user: string,
135
+ fileUrl: string,
136
+ fileType: 'image' | 'pdf',
137
+ maxPages?: number
138
+ ): Promise<ProcessFileResult> {
125
139
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
126
- // Mock mode - simulate successful file upload
140
+
141
+ // Mock mode - return fake processing result
127
142
  if (MOCK_MODE) {
128
- console.log(`🎭 MOCK MODE: Uploading ${fileType} file "${file.name}" to session ${sessionId}`);
129
- session.files.push(file.name);
130
- session.updatedAt = new Date();
131
- this.sessions.set(sessionId, session);
132
- return;
143
+ logger.info(`🎭 MOCK MODE: Processing ${fileType} file from URL for session ${sessionId}`);
144
+ const mockPageCount = fileType === 'pdf' ? 15 : 1;
145
+ return {
146
+ status: 'success',
147
+ textContent: `Mock extracted text content from ${fileType} file. This would contain the full text extracted from the document.`,
148
+ imageDescriptions: Array.from({ length: mockPageCount }, (_, i) => ({
149
+ page: i + 1,
150
+ description: `Page ${i + 1} contains educational content with diagrams and text.`,
151
+ hasVisualContent: true,
152
+ })),
153
+ comprehensiveDescription: `DOCUMENT SUMMARY (${mockPageCount} ${mockPageCount === 1 ? 'page' : 'pages'})\n\nTEXT CONTENT:\nMock extracted text content...\n\nVISUAL CONTENT DESCRIPTIONS:\nPage-by-page descriptions of visual elements.`,
154
+ pageCount: mockPageCount,
155
+ };
133
156
  }
134
157
 
135
- const command = fileType === 'image' ? 'append_image' : 'append_pdflike';
136
-
137
158
  const formData = new FormData();
138
- formData.append('command', command);
139
- formData.append('file', file);
140
- formData.append('session', sessionId);
141
- formData.append('user', user);
142
- try {
143
- const response = await fetch(AI_SERVICE_URL, {
144
- method: 'POST',
145
- body: formData,
146
- });
159
+ formData.append('command', 'process_file');
160
+ formData.append('fileUrl', fileUrl);
161
+ formData.append('fileType', fileType);
162
+ if (maxPages) {
163
+ formData.append('maxPages', maxPages.toString());
164
+ }
147
165
 
148
- if (!response.ok) {
149
- throw new Error(`AI service error: ${response.status} ${response.statusText}`);
150
- }
166
+ console.log('formData', formData);
151
167
 
152
- const result = await response.json();
153
- console.log(`📋 Upload result:`, result);
154
- if (!result.message) {
155
- throw new Error(`AI service error: No response message`);
156
- }
168
+ // Retry logic for file processing
169
+ const maxRetries = 3;
170
+ let lastError: Error | null = null;
157
171
 
158
- // Update session
159
- session.files.push(file.name);
160
- session.updatedAt = new Date();
161
- this.sessions.set(sessionId, session);
162
- } catch (error) {
163
- throw new TRPCError({
164
- code: 'INTERNAL_SERVER_ERROR',
165
- message: `Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}`,
166
- });
172
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
173
+ try {
174
+ logger.info(`📄 Processing ${fileType} file attempt ${attempt}/${maxRetries} for session ${sessionId}`);
175
+
176
+ // Set timeout for large files (5 minutes)
177
+ const controller = new AbortController();
178
+ const timeoutId = setTimeout(() => controller.abort(), 300000); // 5 min timeout
179
+
180
+ const response = await fetch(AI_SERVICE_URL, {
181
+ method: 'POST',
182
+ body: formData,
183
+ // signal: controller.signal,
184
+ });
185
+
186
+ clearTimeout(timeoutId);
187
+
188
+ if (!response.ok) {
189
+ const errorText = await response.text();
190
+ logger.error(`❌ File processing error response:`, errorText);
191
+ throw new Error(`AI service error: ${response.status} ${response.statusText} - ${errorText}`);
192
+ }
193
+
194
+ const result = await response.json();
195
+ logger.info(`📋 File processing result: status=${result.status}, pageCount=${result.pageCount}`);
196
+
197
+ if (result.status === 'error') {
198
+ throw new Error(result.error || 'File processing failed');
199
+ }
200
+
201
+ return result as ProcessFileResult;
202
+
203
+ } catch (error) {
204
+ lastError = error instanceof Error ? error : new Error('Unknown error');
205
+ logger.error(`❌ File processing attempt ${attempt} failed:`, lastError.message);
206
+
207
+ if (attempt < maxRetries) {
208
+ const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s
209
+ logger.info(`⏳ Retrying file processing in ${delay}ms...`);
210
+ await new Promise(resolve => setTimeout(resolve, delay));
211
+ }
212
+ }
167
213
  }
214
+
215
+ logger.error(`💥 All ${maxRetries} file processing attempts failed. Last error:`, lastError?.message);
216
+ return {
217
+ status: 'error',
218
+ textContent: null,
219
+ imageDescriptions: [],
220
+ comprehensiveDescription: null,
221
+ pageCount: 0,
222
+ error: `Failed to process file after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`,
223
+ };
168
224
  }
169
225
 
170
226
 
171
227
 
172
228
  // Generate study guide
173
229
  async generateStudyGuide(sessionId: string, user: string): Promise<string> {
174
- const session = this.sessions.get(sessionId);
175
- if (!session) {
176
- throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
177
- }
178
-
179
230
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
180
231
  // Mock mode - return fake study guide
181
232
  if (MOCK_MODE) {
@@ -227,11 +278,6 @@ This mock study guide demonstrates the structure and format that would be genera
227
278
 
228
279
  // Generate flashcard questions
229
280
  async generateFlashcardQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string> {
230
- // const session = this.sessions.get(sessionId);
231
- // if (!session) {
232
- // throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
233
- // }
234
-
235
281
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
236
282
  // Mock mode - return fake flashcard questions
237
283
  if (MOCK_MODE) {
@@ -503,113 +549,6 @@ This mock study guide demonstrates the structure and format that would be genera
503
549
  }
504
550
 
505
551
 
506
- // Analyse PDF
507
- async analysePDF(sessionId: string, user: string): Promise<string> {
508
- const session = this.sessions.get(sessionId);
509
- if (!session) {
510
- throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
511
- }
512
-
513
- await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
514
- // Mock mode - return fake PDF analysis
515
- if (MOCK_MODE) {
516
- console.log(`🎭 MOCK MODE: Analysing PDF for session ${sessionId}`);
517
- return `Mock PDF Analysis Results:
518
-
519
- Document Type: Educational Material
520
- Pages: 15
521
- Language: English
522
- Key Topics Identified:
523
- - Introduction to Machine Learning
524
- - Data Preprocessing Techniques
525
- - Model Training and Validation
526
- - Performance Metrics
527
-
528
- Summary: This mock PDF analysis shows the structure and content that would be extracted from an uploaded PDF document. The analysis includes document metadata, key topics, and a summary of the content.
529
-
530
- Note: This is a mock response generated when DONT_TEST_INFERENCE=true`;
531
- }
532
-
533
- const formData = new FormData();
534
- formData.append('command', 'analyse_pdf');
535
- formData.append('session', sessionId);
536
- formData.append('user', user);
537
- try {
538
- const response = await fetch(AI_SERVICE_URL, {
539
- method: 'POST',
540
- body: formData,
541
- });
542
-
543
- if (!response.ok) {
544
- throw new Error(`AI service error: ${response.status} ${response.statusText}`);
545
- }
546
-
547
- const result = await response.json();
548
- return result.message || 'PDF analysis completed';
549
- } catch (error) {
550
- throw new TRPCError({
551
- code: 'INTERNAL_SERVER_ERROR',
552
- message: `Failed to analyse PDF: ${error instanceof Error ? error.message : 'Unknown error'}`,
553
- });
554
- }
555
- }
556
-
557
- // Analyse Image
558
- async analyseImage(sessionId: string, user: string): Promise<string> {
559
- const session = this.sessions.get(sessionId);
560
- if (!session) {
561
- throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
562
- }
563
-
564
- await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
565
- // Mock mode - return fake image analysis
566
- if (MOCK_MODE) {
567
- console.log(`🎭 MOCK MODE: Analysing image for session ${sessionId}`);
568
- return `Mock Image Analysis Results:
569
-
570
- Image Type: Educational Diagram
571
- Format: PNG
572
- Dimensions: 1920x1080
573
- Content Description:
574
- - Contains a flowchart or diagram
575
- - Shows a process or system architecture
576
- - Includes text labels and annotations
577
- - Educational or instructional content
578
-
579
- Key Elements Identified:
580
- - Process flow arrows
581
- - Decision points
582
- - Input/output elements
583
- - Descriptive text
584
-
585
- Summary: This mock image analysis demonstrates the type of content extraction that would be performed on uploaded images. The analysis identifies visual elements, text content, and overall structure.
586
-
587
- Note: This is a mock response generated when DONT_TEST_INFERENCE=true`;
588
- }
589
-
590
- const formData = new FormData();
591
- formData.append('command', 'analyse_img');
592
- formData.append('session', sessionId);
593
- formData.append('user', user);
594
- try {
595
- const response = await fetch(AI_SERVICE_URL, {
596
- method: 'POST',
597
- body: formData,
598
- });
599
-
600
- if (!response.ok) {
601
- throw new Error(`AI service error: ${response.status} ${response.statusText}`);
602
- }
603
-
604
- const result = await response.json();
605
- return result.message || 'Image analysis completed';
606
- } catch (error) {
607
- throw new TRPCError({
608
- code: 'INTERNAL_SERVER_ERROR',
609
- message: `Failed to analyse image: ${error instanceof Error ? error.message : 'Unknown error'}`,
610
- });
611
- }
612
- }
613
552
 
614
553
  async generatePodcastImage(sessionId: string, user: string, summary: string): Promise<string> {
615
554
 
@@ -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'
@@ -877,7 +877,6 @@ export const podcast = router({
877
877
  updatedAt: segment.updatedAt,
878
878
  };
879
879
  }),
880
-
881
880
  // Get available voices for TTS
882
881
  getAvailableVoices: authedProcedure
883
882
  .query(async () => {
@@ -281,6 +281,7 @@ export const worksheets = router({
281
281
  userAnswer: input.answer,
282
282
  completedAt: input.completed ? new Date() : null,
283
283
  attempts: { increment: 1 },
284
+ correct: input.correct,
284
285
  },
285
286
  });
286
287
 
@@ -490,7 +491,7 @@ export const worksheets = router({
490
491
 
491
492
  // Parse question meta to get mark_scheme
492
493
  const questionMeta = question.meta ? (typeof question.meta === 'object' ? question.meta : JSON.parse(question.meta as any)) : {} as any;
493
- const markScheme = questionMeta.mark_scheme;
494
+ const markScheme = questionMeta.markScheme;
494
495
 
495
496
  let isCorrect = false;
496
497
  let userMarkScheme = null;
@@ -517,7 +518,7 @@ export const worksheets = router({
517
518
  }
518
519
  } else {
519
520
  // Simple string comparison if no mark scheme
520
- isCorrect = question.answer === input.answer;
521
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'No mark scheme found for question' });
521
522
  }
522
523
 
523
524