@goscribe/server 1.0.11 → 1.1.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/ANALYSIS_PROGRESS_SPEC.md +463 -0
- package/PROGRESS_QUICK_REFERENCE.md +239 -0
- package/dist/lib/ai-session.d.ts +20 -9
- package/dist/lib/ai-session.js +316 -80
- package/dist/lib/auth.d.ts +35 -2
- package/dist/lib/auth.js +88 -15
- package/dist/lib/env.d.ts +32 -0
- package/dist/lib/env.js +46 -0
- package/dist/lib/errors.d.ts +33 -0
- package/dist/lib/errors.js +78 -0
- package/dist/lib/inference.d.ts +4 -1
- package/dist/lib/inference.js +9 -11
- package/dist/lib/logger.d.ts +62 -0
- package/dist/lib/logger.js +342 -0
- package/dist/lib/podcast-prompts.d.ts +43 -0
- package/dist/lib/podcast-prompts.js +135 -0
- package/dist/lib/pusher.d.ts +1 -0
- package/dist/lib/pusher.js +14 -2
- package/dist/lib/storage.d.ts +3 -3
- package/dist/lib/storage.js +51 -47
- package/dist/lib/validation.d.ts +51 -0
- package/dist/lib/validation.js +64 -0
- package/dist/routers/_app.d.ts +697 -111
- package/dist/routers/_app.js +5 -0
- package/dist/routers/auth.d.ts +11 -1
- package/dist/routers/chat.d.ts +11 -1
- package/dist/routers/flashcards.d.ts +205 -6
- package/dist/routers/flashcards.js +144 -66
- package/dist/routers/members.d.ts +165 -0
- package/dist/routers/members.js +531 -0
- package/dist/routers/podcast.d.ts +78 -63
- package/dist/routers/podcast.js +330 -393
- package/dist/routers/studyguide.d.ts +11 -1
- package/dist/routers/worksheets.d.ts +124 -13
- package/dist/routers/worksheets.js +123 -50
- package/dist/routers/workspace.d.ts +213 -26
- package/dist/routers/workspace.js +303 -181
- package/dist/server.js +12 -4
- package/dist/services/flashcard-progress.service.d.ts +183 -0
- package/dist/services/flashcard-progress.service.js +383 -0
- package/dist/services/flashcard.service.d.ts +183 -0
- package/dist/services/flashcard.service.js +224 -0
- package/dist/services/podcast-segment-reorder.d.ts +0 -0
- package/dist/services/podcast-segment-reorder.js +107 -0
- package/dist/services/podcast.service.d.ts +0 -0
- package/dist/services/podcast.service.js +326 -0
- package/dist/services/worksheet.service.d.ts +0 -0
- package/dist/services/worksheet.service.js +295 -0
- package/dist/trpc.d.ts +13 -2
- package/dist/trpc.js +55 -6
- package/dist/types/index.d.ts +126 -0
- package/dist/types/index.js +1 -0
- package/package.json +3 -2
- package/prisma/schema.prisma +142 -4
- package/src/lib/ai-session.ts +356 -85
- package/src/lib/auth.ts +113 -19
- package/src/lib/env.ts +59 -0
- package/src/lib/errors.ts +92 -0
- package/src/lib/inference.ts +11 -11
- package/src/lib/logger.ts +405 -0
- package/src/lib/pusher.ts +15 -3
- package/src/lib/storage.ts +56 -51
- package/src/lib/validation.ts +75 -0
- package/src/routers/_app.ts +5 -0
- package/src/routers/chat.ts +2 -23
- package/src/routers/flashcards.ts +108 -24
- package/src/routers/members.ts +586 -0
- package/src/routers/podcast.ts +385 -420
- package/src/routers/worksheets.ts +117 -35
- package/src/routers/workspace.ts +328 -195
- package/src/server.ts +13 -4
- package/src/services/flashcard-progress.service.ts +541 -0
- package/src/trpc.ts +59 -6
- package/src/types/index.ts +165 -0
- package/AUTH_FRONTEND_SPEC.md +0 -21
- package/CHAT_FRONTEND_SPEC.md +0 -474
- package/DATABASE_SETUP.md +0 -165
- package/MEETINGSUMMARY_FRONTEND_SPEC.md +0 -28
- package/PODCAST_FRONTEND_SPEC.md +0 -595
- package/STUDYGUIDE_FRONTEND_SPEC.md +0 -18
- package/WORKSHEETS_FRONTEND_SPEC.md +0 -26
- package/WORKSPACE_FRONTEND_SPEC.md +0 -47
- package/test-ai-integration.js +0 -134
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { createWriteStream, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
export var LogLevel;
|
|
4
|
+
(function (LogLevel) {
|
|
5
|
+
LogLevel[LogLevel["ERROR"] = 0] = "ERROR";
|
|
6
|
+
LogLevel[LogLevel["WARN"] = 1] = "WARN";
|
|
7
|
+
LogLevel[LogLevel["INFO"] = 2] = "INFO";
|
|
8
|
+
LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
|
|
9
|
+
LogLevel[LogLevel["TRACE"] = 4] = "TRACE";
|
|
10
|
+
})(LogLevel || (LogLevel = {}));
|
|
11
|
+
// Icons and colors for different log levels
|
|
12
|
+
const LOG_STYLES = {
|
|
13
|
+
[LogLevel.ERROR]: {
|
|
14
|
+
icon: '❌',
|
|
15
|
+
color: '\x1b[31m', // Red
|
|
16
|
+
bgColor: '\x1b[41m', // Red background
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
},
|
|
19
|
+
[LogLevel.WARN]: {
|
|
20
|
+
icon: '⚠️ ',
|
|
21
|
+
color: '\x1b[33m', // Yellow
|
|
22
|
+
bgColor: '\x1b[43m', // Yellow background
|
|
23
|
+
bold: '\x1b[1m',
|
|
24
|
+
},
|
|
25
|
+
[LogLevel.INFO]: {
|
|
26
|
+
icon: 'ℹ️ ',
|
|
27
|
+
color: '\x1b[36m', // Cyan
|
|
28
|
+
bgColor: '\x1b[46m', // Cyan background
|
|
29
|
+
bold: '\x1b[1m',
|
|
30
|
+
},
|
|
31
|
+
[LogLevel.DEBUG]: {
|
|
32
|
+
icon: '🐛',
|
|
33
|
+
color: '\x1b[35m', // Magenta
|
|
34
|
+
bgColor: '\x1b[45m', // Magenta background
|
|
35
|
+
bold: '\x1b[1m',
|
|
36
|
+
},
|
|
37
|
+
[LogLevel.TRACE]: {
|
|
38
|
+
icon: '🔍',
|
|
39
|
+
color: '\x1b[37m', // White
|
|
40
|
+
bgColor: '\x1b[47m', // White background
|
|
41
|
+
bold: '\x1b[1m',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
// Context icons for common services
|
|
45
|
+
const CONTEXT_ICONS = {
|
|
46
|
+
'SERVER': '🖥️ ',
|
|
47
|
+
'HTTP': '🌐',
|
|
48
|
+
'API': '🔌',
|
|
49
|
+
'AUTH': '🔐',
|
|
50
|
+
'DATABASE': '🗄️ ',
|
|
51
|
+
'DB': '🗄️ ',
|
|
52
|
+
'TRPC': '⚡',
|
|
53
|
+
'WORKSPACE': '📁',
|
|
54
|
+
'WORKSHEET': '📝',
|
|
55
|
+
'FLASHCARD': '🃏',
|
|
56
|
+
'STUDYGUIDE': '📚',
|
|
57
|
+
'PODCAST': '🎙️ ',
|
|
58
|
+
'MEETING': '🤝',
|
|
59
|
+
'CHAT': '💬',
|
|
60
|
+
'FILE': '📄',
|
|
61
|
+
'STORAGE': '💾',
|
|
62
|
+
'CACHE': '⚡',
|
|
63
|
+
'MIDDLEWARE': '🔧',
|
|
64
|
+
'PERFORMANCE': '⚡',
|
|
65
|
+
'SECURITY': '🛡️ ',
|
|
66
|
+
'VALIDATION': '✅',
|
|
67
|
+
'ERROR': '❌',
|
|
68
|
+
'SUCCESS': '✅',
|
|
69
|
+
'LOGGER': '📋',
|
|
70
|
+
};
|
|
71
|
+
class Logger {
|
|
72
|
+
constructor(config = {}) {
|
|
73
|
+
this.config = {
|
|
74
|
+
level: LogLevel.INFO,
|
|
75
|
+
enableConsole: true,
|
|
76
|
+
enableFile: false,
|
|
77
|
+
logDir: './logs',
|
|
78
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
79
|
+
maxFiles: 5,
|
|
80
|
+
format: 'pretty',
|
|
81
|
+
...config,
|
|
82
|
+
};
|
|
83
|
+
if (this.config.enableFile) {
|
|
84
|
+
this.setupFileLogging();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
setupFileLogging() {
|
|
88
|
+
if (!this.config.logDir)
|
|
89
|
+
return;
|
|
90
|
+
// Ensure log directory exists
|
|
91
|
+
if (!existsSync(this.config.logDir)) {
|
|
92
|
+
mkdirSync(this.config.logDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
const logFile = join(this.config.logDir, `app-${new Date().toISOString().split('T')[0]}.log`);
|
|
95
|
+
this.logStream = createWriteStream(logFile, { flags: 'a' });
|
|
96
|
+
}
|
|
97
|
+
shouldLog(level) {
|
|
98
|
+
return level <= this.config.level;
|
|
99
|
+
}
|
|
100
|
+
formatLogEntry(entry) {
|
|
101
|
+
if (this.config.format === 'json') {
|
|
102
|
+
return JSON.stringify(entry) + '\n';
|
|
103
|
+
}
|
|
104
|
+
// Pretty format with enhanced styling
|
|
105
|
+
const timestamp = this.formatTimestamp(entry.timestamp);
|
|
106
|
+
const level = this.formatLevel(entry.level);
|
|
107
|
+
const context = entry.context ? this.formatContext(entry.context) : '';
|
|
108
|
+
const metadata = entry.metadata ? this.formatMetadata(entry.metadata) : '';
|
|
109
|
+
const error = entry.error ? this.formatError(entry.error) : '';
|
|
110
|
+
return `${timestamp} ${level} ${context}${entry.message}${metadata}${error}`;
|
|
111
|
+
}
|
|
112
|
+
formatLevel(level) {
|
|
113
|
+
const levelNum = LogLevel[level];
|
|
114
|
+
const style = LOG_STYLES[levelNum];
|
|
115
|
+
return `${style.color}${style.bold}${level.padEnd(5)}\x1b[0m`;
|
|
116
|
+
}
|
|
117
|
+
formatTimestamp(timestamp) {
|
|
118
|
+
const date = new Date(timestamp);
|
|
119
|
+
const time = date.toLocaleTimeString('en-US', {
|
|
120
|
+
hour12: false,
|
|
121
|
+
hour: '2-digit',
|
|
122
|
+
minute: '2-digit',
|
|
123
|
+
second: '2-digit',
|
|
124
|
+
});
|
|
125
|
+
return `\x1b[90m${time}\x1b[0m`; // Gray color
|
|
126
|
+
}
|
|
127
|
+
formatContext(context) {
|
|
128
|
+
const icon = CONTEXT_ICONS[context.toUpperCase()] || '📦';
|
|
129
|
+
return `\x1b[94m${icon}${context}\x1b[0m `; // Blue color
|
|
130
|
+
}
|
|
131
|
+
formatMetadata(metadata) {
|
|
132
|
+
const entries = Object.entries(metadata)
|
|
133
|
+
.map(([key, value]) => {
|
|
134
|
+
const formattedValue = this.formatValue(value);
|
|
135
|
+
return `\x1b[93m${key}\x1b[0m=\x1b[96m${formattedValue}\x1b[0m`;
|
|
136
|
+
})
|
|
137
|
+
.join(' \x1b[90m|\x1b[0m ');
|
|
138
|
+
return entries ? `\n \x1b[90m└─\x1b[0m \x1b[90m{\x1b[0m ${entries} \x1b[90m}\x1b[0m` : '';
|
|
139
|
+
}
|
|
140
|
+
formatValue(value) {
|
|
141
|
+
if (value === null)
|
|
142
|
+
return '\x1b[90mnull\x1b[0m';
|
|
143
|
+
if (value === undefined)
|
|
144
|
+
return '\x1b[90mundefined\x1b[0m';
|
|
145
|
+
if (typeof value === 'boolean')
|
|
146
|
+
return value ? '\x1b[92mtrue\x1b[0m' : '\x1b[91mfalse\x1b[0m';
|
|
147
|
+
if (typeof value === 'number')
|
|
148
|
+
return `\x1b[95m${value}\x1b[0m`;
|
|
149
|
+
if (typeof value === 'string')
|
|
150
|
+
return `\x1b[96m"${value}"\x1b[0m`;
|
|
151
|
+
if (typeof value === 'object') {
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
const items = value.map(item => this.formatValue(item)).join('\x1b[90m, \x1b[0m');
|
|
154
|
+
return `\x1b[90m[\x1b[0m${items}\x1b[90m]\x1b[0m`;
|
|
155
|
+
}
|
|
156
|
+
const objEntries = Object.entries(value)
|
|
157
|
+
.map(([k, v]) => `\x1b[93m${k}\x1b[0m:\x1b[96m${this.formatValue(v)}\x1b[0m`)
|
|
158
|
+
.join('\x1b[90m, \x1b[0m');
|
|
159
|
+
return `\x1b[90m{\x1b[0m${objEntries}\x1b[90m}\x1b[0m`;
|
|
160
|
+
}
|
|
161
|
+
return `\x1b[96m${String(value)}\x1b[0m`;
|
|
162
|
+
}
|
|
163
|
+
formatError(error) {
|
|
164
|
+
let errorStr = `\n \x1b[90m└─\x1b[0m \x1b[31m❌ \x1b[93m${error.name}\x1b[0m: \x1b[91m${error.message}\x1b[0m`;
|
|
165
|
+
if (error.stack) {
|
|
166
|
+
const stackLines = error.stack.split('\n').slice(1, 4); // Show first 3 stack lines
|
|
167
|
+
errorStr += `\n \x1b[90m└─ Stack:\x1b[0m`;
|
|
168
|
+
stackLines.forEach((line, index) => {
|
|
169
|
+
const isLast = index === stackLines.length - 1;
|
|
170
|
+
const connector = isLast ? '└─' : '├─';
|
|
171
|
+
errorStr += `\n \x1b[90m${connector} \x1b[0m\x1b[90m${line.trim()}\x1b[0m`;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return errorStr;
|
|
175
|
+
}
|
|
176
|
+
log(level, message, context, metadata, error) {
|
|
177
|
+
if (!this.shouldLog(level))
|
|
178
|
+
return;
|
|
179
|
+
const entry = {
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
level: LogLevel[level],
|
|
182
|
+
message,
|
|
183
|
+
context,
|
|
184
|
+
metadata,
|
|
185
|
+
error: error ? {
|
|
186
|
+
name: error.name,
|
|
187
|
+
message: error.message,
|
|
188
|
+
stack: error.stack,
|
|
189
|
+
} : undefined,
|
|
190
|
+
};
|
|
191
|
+
const formattedLog = this.formatLogEntry(entry);
|
|
192
|
+
if (this.config.enableConsole) {
|
|
193
|
+
// Enhanced console output with icons and colors
|
|
194
|
+
const style = LOG_STYLES[level];
|
|
195
|
+
const reset = '\x1b[0m';
|
|
196
|
+
// Create a beautiful log line with proper spacing and colors
|
|
197
|
+
const logLine = `${style.color}${style.icon} ${formattedLog}${reset}`;
|
|
198
|
+
console.log(logLine);
|
|
199
|
+
}
|
|
200
|
+
if (this.config.enableFile && this.logStream) {
|
|
201
|
+
this.logStream.write(formattedLog + '\n');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
error(message, context, metadata, error) {
|
|
205
|
+
this.log(LogLevel.ERROR, message, context, metadata, error);
|
|
206
|
+
}
|
|
207
|
+
warn(message, context, metadata) {
|
|
208
|
+
this.log(LogLevel.WARN, message, context, metadata);
|
|
209
|
+
}
|
|
210
|
+
info(message, context, metadata) {
|
|
211
|
+
this.log(LogLevel.INFO, message, context, metadata);
|
|
212
|
+
}
|
|
213
|
+
debug(message, context, metadata) {
|
|
214
|
+
this.log(LogLevel.DEBUG, message, context, metadata);
|
|
215
|
+
}
|
|
216
|
+
trace(message, context, metadata) {
|
|
217
|
+
this.log(LogLevel.TRACE, message, context, metadata);
|
|
218
|
+
}
|
|
219
|
+
// Convenience methods for common use cases
|
|
220
|
+
http(method, url, statusCode, responseTime, context) {
|
|
221
|
+
const statusIcon = this.getHttpStatusIcon(statusCode);
|
|
222
|
+
const responseTimeStr = responseTime ? `${responseTime}ms` : undefined;
|
|
223
|
+
const metadata = {
|
|
224
|
+
method,
|
|
225
|
+
url,
|
|
226
|
+
statusCode,
|
|
227
|
+
responseTime: responseTimeStr,
|
|
228
|
+
};
|
|
229
|
+
this.info(`${statusIcon} ${method} ${url} - ${statusCode}`, context || 'HTTP', metadata);
|
|
230
|
+
}
|
|
231
|
+
getHttpStatusIcon(statusCode) {
|
|
232
|
+
if (statusCode >= 200 && statusCode < 300)
|
|
233
|
+
return '✅';
|
|
234
|
+
if (statusCode >= 300 && statusCode < 400)
|
|
235
|
+
return '↩️ ';
|
|
236
|
+
if (statusCode >= 400 && statusCode < 500)
|
|
237
|
+
return '⚠️ ';
|
|
238
|
+
if (statusCode >= 500)
|
|
239
|
+
return '❌';
|
|
240
|
+
return '❓';
|
|
241
|
+
}
|
|
242
|
+
database(operation, table, duration, context) {
|
|
243
|
+
const operationIcon = this.getDatabaseOperationIcon(operation);
|
|
244
|
+
const durationStr = duration ? `${duration}ms` : undefined;
|
|
245
|
+
const metadata = {
|
|
246
|
+
operation,
|
|
247
|
+
table,
|
|
248
|
+
duration: durationStr,
|
|
249
|
+
};
|
|
250
|
+
this.debug(`${operationIcon} ${operation} on ${table}`, context || 'DATABASE', metadata);
|
|
251
|
+
}
|
|
252
|
+
getDatabaseOperationIcon(operation) {
|
|
253
|
+
const op = operation.toUpperCase();
|
|
254
|
+
if (op.includes('SELECT'))
|
|
255
|
+
return '🔍';
|
|
256
|
+
if (op.includes('INSERT'))
|
|
257
|
+
return '➕';
|
|
258
|
+
if (op.includes('UPDATE'))
|
|
259
|
+
return '✏️ ';
|
|
260
|
+
if (op.includes('DELETE'))
|
|
261
|
+
return '🗑️ ';
|
|
262
|
+
if (op.includes('CREATE'))
|
|
263
|
+
return '🏗️ ';
|
|
264
|
+
if (op.includes('DROP'))
|
|
265
|
+
return '💥';
|
|
266
|
+
return '🗄️ ';
|
|
267
|
+
}
|
|
268
|
+
auth(action, userId, context) {
|
|
269
|
+
const metadata = {
|
|
270
|
+
action,
|
|
271
|
+
userId,
|
|
272
|
+
};
|
|
273
|
+
this.info(`Auth ${action}`, context, metadata);
|
|
274
|
+
}
|
|
275
|
+
trpc(procedure, input, output, duration, context) {
|
|
276
|
+
const metadata = {
|
|
277
|
+
procedure,
|
|
278
|
+
input: input ? JSON.stringify(input) : undefined,
|
|
279
|
+
output: output ? JSON.stringify(output) : undefined,
|
|
280
|
+
duration: duration ? `${duration}ms` : undefined,
|
|
281
|
+
};
|
|
282
|
+
this.debug(`tRPC ${procedure}`, context, metadata);
|
|
283
|
+
}
|
|
284
|
+
// Method to update configuration at runtime
|
|
285
|
+
updateConfig(newConfig) {
|
|
286
|
+
this.config = { ...this.config, ...newConfig };
|
|
287
|
+
if (newConfig.enableFile && !this.logStream) {
|
|
288
|
+
this.setupFileLogging();
|
|
289
|
+
}
|
|
290
|
+
else if (!newConfig.enableFile && this.logStream) {
|
|
291
|
+
this.logStream.end();
|
|
292
|
+
this.logStream = undefined;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Progress indicator for long-running operations
|
|
296
|
+
progress(message, current, total, context) {
|
|
297
|
+
const percentage = Math.round((current / total) * 100);
|
|
298
|
+
const progressBar = this.createProgressBar(percentage);
|
|
299
|
+
this.info(`${progressBar} ${message} (${current}/${total} - ${percentage}%)`, context || 'PROGRESS');
|
|
300
|
+
}
|
|
301
|
+
createProgressBar(percentage, width = 20) {
|
|
302
|
+
const filled = Math.round((percentage / 100) * width);
|
|
303
|
+
const empty = width - filled;
|
|
304
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
305
|
+
return `[${bar}]`;
|
|
306
|
+
}
|
|
307
|
+
// Success and failure helpers
|
|
308
|
+
success(message, context, metadata) {
|
|
309
|
+
this.info(`✅ ${message}`, context, metadata);
|
|
310
|
+
}
|
|
311
|
+
failure(message, context, metadata, error) {
|
|
312
|
+
this.error(`❌ ${message}`, context, metadata, error);
|
|
313
|
+
}
|
|
314
|
+
// Method to close file streams (useful for graceful shutdown)
|
|
315
|
+
close() {
|
|
316
|
+
if (this.logStream) {
|
|
317
|
+
this.logStream.end();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Create default logger instance
|
|
322
|
+
const defaultConfig = {
|
|
323
|
+
level: process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.DEBUG,
|
|
324
|
+
enableConsole: true,
|
|
325
|
+
enableFile: process.env.NODE_ENV === 'production',
|
|
326
|
+
logDir: './logs',
|
|
327
|
+
format: process.env.NODE_ENV === 'production' ? 'json' : 'pretty',
|
|
328
|
+
};
|
|
329
|
+
export const logger = new Logger(defaultConfig);
|
|
330
|
+
// Export the Logger class for custom instances
|
|
331
|
+
export { Logger };
|
|
332
|
+
// Graceful shutdown handling
|
|
333
|
+
process.on('SIGINT', () => {
|
|
334
|
+
logger.info('Received SIGINT, closing logger...');
|
|
335
|
+
logger.close();
|
|
336
|
+
process.exit(0);
|
|
337
|
+
});
|
|
338
|
+
process.on('SIGTERM', () => {
|
|
339
|
+
logger.info('Received SIGTERM, closing logger...');
|
|
340
|
+
logger.close();
|
|
341
|
+
process.exit(0);
|
|
342
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Podcast generation prompts
|
|
3
|
+
*/
|
|
4
|
+
export interface StructurePromptParams {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
userPrompt: string;
|
|
8
|
+
studyGuideContent: string;
|
|
9
|
+
generateIntro: boolean;
|
|
10
|
+
generateOutro: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface RegenerateSegmentPromptParams {
|
|
13
|
+
oldContent: string;
|
|
14
|
+
userPrompt: string;
|
|
15
|
+
segmentTitle: string;
|
|
16
|
+
context: {
|
|
17
|
+
episodeTitle: string;
|
|
18
|
+
allSegmentTitles: string[];
|
|
19
|
+
segmentIndex: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generate prompt for structuring podcast content
|
|
24
|
+
*/
|
|
25
|
+
export declare function createStructurePrompt(params: StructurePromptParams): string;
|
|
26
|
+
/**
|
|
27
|
+
* Generate prompt for episode summary
|
|
28
|
+
*/
|
|
29
|
+
export declare function createSummaryPrompt(episodeTitle: string, segments: Array<{
|
|
30
|
+
title: string;
|
|
31
|
+
keyPoints: string[];
|
|
32
|
+
}>): string;
|
|
33
|
+
/**
|
|
34
|
+
* Generate improved prompt for segment regeneration
|
|
35
|
+
*/
|
|
36
|
+
export declare function createRegenerateSegmentPrompt(params: RegenerateSegmentPromptParams): string;
|
|
37
|
+
/**
|
|
38
|
+
* Validate environment for podcast generation
|
|
39
|
+
*/
|
|
40
|
+
export declare function validatePodcastEnvironment(): {
|
|
41
|
+
valid: boolean;
|
|
42
|
+
errors: string[];
|
|
43
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Podcast generation prompts
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Generate prompt for structuring podcast content
|
|
6
|
+
*/
|
|
7
|
+
export function createStructurePrompt(params) {
|
|
8
|
+
const { title, description, userPrompt, studyGuideContent, generateIntro, generateOutro } = params;
|
|
9
|
+
return `You are an expert podcast content creator and scriptwriter. Your task is to create an engaging, educational podcast episode with professional structure and natural conversational language.
|
|
10
|
+
|
|
11
|
+
OBJECTIVE:
|
|
12
|
+
Create a complete podcast episode that is informative, engaging, and flows naturally. The content should be suitable for audio delivery with a conversational tone.
|
|
13
|
+
|
|
14
|
+
USER REQUEST:
|
|
15
|
+
Title: ${title}
|
|
16
|
+
Description: ${description || 'No description provided'}
|
|
17
|
+
Prompt: ${userPrompt}
|
|
18
|
+
|
|
19
|
+
REQUIREMENTS:
|
|
20
|
+
1. Create segments that are 2-5 minutes each when spoken at a normal pace
|
|
21
|
+
2. Use natural, conversational language (avoid reading lists, use storytelling)
|
|
22
|
+
3. Include smooth transitions between segments
|
|
23
|
+
4. Each segment should have a clear focus and takeaways
|
|
24
|
+
5. Make content accessible but informative
|
|
25
|
+
${generateIntro ? '6. Start with an engaging hook that captures attention immediately' : ''}
|
|
26
|
+
${generateOutro ? '7. End with a strong conclusion that summarizes and motivates action' : ''}
|
|
27
|
+
|
|
28
|
+
CONTEXT (if available):
|
|
29
|
+
${studyGuideContent ? `Study Guide Content (use as reference, don't copy verbatim):\n${studyGuideContent.substring(0, 2000)}` : 'No study guide available'}
|
|
30
|
+
|
|
31
|
+
OUTPUT FORMAT (strict JSON):
|
|
32
|
+
{
|
|
33
|
+
"episodeTitle": "Enhanced, engaging title for the podcast",
|
|
34
|
+
"totalEstimatedDuration": "XX minutes",
|
|
35
|
+
"segments": [
|
|
36
|
+
{
|
|
37
|
+
"title": "Segment title (concise and descriptive)",
|
|
38
|
+
"content": "Natural conversational script. Use 'I', 'we', 'you'. Tell stories. Ask rhetorical questions. Use analogies. Make it sound like a real conversation, not a lecture.",
|
|
39
|
+
"keyPoints": ["Main point 1", "Main point 2", "Main point 3"],
|
|
40
|
+
"estimatedDuration": "X minutes",
|
|
41
|
+
"order": 1
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
IMPORTANT:
|
|
47
|
+
- Write like you're having a conversation with a friend
|
|
48
|
+
- Use contractions (I'm, you're, let's)
|
|
49
|
+
- Include verbal cues ("Now, here's the interesting part...", "You know what's fascinating?")
|
|
50
|
+
- Vary sentence length for natural rhythm
|
|
51
|
+
- Each segment should be self-contained but connected to the narrative
|
|
52
|
+
|
|
53
|
+
Return ONLY the JSON, no additional text.`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generate prompt for episode summary
|
|
57
|
+
*/
|
|
58
|
+
export function createSummaryPrompt(episodeTitle, segments) {
|
|
59
|
+
return `Create a comprehensive analysis and summary for this podcast episode.
|
|
60
|
+
|
|
61
|
+
EPISODE: "${episodeTitle}"
|
|
62
|
+
|
|
63
|
+
SEGMENTS:
|
|
64
|
+
${JSON.stringify(segments, null, 2)}
|
|
65
|
+
|
|
66
|
+
Generate a detailed summary with the following structure (return as JSON):
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
"executiveSummary": "2-3 sentence overview of what listeners will learn",
|
|
70
|
+
"learningObjectives": ["Specific, actionable objectives (3-5 items)"],
|
|
71
|
+
"keyConcepts": ["Main concepts covered (5-7 items)"],
|
|
72
|
+
"followUpActions": ["Concrete next steps listeners can take (3-5 items)"],
|
|
73
|
+
"targetAudience": "Detailed description of who will benefit most",
|
|
74
|
+
"prerequisites": ["Knowledge or background helpful but not required (2-3 items, or empty array)"],
|
|
75
|
+
"tags": ["Relevant searchable tags (5-8 items)"]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Make it specific, actionable, and useful for potential listeners trying to decide if this episode is for them.
|
|
79
|
+
|
|
80
|
+
Return ONLY the JSON, no additional text.`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Generate improved prompt for segment regeneration
|
|
84
|
+
*/
|
|
85
|
+
export function createRegenerateSegmentPrompt(params) {
|
|
86
|
+
const { oldContent, userPrompt, segmentTitle, context } = params;
|
|
87
|
+
return `You are revising a specific segment of a podcast episode.
|
|
88
|
+
|
|
89
|
+
EPISODE CONTEXT:
|
|
90
|
+
- Episode Title: "${context.episodeTitle}"
|
|
91
|
+
- All Segments: ${context.allSegmentTitles.map((t, i) => `${i + 1}. ${t}`).join(', ')}
|
|
92
|
+
- Current Segment: #${context.segmentIndex + 1} - "${segmentTitle}"
|
|
93
|
+
|
|
94
|
+
CURRENT CONTENT:
|
|
95
|
+
"""
|
|
96
|
+
${oldContent}
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
USER REQUEST FOR REVISION:
|
|
100
|
+
"""
|
|
101
|
+
${userPrompt}
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
TASK:
|
|
105
|
+
Rewrite this segment following the user's request while maintaining:
|
|
106
|
+
1. Natural, conversational tone
|
|
107
|
+
2. Connection to the overall episode narrative
|
|
108
|
+
3. Appropriate length (2-5 minutes spoken)
|
|
109
|
+
4. Engaging and informative content
|
|
110
|
+
|
|
111
|
+
GUIDELINES:
|
|
112
|
+
- If user asks to make it "longer", expand on concepts with examples and stories
|
|
113
|
+
- If user asks to make it "shorter", focus on key points and remove tangents
|
|
114
|
+
- If user asks to "add more detail", include specific examples, data, or anecdotes
|
|
115
|
+
- If user asks to "simplify", use clearer language and better analogies
|
|
116
|
+
- If user asks to "make it more engaging", add hooks, questions, and storytelling
|
|
117
|
+
|
|
118
|
+
Return ONLY the revised content as plain text, no JSON or formatting. Write as if you're speaking to listeners.`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validate environment for podcast generation
|
|
122
|
+
*/
|
|
123
|
+
export function validatePodcastEnvironment() {
|
|
124
|
+
const errors = [];
|
|
125
|
+
if (!process.env.MURF_TTS_KEY) {
|
|
126
|
+
errors.push('MURF_TTS_KEY environment variable is not set');
|
|
127
|
+
}
|
|
128
|
+
if (!process.env.INFERENCE_API_URL && !process.env.OPENAI_API_KEY) {
|
|
129
|
+
errors.push('No AI inference API configured (need INFERENCE_API_URL or OPENAI_API_KEY)');
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
valid: errors.length === 0,
|
|
133
|
+
errors,
|
|
134
|
+
};
|
|
135
|
+
}
|
package/dist/lib/pusher.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare class PusherService {
|
|
|
9
9
|
static emitPodcastComplete(workspaceId: string, artifact: any): Promise<void>;
|
|
10
10
|
static emitOverallComplete(workspaceId: string, filename: string, artifacts: any): Promise<void>;
|
|
11
11
|
static emitError(workspaceId: string, error: string, analysisType?: string): Promise<void>;
|
|
12
|
+
static emitAnalysisProgress(workspaceId: string, progress: any): Promise<void>;
|
|
12
13
|
static emitChannelEvent(channelId: string, event: string, data: any): Promise<void>;
|
|
13
14
|
}
|
|
14
15
|
export default PusherService;
|
package/dist/lib/pusher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Pusher from 'pusher';
|
|
2
|
+
import { logger } from './logger.js';
|
|
2
3
|
// Server-side Pusher instance
|
|
3
4
|
export const pusher = new Pusher({
|
|
4
5
|
appId: process.env.PUSHER_APP_ID || '',
|
|
@@ -13,9 +14,9 @@ export class PusherService {
|
|
|
13
14
|
static async emitTaskComplete(workspaceId, event, data) {
|
|
14
15
|
try {
|
|
15
16
|
const channel = `workspace_${workspaceId}`;
|
|
16
|
-
const eventName =
|
|
17
|
+
const eventName = event;
|
|
17
18
|
await pusher.trigger(channel, eventName, data);
|
|
18
|
-
|
|
19
|
+
logger.info(`📡 Pusher notification sent: ${eventName} to ${channel}`);
|
|
19
20
|
}
|
|
20
21
|
catch (error) {
|
|
21
22
|
console.error('❌ Pusher notification error:', error);
|
|
@@ -78,6 +79,17 @@ export class PusherService {
|
|
|
78
79
|
timestamp: new Date().toISOString(),
|
|
79
80
|
});
|
|
80
81
|
}
|
|
82
|
+
// Emit analysis progress update (single event for all progress updates)
|
|
83
|
+
static async emitAnalysisProgress(workspaceId, progress) {
|
|
84
|
+
try {
|
|
85
|
+
const channel = `workspace_${workspaceId}`;
|
|
86
|
+
await pusher.trigger(channel, 'analysis_progress', progress);
|
|
87
|
+
logger.info(`📡 Analysis progress sent to ${channel}: ${progress.status}`);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error('❌ Pusher progress notification error:', error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
81
93
|
// Emit channel-specific events (for chat messages)
|
|
82
94
|
static async emitChannelEvent(channelId, event, data) {
|
|
83
95
|
try {
|
package/dist/lib/storage.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ export interface UploadResult {
|
|
|
3
3
|
signedUrl?: string;
|
|
4
4
|
objectKey: string;
|
|
5
5
|
}
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function uploadToSupabase(fileBuffer: Buffer, fileName: string, contentType: string, makePublic?: boolean): Promise<UploadResult>;
|
|
7
7
|
export declare function generateSignedUrl(objectKey: string, expiresInHours?: number): Promise<string>;
|
|
8
|
-
export declare function
|
|
8
|
+
export declare function deleteFromSupabase(objectKey: string): Promise<void>;
|
|
9
9
|
export declare function makeFilePublic(objectKey: string): Promise<void>;
|
|
10
10
|
export declare function makeFilePrivate(objectKey: string): Promise<void>;
|
|
11
|
-
export declare const
|
|
11
|
+
export declare const supabaseClient: import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;
|