@goscribe/server 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/AUTH_FRONTEND_SPEC.md +21 -0
  2. package/CHAT_FRONTEND_SPEC.md +474 -0
  3. package/DATABASE_SETUP.md +165 -0
  4. package/MEETINGSUMMARY_FRONTEND_SPEC.md +28 -0
  5. package/PODCAST_FRONTEND_SPEC.md +595 -0
  6. package/STUDYGUIDE_FRONTEND_SPEC.md +18 -0
  7. package/WORKSHEETS_FRONTEND_SPEC.md +26 -0
  8. package/WORKSPACE_FRONTEND_SPEC.md +47 -0
  9. package/dist/lib/ai-session.d.ts +26 -0
  10. package/dist/lib/ai-session.js +343 -0
  11. package/dist/lib/inference.d.ts +2 -0
  12. package/dist/lib/inference.js +21 -0
  13. package/dist/lib/pusher.d.ts +14 -0
  14. package/dist/lib/pusher.js +94 -0
  15. package/dist/lib/storage.d.ts +10 -2
  16. package/dist/lib/storage.js +63 -6
  17. package/dist/routers/_app.d.ts +840 -58
  18. package/dist/routers/_app.js +6 -0
  19. package/dist/routers/ai-session.d.ts +0 -0
  20. package/dist/routers/ai-session.js +1 -0
  21. package/dist/routers/auth.d.ts +1 -0
  22. package/dist/routers/auth.js +6 -4
  23. package/dist/routers/chat.d.ts +171 -0
  24. package/dist/routers/chat.js +270 -0
  25. package/dist/routers/flashcards.d.ts +37 -0
  26. package/dist/routers/flashcards.js +128 -0
  27. package/dist/routers/meetingsummary.d.ts +0 -0
  28. package/dist/routers/meetingsummary.js +377 -0
  29. package/dist/routers/podcast.d.ts +277 -0
  30. package/dist/routers/podcast.js +847 -0
  31. package/dist/routers/studyguide.d.ts +54 -0
  32. package/dist/routers/studyguide.js +125 -0
  33. package/dist/routers/worksheets.d.ts +138 -51
  34. package/dist/routers/worksheets.js +317 -7
  35. package/dist/routers/workspace.d.ts +162 -7
  36. package/dist/routers/workspace.js +440 -8
  37. package/dist/server.js +6 -2
  38. package/package.json +11 -4
  39. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
  40. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
  41. package/prisma/migrations/migration_lock.toml +3 -0
  42. package/prisma/schema.prisma +87 -6
  43. package/prisma/seed.mjs +135 -0
  44. package/src/lib/ai-session.ts +412 -0
  45. package/src/lib/inference.ts +21 -0
  46. package/src/lib/pusher.ts +104 -0
  47. package/src/lib/storage.ts +89 -6
  48. package/src/routers/_app.ts +6 -0
  49. package/src/routers/auth.ts +8 -4
  50. package/src/routers/chat.ts +275 -0
  51. package/src/routers/flashcards.ts +142 -0
  52. package/src/routers/meetingsummary.ts +416 -0
  53. package/src/routers/podcast.ts +934 -0
  54. package/src/routers/studyguide.ts +144 -0
  55. package/src/routers/worksheets.ts +336 -7
  56. package/src/routers/workspace.ts +487 -8
  57. package/src/server.ts +7 -2
  58. package/test-ai-integration.js +134 -0
@@ -0,0 +1,412 @@
1
+ import { TRPCError } from '@trpc/server';
2
+
3
+ // External AI service configuration
4
+ const AI_SERVICE_URL = 'https://7gzvf7uib04yp9-61016.proxy.runpod.net/upload';
5
+ const AI_RESPONSE_URL = 'https://7gzvf7uib04yp9-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, user: string): Promise<AISession> {
22
+ const sessionId = `${workspaceId}`;
23
+
24
+ const formData = new FormData();
25
+ formData.append('command', 'init_session');
26
+ formData.append('session', sessionId);
27
+ formData.append('user', user);
28
+
29
+ // Retry logic for AI service
30
+ const maxRetries = 3;
31
+ let lastError: Error | null = null;
32
+
33
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
34
+ try {
35
+ console.log(`🤖 AI Session init attempt ${attempt}/${maxRetries} for workspace ${workspaceId}`);
36
+
37
+ const response = await fetch(AI_SERVICE_URL, {
38
+ method: 'POST',
39
+ body: formData,
40
+ });
41
+
42
+ console.log(`📡 AI Service response status: ${response.status} ${response.statusText}`);
43
+
44
+ if (!response.ok) {
45
+ const errorText = await response.text();
46
+ console.error(`❌ AI Service error response:`, errorText);
47
+ throw new Error(`AI service error: ${response.status} ${response.statusText} - ${errorText}`);
48
+ }
49
+
50
+ const result = await response.json();
51
+ console.log(`📋 AI Service result:`, result);
52
+
53
+ // If we get a response with a message, consider it successful
54
+ if (!result.message) {
55
+ throw new Error(`AI service error: No response message`);
56
+ }
57
+
58
+ const session: AISession = {
59
+ id: sessionId,
60
+ workspaceId,
61
+ status: 'initialized',
62
+ files: [],
63
+ createdAt: new Date(),
64
+ updatedAt: new Date(),
65
+ };
66
+
67
+ this.sessions.set(sessionId, session);
68
+ console.log(`✅ AI Session initialized successfully on attempt ${attempt}`);
69
+ return session;
70
+
71
+ } catch (error) {
72
+ lastError = error instanceof Error ? error : new Error('Unknown error');
73
+ console.error(`❌ AI Session init attempt ${attempt} failed:`, lastError.message);
74
+
75
+ if (attempt < maxRetries) {
76
+ const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s
77
+ console.log(`⏳ Retrying in ${delay}ms...`);
78
+ await new Promise(resolve => setTimeout(resolve, delay));
79
+ }
80
+ }
81
+ }
82
+
83
+ console.error(`💥 All ${maxRetries} attempts failed. Last error:`, lastError?.message);
84
+ throw new TRPCError({
85
+ code: 'INTERNAL_SERVER_ERROR',
86
+ message: `Failed to initialize AI session after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`,
87
+ });
88
+ }
89
+
90
+ // Upload file to AI session
91
+ async uploadFile(sessionId: string, file: File, fileType: 'image' | 'pdf'): Promise<void> {
92
+ const session = this.sessions.get(sessionId);
93
+ if (!session) {
94
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
95
+ }
96
+
97
+ const command = fileType === 'image' ? 'append_image' : 'append_pdflike';
98
+
99
+ const formData = new FormData();
100
+ formData.append('command', command);
101
+ formData.append('file', file);
102
+
103
+ try {
104
+ const response = await fetch(AI_SERVICE_URL, {
105
+ method: 'POST',
106
+ body: formData,
107
+ });
108
+
109
+ if (!response.ok) {
110
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
111
+ }
112
+
113
+ const result = await response.json();
114
+ console.log(`📋 Upload result:`, result);
115
+ if (!result.message) {
116
+ throw new Error(`AI service error: No response message`);
117
+ }
118
+
119
+ // Update session
120
+ session.files.push(file.name);
121
+ session.updatedAt = new Date();
122
+ this.sessions.set(sessionId, session);
123
+ } catch (error) {
124
+ throw new TRPCError({
125
+ code: 'INTERNAL_SERVER_ERROR',
126
+ message: `Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}`,
127
+ });
128
+ }
129
+ }
130
+
131
+ // Set instruction text
132
+ async setInstruction(sessionId: string, instructionText: string): Promise<void> {
133
+ const session = this.sessions.get(sessionId);
134
+ if (!session) {
135
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
136
+ }
137
+
138
+ const formData = new FormData();
139
+ formData.append('command', 'set_instruct');
140
+ formData.append('instruction_text', instructionText);
141
+
142
+ try {
143
+ const response = await fetch(AI_SERVICE_URL, {
144
+ method: 'POST',
145
+ body: formData,
146
+ });
147
+
148
+ if (!response.ok) {
149
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
150
+ }
151
+
152
+ const result = await response.json();
153
+ console.log(`📋 Set instruction result:`, result);
154
+ if (!result.message) {
155
+ throw new Error(`AI service error: No response message`);
156
+ }
157
+
158
+ // Update session
159
+ session.instructionText = instructionText;
160
+ session.updatedAt = new Date();
161
+ this.sessions.set(sessionId, session);
162
+ } catch (error) {
163
+ throw new TRPCError({
164
+ code: 'INTERNAL_SERVER_ERROR',
165
+ message: `Failed to set instruction: ${error instanceof Error ? error.message : 'Unknown error'}`,
166
+ });
167
+ }
168
+ }
169
+
170
+ // Start LLM session
171
+ async startLLMSession(sessionId: string): Promise<void> {
172
+ const session = this.sessions.get(sessionId);
173
+ if (!session) {
174
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
175
+ }
176
+
177
+ const formData = new FormData();
178
+ formData.append('command', 'start_LLM_session');
179
+
180
+ try {
181
+ const response = await fetch(AI_SERVICE_URL, {
182
+ method: 'POST',
183
+ body: formData,
184
+ });
185
+
186
+ if (!response.ok) {
187
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
188
+ }
189
+
190
+ const result = await response.json();
191
+ console.log(`📋 Start LLM result:`, result);
192
+ if (!result.message) {
193
+ throw new Error(`AI service error: No response message`);
194
+ }
195
+
196
+ // Update session
197
+ session.status = 'ready';
198
+ session.updatedAt = new Date();
199
+ this.sessions.set(sessionId, session);
200
+ } catch (error) {
201
+ throw new TRPCError({
202
+ code: 'INTERNAL_SERVER_ERROR',
203
+ message: `Failed to start LLM session: ${error instanceof Error ? error.message : 'Unknown error'}`,
204
+ });
205
+ }
206
+ }
207
+
208
+ // Generate study guide
209
+ async generateStudyGuide(sessionId: string): Promise<string> {
210
+ const session = this.sessions.get(sessionId);
211
+ if (!session) {
212
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
213
+ }
214
+
215
+ const formData = new FormData();
216
+ formData.append('command', 'generate_study_guide');
217
+
218
+ try {
219
+ const response = await fetch(AI_SERVICE_URL, {
220
+ method: 'POST',
221
+ body: formData,
222
+ });
223
+
224
+ if (!response.ok) {
225
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
226
+ }
227
+
228
+ // Get the generated content from the response endpoint
229
+ const contentResponse = await fetch(AI_RESPONSE_URL);
230
+ if (!contentResponse.ok) {
231
+ throw new Error(`Failed to retrieve generated content: ${contentResponse.status}`);
232
+ }
233
+ return (await contentResponse.json())['last_response'];
234
+ } catch (error) {
235
+ throw new TRPCError({
236
+ code: 'INTERNAL_SERVER_ERROR',
237
+ message: `Failed to generate study guide: ${error instanceof Error ? error.message : 'Unknown error'}`,
238
+ });
239
+ }
240
+ }
241
+
242
+ // Generate flashcard questions
243
+ async generateFlashcardQuestions(sessionId: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string> {
244
+ const session = this.sessions.get(sessionId);
245
+ if (!session) {
246
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
247
+ }
248
+
249
+ const formData = new FormData();
250
+ formData.append('command', 'generate_flashcard_questions');
251
+ formData.append('num_questions', numQuestions.toString());
252
+ formData.append('difficulty', difficulty);
253
+
254
+ try {
255
+ const response = await fetch(AI_SERVICE_URL, {
256
+ method: 'POST',
257
+ body: formData,
258
+ });
259
+
260
+ if (!response.ok) {
261
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
262
+ }
263
+
264
+ // Get the generated content from the response endpoint
265
+ const contentResponse = await fetch(AI_RESPONSE_URL);
266
+ if (!contentResponse.ok) {
267
+ throw new Error(`Failed to retrieve generated content: ${contentResponse.status}`);
268
+ }
269
+
270
+ return (await contentResponse.json())['last_response'];
271
+ } catch (error) {
272
+ throw new TRPCError({
273
+ code: 'INTERNAL_SERVER_ERROR',
274
+ message: `Failed to generate flashcard questions: ${error instanceof Error ? error.message : 'Unknown error'}`,
275
+ });
276
+ }
277
+ }
278
+
279
+ // Generate worksheet questions
280
+ async generateWorksheetQuestions(sessionId: string, numQuestions: number, difficulty: 'easy' | 'medium' | 'hard'): Promise<string> {
281
+ const session = this.sessions.get(sessionId);
282
+ if (!session) {
283
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
284
+ }
285
+
286
+ const formData = new FormData();
287
+ formData.append('command', 'generate_worksheet_questions');
288
+ formData.append('num_questions', numQuestions.toString());
289
+ formData.append('difficulty', difficulty);
290
+
291
+ try {
292
+ const response = await fetch(AI_SERVICE_URL, {
293
+ method: 'POST',
294
+ body: formData,
295
+ });
296
+
297
+ if (!response.ok) {
298
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
299
+ }
300
+
301
+ // Get the generated content from the response endpoint
302
+ const contentResponse = await fetch(AI_RESPONSE_URL);
303
+ if (!contentResponse.ok) {
304
+ throw new Error(`Failed to retrieve generated content: ${contentResponse.status}`);
305
+ }
306
+
307
+ return (await contentResponse.json())['last_response'];
308
+ } catch (error) {
309
+ throw new TRPCError({
310
+ code: 'INTERNAL_SERVER_ERROR',
311
+ message: `Failed to generate worksheet questions: ${error instanceof Error ? error.message : 'Unknown error'}`,
312
+ });
313
+ }
314
+ }
315
+
316
+ // Analyse PDF
317
+ async analysePDF(sessionId: string): Promise<string> {
318
+ const session = this.sessions.get(sessionId);
319
+ if (!session) {
320
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
321
+ }
322
+
323
+ const formData = new FormData();
324
+ formData.append('command', 'analyse_pdf');
325
+
326
+ try {
327
+ const response = await fetch(AI_SERVICE_URL, {
328
+ method: 'POST',
329
+ body: formData,
330
+ });
331
+
332
+ if (!response.ok) {
333
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
334
+ }
335
+
336
+ const result = await response.json();
337
+ return result.message || 'PDF analysis completed';
338
+ } catch (error) {
339
+ throw new TRPCError({
340
+ code: 'INTERNAL_SERVER_ERROR',
341
+ message: `Failed to analyse PDF: ${error instanceof Error ? error.message : 'Unknown error'}`,
342
+ });
343
+ }
344
+ }
345
+
346
+ // Analyse Image
347
+ async analyseImage(sessionId: string): Promise<string> {
348
+ const session = this.sessions.get(sessionId);
349
+ if (!session) {
350
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'AI session not found' });
351
+ }
352
+
353
+ const formData = new FormData();
354
+ formData.append('command', 'analyse_img');
355
+
356
+ try {
357
+ const response = await fetch(AI_SERVICE_URL, {
358
+ method: 'POST',
359
+ body: formData,
360
+ });
361
+
362
+ if (!response.ok) {
363
+ throw new Error(`AI service error: ${response.status} ${response.statusText}`);
364
+ }
365
+
366
+ const result = await response.json();
367
+ return result.message || 'Image analysis completed';
368
+ } catch (error) {
369
+ throw new TRPCError({
370
+ code: 'INTERNAL_SERVER_ERROR',
371
+ message: `Failed to analyse image: ${error instanceof Error ? error.message : 'Unknown error'}`,
372
+ });
373
+ }
374
+ }
375
+
376
+ // Get session by ID
377
+ getSession(sessionId: string): AISession | undefined {
378
+ return this.sessions.get(sessionId);
379
+ }
380
+
381
+ // Get sessions by user and workspace
382
+ getSessionsByUserAndWorkspace(userId: string, workspaceId: string): AISession[] {
383
+ return Array.from(this.sessions.values()).filter(
384
+ session => session.workspaceId === workspaceId
385
+ );
386
+ }
387
+
388
+ // Delete session
389
+ deleteSession(sessionId: string): boolean {
390
+ return this.sessions.delete(sessionId);
391
+ }
392
+
393
+ // Check if AI service is available
394
+ async checkHealth(): Promise<boolean> {
395
+ try {
396
+ console.log('🏥 Checking AI service health...');
397
+ const response = await fetch(AI_SERVICE_URL, {
398
+ method: 'POST',
399
+ body: new FormData(), // Empty form data
400
+ });
401
+
402
+ console.log(`🏥 AI Service health check status: ${response.status}`);
403
+ return response.ok;
404
+ } catch (error) {
405
+ console.error('🏥 AI Service health check failed:', error);
406
+ return false;
407
+ }
408
+ }
409
+ }
410
+
411
+ // Global instance
412
+ export const aiSessionService = new AISessionService();
@@ -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;
@@ -1,13 +1,96 @@
1
1
  // src/server/lib/gcs.ts
2
- import { Storage } from "@google-cloud/storage";
2
+ import { Storage } from '@google-cloud/storage';
3
+ import { v4 as uuidv4 } from 'uuid';
3
4
 
4
- export const storage = new Storage({
5
- projectId: process.env.GCP_PROJECT_ID,
6
- credentials: {
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
- export const bucket = storage.bucket(process.env.GCP_BUCKET!);
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);
@@ -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
@@ -67,22 +67,26 @@ export const auth = router({
67
67
  // Create custom auth token
68
68
  const authToken = createCustomAuthToken(user.id);
69
69
 
70
- // Set the cookie immediately after successful login
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: process.env.NODE_ENV === "production",
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 }) => {