@goscribe/server 1.3.0 → 1.3.1

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 (79) hide show
  1. package/dist/context.d.ts +5 -1
  2. package/dist/lib/activity_human_description.d.ts +13 -0
  3. package/dist/lib/activity_human_description.js +221 -0
  4. package/dist/lib/activity_human_description.test.d.ts +1 -0
  5. package/dist/lib/activity_human_description.test.js +16 -0
  6. package/dist/lib/activity_log_service.d.ts +87 -0
  7. package/dist/lib/activity_log_service.js +276 -0
  8. package/dist/lib/activity_log_service.test.d.ts +1 -0
  9. package/dist/lib/activity_log_service.test.js +27 -0
  10. package/dist/lib/ai-session.d.ts +15 -2
  11. package/dist/lib/ai-session.js +147 -85
  12. package/dist/lib/constants.d.ts +13 -0
  13. package/dist/lib/constants.js +12 -0
  14. package/dist/lib/email.d.ts +11 -0
  15. package/dist/lib/email.js +193 -0
  16. package/dist/lib/env.d.ts +13 -0
  17. package/dist/lib/env.js +16 -0
  18. package/dist/lib/inference.d.ts +4 -1
  19. package/dist/lib/inference.js +3 -3
  20. package/dist/lib/logger.d.ts +4 -4
  21. package/dist/lib/logger.js +30 -8
  22. package/dist/lib/notification-service.d.ts +152 -0
  23. package/dist/lib/notification-service.js +473 -0
  24. package/dist/lib/notification-service.test.d.ts +1 -0
  25. package/dist/lib/notification-service.test.js +87 -0
  26. package/dist/lib/prisma.d.ts +2 -1
  27. package/dist/lib/prisma.js +5 -1
  28. package/dist/lib/pusher.d.ts +23 -0
  29. package/dist/lib/pusher.js +69 -5
  30. package/dist/lib/retry.d.ts +15 -0
  31. package/dist/lib/retry.js +37 -0
  32. package/dist/lib/storage.js +2 -2
  33. package/dist/lib/stripe.d.ts +9 -0
  34. package/dist/lib/stripe.js +36 -0
  35. package/dist/lib/subscription_service.d.ts +37 -0
  36. package/dist/lib/subscription_service.js +654 -0
  37. package/dist/lib/usage_service.d.ts +26 -0
  38. package/dist/lib/usage_service.js +59 -0
  39. package/dist/lib/worksheet-generation.d.ts +91 -0
  40. package/dist/lib/worksheet-generation.js +95 -0
  41. package/dist/lib/worksheet-generation.test.d.ts +1 -0
  42. package/dist/lib/worksheet-generation.test.js +20 -0
  43. package/dist/lib/workspace-access.d.ts +18 -0
  44. package/dist/lib/workspace-access.js +13 -0
  45. package/dist/routers/_app.d.ts +1349 -253
  46. package/dist/routers/_app.js +10 -0
  47. package/dist/routers/admin.d.ts +361 -0
  48. package/dist/routers/admin.js +633 -0
  49. package/dist/routers/annotations.d.ts +219 -0
  50. package/dist/routers/annotations.js +187 -0
  51. package/dist/routers/auth.d.ts +88 -7
  52. package/dist/routers/auth.js +339 -19
  53. package/dist/routers/chat.d.ts +6 -12
  54. package/dist/routers/copilot.d.ts +199 -0
  55. package/dist/routers/copilot.js +571 -0
  56. package/dist/routers/flashcards.d.ts +47 -81
  57. package/dist/routers/flashcards.js +143 -27
  58. package/dist/routers/members.d.ts +36 -7
  59. package/dist/routers/members.js +200 -19
  60. package/dist/routers/notifications.d.ts +99 -0
  61. package/dist/routers/notifications.js +127 -0
  62. package/dist/routers/payment.d.ts +89 -0
  63. package/dist/routers/payment.js +403 -0
  64. package/dist/routers/podcast.d.ts +8 -13
  65. package/dist/routers/podcast.js +54 -31
  66. package/dist/routers/studyguide.d.ts +1 -29
  67. package/dist/routers/studyguide.js +80 -71
  68. package/dist/routers/worksheets.d.ts +105 -38
  69. package/dist/routers/worksheets.js +258 -68
  70. package/dist/routers/workspace.d.ts +139 -60
  71. package/dist/routers/workspace.js +455 -315
  72. package/dist/scripts/purge-deleted-users.d.ts +1 -0
  73. package/dist/scripts/purge-deleted-users.js +149 -0
  74. package/dist/server.js +130 -10
  75. package/dist/services/flashcard-progress.service.d.ts +18 -66
  76. package/dist/services/flashcard-progress.service.js +51 -42
  77. package/dist/trpc.d.ts +20 -21
  78. package/dist/trpc.js +150 -1
  79. package/package.json +1 -1
@@ -25,8 +25,13 @@ export declare class AISessionService {
25
25
  initSession(workspaceId: string, user: string): Promise<AISession>;
26
26
  processFile(sessionId: string, user: string, fileUrl: string, fileType: 'image' | 'pdf', maxPages?: number): Promise<ProcessFileResult>;
27
27
  generateStudyGuide(sessionId: string, user: string): Promise<string>;
28
- generateFlashcardQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string>;
29
- generateWorksheetQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'EASY' | 'MEDIUM' | 'HARD'): Promise<string>;
28
+ generateFlashcardQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard', prompt?: string): Promise<string>;
29
+ generateWorksheetQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'EASY' | 'MEDIUM' | 'HARD', options?: {
30
+ mode?: 'practice' | 'quiz';
31
+ mcqRatio?: number;
32
+ questionTypes?: string[];
33
+ prompt?: string;
34
+ }): Promise<string>;
30
35
  checkWorksheetQuestions(sessionId: string, user: string, question: string, answer: string, mark_scheme: MarkScheme): Promise<UserMarkScheme>;
31
36
  generatePodcastStructure(sessionId: string, user: string, title: string, description: string, prompt: string, speakers: Array<{
32
37
  id: string;
@@ -39,6 +44,14 @@ export declare class AISessionService {
39
44
  name?: string;
40
45
  }>, voiceId?: string): Promise<any>;
41
46
  generatePodcastImage(sessionId: string, user: string, summary: string): Promise<string>;
47
+ segmentStudyGuide(sessionId: string, user: string, studyGuide: string): Promise<{
48
+ hint: string;
49
+ content: string;
50
+ }[]>;
51
+ validateSegmentSummary(sessionId: string, user: string, segmentContent: string, studentResponse: string, studyGuide: string): Promise<{
52
+ valid: boolean;
53
+ feedback: string;
54
+ }>;
42
55
  getSession(sessionId: string): AISession | undefined;
43
56
  getSessionsByUserAndWorkspace(userId: string, workspaceId: string): AISession[];
44
57
  deleteSession(sessionId: string): boolean;
@@ -1,12 +1,13 @@
1
1
  import { TRPCError } from '@trpc/server';
2
2
  import { logger } from './logger.js';
3
+ import { withRetry } from './retry.js';
3
4
  // External AI service configuration
4
5
  // const AI_SERVICE_URL = 'https://7gzvf7uib04yp9-61016.proxy.runpod.net/upload';
5
6
  // const AI_RESPONSE_URL = 'https://7gzvf7uib04yp9-61016.proxy.runpod.net/last_response';
6
7
  const AI_SERVICE_URL = process.env.INFERENCE_API_URL + '/upload';
7
8
  const AI_RESPONSE_URL = process.env.INFERENCE_API_URL + '/last_response';
8
- console.log('AI_SERVICE_URL', AI_SERVICE_URL);
9
- console.log('AI_RESPONSE_URL', AI_RESPONSE_URL);
9
+ logger.info(`AI_SERVICE_URL: ${AI_SERVICE_URL}`);
10
+ logger.info(`AI_RESPONSE_URL: ${AI_RESPONSE_URL}`);
10
11
  // Mock mode flag - when true, returns fake responses instead of calling AI service
11
12
  const MOCK_MODE = process.env.DONT_TEST_INFERENCE === 'true';
12
13
  const IMITATE_WAIT_TIME_MS = MOCK_MODE ? 1000 * 10 : 0;
@@ -20,7 +21,7 @@ export class AISessionService {
20
21
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
21
22
  // Mock mode - return fake session
22
23
  if (MOCK_MODE) {
23
- console.log(`🎭 MOCK MODE: Initializing AI session for workspace ${workspaceId}`);
24
+ logger.info(`MOCK MODE: Initializing AI session for workspace ${workspaceId}`);
24
25
  const session = {
25
26
  id: sessionId,
26
27
  workspaceId,
@@ -41,19 +42,19 @@ export class AISessionService {
41
42
  let lastError = null;
42
43
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
43
44
  try {
44
- console.log(`🤖 AI Session init attempt ${attempt}/${maxRetries} for workspace ${workspaceId}`);
45
+ logger.info(`AI Session init attempt ${attempt}/${maxRetries} for workspace ${workspaceId}`);
45
46
  const response = await fetch(AI_SERVICE_URL, {
46
47
  method: 'POST',
47
48
  body: formData,
48
49
  });
49
- console.log(`📡 AI Service response status: ${response.status} ${response.statusText}`);
50
+ logger.info(`AI Service response status: ${response.status} ${response.statusText}`);
50
51
  if (!response.ok) {
51
52
  const errorText = await response.text();
52
- console.error(`❌ AI Service error response:`, errorText);
53
+ logger.error(`AI Service error response: ${errorText}`);
53
54
  throw new Error(`AI service error: ${response.status} ${response.statusText} - ${errorText}`);
54
55
  }
55
56
  const result = await response.json();
56
- console.log(`📋 AI Service result:`, result);
57
+ logger.debug(`AI Service result: ${JSON.stringify(result)}`);
57
58
  // If we get a response with a message, consider it successful
58
59
  if (!result.message) {
59
60
  throw new Error(`AI service error: No response message`);
@@ -67,20 +68,20 @@ export class AISessionService {
67
68
  updatedAt: new Date(),
68
69
  };
69
70
  this.sessions.set(sessionId, session);
70
- console.log(`✅ AI Session initialized successfully on attempt ${attempt}`);
71
+ logger.info(`AI Session initialized successfully on attempt ${attempt}`);
71
72
  return session;
72
73
  }
73
74
  catch (error) {
74
75
  lastError = error instanceof Error ? error : new Error('Unknown error');
75
- console.error(`❌ AI Session init attempt ${attempt} failed:`, lastError.message);
76
+ logger.error(`AI Session init attempt ${attempt} failed: ${lastError.message}`);
76
77
  if (attempt < maxRetries) {
77
78
  const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s
78
- console.log(`⏳ Retrying in ${delay}ms...`);
79
+ logger.info(`Retrying in ${delay}ms...`);
79
80
  await new Promise(resolve => setTimeout(resolve, delay));
80
81
  }
81
82
  }
82
83
  }
83
- console.error(`💥 All ${maxRetries} attempts failed. Last error:`, lastError?.message);
84
+ logger.error(`All ${maxRetries} attempts failed. Last error: ${lastError?.message}`);
84
85
  throw new TRPCError({
85
86
  code: 'INTERNAL_SERVER_ERROR',
86
87
  message: `Failed to initialize AI session after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`,
@@ -112,7 +113,7 @@ export class AISessionService {
112
113
  if (maxPages) {
113
114
  formData.append('maxPages', maxPages.toString());
114
115
  }
115
- console.log('formData', formData);
116
+ logger.debug('Processing file with formData');
116
117
  // Retry logic for file processing
117
118
  const maxRetries = 3;
118
119
  let lastError = null;
@@ -125,7 +126,7 @@ export class AISessionService {
125
126
  const response = await fetch(AI_SERVICE_URL, {
126
127
  method: 'POST',
127
128
  body: formData,
128
- // signal: controller.signal,
129
+ signal: controller.signal,
129
130
  });
130
131
  clearTimeout(timeoutId);
131
132
  if (!response.ok) {
@@ -165,7 +166,7 @@ export class AISessionService {
165
166
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
166
167
  // Mock mode - return fake study guide
167
168
  if (MOCK_MODE) {
168
- console.log(`🎭 MOCK MODE: Generating study guide for session ${sessionId}`);
169
+ logger.info(`MOCK MODE: Generating study guide for session ${sessionId}`);
169
170
  return `# Mock Study Guide
170
171
 
171
172
  ## Overview
@@ -186,30 +187,27 @@ This mock study guide demonstrates the structure and format that would be genera
186
187
 
187
188
  *Note: This is a mock response generated when DONT_TEST_INFERENCE=true*`;
188
189
  }
189
- const formData = new FormData();
190
- formData.append('command', 'generate_study_guide');
191
- formData.append('session', sessionId);
192
- formData.append('user', user);
193
- try {
190
+ return withRetry(async () => {
191
+ const formData = new FormData();
192
+ formData.append('command', 'generate_study_guide');
193
+ formData.append('session', sessionId);
194
+ formData.append('user', user);
194
195
  const response = await fetch(AI_SERVICE_URL, {
195
196
  method: 'POST',
196
197
  body: formData,
197
198
  });
198
199
  if (!response.ok) {
199
- throw new Error(`AI service error: ${response.status} ${response.statusText}`);
200
+ const errorBody = await response.text().catch(() => '');
201
+ throw new Error(`AI service error: ${response.status} ${response.statusText} - ${errorBody}`);
200
202
  }
201
203
  const result = await response.json();
204
+ if (!result.markdown) {
205
+ throw new Error('AI service returned empty study guide');
206
+ }
202
207
  return result.markdown;
203
- }
204
- catch (error) {
205
- throw new TRPCError({
206
- code: 'INTERNAL_SERVER_ERROR',
207
- message: `Failed to generate study guide: ${error instanceof Error ? error.message : 'Unknown error'}`,
208
- });
209
- }
208
+ }, { maxRetries: 3, timeoutMs: 300000, label: 'generateStudyGuide' });
210
209
  }
211
- // Generate flashcard questions
212
- async generateFlashcardQuestions(sessionId, user, numQuestions, difficulty) {
210
+ async generateFlashcardQuestions(sessionId, user, numQuestions, difficulty, prompt) {
213
211
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
214
212
  // Mock mode - return fake flashcard questions
215
213
  if (MOCK_MODE) {
@@ -222,13 +220,16 @@ This mock study guide demonstrates the structure and format that would be genera
222
220
  category: `Mock Category ${(i % 3) + 1}`
223
221
  })));
224
222
  }
225
- const formData = new FormData();
226
- formData.append('command', 'generate_flashcard_questions');
227
- formData.append('session', sessionId);
228
- formData.append('user', user);
229
- formData.append('num_questions', numQuestions.toString());
230
- formData.append('difficulty', difficulty);
231
- try {
223
+ return withRetry(async () => {
224
+ const formData = new FormData();
225
+ formData.append('command', 'generate_flashcard_questions');
226
+ formData.append('session', sessionId);
227
+ formData.append('user', user);
228
+ formData.append('num_questions', numQuestions.toString());
229
+ formData.append('difficulty', difficulty);
230
+ if (prompt) {
231
+ formData.append('prompt', prompt);
232
+ }
232
233
  const response = await fetch(AI_SERVICE_URL, {
233
234
  method: 'POST',
234
235
  body: formData,
@@ -237,52 +238,59 @@ This mock study guide demonstrates the structure and format that would be genera
237
238
  throw new Error(`AI service error: ${response.status} ${response.statusText}`);
238
239
  }
239
240
  const result = await response.json();
240
- console.log(JSON.parse(result.flashcards));
241
241
  return JSON.parse(result.flashcards).flashcards;
242
- }
243
- catch (error) {
244
- throw new TRPCError({
245
- code: 'INTERNAL_SERVER_ERROR',
246
- message: `Failed to generate flashcard questions: ${error instanceof Error ? error.message : 'Unknown error'}`,
247
- });
248
- }
242
+ }, { maxRetries: 3, timeoutMs: 300000, label: 'generateFlashcardQuestions' });
249
243
  }
250
244
  // Generate worksheet questions
251
- async generateWorksheetQuestions(sessionId, user, numQuestions, difficulty) {
245
+ async generateWorksheetQuestions(sessionId, user, numQuestions, difficulty, options) {
252
246
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
253
247
  // Mock mode - return fake worksheet questions
254
248
  if (MOCK_MODE) {
255
249
  logger.info(`🎭 MOCK MODE: Generating ${numQuestions} ${difficulty} worksheet questions for session ${sessionId}`);
250
+ const mode = options?.mode ?? 'practice';
251
+ const isQuiz = mode === 'quiz';
256
252
  return JSON.stringify({
257
- worksheet: {
258
- title: `Mock Worksheet - ${difficulty} Level`,
259
- questions: Array.from({ length: numQuestions }, (_, i) => ({
260
- id: `mock-worksheet-q${i + 1}`,
261
- question: `Mock worksheet question ${i + 1}: Based on the uploaded material, explain the key concept and provide examples.`,
262
- type: i % 2 === 0 ? 'short_answer' : 'essay',
263
- difficulty: difficulty,
264
- estimatedTime: difficulty === 'EASY' ? '2-3 minutes' : difficulty === 'MEDIUM' ? '5-7 minutes' : '10-15 minutes',
265
- points: difficulty === 'EASY' ? 5 : difficulty === 'MEDIUM' ? 10 : 15
266
- })),
267
- instructions: "This is a mock worksheet generated for testing purposes. Answer all questions based on the uploaded materials.",
268
- totalPoints: numQuestions * (difficulty === 'EASY' ? 5 : difficulty === 'MEDIUM' ? 10 : 15),
269
- estimatedTime: `${numQuestions * (difficulty === 'EASY' ? 3 : difficulty === 'MEDIUM' ? 6 : 12)} minutes`
270
- },
271
- metadata: {
272
- totalQuestions: numQuestions,
273
- difficulty: difficulty,
274
- generatedAt: new Date().toISOString(),
275
- isMock: true
276
- }
253
+ title: isQuiz ? `Mock Quiz - ${difficulty}` : `Mock Worksheet - ${difficulty} Level`,
254
+ description: 'Mock generated content',
255
+ difficulty,
256
+ estimatedTime: `${numQuestions * 2} min`,
257
+ problems: Array.from({ length: numQuestions }, (_, i) => {
258
+ if (isQuiz) {
259
+ return {
260
+ question: `Mock MCQ ${i + 1}: What is 2+2?`,
261
+ answer: '1',
262
+ type: 'MULTIPLE_CHOICE',
263
+ options: ['3', '4', '5', '6'],
264
+ mark_scheme: { points: [{ point: 1, requirements: 'Select correct option' }], totalPoints: 1 },
265
+ };
266
+ }
267
+ return {
268
+ question: `Mock question ${i + 1}: Explain a concept.`,
269
+ answer: 'Mock answer',
270
+ type: 'TEXT',
271
+ options: [],
272
+ mark_scheme: { points: [{ point: 1, requirements: 'Clear explanation' }], totalPoints: 1 },
273
+ };
274
+ }),
277
275
  });
278
276
  }
279
- const formData = new FormData();
280
- formData.append('command', 'generate_worksheet_questions');
281
- formData.append('session', sessionId);
282
- formData.append('user', user);
283
- formData.append('num_questions', numQuestions.toString());
284
- formData.append('difficulty', difficulty);
285
- try {
277
+ return withRetry(async () => {
278
+ const formData = new FormData();
279
+ formData.append('command', 'generate_worksheet_questions');
280
+ formData.append('session', sessionId);
281
+ formData.append('user', user);
282
+ formData.append('num_questions', numQuestions.toString());
283
+ formData.append('difficulty', difficulty);
284
+ formData.append('mode', options?.mode ?? 'practice');
285
+ if (options?.mcqRatio !== undefined) {
286
+ formData.append('mcq_ratio', String(options.mcqRatio));
287
+ }
288
+ if (options?.questionTypes?.length) {
289
+ formData.append('question_types', JSON.stringify(options.questionTypes));
290
+ }
291
+ if (options?.prompt) {
292
+ formData.append('worksheet_prompt', options.prompt);
293
+ }
286
294
  const response = await fetch(AI_SERVICE_URL, {
287
295
  method: 'POST',
288
296
  body: formData,
@@ -291,15 +299,8 @@ This mock study guide demonstrates the structure and format that would be genera
291
299
  throw new Error(`AI service error: ${response.status} ${response.statusText}`);
292
300
  }
293
301
  const result = await response.json();
294
- console.log(JSON.parse(result.worksheet));
295
302
  return result.worksheet;
296
- }
297
- catch (error) {
298
- throw new TRPCError({
299
- code: 'INTERNAL_SERVER_ERROR',
300
- message: `Failed to generate worksheet questions: ${error instanceof Error ? error.message : 'Unknown error'}`,
301
- });
302
- }
303
+ }, { maxRetries: 3, timeoutMs: 300000, label: 'generateWorksheetQuestions' });
303
304
  }
304
305
  async checkWorksheetQuestions(sessionId, user, question, answer, mark_scheme) {
305
306
  const formData = new FormData();
@@ -317,7 +318,7 @@ This mock study guide demonstrates the structure and format that would be genera
317
318
  throw new Error(`AI service error: ${response.status} ${response.statusText}`);
318
319
  }
319
320
  const result = await response.json();
320
- console.log(result.marking);
321
+ logger.debug(`Worksheet marking result received`);
321
322
  return JSON.parse(result.marking);
322
323
  }
323
324
  // Generate podcast structure
@@ -460,6 +461,67 @@ This mock study guide demonstrates the structure and format that would be genera
460
461
  });
461
462
  }
462
463
  }
464
+ async segmentStudyGuide(sessionId, user, studyGuide) {
465
+ // def generate_study_guide_segmentation(request):
466
+ // user = request.form.get("user")
467
+ // session = request.form.get("session")
468
+ // study_guide = request.form.get("study_guide")
469
+ // if not user or not session:
470
+ // return {"error": "Session not initialized."}, 400
471
+ // if not study_guide:
472
+ // print("Study guide not provided.")
473
+ // return {"error": "Study guide not provided."}, 400
474
+ // messages = generate_segmentation(study_guide)
475
+ // return {"segmentation": messages}, 200
476
+ const formData = new FormData();
477
+ formData.append('command', 'generate_study_guide_segmentation');
478
+ formData.append('session', sessionId);
479
+ formData.append('user', user);
480
+ formData.append('study_guide', studyGuide);
481
+ try {
482
+ const response = await fetch(AI_SERVICE_URL, {
483
+ method: 'POST',
484
+ body: formData,
485
+ });
486
+ if (!response.ok) {
487
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
488
+ }
489
+ const result = await response.json();
490
+ return result.segmentation;
491
+ }
492
+ catch (error) {
493
+ throw new TRPCError({
494
+ code: 'INTERNAL_SERVER_ERROR',
495
+ message: `Failed to segment study guide: ${error instanceof Error ? error.message : 'Unknown error'}`,
496
+ });
497
+ }
498
+ }
499
+ async validateSegmentSummary(sessionId, user, segmentContent, studentResponse, studyGuide) {
500
+ const formData = new FormData();
501
+ formData.append('command', 'validate_segment_summary');
502
+ formData.append('session', sessionId);
503
+ formData.append('user', user);
504
+ formData.append('segment_content', segmentContent);
505
+ formData.append('student_response', studentResponse);
506
+ formData.append('study_guide', studyGuide);
507
+ try {
508
+ const response = await fetch(AI_SERVICE_URL, {
509
+ method: 'POST',
510
+ body: formData,
511
+ });
512
+ if (!response.ok) {
513
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
514
+ }
515
+ const result = await response.json();
516
+ return result.feedback;
517
+ }
518
+ catch (error) {
519
+ throw new TRPCError({
520
+ code: 'INTERNAL_SERVER_ERROR',
521
+ message: `Failed to validate segment summary: ${error instanceof Error ? error.message : 'Unknown error'}`,
522
+ });
523
+ }
524
+ }
463
525
  // Get session by ID
464
526
  getSession(sessionId) {
465
527
  return this.sessions.get(sessionId);
@@ -477,20 +539,20 @@ This mock study guide demonstrates the structure and format that would be genera
477
539
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
478
540
  // Mock mode - always return healthy
479
541
  if (MOCK_MODE) {
480
- console.log('🎭 MOCK MODE: AI service health check - returning healthy');
542
+ logger.info('MOCK MODE: AI service health check - returning healthy');
481
543
  return true;
482
544
  }
483
545
  try {
484
- console.log('🏥 Checking AI service health...');
546
+ logger.info('Checking AI service health...');
485
547
  const response = await fetch(AI_SERVICE_URL, {
486
548
  method: 'POST',
487
549
  body: new FormData(), // Empty form data
488
550
  });
489
- console.log(`🏥 AI Service health check status: ${response.status}`);
551
+ logger.info(`AI Service health check status: ${response.status}`);
490
552
  return response.ok;
491
553
  }
492
554
  catch (error) {
493
- console.error('🏥 AI Service health check failed:', error);
555
+ logger.error('AI Service health check failed:', error);
494
556
  return false;
495
557
  }
496
558
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared constants for artifact types.
3
+ * Mirrors the Prisma ArtifactType enum to avoid direct type imports
4
+ * in contexts where Prisma client types may not be available.
5
+ */
6
+ export declare const ArtifactType: {
7
+ readonly STUDY_GUIDE: "STUDY_GUIDE";
8
+ readonly FLASHCARD_SET: "FLASHCARD_SET";
9
+ readonly WORKSHEET: "WORKSHEET";
10
+ readonly MEETING_SUMMARY: "MEETING_SUMMARY";
11
+ readonly PODCAST_EPISODE: "PODCAST_EPISODE";
12
+ };
13
+ export type ArtifactTypeValue = (typeof ArtifactType)[keyof typeof ArtifactType];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shared constants for artifact types.
3
+ * Mirrors the Prisma ArtifactType enum to avoid direct type imports
4
+ * in contexts where Prisma client types may not be available.
5
+ */
6
+ export const ArtifactType = {
7
+ STUDY_GUIDE: 'STUDY_GUIDE',
8
+ FLASHCARD_SET: 'FLASHCARD_SET',
9
+ WORKSHEET: 'WORKSHEET',
10
+ MEETING_SUMMARY: 'MEETING_SUMMARY',
11
+ PODCAST_EPISODE: 'PODCAST_EPISODE',
12
+ };
@@ -0,0 +1,11 @@
1
+ export declare function sendVerificationEmail(email: string, token: string, name?: string | null): Promise<boolean>;
2
+ export declare function sendInvitationEmail(invitation: {
3
+ email: string;
4
+ token: string;
5
+ role: string;
6
+ workspaceTitle: string;
7
+ invitedByName: string;
8
+ }): Promise<boolean>;
9
+ export declare function sendAccountDeletionScheduledEmail(email: string, token: string): Promise<boolean>;
10
+ export declare function sendPasswordResetEmail(email: string, token: string, name?: string | null): Promise<boolean>;
11
+ export declare function sendAccountRestoredEmail(email: string): Promise<boolean>;
@@ -0,0 +1,193 @@
1
+ import nodemailer from 'nodemailer';
2
+ import { logger } from './logger.js';
3
+ import { env } from './env.js';
4
+ // Commented out Resend flow as requested
5
+ // import { Resend } from 'resend';
6
+ // const resend = env.RESEND_API_KEY ? new Resend(env.RESEND_API_KEY) : null;
7
+ const transporter = nodemailer.createTransport({
8
+ host: env.SMTP_HOST,
9
+ port: env.SMTP_PORT,
10
+ secure: env.SMTP_SECURE,
11
+ auth: {
12
+ user: env.SMTP_USER,
13
+ pass: env.SMTP_PASSWORD,
14
+ },
15
+ });
16
+ const FROM_EMAIL = env.EMAIL_FROM;
17
+ const APP_URL = env.FRONTEND_URL;
18
+ export async function sendVerificationEmail(email, token, name) {
19
+ const verifyUrl = `${APP_URL}/verify-email?token=${token}`;
20
+ const greeting = name ? `, ${name}` : '';
21
+ const html = `
22
+ <div style="font-family:system-ui,sans-serif;max-width:480px;margin:0 auto;padding:40px 20px;">
23
+ <h2 style="font-size:20px;font-weight:600;margin-bottom:8px;">Welcome to Scribe${greeting}!</h2>
24
+ <p style="color:#6b7280;font-size:14px;line-height:1.6;margin-bottom:24px;">
25
+ Please verify your email address to get the most out of your account.
26
+ </p>
27
+ <a href="${verifyUrl}" style="display:inline-block;background-color:#7c3aed;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-size:14px;font-weight:500;">Verify Email</a>
28
+ <p style="color:#9ca3af;font-size:12px;margin-top:32px;line-height:1.5;">
29
+ If you did not create an account on Scribe, you can safely ignore this email.
30
+ This link expires in 24 hours.
31
+ </p>
32
+ </div>
33
+ `;
34
+ try {
35
+ if (!env.SMTP_HOST) {
36
+ logger.warn('Email service not configured (SMTP_HOST missing). Logging email content instead:');
37
+ logger.info(`Verification Email to ${email}: ${verifyUrl}`);
38
+ return true;
39
+ }
40
+ await transporter.sendMail({
41
+ from: FROM_EMAIL,
42
+ to: email,
43
+ subject: 'Verify your email - Scribe',
44
+ html,
45
+ });
46
+ logger.info(`Verification email sent to ${email}`);
47
+ return true;
48
+ }
49
+ catch (err) {
50
+ logger.error('Email send error:', err);
51
+ return false;
52
+ }
53
+ }
54
+ export async function sendInvitationEmail(invitation) {
55
+ const inviteUrl = `${APP_URL}/accept-invite?token=${invitation.token}`;
56
+ const html = `
57
+ <div style="font-family:system-ui,sans-serif;max-width:480px;margin:0 auto;padding:40px 20px;">
58
+ <h2 style="font-size:20px;font-weight:600;margin-bottom:8px;">Workspace Invitation</h2>
59
+ <p style="color:#374151;font-size:14px;line-height:1.6;margin-bottom:16px;">
60
+ <strong>${invitation.invitedByName}</strong> has invited you to join the <strong>${invitation.workspaceTitle}</strong> workspace as a <strong>${invitation.role}</strong>.
61
+ </p>
62
+ <p style="color:#6b7280;font-size:14px;line-height:1.6;margin-bottom:24px;">
63
+ Click the button below to accept the invitation and start collaborating.
64
+ </p>
65
+ <a href="${inviteUrl}" style="display:inline-block;background-color:#7c3aed;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-size:14px;font-weight:500;">Accept Invitation</a>
66
+ <p style="color:#9ca3af;font-size:12px;margin-top:32px;line-height:1.5;">
67
+ This invitation was sent to ${invitation.email}. If you weren't expecting this invitation, you can safely ignore this email.
68
+ </p>
69
+ </div>
70
+ `;
71
+ try {
72
+ if (!env.SMTP_HOST) {
73
+ logger.warn('Email service not configured (SMTP_HOST missing). Logging invitation link instead:');
74
+ logger.info(`Invitation Link for ${invitation.email}: ${inviteUrl}`);
75
+ return true;
76
+ }
77
+ await transporter.sendMail({
78
+ from: FROM_EMAIL,
79
+ to: invitation.email,
80
+ subject: `Invitation to join ${invitation.workspaceTitle} on Scribe`,
81
+ html,
82
+ });
83
+ logger.info(`Invitation email sent to ${invitation.email}`);
84
+ return true;
85
+ }
86
+ catch (err) {
87
+ logger.error('Email send error:', err);
88
+ return false;
89
+ }
90
+ }
91
+ export async function sendAccountDeletionScheduledEmail(email, token) {
92
+ const restoreUrl = `${APP_URL}/restore-account?token=${token}`;
93
+ const html = `
94
+ <div style="font-family:system-ui,sans-serif;max-width:480px;margin:0 auto;padding:40px 20px;">
95
+ <h2 style="font-size:20px;font-weight:600;margin-bottom:8px;color:#dc2626;">Account Deletion Scheduled</h2>
96
+ <p style="color:#374151;font-size:14px;line-height:1.6;margin-bottom:16px;">
97
+ Your account is scheduled for permanent deletion in 30 days.
98
+ </p>
99
+ <p style="color:#6b7280;font-size:14px;line-height:1.6;margin-bottom:24px;">
100
+ If you change your mind, you can restore your account by clicking the button below. This link will remain active during the 30-day grace period.
101
+ </p>
102
+ <a href="${restoreUrl}" style="display:inline-block;background-color:#7c3aed;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-size:14px;font-weight:500;">Restore Account</a>
103
+ <p style="color:#9ca3af;font-size:12px;margin-top:32px;line-height:1.5;">
104
+ If you meant to delete your account, you can safely ignore this email.
105
+ </p>
106
+ </div>
107
+ `;
108
+ try {
109
+ if (!env.SMTP_HOST) {
110
+ logger.warn('Email service not configured (SMTP_HOST missing). Logging email content instead:');
111
+ logger.info(`Account Deletion Scheduled Email to ${email}: ${restoreUrl}`);
112
+ return true;
113
+ }
114
+ await transporter.sendMail({
115
+ from: FROM_EMAIL,
116
+ to: email,
117
+ subject: 'Account Deletion Scheduled - Scribe',
118
+ html,
119
+ });
120
+ logger.info(`Account deletion scheduled email sent to ${email}`);
121
+ return true;
122
+ }
123
+ catch (err) {
124
+ logger.error('Email send error:', err);
125
+ return false;
126
+ }
127
+ }
128
+ export async function sendPasswordResetEmail(email, token, name) {
129
+ const resetUrl = `${APP_URL}/reset-password?token=${encodeURIComponent(token)}`;
130
+ const greeting = name ? `, ${name}` : '';
131
+ const html = `
132
+ <div style="font-family:system-ui,sans-serif;max-width:480px;margin:0 auto;padding:40px 20px;">
133
+ <h2 style="font-size:20px;font-weight:600;margin-bottom:8px;">Reset your password${greeting}</h2>
134
+ <p style="color:#6b7280;font-size:14px;line-height:1.6;margin-bottom:24px;">
135
+ We received a request to reset your Scribe password. Click the button below to choose a new password.
136
+ </p>
137
+ <a href="${resetUrl}" style="display:inline-block;background-color:#7c3aed;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-size:14px;font-weight:500;">Reset password</a>
138
+ <p style="color:#9ca3af;font-size:12px;margin-top:32px;line-height:1.5;">
139
+ If you did not request this, you can ignore this email. This link expires in 1 hour.
140
+ </p>
141
+ </div>
142
+ `;
143
+ try {
144
+ if (!env.SMTP_HOST) {
145
+ logger.warn('Email service not configured (SMTP_HOST missing). Logging reset link instead:');
146
+ logger.info(`Password reset for ${email}: ${resetUrl}`);
147
+ return true;
148
+ }
149
+ await transporter.sendMail({
150
+ from: FROM_EMAIL,
151
+ to: email,
152
+ subject: 'Reset your password - Scribe',
153
+ html,
154
+ });
155
+ logger.info(`Password reset email sent to ${email}`);
156
+ return true;
157
+ }
158
+ catch (err) {
159
+ logger.error('Email send error:', err);
160
+ return false;
161
+ }
162
+ }
163
+ export async function sendAccountRestoredEmail(email) {
164
+ const loginUrl = `${APP_URL}/login`;
165
+ const html = `
166
+ <div style="font-family:system-ui,sans-serif;max-width:480px;margin:0 auto;padding:40px 20px;">
167
+ <h2 style="font-size:20px;font-weight:600;margin-bottom:8px;color:#16a34a;">Account Restored Successfully</h2>
168
+ <p style="color:#374151;font-size:14px;line-height:1.6;margin-bottom:16px;">
169
+ Your account has been successfully restored. Your data is safe and your account deletion process has been cancelled.
170
+ </p>
171
+ <a href="${loginUrl}" style="display:inline-block;background-color:#7c3aed;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-size:14px;font-weight:500;">Log In</a>
172
+ </div>
173
+ `;
174
+ try {
175
+ if (!env.SMTP_HOST) {
176
+ logger.warn('Email service not configured (SMTP_HOST missing). Logging email content instead:');
177
+ logger.info(`Account Restored Email to ${email}`);
178
+ return true;
179
+ }
180
+ await transporter.sendMail({
181
+ from: FROM_EMAIL,
182
+ to: email,
183
+ subject: 'Account Restored - Scribe',
184
+ html,
185
+ });
186
+ logger.info(`Account restored email sent to ${email}`);
187
+ return true;
188
+ }
189
+ catch (err) {
190
+ logger.error('Email send error:', err);
191
+ return false;
192
+ }
193
+ }