@goscribe/server 1.1.7 → 1.3.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 (56) hide show
  1. package/.env.example +43 -0
  2. package/check-difficulty.cjs +14 -0
  3. package/check-questions.cjs +14 -0
  4. package/db-summary.cjs +22 -0
  5. package/dist/routers/auth.js +1 -1
  6. package/mcq-test.cjs +36 -0
  7. package/package.json +10 -2
  8. package/prisma/migrations/20260413143206_init/migration.sql +873 -0
  9. package/prisma/schema.prisma +485 -292
  10. package/src/context.ts +4 -1
  11. package/src/lib/activity_human_description.test.ts +28 -0
  12. package/src/lib/activity_human_description.ts +239 -0
  13. package/src/lib/activity_log_service.test.ts +37 -0
  14. package/src/lib/activity_log_service.ts +353 -0
  15. package/src/lib/ai-session.ts +194 -112
  16. package/src/lib/constants.ts +14 -0
  17. package/src/lib/email.ts +230 -0
  18. package/src/lib/env.ts +23 -6
  19. package/src/lib/inference.ts +3 -3
  20. package/src/lib/logger.ts +26 -9
  21. package/src/lib/notification-service.test.ts +106 -0
  22. package/src/lib/notification-service.ts +677 -0
  23. package/src/lib/prisma.ts +6 -1
  24. package/src/lib/pusher.ts +90 -6
  25. package/src/lib/retry.ts +61 -0
  26. package/src/lib/storage.ts +2 -2
  27. package/src/lib/stripe.ts +39 -0
  28. package/src/lib/subscription_service.ts +722 -0
  29. package/src/lib/usage_service.ts +74 -0
  30. package/src/lib/worksheet-generation.test.ts +31 -0
  31. package/src/lib/worksheet-generation.ts +139 -0
  32. package/src/lib/workspace-access.ts +13 -0
  33. package/src/routers/_app.ts +11 -0
  34. package/src/routers/admin.ts +710 -0
  35. package/src/routers/annotations.ts +227 -0
  36. package/src/routers/auth.ts +432 -33
  37. package/src/routers/copilot.ts +719 -0
  38. package/src/routers/flashcards.ts +207 -80
  39. package/src/routers/members.ts +280 -80
  40. package/src/routers/notifications.ts +142 -0
  41. package/src/routers/payment.ts +448 -0
  42. package/src/routers/podcast.ts +133 -108
  43. package/src/routers/studyguide.ts +80 -74
  44. package/src/routers/worksheets.ts +300 -80
  45. package/src/routers/workspace.ts +538 -328
  46. package/src/scripts/purge-deleted-users.ts +167 -0
  47. package/src/server.ts +140 -12
  48. package/src/services/flashcard-progress.service.ts +52 -43
  49. package/src/trpc.ts +184 -5
  50. package/test-generate.js +30 -0
  51. package/test-ratio.cjs +9 -0
  52. package/zod-test.cjs +22 -0
  53. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
  54. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
  55. package/prisma/seed.mjs +0 -135
  56. package/src/routers/meetingsummary.ts +0 -416
@@ -1,5 +1,6 @@
1
1
  import { TRPCError } from '@trpc/server';
2
2
  import { logger } from './logger.js';
3
+ import { withRetry } from './retry.js';
3
4
  import { MarkScheme, UserMarkScheme } from '../types/index.js';
4
5
 
5
6
  // External AI service configuration
@@ -8,8 +9,8 @@ import { MarkScheme, UserMarkScheme } from '../types/index.js';
8
9
  const AI_SERVICE_URL = process.env.INFERENCE_API_URL + '/upload';
9
10
  const AI_RESPONSE_URL = process.env.INFERENCE_API_URL + '/last_response';
10
11
 
11
- console.log('AI_SERVICE_URL', AI_SERVICE_URL);
12
- console.log('AI_RESPONSE_URL', AI_RESPONSE_URL);
12
+ logger.info(`AI_SERVICE_URL: ${AI_SERVICE_URL}`);
13
+ logger.info(`AI_RESPONSE_URL: ${AI_RESPONSE_URL}`);
13
14
 
14
15
  // Mock mode flag - when true, returns fake responses instead of calling AI service
15
16
  const MOCK_MODE = process.env.DONT_TEST_INFERENCE === 'true';
@@ -24,7 +25,7 @@ export interface AISession {
24
25
  updatedAt: Date;
25
26
  }
26
27
 
27
- const IMITATE_WAIT_TIME_MS = MOCK_MODE ? 1000 * 10 : 0;
28
+ const IMITATE_WAIT_TIME_MS = MOCK_MODE ? 1000 * 10 : 0;
28
29
 
29
30
  export interface ProcessFileResult {
30
31
  status: 'success' | 'error';
@@ -49,7 +50,7 @@ export class AISessionService {
49
50
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
50
51
  // Mock mode - return fake session
51
52
  if (MOCK_MODE) {
52
- console.log(`🎭 MOCK MODE: Initializing AI session for workspace ${workspaceId}`);
53
+ logger.info(`MOCK MODE: Initializing AI session for workspace ${workspaceId}`);
53
54
  const session: AISession = {
54
55
  id: sessionId,
55
56
  workspaceId,
@@ -61,7 +62,7 @@ export class AISessionService {
61
62
  this.sessions.set(sessionId, session);
62
63
  return session;
63
64
  }
64
-
65
+
65
66
  const formData = new FormData();
66
67
  formData.append('command', 'init_session');
67
68
  formData.append('session', sessionId);
@@ -73,24 +74,24 @@ export class AISessionService {
73
74
 
74
75
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
75
76
  try {
76
- console.log(`🤖 AI Session init attempt ${attempt}/${maxRetries} for workspace ${workspaceId}`);
77
-
77
+ logger.info(`AI Session init attempt ${attempt}/${maxRetries} for workspace ${workspaceId}`);
78
+
78
79
  const response = await fetch(AI_SERVICE_URL, {
79
80
  method: 'POST',
80
81
  body: formData,
81
82
  });
82
83
 
83
- console.log(`📡 AI Service response status: ${response.status} ${response.statusText}`);
84
+ logger.info(`AI Service response status: ${response.status} ${response.statusText}`);
84
85
 
85
86
  if (!response.ok) {
86
87
  const errorText = await response.text();
87
- console.error(`❌ AI Service error response:`, errorText);
88
+ logger.error(`AI Service error response: ${errorText}`);
88
89
  throw new Error(`AI service error: ${response.status} ${response.statusText} - ${errorText}`);
89
90
  }
90
91
 
91
92
  const result = await response.json();
92
- console.log(`📋 AI Service result:`, result);
93
-
93
+ logger.debug(`AI Service result: ${JSON.stringify(result)}`);
94
+
94
95
  // If we get a response with a message, consider it successful
95
96
  if (!result.message) {
96
97
  throw new Error(`AI service error: No response message`);
@@ -106,22 +107,22 @@ export class AISessionService {
106
107
  };
107
108
 
108
109
  this.sessions.set(sessionId, session);
109
- console.log(`✅ AI Session initialized successfully on attempt ${attempt}`);
110
+ logger.info(`AI Session initialized successfully on attempt ${attempt}`);
110
111
  return session;
111
-
112
+
112
113
  } catch (error) {
113
114
  lastError = error instanceof Error ? error : new Error('Unknown error');
114
- console.error(`❌ AI Session init attempt ${attempt} failed:`, lastError.message);
115
-
115
+ logger.error(`AI Session init attempt ${attempt} failed: ${lastError.message}`);
116
+
116
117
  if (attempt < maxRetries) {
117
118
  const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s
118
- console.log(`⏳ Retrying in ${delay}ms...`);
119
+ logger.info(`Retrying in ${delay}ms...`);
119
120
  await new Promise(resolve => setTimeout(resolve, delay));
120
121
  }
121
122
  }
122
123
  }
123
124
 
124
- console.error(`💥 All ${maxRetries} attempts failed. Last error:`, lastError?.message);
125
+ logger.error(`All ${maxRetries} attempts failed. Last error: ${lastError?.message}`);
125
126
  throw new TRPCError({
126
127
  code: 'INTERNAL_SERVER_ERROR',
127
128
  message: `Failed to initialize AI session after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`,
@@ -137,7 +138,7 @@ export class AISessionService {
137
138
  maxPages?: number
138
139
  ): Promise<ProcessFileResult> {
139
140
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
140
-
141
+
141
142
  // Mock mode - return fake processing result
142
143
  if (MOCK_MODE) {
143
144
  logger.info(`🎭 MOCK MODE: Processing ${fileType} file from URL for session ${sessionId}`);
@@ -163,7 +164,7 @@ export class AISessionService {
163
164
  formData.append('maxPages', maxPages.toString());
164
165
  }
165
166
 
166
- console.log('formData', formData);
167
+ logger.debug('Processing file with formData');
167
168
 
168
169
  // Retry logic for file processing
169
170
  const maxRetries = 3;
@@ -172,7 +173,7 @@ export class AISessionService {
172
173
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
173
174
  try {
174
175
  logger.info(`📄 Processing ${fileType} file attempt ${attempt}/${maxRetries} for session ${sessionId}`);
175
-
176
+
176
177
  // Set timeout for large files (5 minutes)
177
178
  const controller = new AbortController();
178
179
  const timeoutId = setTimeout(() => controller.abort(), 300000); // 5 min timeout
@@ -180,7 +181,7 @@ export class AISessionService {
180
181
  const response = await fetch(AI_SERVICE_URL, {
181
182
  method: 'POST',
182
183
  body: formData,
183
- // signal: controller.signal,
184
+ signal: controller.signal,
184
185
  });
185
186
 
186
187
  clearTimeout(timeoutId);
@@ -199,11 +200,11 @@ export class AISessionService {
199
200
  }
200
201
 
201
202
  return result as ProcessFileResult;
202
-
203
+
203
204
  } catch (error) {
204
205
  lastError = error instanceof Error ? error : new Error('Unknown error');
205
206
  logger.error(`❌ File processing attempt ${attempt} failed:`, lastError.message);
206
-
207
+
207
208
  if (attempt < maxRetries) {
208
209
  const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s
209
210
  logger.info(`⏳ Retrying file processing in ${delay}ms...`);
@@ -230,7 +231,7 @@ export class AISessionService {
230
231
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
231
232
  // Mock mode - return fake study guide
232
233
  if (MOCK_MODE) {
233
- console.log(`🎭 MOCK MODE: Generating study guide for session ${sessionId}`);
234
+ logger.info(`MOCK MODE: Generating study guide for session ${sessionId}`);
234
235
  return `# Mock Study Guide
235
236
 
236
237
  ## Overview
@@ -252,53 +253,55 @@ This mock study guide demonstrates the structure and format that would be genera
252
253
  *Note: This is a mock response generated when DONT_TEST_INFERENCE=true*`;
253
254
  }
254
255
 
255
- const formData = new FormData();
256
- formData.append('command', 'generate_study_guide');
257
- formData.append('session', sessionId);
258
- formData.append('user', user);
259
- try {
256
+ return withRetry(async () => {
257
+ const formData = new FormData();
258
+ formData.append('command', 'generate_study_guide');
259
+ formData.append('session', sessionId);
260
+ formData.append('user', user);
261
+
260
262
  const response = await fetch(AI_SERVICE_URL, {
261
263
  method: 'POST',
262
264
  body: formData,
263
265
  });
264
266
 
265
267
  if (!response.ok) {
266
- throw new Error(`AI service error: ${response.status} ${response.statusText}`);
268
+ const errorBody = await response.text().catch(() => '');
269
+ throw new Error(`AI service error: ${response.status} ${response.statusText} - ${errorBody}`);
267
270
  }
268
271
 
269
272
  const result = await response.json();
273
+ if (!result.markdown) {
274
+ throw new Error('AI service returned empty study guide');
275
+ }
270
276
  return result.markdown;
271
- } catch (error) {
272
- throw new TRPCError({
273
- code: 'INTERNAL_SERVER_ERROR',
274
- message: `Failed to generate study guide: ${error instanceof Error ? error.message : 'Unknown error'}`,
275
- });
276
- }
277
+ }, { maxRetries: 3, timeoutMs: 300000, label: 'generateStudyGuide' });
277
278
  }
278
279
 
279
- // Generate flashcard questions
280
- async generateFlashcardQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string> {
280
+ async generateFlashcardQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard', prompt?: string): Promise<string> {
281
281
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
282
282
  // Mock mode - return fake flashcard questions
283
283
  if (MOCK_MODE) {
284
284
  logger.info(`🎭 MOCK MODE: Generating ${numQuestions} ${difficulty} flashcard questions for session ${sessionId}`);
285
285
  return JSON.stringify(Array.from({ length: numQuestions }, (_, i) => ({
286
- id: `mock-flashcard-${i + 1}`,
287
- question: `Mock question ${i + 1}: What is the main concept covered in this material?`,
288
- answer: `Mock answer ${i + 1}: This is a sample answer that would be generated based on the uploaded content.`,
289
- difficulty: difficulty,
290
- category: `Mock Category ${(i % 3) + 1}`
291
- })));
286
+ id: `mock-flashcard-${i + 1}`,
287
+ question: `Mock question ${i + 1}: What is the main concept covered in this material?`,
288
+ answer: `Mock answer ${i + 1}: This is a sample answer that would be generated based on the uploaded content.`,
289
+ difficulty: difficulty,
290
+ category: `Mock Category ${(i % 3) + 1}`
291
+ })));
292
292
  }
293
293
 
294
- const formData = new FormData();
295
- formData.append('command', 'generate_flashcard_questions');
296
- formData.append('session', sessionId);
297
- formData.append('user', user);
298
- formData.append('num_questions', numQuestions.toString());
299
- formData.append('difficulty', difficulty);
294
+ return withRetry(async () => {
295
+ const formData = new FormData();
296
+ formData.append('command', 'generate_flashcard_questions');
297
+ formData.append('session', sessionId);
298
+ formData.append('user', user);
299
+ formData.append('num_questions', numQuestions.toString());
300
+ formData.append('difficulty', difficulty);
301
+ if (prompt) {
302
+ formData.append('prompt', prompt);
303
+ }
300
304
 
301
- try {
302
305
  const response = await fetch(AI_SERVICE_URL, {
303
306
  method: 'POST',
304
307
  body: formData,
@@ -309,57 +312,73 @@ This mock study guide demonstrates the structure and format that would be genera
309
312
  }
310
313
 
311
314
  const result = await response.json();
312
-
313
- console.log(JSON.parse(result.flashcards))
314
-
315
315
  return JSON.parse(result.flashcards).flashcards;
316
- } catch (error) {
317
- throw new TRPCError({
318
- code: 'INTERNAL_SERVER_ERROR',
319
- message: `Failed to generate flashcard questions: ${error instanceof Error ? error.message : 'Unknown error'}`,
320
- });
321
- }
316
+ }, { maxRetries: 3, timeoutMs: 300000, label: 'generateFlashcardQuestions' });
322
317
  }
323
318
 
324
319
  // Generate worksheet questions
325
- async generateWorksheetQuestions(sessionId: string, user: string, numQuestions: number, difficulty: 'EASY' | 'MEDIUM' | 'HARD'): Promise<string> {
320
+ async generateWorksheetQuestions(
321
+ sessionId: string,
322
+ user: string,
323
+ numQuestions: number,
324
+ difficulty: 'EASY' | 'MEDIUM' | 'HARD',
325
+ options?: {
326
+ mode?: 'practice' | 'quiz';
327
+ mcqRatio?: number;
328
+ questionTypes?: string[];
329
+ prompt?: string;
330
+ },
331
+ ): Promise<string> {
326
332
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
327
333
  // Mock mode - return fake worksheet questions
328
334
  if (MOCK_MODE) {
329
335
  logger.info(`🎭 MOCK MODE: Generating ${numQuestions} ${difficulty} worksheet questions for session ${sessionId}`);
336
+ const mode = options?.mode ?? 'practice';
337
+ const isQuiz = mode === 'quiz';
330
338
  return JSON.stringify({
331
- worksheet: {
332
- title: `Mock Worksheet - ${difficulty} Level`,
333
- questions: Array.from({ length: numQuestions }, (_, i) => ({
334
- id: `mock-worksheet-q${i + 1}`,
335
- question: `Mock worksheet question ${i + 1}: Based on the uploaded material, explain the key concept and provide examples.`,
336
- type: i % 2 === 0 ? 'short_answer' : 'essay',
337
- difficulty: difficulty,
338
- estimatedTime: difficulty === 'EASY' ? '2-3 minutes' : difficulty === 'MEDIUM' ? '5-7 minutes' : '10-15 minutes',
339
- points: difficulty === 'EASY' ? 5 : difficulty === 'MEDIUM' ? 10 : 15
340
- })),
341
- instructions: "This is a mock worksheet generated for testing purposes. Answer all questions based on the uploaded materials.",
342
- totalPoints: numQuestions * (difficulty === 'EASY' ? 5 : difficulty === 'MEDIUM' ? 10 : 15),
343
- estimatedTime: `${numQuestions * (difficulty === 'EASY' ? 3 : difficulty === 'MEDIUM' ? 6 : 12)} minutes`
344
- },
345
- metadata: {
346
- totalQuestions: numQuestions,
347
- difficulty: difficulty,
348
- generatedAt: new Date().toISOString(),
349
- isMock: true
350
- }
339
+ title: isQuiz ? `Mock Quiz - ${difficulty}` : `Mock Worksheet - ${difficulty} Level`,
340
+ description: 'Mock generated content',
341
+ difficulty,
342
+ estimatedTime: `${numQuestions * 2} min`,
343
+ problems: Array.from({ length: numQuestions }, (_, i) => {
344
+ if (isQuiz) {
345
+ return {
346
+ question: `Mock MCQ ${i + 1}: What is 2+2?`,
347
+ answer: '1',
348
+ type: 'MULTIPLE_CHOICE',
349
+ options: ['3', '4', '5', '6'],
350
+ mark_scheme: { points: [{ point: 1, requirements: 'Select correct option' }], totalPoints: 1 },
351
+ };
352
+ }
353
+ return {
354
+ question: `Mock question ${i + 1}: Explain a concept.`,
355
+ answer: 'Mock answer',
356
+ type: 'TEXT',
357
+ options: [],
358
+ mark_scheme: { points: [{ point: 1, requirements: 'Clear explanation' }], totalPoints: 1 },
359
+ };
360
+ }),
351
361
  });
352
362
  }
353
363
 
354
- const formData = new FormData();
355
- formData.append('command', 'generate_worksheet_questions');
356
- formData.append('session', sessionId);
357
- formData.append('user', user);
358
- formData.append('num_questions', numQuestions.toString());
359
- formData.append('difficulty', difficulty);
360
-
364
+ return withRetry(async () => {
365
+ const formData = new FormData();
366
+ formData.append('command', 'generate_worksheet_questions');
367
+ formData.append('session', sessionId);
368
+ formData.append('user', user);
369
+ formData.append('num_questions', numQuestions.toString());
370
+ formData.append('difficulty', difficulty);
371
+ formData.append('mode', options?.mode ?? 'practice');
372
+ if (options?.mcqRatio !== undefined) {
373
+ formData.append('mcq_ratio', String(options.mcqRatio));
374
+ }
375
+ if (options?.questionTypes?.length) {
376
+ formData.append('question_types', JSON.stringify(options.questionTypes));
377
+ }
378
+ if (options?.prompt) {
379
+ formData.append('worksheet_prompt', options.prompt);
380
+ }
361
381
 
362
- try {
363
382
  const response = await fetch(AI_SERVICE_URL, {
364
383
  method: 'POST',
365
384
  body: formData,
@@ -370,16 +389,8 @@ This mock study guide demonstrates the structure and format that would be genera
370
389
  }
371
390
 
372
391
  const result = await response.json();
373
-
374
- console.log(JSON.parse(result.worksheet));
375
-
376
392
  return result.worksheet;
377
- } catch (error) {
378
- throw new TRPCError({
379
- code: 'INTERNAL_SERVER_ERROR',
380
- message: `Failed to generate worksheet questions: ${error instanceof Error ? error.message : 'Unknown error'}`,
381
- });
382
- }
393
+ }, { maxRetries: 3, timeoutMs: 300000, label: 'generateWorksheetQuestions' });
383
394
  }
384
395
 
385
396
  async checkWorksheetQuestions(sessionId: string, user: string, question: string, answer: string, mark_scheme: MarkScheme): Promise<UserMarkScheme> {
@@ -402,21 +413,21 @@ This mock study guide demonstrates the structure and format that would be genera
402
413
  }
403
414
 
404
415
  const result = await response.json();
405
- console.log(result.marking);
416
+ logger.debug(`Worksheet marking result received`);
406
417
  return JSON.parse(result.marking);
407
418
  }
408
419
 
409
420
  // Generate podcast structure
410
421
  async generatePodcastStructure(
411
- sessionId: string,
412
- user: string,
413
- title: string,
414
- description: string,
422
+ sessionId: string,
423
+ user: string,
424
+ title: string,
425
+ description: string,
415
426
  prompt: string,
416
427
  speakers: Array<{ id: string; role: string; name?: string }>
417
428
  ): Promise<any> {
418
429
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
419
-
430
+
420
431
  // Mock mode - return fake podcast structure
421
432
  if (MOCK_MODE) {
422
433
  logger.info(`🎭 MOCK MODE: Generating podcast structure for session ${sessionId}`);
@@ -499,7 +510,7 @@ This mock study guide demonstrates the structure and format that would be genera
499
510
  voiceId?: string
500
511
  ): Promise<any> {
501
512
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
502
-
513
+
503
514
  // Mock mode - return fake audio generation result
504
515
  if (MOCK_MODE) {
505
516
  logger.info(`🎭 MOCK MODE: Generating audio for segment ${segmentIndex} of podcast ${podcastId}`);
@@ -522,7 +533,7 @@ This mock study guide demonstrates the structure and format that would be genera
522
533
  formData.append('segment_index', segmentIndex.toString());
523
534
  formData.append('text', text);
524
535
  formData.append('speakers', JSON.stringify(speakers));
525
-
536
+
526
537
  if (voiceId) {
527
538
  formData.append('voice_id', voiceId);
528
539
  }
@@ -547,7 +558,7 @@ This mock study guide demonstrates the structure and format that would be genera
547
558
  });
548
559
  }
549
560
  }
550
-
561
+
551
562
 
552
563
 
553
564
  async generatePodcastImage(sessionId: string, user: string, summary: string): Promise<string> {
@@ -576,6 +587,77 @@ This mock study guide demonstrates the structure and format that would be genera
576
587
  });
577
588
  }
578
589
  }
590
+
591
+ async segmentStudyGuide(sessionId: string, user: string, studyGuide: string): Promise<{
592
+ hint: string;
593
+ content: string
594
+ }[]> {
595
+ // def generate_study_guide_segmentation(request):
596
+ // user = request.form.get("user")
597
+ // session = request.form.get("session")
598
+ // study_guide = request.form.get("study_guide")
599
+
600
+ // if not user or not session:
601
+ // return {"error": "Session not initialized."}, 400
602
+ // if not study_guide:
603
+ // print("Study guide not provided.")
604
+ // return {"error": "Study guide not provided."}, 400
605
+
606
+ // messages = generate_segmentation(study_guide)
607
+ // return {"segmentation": messages}, 200
608
+
609
+ const formData = new FormData();
610
+ formData.append('command', 'generate_study_guide_segmentation');
611
+ formData.append('session', sessionId);
612
+ formData.append('user', user);
613
+ formData.append('study_guide', studyGuide);
614
+ try {
615
+ const response = await fetch(AI_SERVICE_URL, {
616
+ method: 'POST',
617
+ body: formData,
618
+ });
619
+ if (!response.ok) {
620
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
621
+ }
622
+ const result = await response.json();
623
+ return result.segmentation;
624
+ } catch (error) {
625
+ throw new TRPCError({
626
+ code: 'INTERNAL_SERVER_ERROR',
627
+ message: `Failed to segment study guide: ${error instanceof Error ? error.message : 'Unknown error'}`,
628
+ });
629
+ }
630
+ }
631
+ async validateSegmentSummary(sessionId: string, user: string, segmentContent: string, studentResponse: string, studyGuide: string): Promise<{
632
+ valid: boolean;
633
+ feedback: string;
634
+ }> {
635
+ const formData = new FormData();
636
+ formData.append('command', 'validate_segment_summary');
637
+ formData.append('session', sessionId);
638
+ formData.append('user', user);
639
+
640
+ formData.append('segment_content', segmentContent);
641
+ formData.append('student_response', studentResponse);
642
+ formData.append('study_guide', studyGuide);
643
+ try {
644
+ const response = await fetch(AI_SERVICE_URL, {
645
+ method: 'POST',
646
+ body: formData,
647
+ });
648
+ if (!response.ok) {
649
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
650
+ }
651
+ const result = await response.json();
652
+ return result.feedback;
653
+ } catch (error) {
654
+ throw new TRPCError({
655
+ code: 'INTERNAL_SERVER_ERROR',
656
+ message: `Failed to validate segment summary: ${error instanceof Error ? error.message : 'Unknown error'}`,
657
+ });
658
+ }
659
+ }
660
+
579
661
  // Get session by ID
580
662
  getSession(sessionId: string): AISession | undefined {
581
663
  return this.sessions.get(sessionId);
@@ -598,21 +680,21 @@ This mock study guide demonstrates the structure and format that would be genera
598
680
  await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
599
681
  // Mock mode - always return healthy
600
682
  if (MOCK_MODE) {
601
- console.log('🎭 MOCK MODE: AI service health check - returning healthy');
683
+ logger.info('MOCK MODE: AI service health check - returning healthy');
602
684
  return true;
603
685
  }
604
686
 
605
687
  try {
606
- console.log('🏥 Checking AI service health...');
688
+ logger.info('Checking AI service health...');
607
689
  const response = await fetch(AI_SERVICE_URL, {
608
690
  method: 'POST',
609
691
  body: new FormData(), // Empty form data
610
692
  });
611
-
612
- console.log(`🏥 AI Service health check status: ${response.status}`);
693
+
694
+ logger.info(`AI Service health check status: ${response.status}`);
613
695
  return response.ok;
614
696
  } catch (error) {
615
- console.error('🏥 AI Service health check failed:', error);
697
+ logger.error('AI Service health check failed:', error);
616
698
  return false;
617
699
  }
618
700
  }
@@ -0,0 +1,14 @@
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
+ } as const;
13
+
14
+ export type ArtifactTypeValue = (typeof ArtifactType)[keyof typeof ArtifactType];