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