@goscribe/server 1.0.7 → 1.0.9
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/AUTH_FRONTEND_SPEC.md +21 -0
- package/CHAT_FRONTEND_SPEC.md +474 -0
- package/MEETINGSUMMARY_FRONTEND_SPEC.md +28 -0
- package/PODCAST_FRONTEND_SPEC.md +595 -0
- package/STUDYGUIDE_FRONTEND_SPEC.md +18 -0
- package/WORKSHEETS_FRONTEND_SPEC.md +26 -0
- package/WORKSPACE_FRONTEND_SPEC.md +47 -0
- package/dist/context.d.ts +1 -1
- package/dist/lib/ai-session.d.ts +26 -0
- package/dist/lib/ai-session.js +343 -0
- package/dist/lib/auth.js +10 -6
- package/dist/lib/inference.d.ts +2 -0
- package/dist/lib/inference.js +21 -0
- package/dist/lib/pusher.d.ts +14 -0
- package/dist/lib/pusher.js +94 -0
- package/dist/lib/storage.d.ts +10 -2
- package/dist/lib/storage.js +63 -6
- package/dist/routers/_app.d.ts +878 -100
- package/dist/routers/_app.js +8 -2
- package/dist/routers/ai-session.d.ts +0 -0
- package/dist/routers/ai-session.js +1 -0
- package/dist/routers/auth.d.ts +13 -11
- package/dist/routers/auth.js +50 -21
- package/dist/routers/chat.d.ts +171 -0
- package/dist/routers/chat.js +270 -0
- package/dist/routers/flashcards.d.ts +51 -39
- package/dist/routers/flashcards.js +143 -31
- package/dist/routers/meetingsummary.d.ts +0 -0
- package/dist/routers/meetingsummary.js +377 -0
- package/dist/routers/podcast.d.ts +277 -0
- package/dist/routers/podcast.js +847 -0
- package/dist/routers/studyguide.d.ts +54 -0
- package/dist/routers/studyguide.js +125 -0
- package/dist/routers/worksheets.d.ts +147 -40
- package/dist/routers/worksheets.js +348 -33
- package/dist/routers/workspace.d.ts +163 -8
- package/dist/routers/workspace.js +453 -8
- package/dist/server.d.ts +1 -1
- package/dist/server.js +7 -2
- package/dist/trpc.d.ts +5 -5
- package/package.json +11 -3
- package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
- package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +87 -6
- package/prisma/seed.mjs +135 -0
- package/src/lib/ai-session.ts +411 -0
- package/src/lib/auth.ts +1 -1
- package/src/lib/inference.ts +21 -0
- package/src/lib/pusher.ts +104 -0
- package/src/lib/storage.ts +89 -6
- package/src/routers/_app.ts +6 -0
- package/src/routers/auth.ts +8 -4
- package/src/routers/chat.ts +275 -0
- package/src/routers/flashcards.ts +151 -33
- package/src/routers/meetingsummary.ts +416 -0
- package/src/routers/podcast.ts +934 -0
- package/src/routers/studyguide.ts +144 -0
- package/src/routers/worksheets.ts +346 -18
- package/src/routers/workspace.ts +500 -8
- package/src/server.ts +7 -2
- package/test-ai-integration.js +134 -0
- package/dist/context.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/auth.d.ts.map +0 -1
- package/dist/lib/file.d.ts.map +0 -1
- package/dist/lib/prisma.d.ts.map +0 -1
- package/dist/lib/storage.d.ts.map +0 -1
- package/dist/routers/_app.d.ts.map +0 -1
- package/dist/routers/auth.d.ts.map +0 -1
- package/dist/routers/sample.js +0 -21
- package/dist/routers/workspace.d.ts.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/trpc.d.ts.map +0 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
|
2
|
+
|
|
3
|
+
// External AI service configuration
|
|
4
|
+
const AI_SERVICE_URL = 'https://hwln4560erf7pr-61016.proxy.runpod.net/upload';
|
|
5
|
+
const AI_RESPONSE_URL = 'https://hwln4560erf7pr-61016.proxy.runpod.net/last_response';
|
|
6
|
+
|
|
7
|
+
export interface AISession {
|
|
8
|
+
id: string;
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
status: 'initialized' | 'processing' | 'ready' | 'error';
|
|
11
|
+
files: string[];
|
|
12
|
+
instructionText?: string;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
updatedAt: Date;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class AISessionService {
|
|
18
|
+
private sessions = new Map<string, AISession>();
|
|
19
|
+
|
|
20
|
+
// Initialize a new AI session
|
|
21
|
+
async initSession(workspaceId: string): Promise<AISession> {
|
|
22
|
+
const sessionId = `${workspaceId}`;
|
|
23
|
+
|
|
24
|
+
const formData = new FormData();
|
|
25
|
+
formData.append('command', 'init_session');
|
|
26
|
+
formData.append('id', sessionId);
|
|
27
|
+
|
|
28
|
+
// Retry logic for AI service
|
|
29
|
+
const maxRetries = 3;
|
|
30
|
+
let lastError: Error | null = null;
|
|
31
|
+
|
|
32
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
33
|
+
try {
|
|
34
|
+
console.log(`🤖 AI Session init attempt ${attempt}/${maxRetries} for workspace ${workspaceId}`);
|
|
35
|
+
|
|
36
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: formData,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log(`📡 AI Service response status: ${response.status} ${response.statusText}`);
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const errorText = await response.text();
|
|
45
|
+
console.error(`❌ AI Service error response:`, errorText);
|
|
46
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = await response.json();
|
|
50
|
+
console.log(`📋 AI Service result:`, result);
|
|
51
|
+
|
|
52
|
+
// If we get a response with a message, consider it successful
|
|
53
|
+
if (!result.message) {
|
|
54
|
+
throw new Error(`AI service error: No response message`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const session: AISession = {
|
|
58
|
+
id: sessionId,
|
|
59
|
+
workspaceId,
|
|
60
|
+
status: 'initialized',
|
|
61
|
+
files: [],
|
|
62
|
+
createdAt: new Date(),
|
|
63
|
+
updatedAt: new Date(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.sessions.set(sessionId, session);
|
|
67
|
+
console.log(`✅ AI Session initialized successfully on attempt ${attempt}`);
|
|
68
|
+
return session;
|
|
69
|
+
|
|
70
|
+
} catch (error) {
|
|
71
|
+
lastError = error instanceof Error ? error : new Error('Unknown error');
|
|
72
|
+
console.error(`❌ AI Session init attempt ${attempt} failed:`, lastError.message);
|
|
73
|
+
|
|
74
|
+
if (attempt < maxRetries) {
|
|
75
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s
|
|
76
|
+
console.log(`⏳ Retrying in ${delay}ms...`);
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.error(`💥 All ${maxRetries} attempts failed. Last error:`, lastError?.message);
|
|
83
|
+
throw new TRPCError({
|
|
84
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
85
|
+
message: `Failed to initialize AI session after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Upload file to AI session
|
|
90
|
+
async uploadFile(sessionId: string, file: File, fileType: 'image' | 'pdf'): Promise<void> {
|
|
91
|
+
const session = this.sessions.get(sessionId);
|
|
92
|
+
if (!session) {
|
|
93
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const command = fileType === 'image' ? 'append_image' : 'append_pdflike';
|
|
97
|
+
|
|
98
|
+
const formData = new FormData();
|
|
99
|
+
formData.append('command', command);
|
|
100
|
+
formData.append('file', file);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
body: formData,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = await response.json();
|
|
113
|
+
console.log(`📋 Upload result:`, result);
|
|
114
|
+
if (!result.message) {
|
|
115
|
+
throw new Error(`AI service error: No response message`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Update session
|
|
119
|
+
session.files.push(file.name);
|
|
120
|
+
session.updatedAt = new Date();
|
|
121
|
+
this.sessions.set(sessionId, session);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new TRPCError({
|
|
124
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
125
|
+
message: `Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Set instruction text
|
|
131
|
+
async setInstruction(sessionId: string, instructionText: string): Promise<void> {
|
|
132
|
+
const session = this.sessions.get(sessionId);
|
|
133
|
+
if (!session) {
|
|
134
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const formData = new FormData();
|
|
138
|
+
formData.append('command', 'set_instruct');
|
|
139
|
+
formData.append('instruction_text', instructionText);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
body: formData,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const result = await response.json();
|
|
152
|
+
console.log(`📋 Set instruction result:`, result);
|
|
153
|
+
if (!result.message) {
|
|
154
|
+
throw new Error(`AI service error: No response message`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update session
|
|
158
|
+
session.instructionText = instructionText;
|
|
159
|
+
session.updatedAt = new Date();
|
|
160
|
+
this.sessions.set(sessionId, session);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new TRPCError({
|
|
163
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
164
|
+
message: `Failed to set instruction: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Start LLM session
|
|
170
|
+
async startLLMSession(sessionId: string): Promise<void> {
|
|
171
|
+
const session = this.sessions.get(sessionId);
|
|
172
|
+
if (!session) {
|
|
173
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const formData = new FormData();
|
|
177
|
+
formData.append('command', 'start_LLM_session');
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: formData,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await response.json();
|
|
190
|
+
console.log(`📋 Start LLM result:`, result);
|
|
191
|
+
if (!result.message) {
|
|
192
|
+
throw new Error(`AI service error: No response message`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Update session
|
|
196
|
+
session.status = 'ready';
|
|
197
|
+
session.updatedAt = new Date();
|
|
198
|
+
this.sessions.set(sessionId, session);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new TRPCError({
|
|
201
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
202
|
+
message: `Failed to start LLM session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Generate study guide
|
|
208
|
+
async generateStudyGuide(sessionId: string): Promise<string> {
|
|
209
|
+
const session = this.sessions.get(sessionId);
|
|
210
|
+
if (!session) {
|
|
211
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const formData = new FormData();
|
|
215
|
+
formData.append('command', 'generate_study_guide');
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
body: formData,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Get the generated content from the response endpoint
|
|
228
|
+
const contentResponse = await fetch(AI_RESPONSE_URL);
|
|
229
|
+
if (!contentResponse.ok) {
|
|
230
|
+
throw new Error(`Failed to retrieve generated content: ${contentResponse.status}`);
|
|
231
|
+
}
|
|
232
|
+
return (await contentResponse.json())['last_response'];
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw new TRPCError({
|
|
235
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
236
|
+
message: `Failed to generate study guide: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Generate flashcard questions
|
|
242
|
+
async generateFlashcardQuestions(sessionId: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string> {
|
|
243
|
+
const session = this.sessions.get(sessionId);
|
|
244
|
+
if (!session) {
|
|
245
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const formData = new FormData();
|
|
249
|
+
formData.append('command', 'generate_flashcard_questions');
|
|
250
|
+
formData.append('num_questions', numQuestions.toString());
|
|
251
|
+
formData.append('difficulty', difficulty);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
body: formData,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Get the generated content from the response endpoint
|
|
264
|
+
const contentResponse = await fetch(AI_RESPONSE_URL);
|
|
265
|
+
if (!contentResponse.ok) {
|
|
266
|
+
throw new Error(`Failed to retrieve generated content: ${contentResponse.status}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return (await contentResponse.json())['last_response'];
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw new TRPCError({
|
|
272
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
273
|
+
message: `Failed to generate flashcard questions: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Generate worksheet questions
|
|
279
|
+
async generateWorksheetQuestions(sessionId: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string> {
|
|
280
|
+
const session = this.sessions.get(sessionId);
|
|
281
|
+
if (!session) {
|
|
282
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const formData = new FormData();
|
|
286
|
+
formData.append('command', 'generate_worksheet_questions');
|
|
287
|
+
formData.append('num_questions', numQuestions.toString());
|
|
288
|
+
formData.append('difficulty', difficulty);
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
body: formData,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Get the generated content from the response endpoint
|
|
301
|
+
const contentResponse = await fetch(AI_RESPONSE_URL);
|
|
302
|
+
if (!contentResponse.ok) {
|
|
303
|
+
throw new Error(`Failed to retrieve generated content: ${contentResponse.status}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return (await contentResponse.json())['last_response'];
|
|
307
|
+
} catch (error) {
|
|
308
|
+
throw new TRPCError({
|
|
309
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
310
|
+
message: `Failed to generate worksheet questions: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Analyse PDF
|
|
316
|
+
async analysePDF(sessionId: string): Promise<string> {
|
|
317
|
+
const session = this.sessions.get(sessionId);
|
|
318
|
+
if (!session) {
|
|
319
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const formData = new FormData();
|
|
323
|
+
formData.append('command', 'analyse_pdf');
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
body: formData,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (!response.ok) {
|
|
332
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const result = await response.json();
|
|
336
|
+
return result.message || 'PDF analysis completed';
|
|
337
|
+
} catch (error) {
|
|
338
|
+
throw new TRPCError({
|
|
339
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
340
|
+
message: `Failed to analyse PDF: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Analyse Image
|
|
346
|
+
async analyseImage(sessionId: string): Promise<string> {
|
|
347
|
+
const session = this.sessions.get(sessionId);
|
|
348
|
+
if (!session) {
|
|
349
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const formData = new FormData();
|
|
353
|
+
formData.append('command', 'analyse_img');
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
357
|
+
method: 'POST',
|
|
358
|
+
body: formData,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
throw new Error(`AI service error: ${response.status} ${response.statusText}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const result = await response.json();
|
|
366
|
+
return result.message || 'Image analysis completed';
|
|
367
|
+
} catch (error) {
|
|
368
|
+
throw new TRPCError({
|
|
369
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
370
|
+
message: `Failed to analyse image: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Get session by ID
|
|
376
|
+
getSession(sessionId: string): AISession | undefined {
|
|
377
|
+
return this.sessions.get(sessionId);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Get sessions by user and workspace
|
|
381
|
+
getSessionsByUserAndWorkspace(userId: string, workspaceId: string): AISession[] {
|
|
382
|
+
return Array.from(this.sessions.values()).filter(
|
|
383
|
+
session => session.workspaceId === workspaceId
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Delete session
|
|
388
|
+
deleteSession(sessionId: string): boolean {
|
|
389
|
+
return this.sessions.delete(sessionId);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check if AI service is available
|
|
393
|
+
async checkHealth(): Promise<boolean> {
|
|
394
|
+
try {
|
|
395
|
+
console.log('🏥 Checking AI service health...');
|
|
396
|
+
const response = await fetch(AI_SERVICE_URL, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
body: new FormData(), // Empty form data
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
console.log(`🏥 AI Service health check status: ${response.status}`);
|
|
402
|
+
return response.ok;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error('🏥 AI Service health check failed:', error);
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Global instance
|
|
411
|
+
export const aiSessionService = new AISessionService();
|
package/src/lib/auth.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
async function inference(prompt: string, tag: string) {
|
|
2
|
+
try {
|
|
3
|
+
const response = await fetch("https://proxy-ai.onrender.com/api/cohere/inference", {
|
|
4
|
+
method: "POST",
|
|
5
|
+
headers: {
|
|
6
|
+
"Content-Type": "application/json",
|
|
7
|
+
},
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
prompt: prompt,
|
|
10
|
+
model: "command-r-plus",
|
|
11
|
+
max_tokens: 2000,
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
return response;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error('Inference error:', error);
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default inference;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import Pusher from 'pusher';
|
|
2
|
+
|
|
3
|
+
// Server-side Pusher instance
|
|
4
|
+
export const pusher = new Pusher({
|
|
5
|
+
appId: process.env.PUSHER_APP_ID || '',
|
|
6
|
+
key: process.env.PUSHER_KEY || '',
|
|
7
|
+
secret: process.env.PUSHER_SECRET || '',
|
|
8
|
+
cluster: process.env.PUSHER_CLUSTER || 'us2',
|
|
9
|
+
useTLS: true,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Pusher service for managing notifications
|
|
13
|
+
export class PusherService {
|
|
14
|
+
// Emit task completion notification
|
|
15
|
+
static async emitTaskComplete(workspaceId: string, event: string, data: any) {
|
|
16
|
+
try {
|
|
17
|
+
const channel = `workspace_${workspaceId}`;
|
|
18
|
+
const eventName = `${workspaceId}_${event}`;
|
|
19
|
+
await pusher.trigger(channel, eventName, data);
|
|
20
|
+
console.log(`📡 Pusher notification sent: ${eventName} to ${channel}`);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('❌ Pusher notification error:', error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Emit AI analysis completion
|
|
27
|
+
static async emitAnalysisComplete(workspaceId: string, analysisType: string, result: any) {
|
|
28
|
+
await this.emitTaskComplete(workspaceId, `${analysisType}_ended`, {
|
|
29
|
+
type: analysisType,
|
|
30
|
+
result,
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Emit study guide completion
|
|
36
|
+
static async emitStudyGuideComplete(workspaceId: string, artifact: any) {
|
|
37
|
+
await this.emitAnalysisComplete(workspaceId, 'studyguide', {
|
|
38
|
+
artifactId: artifact.id,
|
|
39
|
+
title: artifact.title,
|
|
40
|
+
status: 'completed'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Emit flashcard completion
|
|
45
|
+
static async emitFlashcardComplete(workspaceId: string, artifact: any) {
|
|
46
|
+
await this.emitAnalysisComplete(workspaceId, 'flashcard', {
|
|
47
|
+
artifactId: artifact.id,
|
|
48
|
+
title: artifact.title,
|
|
49
|
+
status: 'completed'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Emit worksheet completion
|
|
54
|
+
static async emitWorksheetComplete(workspaceId: string, artifact: any) {
|
|
55
|
+
await this.emitAnalysisComplete(workspaceId, 'worksheet', {
|
|
56
|
+
artifactId: artifact.id,
|
|
57
|
+
title: artifact.title,
|
|
58
|
+
status: 'completed'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Emit podcast completion
|
|
63
|
+
static async emitPodcastComplete(workspaceId: string, artifact: any) {
|
|
64
|
+
await this.emitAnalysisComplete(workspaceId, 'podcast', {
|
|
65
|
+
artifactId: artifact.id,
|
|
66
|
+
title: artifact.title,
|
|
67
|
+
status: 'completed'
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Emit overall analysis completion
|
|
72
|
+
static async emitOverallComplete(workspaceId: string, filename: string, artifacts: any) {
|
|
73
|
+
await this.emitTaskComplete(workspaceId, 'analysis_ended', {
|
|
74
|
+
filename,
|
|
75
|
+
artifacts,
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Emit error notification
|
|
81
|
+
static async emitError(workspaceId: string, error: string, analysisType?: string) {
|
|
82
|
+
const event = analysisType ? `${analysisType}_error` : 'analysis_error';
|
|
83
|
+
|
|
84
|
+
await this.emitTaskComplete(workspaceId, event, {
|
|
85
|
+
error,
|
|
86
|
+
analysisType,
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Emit channel-specific events (for chat messages)
|
|
92
|
+
static async emitChannelEvent(channelId: string, event: string, data: any) {
|
|
93
|
+
try {
|
|
94
|
+
const channel = channelId; // Use channelId directly as channel name
|
|
95
|
+
const eventName = `${channelId}_${event}`;
|
|
96
|
+
await pusher.trigger(channel, eventName, data);
|
|
97
|
+
console.log(`📡 Pusher notification sent: ${eventName} to ${channel}`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('❌ Pusher notification error:', error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default PusherService;
|
package/src/lib/storage.ts
CHANGED
|
@@ -1,13 +1,96 @@
|
|
|
1
1
|
// src/server/lib/gcs.ts
|
|
2
|
-
import { Storage } from
|
|
2
|
+
import { Storage } from '@google-cloud/storage';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Initialize Google Cloud Storage
|
|
6
|
+
const storage = new Storage({
|
|
7
|
+
projectId: process.env.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT_ID,
|
|
8
|
+
credentials: process.env.GCP_CLIENT_EMAIL && process.env.GCP_PRIVATE_KEY ? {
|
|
7
9
|
client_email: process.env.GCP_CLIENT_EMAIL,
|
|
8
10
|
private_key: process.env.GCP_PRIVATE_KEY?.replace(/\\n/g, "\n"),
|
|
9
|
-
},
|
|
11
|
+
} : undefined,
|
|
12
|
+
keyFilename: process.env.GOOGLE_CLOUD_KEY_FILE || process.env.GCP_KEY_FILE,
|
|
10
13
|
});
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
const bucketName = process.env.GCP_BUCKET || process.env.GOOGLE_CLOUD_BUCKET_NAME || 'your-bucket-name';
|
|
13
16
|
|
|
17
|
+
export interface UploadResult {
|
|
18
|
+
url: string;
|
|
19
|
+
signedUrl?: string;
|
|
20
|
+
objectKey: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function uploadToGCS(
|
|
24
|
+
fileBuffer: Buffer,
|
|
25
|
+
fileName: string,
|
|
26
|
+
contentType: string,
|
|
27
|
+
makePublic: boolean = false
|
|
28
|
+
): Promise<UploadResult> {
|
|
29
|
+
const bucket = storage.bucket(bucketName);
|
|
30
|
+
const objectKey = `podcasts/${uuidv4()}_${fileName}`;
|
|
31
|
+
const file = bucket.file(objectKey);
|
|
32
|
+
|
|
33
|
+
// Upload the file
|
|
34
|
+
await file.save(fileBuffer, {
|
|
35
|
+
metadata: {
|
|
36
|
+
contentType,
|
|
37
|
+
},
|
|
38
|
+
public: makePublic,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const url = `gs://${bucketName}/${objectKey}`;
|
|
42
|
+
|
|
43
|
+
// Generate signed URL for private files
|
|
44
|
+
let signedUrl: string | undefined;
|
|
45
|
+
if (!makePublic) {
|
|
46
|
+
const [signedUrlResult] = await file.getSignedUrl({
|
|
47
|
+
version: 'v4',
|
|
48
|
+
action: 'read',
|
|
49
|
+
expires: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
|
|
50
|
+
});
|
|
51
|
+
signedUrl = signedUrlResult;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
url,
|
|
56
|
+
signedUrl,
|
|
57
|
+
objectKey,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function generateSignedUrl(objectKey: string, expiresInHours: number = 24): Promise<string> {
|
|
62
|
+
const bucket = storage.bucket(bucketName);
|
|
63
|
+
const file = bucket.file(objectKey);
|
|
64
|
+
|
|
65
|
+
const [signedUrl] = await file.getSignedUrl({
|
|
66
|
+
version: 'v4',
|
|
67
|
+
action: 'read',
|
|
68
|
+
expires: Date.now() + expiresInHours * 60 * 60 * 1000,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return signedUrl;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function deleteFromGCS(objectKey: string): Promise<void> {
|
|
75
|
+
const bucket = storage.bucket(bucketName);
|
|
76
|
+
const file = bucket.file(objectKey);
|
|
77
|
+
|
|
78
|
+
await file.delete();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function makeFilePublic(objectKey: string): Promise<void> {
|
|
82
|
+
const bucket = storage.bucket(bucketName);
|
|
83
|
+
const file = bucket.file(objectKey);
|
|
84
|
+
|
|
85
|
+
await file.makePublic();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function makeFilePrivate(objectKey: string): Promise<void> {
|
|
89
|
+
const bucket = storage.bucket(bucketName);
|
|
90
|
+
const file = bucket.file(objectKey);
|
|
91
|
+
|
|
92
|
+
await file.makePrivate();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
export const bucket = storage.bucket(bucketName);
|
package/src/routers/_app.ts
CHANGED
|
@@ -4,12 +4,18 @@ import { auth } from './auth.js';
|
|
|
4
4
|
import { workspace } from './workspace.js';
|
|
5
5
|
import { flashcards } from './flashcards.js';
|
|
6
6
|
import { worksheets } from './worksheets.js';
|
|
7
|
+
import { studyguide } from './studyguide.js';
|
|
8
|
+
import { podcast } from './podcast.js';
|
|
9
|
+
import { chat } from './chat.js';
|
|
7
10
|
|
|
8
11
|
export const appRouter = router({
|
|
9
12
|
auth,
|
|
10
13
|
workspace,
|
|
11
14
|
flashcards,
|
|
12
15
|
worksheets,
|
|
16
|
+
studyguide,
|
|
17
|
+
podcast,
|
|
18
|
+
chat,
|
|
13
19
|
});
|
|
14
20
|
|
|
15
21
|
// Export type for client inference
|
package/src/routers/auth.ts
CHANGED
|
@@ -67,22 +67,26 @@ export const auth = router({
|
|
|
67
67
|
// Create custom auth token
|
|
68
68
|
const authToken = createCustomAuthToken(user.id);
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
const isProduction = (process.env.NODE_ENV === "production" || process.env.RENDER) as boolean;
|
|
71
|
+
|
|
71
72
|
const cookieValue = serialize("auth_token", authToken, {
|
|
72
73
|
httpOnly: true,
|
|
73
|
-
secure:
|
|
74
|
-
sameSite: "lax",
|
|
74
|
+
secure: isProduction, // true for production/HTTPS, false for localhost
|
|
75
|
+
sameSite: isProduction ? "none" : "lax", // none for cross-origin, lax for same-origin
|
|
75
76
|
path: "/",
|
|
77
|
+
domain: isProduction ? "server-w8mz.onrender.com" : undefined,
|
|
76
78
|
maxAge: 60 * 60 * 24 * 30, // 30 days
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
ctx.res.setHeader("Set-Cookie", cookieValue);
|
|
80
82
|
|
|
83
|
+
|
|
81
84
|
return {
|
|
82
85
|
id: user.id,
|
|
83
86
|
email: user.email,
|
|
84
87
|
name: user.name,
|
|
85
|
-
image: user.image
|
|
88
|
+
image: user.image,
|
|
89
|
+
token: authToken
|
|
86
90
|
};
|
|
87
91
|
}),
|
|
88
92
|
getSession: publicProcedure.query(async ({ ctx }) => {
|