@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.
- package/.env.example +43 -0
- package/check-difficulty.cjs +14 -0
- package/check-questions.cjs +14 -0
- package/db-summary.cjs +22 -0
- package/dist/routers/auth.js +1 -1
- package/mcq-test.cjs +36 -0
- package/package.json +10 -2
- package/prisma/migrations/20260413143206_init/migration.sql +873 -0
- package/prisma/schema.prisma +485 -292
- package/src/context.ts +4 -1
- package/src/lib/activity_human_description.test.ts +28 -0
- package/src/lib/activity_human_description.ts +239 -0
- package/src/lib/activity_log_service.test.ts +37 -0
- package/src/lib/activity_log_service.ts +353 -0
- package/src/lib/ai-session.ts +194 -112
- package/src/lib/constants.ts +14 -0
- package/src/lib/email.ts +230 -0
- package/src/lib/env.ts +23 -6
- package/src/lib/inference.ts +3 -3
- package/src/lib/logger.ts +26 -9
- package/src/lib/notification-service.test.ts +106 -0
- package/src/lib/notification-service.ts +677 -0
- package/src/lib/prisma.ts +6 -1
- package/src/lib/pusher.ts +90 -6
- package/src/lib/retry.ts +61 -0
- package/src/lib/storage.ts +2 -2
- package/src/lib/stripe.ts +39 -0
- package/src/lib/subscription_service.ts +722 -0
- package/src/lib/usage_service.ts +74 -0
- package/src/lib/worksheet-generation.test.ts +31 -0
- package/src/lib/worksheet-generation.ts +139 -0
- package/src/lib/workspace-access.ts +13 -0
- package/src/routers/_app.ts +11 -0
- package/src/routers/admin.ts +710 -0
- package/src/routers/annotations.ts +227 -0
- package/src/routers/auth.ts +432 -33
- package/src/routers/copilot.ts +719 -0
- package/src/routers/flashcards.ts +207 -80
- package/src/routers/members.ts +280 -80
- package/src/routers/notifications.ts +142 -0
- package/src/routers/payment.ts +448 -0
- package/src/routers/podcast.ts +133 -108
- package/src/routers/studyguide.ts +80 -74
- package/src/routers/worksheets.ts +300 -80
- package/src/routers/workspace.ts +538 -328
- package/src/scripts/purge-deleted-users.ts +167 -0
- package/src/server.ts +140 -12
- package/src/services/flashcard-progress.service.ts +52 -43
- package/src/trpc.ts +184 -5
- package/test-generate.js +30 -0
- package/test-ratio.cjs +9 -0
- package/zod-test.cjs +22 -0
- package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
- package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
- package/prisma/seed.mjs +0 -135
- package/src/routers/meetingsummary.ts +0 -416
package/src/lib/ai-session.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
}
|
|
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(
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
683
|
+
logger.info('MOCK MODE: AI service health check - returning healthy');
|
|
602
684
|
return true;
|
|
603
685
|
}
|
|
604
686
|
|
|
605
687
|
try {
|
|
606
|
-
|
|
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
|
-
|
|
693
|
+
|
|
694
|
+
logger.info(`AI Service health check status: ${response.status}`);
|
|
613
695
|
return response.ok;
|
|
614
696
|
} catch (error) {
|
|
615
|
-
|
|
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];
|