@goscribe/server 1.1.6 → 1.2.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/dist/routers/_app.d.ts +1 -1
- package/dist/routers/auth.js +9 -3
- package/dist/routers/workspace.d.ts +1 -1
- package/dist/routers/workspace.js +1 -1
- package/package.json +2 -1
- package/prisma/schema.prisma +46 -0
- package/src/lib/ai-session.ts +117 -63
- package/src/lib/constants.ts +14 -0
- package/src/lib/email.ts +46 -0
- package/src/lib/inference.ts +1 -1
- package/src/lib/logger.ts +26 -9
- package/src/lib/pusher.ts +4 -4
- package/src/lib/retry.ts +61 -0
- package/src/lib/storage.ts +2 -2
- package/src/lib/workspace-access.ts +13 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/annotations.ts +186 -0
- package/src/routers/auth.ts +98 -9
- package/src/routers/flashcards.ts +7 -13
- package/src/routers/podcast.ts +24 -28
- package/src/routers/studyguide.ts +68 -74
- package/src/routers/worksheets.ts +11 -14
- package/src/routers/workspace.ts +275 -272
- package/src/server.ts +4 -2
- package/src/services/flashcard-progress.service.ts +3 -6
- package/src/routers/meetingsummary.ts +0 -416
package/.env.example
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# ──────────────────────────────────────────────
|
|
2
|
+
# Scribe Server Environment Variables
|
|
3
|
+
# Copy this file to .env and fill in your values
|
|
4
|
+
# ──────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
# Auth
|
|
7
|
+
AUTH_SECRET="your-auth-secret-here"
|
|
8
|
+
AUTH_TRUST_HOST=true
|
|
9
|
+
AUTH_URL=http://localhost:3001
|
|
10
|
+
|
|
11
|
+
# Database (PostgreSQL via Supabase)
|
|
12
|
+
DATABASE_URL="postgresql://user:password@host:6543/postgres?pgbouncer=true"
|
|
13
|
+
DIRECT_URL="postgresql://user:password@host:5432/postgres"
|
|
14
|
+
|
|
15
|
+
# AI Inference
|
|
16
|
+
DONT_TEST_INFERENCE=false
|
|
17
|
+
INFERENCE_API_URL=http://localhost:5000
|
|
18
|
+
INFERENCE_API_KEY=your-inference-api-key
|
|
19
|
+
INFERENCE_BASE_URL=https://api.cohere.ai/compatibility/v1
|
|
20
|
+
|
|
21
|
+
# Supabase Storage
|
|
22
|
+
SUPABASE_URL=https://your-project.supabase.co
|
|
23
|
+
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key
|
|
24
|
+
|
|
25
|
+
# Google Cloud Storage (for podcast audio)
|
|
26
|
+
GCP_PROJECT_ID=your-gcp-project
|
|
27
|
+
GCP_BUCKET=your-bucket-name
|
|
28
|
+
GCP_CLIENT_EMAIL=your-service-account@your-project.iam.gserviceaccount.com
|
|
29
|
+
GCP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nyour-private-key\n-----END PRIVATE KEY-----\n"
|
|
30
|
+
|
|
31
|
+
# Pusher (real-time events)
|
|
32
|
+
PUSHER_APP_ID=your-pusher-app-id
|
|
33
|
+
PUSHER_KEY=your-pusher-key
|
|
34
|
+
PUSHER_SECRET=your-pusher-secret
|
|
35
|
+
PUSHER_CLUSTER=your-cluster
|
|
36
|
+
|
|
37
|
+
# Email (Resend)
|
|
38
|
+
RESEND_API_KEY=re_your_resend_api_key
|
|
39
|
+
EMAIL_FROM=Scribe <noreply@yourdomain.com>
|
|
40
|
+
|
|
41
|
+
# Optional
|
|
42
|
+
COHERE_API_KEY=your-cohere-key
|
|
43
|
+
MURF_TTS_KEY=your-murf-tts-key
|
package/dist/routers/_app.d.ts
CHANGED
package/dist/routers/auth.js
CHANGED
|
@@ -4,7 +4,7 @@ import bcrypt from 'bcryptjs';
|
|
|
4
4
|
import { serialize } from 'cookie';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { TRPCError } from '@trpc/server';
|
|
7
|
-
import { supabaseClient } from '
|
|
7
|
+
import { supabaseClient } from '../lib/storage.js';
|
|
8
8
|
// Helper to create custom auth token
|
|
9
9
|
function createCustomAuthToken(userId) {
|
|
10
10
|
const secret = process.env.AUTH_SECRET;
|
|
@@ -42,7 +42,7 @@ export const auth = router({
|
|
|
42
42
|
const objectKey = `profile_picture_${ctx.session.user.id}`;
|
|
43
43
|
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
44
44
|
.from('media')
|
|
45
|
-
.createSignedUploadUrl(objectKey); // 5 minutes
|
|
45
|
+
.createSignedUploadUrl(objectKey, { upsert: true }); // 5 minutes
|
|
46
46
|
if (signedUrlError) {
|
|
47
47
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
|
|
48
48
|
}
|
|
@@ -141,7 +141,13 @@ export const auth = router({
|
|
|
141
141
|
};
|
|
142
142
|
}),
|
|
143
143
|
logout: publicProcedure.mutation(async ({ ctx }) => {
|
|
144
|
-
|
|
144
|
+
const token = ctx.cookies["auth_token"];
|
|
145
|
+
if (!token) {
|
|
146
|
+
throw new Error("No token found");
|
|
147
|
+
}
|
|
148
|
+
await ctx.db.session.delete({
|
|
149
|
+
where: { id: token },
|
|
150
|
+
});
|
|
145
151
|
ctx.res.setHeader("Set-Cookie", serialize("auth_token", "", {
|
|
146
152
|
httpOnly: true,
|
|
147
153
|
secure: process.env.NODE_ENV === "production",
|
|
@@ -174,7 +174,7 @@ export const workspace = router({
|
|
|
174
174
|
folders: folders.length,
|
|
175
175
|
lastUpdated: lastUpdated?.updatedAt,
|
|
176
176
|
spaceUsed: spaceLeft._sum?.size ?? 0,
|
|
177
|
-
|
|
177
|
+
spaceTotal: 1000000000,
|
|
178
178
|
};
|
|
179
179
|
}),
|
|
180
180
|
update: authedProcedure
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goscribe/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"prisma": "^6.14.0",
|
|
41
41
|
"pusher": "^5.2.0",
|
|
42
42
|
"pusher-js": "^8.4.0",
|
|
43
|
+
"resend": "^6.9.2",
|
|
43
44
|
"socket.io": "^4.8.1",
|
|
44
45
|
"superjson": "^2.2.2",
|
|
45
46
|
"uuid": "^13.0.0",
|
package/prisma/schema.prisma
CHANGED
|
@@ -62,6 +62,10 @@ model User {
|
|
|
62
62
|
// Invitations
|
|
63
63
|
sentInvitations WorkspaceInvitation[] @relation("UserInvitations")
|
|
64
64
|
|
|
65
|
+
// Study guide annotations
|
|
66
|
+
highlights StudyGuideHighlight[] @relation("UserHighlights")
|
|
67
|
+
studyGuideComments StudyGuideComment[] @relation("UserStudyGuideComments")
|
|
68
|
+
|
|
65
69
|
notifications Notification[]
|
|
66
70
|
chats Chat[]
|
|
67
71
|
createdAt DateTime @default(now())
|
|
@@ -270,12 +274,54 @@ model ArtifactVersion {
|
|
|
270
274
|
createdById String?
|
|
271
275
|
createdBy User? @relation("UserArtifactVersions", fields: [createdById], references: [id], onDelete: SetNull)
|
|
272
276
|
|
|
277
|
+
// Highlights on this version
|
|
278
|
+
highlights StudyGuideHighlight[]
|
|
279
|
+
|
|
273
280
|
createdAt DateTime @default(now())
|
|
274
281
|
|
|
275
282
|
@@unique([artifactId, version]) // each artifact has 1,2,3...
|
|
276
283
|
@@index([artifactId])
|
|
277
284
|
}
|
|
278
285
|
|
|
286
|
+
//
|
|
287
|
+
// Study Guide Highlights and Comments
|
|
288
|
+
//
|
|
289
|
+
model StudyGuideHighlight {
|
|
290
|
+
id String @id @default(cuid())
|
|
291
|
+
artifactVersionId String
|
|
292
|
+
artifactVersion ArtifactVersion @relation(fields: [artifactVersionId], references: [id], onDelete: Cascade)
|
|
293
|
+
|
|
294
|
+
userId String
|
|
295
|
+
user User @relation("UserHighlights", fields: [userId], references: [id], onDelete: Cascade)
|
|
296
|
+
|
|
297
|
+
startOffset Int // character offset in the content
|
|
298
|
+
endOffset Int
|
|
299
|
+
selectedText String // the highlighted text (preserved even if content changes)
|
|
300
|
+
color String @default("#FBBF24") // highlight color
|
|
301
|
+
|
|
302
|
+
comments StudyGuideComment[]
|
|
303
|
+
|
|
304
|
+
createdAt DateTime @default(now())
|
|
305
|
+
updatedAt DateTime @updatedAt
|
|
306
|
+
|
|
307
|
+
@@index([artifactVersionId, userId])
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
model StudyGuideComment {
|
|
311
|
+
id String @id @default(cuid())
|
|
312
|
+
highlightId String
|
|
313
|
+
highlight StudyGuideHighlight @relation(fields: [highlightId], references: [id], onDelete: Cascade)
|
|
314
|
+
|
|
315
|
+
userId String
|
|
316
|
+
user User @relation("UserStudyGuideComments", fields: [userId], references: [id], onDelete: Cascade)
|
|
317
|
+
content String
|
|
318
|
+
|
|
319
|
+
createdAt DateTime @default(now())
|
|
320
|
+
updatedAt DateTime @updatedAt
|
|
321
|
+
|
|
322
|
+
@@index([highlightId])
|
|
323
|
+
}
|
|
324
|
+
|
|
279
325
|
//
|
|
280
326
|
// Flashcards (child items of a FLASHCARD_SET Artifact)
|
|
281
327
|
//
|
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';
|
|
@@ -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,
|
|
@@ -73,23 +74,23 @@ export class AISessionService {
|
|
|
73
74
|
|
|
74
75
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
75
76
|
try {
|
|
76
|
-
|
|
77
|
+
logger.info(`AI Session init attempt ${attempt}/${maxRetries} for workspace ${workspaceId}`);
|
|
77
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
|
+
logger.debug(`AI Service result: ${JSON.stringify(result)}`);
|
|
93
94
|
|
|
94
95
|
// If we get a response with a message, consider it successful
|
|
95
96
|
if (!result.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
|
+
logger.error(`AI Session init attempt ${attempt} failed: ${lastError.message}`);
|
|
115
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'}`,
|
|
@@ -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;
|
|
@@ -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);
|
|
@@ -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,11 +253,12 @@ 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,
|
|
@@ -267,13 +269,11 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
267
269
|
}
|
|
268
270
|
|
|
269
271
|
const result = await response.json();
|
|
272
|
+
if (!result.markdown) {
|
|
273
|
+
throw new Error('AI service returned empty study guide');
|
|
274
|
+
}
|
|
270
275
|
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
|
-
}
|
|
276
|
+
}, { maxRetries: 3, timeoutMs: 300000, label: 'generateStudyGuide' });
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
// Generate flashcard questions
|
|
@@ -291,14 +291,14 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
291
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);
|
|
300
301
|
|
|
301
|
-
try {
|
|
302
302
|
const response = await fetch(AI_SERVICE_URL, {
|
|
303
303
|
method: 'POST',
|
|
304
304
|
body: formData,
|
|
@@ -309,16 +309,8 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
const result = await response.json();
|
|
312
|
-
|
|
313
|
-
console.log(JSON.parse(result.flashcards))
|
|
314
|
-
|
|
315
312
|
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
|
-
}
|
|
313
|
+
}, { maxRetries: 3, timeoutMs: 300000, label: 'generateFlashcardQuestions' });
|
|
322
314
|
}
|
|
323
315
|
|
|
324
316
|
// Generate worksheet questions
|
|
@@ -351,15 +343,14 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
351
343
|
});
|
|
352
344
|
}
|
|
353
345
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
346
|
+
return withRetry(async () => {
|
|
347
|
+
const formData = new FormData();
|
|
348
|
+
formData.append('command', 'generate_worksheet_questions');
|
|
349
|
+
formData.append('session', sessionId);
|
|
350
|
+
formData.append('user', user);
|
|
351
|
+
formData.append('num_questions', numQuestions.toString());
|
|
352
|
+
formData.append('difficulty', difficulty);
|
|
361
353
|
|
|
362
|
-
try {
|
|
363
354
|
const response = await fetch(AI_SERVICE_URL, {
|
|
364
355
|
method: 'POST',
|
|
365
356
|
body: formData,
|
|
@@ -370,16 +361,8 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
370
361
|
}
|
|
371
362
|
|
|
372
363
|
const result = await response.json();
|
|
373
|
-
|
|
374
|
-
console.log(JSON.parse(result.worksheet));
|
|
375
|
-
|
|
376
364
|
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
|
-
}
|
|
365
|
+
}, { maxRetries: 3, timeoutMs: 300000, label: 'generateWorksheetQuestions' });
|
|
383
366
|
}
|
|
384
367
|
|
|
385
368
|
async checkWorksheetQuestions(sessionId: string, user: string, question: string, answer: string, mark_scheme: MarkScheme): Promise<UserMarkScheme> {
|
|
@@ -402,7 +385,7 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
402
385
|
}
|
|
403
386
|
|
|
404
387
|
const result = await response.json();
|
|
405
|
-
|
|
388
|
+
logger.debug(`Worksheet marking result received`);
|
|
406
389
|
return JSON.parse(result.marking);
|
|
407
390
|
}
|
|
408
391
|
|
|
@@ -576,6 +559,77 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
576
559
|
});
|
|
577
560
|
}
|
|
578
561
|
}
|
|
562
|
+
|
|
563
|
+
async segmentStudyGuide (sessionId: string, user: string, studyGuide: string): Promise<{
|
|
564
|
+
hint: string;
|
|
565
|
+
content: string
|
|
566
|
+
}[]> {
|
|
567
|
+
// def generate_study_guide_segmentation(request):
|
|
568
|
+
// user = request.form.get("user")
|
|
569
|
+
// session = request.form.get("session")
|
|
570
|
+
// study_guide = request.form.get("study_guide")
|
|
571
|
+
|
|
572
|
+
// if not user or not session:
|
|
573
|
+
// return {"error": "Session not initialized."}, 400
|
|
574
|
+
// if not study_guide:
|
|
575
|
+
// print("Study guide not provided.")
|
|
576
|
+
// return {"error": "Study guide not provided."}, 400
|
|
577
|
+
|
|
578
|
+
// messages = generate_segmentation(study_guide)
|
|
579
|
+
// return {"segmentation": messages}, 200
|
|
580
|
+
|
|
581
|
+
const formData = new FormData();
|
|
582
|
+
formData.append('command', 'generate_study_guide_segmentation');
|
|
583
|
+
formData.append('session', sessionId);
|
|
584
|
+
formData.append('user', user);
|
|
585
|
+
formData.append('study_guide', studyGuide);
|
|
586
|
+
try {
|
|
587
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
588
|
+
method: 'POST',
|
|
589
|
+
body: formData,
|
|
590
|
+
});
|
|
591
|
+
if (!response.ok) {
|
|
592
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
593
|
+
}
|
|
594
|
+
const result = await response.json();
|
|
595
|
+
return result.segmentation;
|
|
596
|
+
} catch (error) {
|
|
597
|
+
throw new TRPCError({
|
|
598
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
599
|
+
message: `Failed to segment study guide: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async validateSegmentSummary(sessionId: string, user: string, segmentContent: string, studentResponse: string, studyGuide: string): Promise<{
|
|
604
|
+
valid: boolean;
|
|
605
|
+
feedback: string;
|
|
606
|
+
}> {
|
|
607
|
+
const formData = new FormData();
|
|
608
|
+
formData.append('command', 'validate_segment_summary');
|
|
609
|
+
formData.append('session', sessionId);
|
|
610
|
+
formData.append('user', user);
|
|
611
|
+
|
|
612
|
+
formData.append('segment_content', segmentContent);
|
|
613
|
+
formData.append('student_response', studentResponse);
|
|
614
|
+
formData.append('study_guide', studyGuide);
|
|
615
|
+
try {
|
|
616
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
617
|
+
method: 'POST',
|
|
618
|
+
body: formData,
|
|
619
|
+
});
|
|
620
|
+
if (!response.ok) {
|
|
621
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
622
|
+
}
|
|
623
|
+
const result = await response.json();
|
|
624
|
+
return result.feedback;
|
|
625
|
+
} catch (error) {
|
|
626
|
+
throw new TRPCError({
|
|
627
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
628
|
+
message: `Failed to validate segment summary: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
579
633
|
// Get session by ID
|
|
580
634
|
getSession(sessionId: string): AISession | undefined {
|
|
581
635
|
return this.sessions.get(sessionId);
|
|
@@ -598,21 +652,21 @@ This mock study guide demonstrates the structure and format that would be genera
|
|
|
598
652
|
await new Promise(resolve => setTimeout(resolve, IMITATE_WAIT_TIME_MS));
|
|
599
653
|
// Mock mode - always return healthy
|
|
600
654
|
if (MOCK_MODE) {
|
|
601
|
-
|
|
655
|
+
logger.info('MOCK MODE: AI service health check - returning healthy');
|
|
602
656
|
return true;
|
|
603
657
|
}
|
|
604
658
|
|
|
605
659
|
try {
|
|
606
|
-
|
|
660
|
+
logger.info('Checking AI service health...');
|
|
607
661
|
const response = await fetch(AI_SERVICE_URL, {
|
|
608
662
|
method: 'POST',
|
|
609
663
|
body: new FormData(), // Empty form data
|
|
610
664
|
});
|
|
611
665
|
|
|
612
|
-
|
|
666
|
+
logger.info(`AI Service health check status: ${response.status}`);
|
|
613
667
|
return response.ok;
|
|
614
668
|
} catch (error) {
|
|
615
|
-
|
|
669
|
+
logger.error('AI Service health check failed:', error);
|
|
616
670
|
return false;
|
|
617
671
|
}
|
|
618
672
|
}
|
|
@@ -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];
|
package/src/lib/email.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Resend } from 'resend';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
|
|
4
|
+
// const resend = new Resend(process.env.RESEND_API_KEY);
|
|
5
|
+
|
|
6
|
+
const FROM_EMAIL = process.env.EMAIL_FROM || 'Scribe <noreply@goscribe.app>';
|
|
7
|
+
const APP_URL = process.env.AUTH_URL || 'http://localhost:3000';
|
|
8
|
+
|
|
9
|
+
export async function sendVerificationEmail(
|
|
10
|
+
email: string,
|
|
11
|
+
token: string,
|
|
12
|
+
name?: string | null
|
|
13
|
+
): Promise<boolean> {
|
|
14
|
+
const verifyUrl = APP_URL + '/verify-email?token=' + token;
|
|
15
|
+
const greeting = name ? ', ' + name : '';
|
|
16
|
+
|
|
17
|
+
const html = '<div style="font-family:system-ui,sans-serif;max-width:480px;margin:0 auto;padding:40px 20px;">'
|
|
18
|
+
+ '<h2 style="font-size:20px;font-weight:600;margin-bottom:8px;">Welcome to Scribe' + greeting + '!</h2>'
|
|
19
|
+
+ '<p style="color:#6b7280;font-size:14px;line-height:1.6;margin-bottom:24px;">'
|
|
20
|
+
+ 'Please verify your email address to get the most out of your account.</p>'
|
|
21
|
+
+ '<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>'
|
|
22
|
+
+ '<p style="color:#9ca3af;font-size:12px;margin-top:32px;line-height:1.5;">'
|
|
23
|
+
+ 'If you did not create an account on Scribe, you can safely ignore this email.'
|
|
24
|
+
+ ' This link expires in 24 hours.</p>'
|
|
25
|
+
+ '</div>';
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// const { error } = await resend.emails.send({
|
|
29
|
+
// from: FROM_EMAIL,
|
|
30
|
+
// to: email,
|
|
31
|
+
// subject: 'Verify your email - Scribe',
|
|
32
|
+
// html,
|
|
33
|
+
// });
|
|
34
|
+
|
|
35
|
+
// if (error) {
|
|
36
|
+
// logger.error('Failed to send verification email to ' + email + ': ' + error.message);
|
|
37
|
+
// return false;
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
logger.info('Verification email sent to ' + email);
|
|
41
|
+
return true;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
logger.error('Email send error:', err);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/lib/inference.ts
CHANGED
package/src/lib/logger.ts
CHANGED
|
@@ -163,7 +163,8 @@ class Logger {
|
|
|
163
163
|
return `\x1b[90m${time}\x1b[0m`; // Gray color
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
private formatContext(context: string): string {
|
|
166
|
+
private formatContext(context: string | unknown): string {
|
|
167
|
+
if (typeof context !== 'string') return '';
|
|
167
168
|
const icon = CONTEXT_ICONS[context.toUpperCase()] || '📦';
|
|
168
169
|
return `\x1b[94m${icon}${context}\x1b[0m `; // Blue color
|
|
169
170
|
}
|
|
@@ -248,20 +249,36 @@ class Logger {
|
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
error(message: string,
|
|
252
|
-
|
|
252
|
+
error(message: string, contextOrError?: string | Error | unknown, metadata?: Record<string, any>, error?: Error): void {
|
|
253
|
+
if (contextOrError instanceof Error) {
|
|
254
|
+
this.log(LogLevel.ERROR, message, undefined, metadata, contextOrError);
|
|
255
|
+
} else {
|
|
256
|
+
this.log(LogLevel.ERROR, message, contextOrError as string | undefined, metadata, error);
|
|
257
|
+
}
|
|
253
258
|
}
|
|
254
259
|
|
|
255
|
-
warn(message: string,
|
|
256
|
-
|
|
260
|
+
warn(message: string, contextOrMeta?: string | Record<string, any>, metadata?: Record<string, any>): void {
|
|
261
|
+
if (typeof contextOrMeta === 'object' && contextOrMeta !== null) {
|
|
262
|
+
this.log(LogLevel.WARN, message, undefined, contextOrMeta as Record<string, any>);
|
|
263
|
+
} else {
|
|
264
|
+
this.log(LogLevel.WARN, message, contextOrMeta as string | undefined, metadata);
|
|
265
|
+
}
|
|
257
266
|
}
|
|
258
267
|
|
|
259
|
-
info(message: string,
|
|
260
|
-
|
|
268
|
+
info(message: string, contextOrMeta?: string | Record<string, any>, metadata?: Record<string, any>): void {
|
|
269
|
+
if (typeof contextOrMeta === 'object' && contextOrMeta !== null) {
|
|
270
|
+
this.log(LogLevel.INFO, message, undefined, contextOrMeta as Record<string, any>);
|
|
271
|
+
} else {
|
|
272
|
+
this.log(LogLevel.INFO, message, contextOrMeta as string | undefined, metadata);
|
|
273
|
+
}
|
|
261
274
|
}
|
|
262
275
|
|
|
263
|
-
debug(message: string,
|
|
264
|
-
|
|
276
|
+
debug(message: string, contextOrMeta?: string | Record<string, any>, metadata?: Record<string, any>): void {
|
|
277
|
+
if (typeof contextOrMeta === 'object' && contextOrMeta !== null) {
|
|
278
|
+
this.log(LogLevel.DEBUG, message, undefined, contextOrMeta as Record<string, any>);
|
|
279
|
+
} else {
|
|
280
|
+
this.log(LogLevel.DEBUG, message, contextOrMeta as string | undefined, metadata);
|
|
281
|
+
}
|
|
265
282
|
}
|
|
266
283
|
|
|
267
284
|
trace(message: string, context?: string, metadata?: Record<string, any>): void {
|