@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.
- package/dist/context.d.ts +5 -1
- package/dist/lib/activity_human_description.d.ts +13 -0
- package/dist/lib/activity_human_description.js +221 -0
- package/dist/lib/activity_human_description.test.d.ts +1 -0
- package/dist/lib/activity_human_description.test.js +16 -0
- package/dist/lib/activity_log_service.d.ts +87 -0
- package/dist/lib/activity_log_service.js +276 -0
- package/dist/lib/activity_log_service.test.d.ts +1 -0
- package/dist/lib/activity_log_service.test.js +27 -0
- package/dist/lib/ai-session.d.ts +15 -2
- package/dist/lib/ai-session.js +147 -85
- package/dist/lib/constants.d.ts +13 -0
- package/dist/lib/constants.js +12 -0
- package/dist/lib/email.d.ts +11 -0
- package/dist/lib/email.js +193 -0
- package/dist/lib/env.d.ts +13 -0
- package/dist/lib/env.js +16 -0
- package/dist/lib/inference.d.ts +4 -1
- package/dist/lib/inference.js +3 -3
- package/dist/lib/logger.d.ts +4 -4
- package/dist/lib/logger.js +30 -8
- package/dist/lib/notification-service.d.ts +152 -0
- package/dist/lib/notification-service.js +473 -0
- package/dist/lib/notification-service.test.d.ts +1 -0
- package/dist/lib/notification-service.test.js +87 -0
- package/dist/lib/prisma.d.ts +2 -1
- package/dist/lib/prisma.js +5 -1
- package/dist/lib/pusher.d.ts +23 -0
- package/dist/lib/pusher.js +69 -5
- package/dist/lib/retry.d.ts +15 -0
- package/dist/lib/retry.js +37 -0
- package/dist/lib/storage.js +2 -2
- package/dist/lib/stripe.d.ts +9 -0
- package/dist/lib/stripe.js +36 -0
- package/dist/lib/subscription_service.d.ts +37 -0
- package/dist/lib/subscription_service.js +654 -0
- package/dist/lib/usage_service.d.ts +26 -0
- package/dist/lib/usage_service.js +59 -0
- package/dist/lib/worksheet-generation.d.ts +91 -0
- package/dist/lib/worksheet-generation.js +95 -0
- package/dist/lib/worksheet-generation.test.d.ts +1 -0
- package/dist/lib/worksheet-generation.test.js +20 -0
- package/dist/lib/workspace-access.d.ts +18 -0
- package/dist/lib/workspace-access.js +13 -0
- package/dist/routers/_app.d.ts +1349 -253
- package/dist/routers/_app.js +10 -0
- package/dist/routers/admin.d.ts +361 -0
- package/dist/routers/admin.js +633 -0
- package/dist/routers/annotations.d.ts +219 -0
- package/dist/routers/annotations.js +187 -0
- package/dist/routers/auth.d.ts +88 -7
- package/dist/routers/auth.js +339 -19
- package/dist/routers/chat.d.ts +6 -12
- package/dist/routers/copilot.d.ts +199 -0
- package/dist/routers/copilot.js +571 -0
- package/dist/routers/flashcards.d.ts +47 -81
- package/dist/routers/flashcards.js +143 -27
- package/dist/routers/members.d.ts +36 -7
- package/dist/routers/members.js +200 -19
- package/dist/routers/notifications.d.ts +99 -0
- package/dist/routers/notifications.js +127 -0
- package/dist/routers/payment.d.ts +89 -0
- package/dist/routers/payment.js +403 -0
- package/dist/routers/podcast.d.ts +8 -13
- package/dist/routers/podcast.js +54 -31
- package/dist/routers/studyguide.d.ts +1 -29
- package/dist/routers/studyguide.js +80 -71
- package/dist/routers/worksheets.d.ts +105 -38
- package/dist/routers/worksheets.js +258 -68
- package/dist/routers/workspace.d.ts +139 -60
- package/dist/routers/workspace.js +455 -315
- package/dist/scripts/purge-deleted-users.d.ts +1 -0
- package/dist/scripts/purge-deleted-users.js +149 -0
- package/dist/server.js +130 -10
- package/dist/services/flashcard-progress.service.d.ts +18 -66
- package/dist/services/flashcard-progress.service.js +51 -42
- package/dist/trpc.d.ts +20 -21
- package/dist/trpc.js +150 -1
- package/package.json +1 -1
package/dist/lib/ai-session.d.ts
CHANGED
|
@@ -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'
|
|
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;
|
package/dist/lib/ai-session.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
logger.info(`AI Service response status: ${response.status} ${response.statusText}`);
|
|
50
51
|
if (!response.ok) {
|
|
51
52
|
const errorText = await response.text();
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
logger.info(`Retrying in ${delay}ms...`);
|
|
79
80
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
542
|
+
logger.info('MOCK MODE: AI service health check - returning healthy');
|
|
481
543
|
return true;
|
|
482
544
|
}
|
|
483
545
|
try {
|
|
484
|
-
|
|
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
|
-
|
|
551
|
+
logger.info(`AI Service health check status: ${response.status}`);
|
|
490
552
|
return response.ok;
|
|
491
553
|
}
|
|
492
554
|
catch (error) {
|
|
493
|
-
|
|
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
|
+
}
|