CognxSafeTrack commited on
Commit
3c6fc2a
Β·
1 Parent(s): 29d83ee

feat: sync all recent changes (AI, whatsapp, redis passwords, script fixes)

Browse files
apps/api/src/routes/ai.ts CHANGED
@@ -52,4 +52,32 @@ export async function aiRoutes(fastify: FastifyInstance) {
52
 
53
  return { success: true, url: downloadUrl, data: deckData };
54
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
 
52
 
53
  return { success: true, url: downloadUrl, data: deckData };
54
  });
55
+
56
+ // 3. Personalize Lesson Content (Dynamic Rewriting)
57
+ fastify.post('/personalize-lesson', async (request) => {
58
+ const bodySchema = z.object({
59
+ lessonText: z.string(),
60
+ userActivity: z.string()
61
+ });
62
+ const { lessonText, userActivity } = bodySchema.parse(request.body);
63
+
64
+ console.log(`Personalizing lesson for activity: ${userActivity}`);
65
+
66
+ const personalizedText = await aiService.generatePersonalizedLesson(lessonText, userActivity);
67
+
68
+ return { success: true, text: personalizedText };
69
+ });
70
+
71
+ // 4. Generate TTS Audio for a lesson
72
+ fastify.post('/tts', async (request) => {
73
+ const bodySchema = z.object({ text: z.string() });
74
+ const { text } = bodySchema.parse(request.body);
75
+
76
+ console.log(`Generating TTS audio...`);
77
+
78
+ const audioBuffer = await aiService.generateSpeech(text);
79
+ const downloadUrl = await mockS3Upload(audioBuffer, `lesson-audio-${Date.now()}.mp3`);
80
+
81
+ return { success: true, url: downloadUrl };
82
+ });
83
  }
apps/api/src/routes/whatsapp.ts CHANGED
@@ -27,9 +27,44 @@ export async function whatsappRoutes(fastify: FastifyInstance) {
27
  const value = changes?.value;
28
  const message = value?.messages?.[0];
29
 
30
- if (message && message.type === 'text') {
31
  const phone = message.from; // WhatsApp ID
32
- const text = message.text?.body;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  if (phone && text) {
35
  await WhatsAppService.handleIncomingMessage(phone, text);
 
27
  const value = changes?.value;
28
  const message = value?.messages?.[0];
29
 
30
+ if (message) {
31
  const phone = message.from; // WhatsApp ID
32
+ let text = '';
33
+
34
+ if (message.type === 'text') {
35
+ text = message.text?.body;
36
+ } else if (message.type === 'audio') {
37
+ // 🌟 Support Multi-language Audio (STT) 🌟
38
+ const audioId = message.audio?.id;
39
+ if (audioId) {
40
+ try {
41
+ console.log(`[WEBHOOK] Downloading audio message ${audioId}...`);
42
+ const token = process.env.WHATSAPP_ACCESS_TOKEN; // Ensure this env is accessible
43
+
44
+ // 1. Get media URL
45
+ const metaRes = await fetch(`https://graph.facebook.com/v18.0/${audioId}`, {
46
+ headers: { 'Authorization': `Bearer ${token}` }
47
+ });
48
+ const meta = await metaRes.json();
49
+
50
+ // 2. Download binary media
51
+ if (meta.url) {
52
+ const mediaRes = await fetch(meta.url, {
53
+ headers: { 'Authorization': `Bearer ${token}` }
54
+ });
55
+ const arrayBuffer = await mediaRes.arrayBuffer();
56
+ const buffer = Buffer.from(arrayBuffer);
57
+
58
+ // 3. Transcribe with Whisper (via AIService)
59
+ const { aiService } = await import('../services/ai');
60
+ text = await aiService.transcribeAudio(buffer, 'message.ogg');
61
+ console.log(`[STT] Transcribed audio: "${text}"`);
62
+ }
63
+ } catch (err) {
64
+ console.error('[WEBHOOK] Failed to process audio message:', err);
65
+ }
66
+ }
67
+ }
68
 
69
  if (phone && text) {
70
  await WhatsAppService.handleIncomingMessage(phone, text);
apps/api/src/services/ai/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LLMProvider, OnePagerData, OnePagerSchema, PitchDeckData, PitchDeckSchema } from './types';
2
  import { MockLLMProvider } from './mock-provider';
3
  import { OpenAIProvider } from './openai-provider';
4
 
@@ -45,6 +45,37 @@ class AIService {
45
 
46
  return this.provider.generateStructuredData(prompt, PitchDeckSchema);
47
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
 
50
  // Export a singleton instance
 
1
+ import { LLMProvider, OnePagerData, OnePagerSchema, PitchDeckData, PitchDeckSchema, PersonalizedLessonSchema } from './types';
2
  import { MockLLMProvider } from './mock-provider';
3
  import { OpenAIProvider } from './openai-provider';
4
 
 
45
 
46
  return this.provider.generateStructuredData(prompt, PitchDeckSchema);
47
  }
48
+
49
+ /**
50
+ * Rewrites a daily lesson to use analogies relevant to the user's business sector.
51
+ */
52
+ async generatePersonalizedLesson(lessonText: string, userActivity: string): Promise<string> {
53
+ const prompt = `
54
+ You are an expert business coach. Rewrite the following daily lesson to specifically target an entrepreneur in the following business sector/activity: "${userActivity}".
55
+ Keep the core educational value exactly the same, but use analogies, examples, and a tone that resonates directly with their specific industry.
56
+ Do NOT make the lesson significantly longer than the original. Maintain a friendly, direct WhatsApp formatting (use *bold* and emojis appropriately).
57
+
58
+ ORIGINAL LESSON:
59
+ ${lessonText}
60
+ `;
61
+
62
+ const result = await this.provider.generateStructuredData(prompt, PersonalizedLessonSchema);
63
+ return result.lessonText;
64
+ }
65
+
66
+ /**
67
+ * Transcribes an audio buffer to text (useful for Wolof/FR voice messages).
68
+ */
69
+ async transcribeAudio(audioBuffer: Buffer, filename: string): Promise<string> {
70
+ return this.provider.transcribeAudio(audioBuffer, filename);
71
+ }
72
+
73
+ /**
74
+ * Converts text into an audio MP3 buffer (TTS).
75
+ */
76
+ async generateSpeech(text: string): Promise<Buffer> {
77
+ return this.provider.generateSpeech(text);
78
+ }
79
  }
80
 
81
  // Export a singleton instance
apps/api/src/services/ai/mock-provider.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LLMProvider, OnePagerSchema, PitchDeckSchema } from './types';
2
 
3
  /**
4
  * A Provider for local development that doesn't require an API Key.
@@ -40,6 +40,23 @@ export class MockLLMProvider implements LLMProvider {
40
  return schema.parse(mockDeck) as any;
41
  }
42
 
 
 
 
 
 
 
 
43
  throw new Error("MockLLMProvider does not support this schema.");
44
  }
 
 
 
 
 
 
 
 
 
 
45
  }
 
1
+ import { LLMProvider, OnePagerSchema, PitchDeckSchema, PersonalizedLessonSchema } from './types';
2
 
3
  /**
4
  * A Provider for local development that doesn't require an API Key.
 
40
  return schema.parse(mockDeck) as any;
41
  }
42
 
43
+ if (schema === PersonalizedLessonSchema) {
44
+ const mockPersonalized = {
45
+ lessonText: "Voici une leΓ§on adaptΓ©e: Pensez Γ  votre ferme comme..."
46
+ };
47
+ return schema.parse(mockPersonalized) as any;
48
+ }
49
+
50
  throw new Error("MockLLMProvider does not support this schema.");
51
  }
52
+
53
+ async transcribeAudio(_audioBuffer: Buffer, filename: string): Promise<string> {
54
+ console.log(`[MOCK LLM] Transcribing audio from ${filename}...`);
55
+ return "INSCRIPTION";
56
+ }
57
+
58
+ async generateSpeech(text: string): Promise<Buffer> {
59
+ console.log(`[MOCK LLM] Generating speech for text: ${text.substring(0, 30)}...`);
60
+ return Buffer.from("mock_audio_data");
61
+ }
62
  }
apps/api/src/services/ai/openai-provider.ts CHANGED
@@ -36,4 +36,31 @@ export class OpenAIProvider implements LLMProvider {
36
 
37
  return result as T;
38
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
 
36
 
37
  return result as T;
38
  }
39
+
40
+ async transcribeAudio(audioBuffer: Buffer, filename: string): Promise<string> {
41
+ console.log(`[OPENAI] Transcribing audio file ${filename}...`);
42
+
43
+ // Convert Buffer to File for OpenAI SDK
44
+ const { toFile } = await import('openai');
45
+ const file = await toFile(audioBuffer, filename);
46
+
47
+ const response = await this.openai.audio.transcriptions.create({
48
+ file: file,
49
+ model: 'whisper-1',
50
+ });
51
+
52
+ return response.text;
53
+ }
54
+
55
+ async generateSpeech(text: string): Promise<Buffer> {
56
+ console.log('[OPENAI] Generating speech TTS...');
57
+
58
+ const mp3 = await this.openai.audio.speech.create({
59
+ model: 'tts-1',
60
+ voice: 'alloy',
61
+ input: text,
62
+ });
63
+
64
+ return Buffer.from(await mp3.arrayBuffer());
65
+ }
66
  }
apps/api/src/services/ai/types.ts CHANGED
@@ -3,6 +3,8 @@ import { z } from 'zod';
3
  // Base interface for all LLM Providers
4
  export interface LLMProvider {
5
  generateStructuredData<T>(prompt: string, schema: z.ZodSchema<T>): Promise<T>;
 
 
6
  }
7
 
8
  // -----------------------------------------------------
@@ -29,6 +31,7 @@ export const SlideSchema = z.object({
29
  });
30
  export type SlideData = z.infer<typeof SlideSchema>;
31
 
 
32
  // Schema for the V1 Pitch Deck (PPTX)
33
  export const PitchDeckSchema = z.object({
34
  title: z.string(),
@@ -36,3 +39,9 @@ export const PitchDeckSchema = z.object({
36
  slides: z.array(SlideSchema).min(5).max(12).describe("The sequence of slides for the pitch deck")
37
  });
38
  export type PitchDeckData = z.infer<typeof PitchDeckSchema>;
 
 
 
 
 
 
 
3
  // Base interface for all LLM Providers
4
  export interface LLMProvider {
5
  generateStructuredData<T>(prompt: string, schema: z.ZodSchema<T>): Promise<T>;
6
+ transcribeAudio(audioBuffer: Buffer, filename: string): Promise<string>;
7
+ generateSpeech(text: string): Promise<Buffer>;
8
  }
9
 
10
  // -----------------------------------------------------
 
31
  });
32
  export type SlideData = z.infer<typeof SlideSchema>;
33
 
34
+
35
  // Schema for the V1 Pitch Deck (PPTX)
36
  export const PitchDeckSchema = z.object({
37
  title: z.string(),
 
39
  slides: z.array(SlideSchema).min(5).max(12).describe("The sequence of slides for the pitch deck")
40
  });
41
  export type PitchDeckData = z.infer<typeof PitchDeckSchema>;
42
+
43
+ // Schema for personalized WhatsApp lesson
44
+ export const PersonalizedLessonSchema = z.object({
45
+ lessonText: z.string().describe("The rewritten lesson text adapted to the user's business sector.")
46
+ });
47
+ export type PersonalizedLessonData = z.infer<typeof PersonalizedLessonSchema>;
apps/api/src/services/queue.ts CHANGED
@@ -3,6 +3,8 @@ import { Queue } from 'bullmq';
3
  const connection = {
4
  host: process.env.REDIS_HOST || 'localhost',
5
  port: parseInt(process.env.REDIS_PORT || '6379'),
 
 
6
  };
7
 
8
  export const whatsappQueue = new Queue('whatsapp-queue', { connection });
@@ -25,3 +27,10 @@ export async function scheduleTrackDay(userId: string, trackId: string, dayNumbe
25
  delay: delayMs
26
  });
27
  }
 
 
 
 
 
 
 
 
3
  const connection = {
4
  host: process.env.REDIS_HOST || 'localhost',
5
  port: parseInt(process.env.REDIS_PORT || '6379'),
6
+ password: process.env.REDIS_PASSWORD || undefined,
7
+ tls: process.env.REDIS_TLS === 'true' ? {} : undefined,
8
  };
9
 
10
  export const whatsappQueue = new Queue('whatsapp-queue', { connection });
 
27
  delay: delayMs
28
  });
29
  }
30
+
31
+ export async function enrollUser(userId: string, trackId: string) {
32
+ await whatsappQueue.add('enroll-user', {
33
+ userId,
34
+ trackId
35
+ });
36
+ }
apps/api/src/services/renderers/pdf-renderer.ts CHANGED
@@ -116,6 +116,7 @@ export class PdfOnePagerRenderer implements DocumentRenderer<OnePagerData> {
116
  `;
117
 
118
  const browser = await puppeteer.launch({
 
119
  headless: true,
120
  args: ['--no-sandbox', '--disable-setuid-sandbox'], // Required for running as root in Docker/HF
121
  });
 
116
  `;
117
 
118
  const browser = await puppeteer.launch({
119
+ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
120
  headless: true,
121
  args: ['--no-sandbox', '--disable-setuid-sandbox'], // Required for running as root in Docker/HF
122
  });
apps/api/src/services/whatsapp.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { prisma } from './prisma';
 
2
 
3
  export class WhatsAppService {
4
  static async handleIncomingMessage(phone: string, text: string) {
@@ -13,17 +14,63 @@ export class WhatsAppService {
13
  user = await prisma.user.create({
14
  data: { phone }
15
  });
16
- // TODO: Send welcome message & ask for name
17
- console.log('New user created, starting onboarding...');
18
  return;
19
  } else {
20
- // TODO: Send "Type INSCRIPTION to start"
21
- console.log('User not found, waiting for INSCRIPTION');
22
  return;
23
  }
24
  }
25
 
26
- // 2. Check Active Enrollment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  const activeEnrollment = await prisma.enrollment.findFirst({
28
  where: { userId: user.id, status: 'ACTIVE' },
29
  include: { track: true }
@@ -43,48 +90,15 @@ export class WhatsAppService {
43
  }
44
  });
45
 
46
- // TODO: Schedule next day or send immediate feedback
47
- // For MVP, just acknowledge reception
48
- // await sendMessage(phone, "RΓ©ponse enregistrΓ©e ! Γ€ demain pour la suite.");
49
-
50
  return;
51
  }
52
 
53
- // 3. Handle Commands
54
  if (normalizedText === 'INSCRIPTION') {
55
- console.log('Processing new enrollment...');
56
-
57
- // Find default track (Business Model Express)
58
- const defaultTrack = await prisma.track.findFirst({
59
- where: { title: "Business Model Express" }
60
- });
61
-
62
- if (!defaultTrack) {
63
- console.error('Default track not found!');
64
- return;
65
- }
66
-
67
- // Create enrollment
68
- await prisma.enrollment.create({
69
- data: {
70
- userId: user.id,
71
- trackId: defaultTrack.id,
72
- startedAt: new Date(),
73
- status: 'ACTIVE',
74
- currentDay: 1
75
- }
76
- });
77
-
78
- console.log(`User ${user.id} enrolled in ${defaultTrack.title}`);
79
-
80
- // Schedule Day 1 Content immediately (or short delay)
81
- // Import dynamically to avoid circular deps if necessary, or just import at top
82
- const { scheduleTrackDay } = await import('./queue');
83
- await scheduleTrackDay(user.id, defaultTrack.id, 1, 0);
84
-
85
  } else {
86
- console.log('Unknown command, sending menu...');
87
- // TODO: Send "Envoyez INSCRIPTION pour commencer"
88
  }
89
  }
90
  }
 
1
  import { prisma } from './prisma';
2
+ import { scheduleMessage, enrollUser } from './queue';
3
 
4
  export class WhatsAppService {
5
  static async handleIncomingMessage(phone: string, text: string) {
 
14
  user = await prisma.user.create({
15
  data: { phone }
16
  });
17
+ await scheduleMessage(user.id, "Bienvenue sur SafeTrack Edu ! πŸŽ‰\nChoisissez votre langue / TΓ nnal sa lΓ kk:\n1. FranΓ§ais πŸ‡«πŸ‡·\n2. Wolof πŸ‡ΈπŸ‡³");
18
+ console.log('New user created, asked for language.');
19
  return;
20
  } else {
21
+ // Not registered yet, hasn't typed INSCRIPTION. We ignore or prompt:
22
+ console.log(`Unregistered user ${phone} sent a message. Need INSCRIPTION.`);
23
  return;
24
  }
25
  }
26
 
27
+ // 2. Check Onboarding State (Missing Language -> Missing Activity)
28
+ if (!user.city) {
29
+ // First time after INSCRIPTION they should answer 1 or 2
30
+ if (normalizedText === '1' || normalizedText === '2') {
31
+ const lang = normalizedText === '1' ? 'FR' : 'WOLOF';
32
+ user = await prisma.user.update({
33
+ where: { id: user.id },
34
+ data: { language: lang, city: 'SET' } // Using city as a step flag for MVP
35
+ });
36
+
37
+ const prompt = lang === 'FR'
38
+ ? "Parfait ! Pour personnaliser votre expΓ©rience, quel est votre secteur d'activitΓ© ou projet professionnel ? (ex: Agriculture, Commerce, Tech...)"
39
+ : "Baax na ! Ngir gΓ«n a waajal sa njΓ ng, ban mbir ngay def ? (Mbay, Njaay, Tech...)";
40
+ await scheduleMessage(user.id, prompt);
41
+ return;
42
+ } else {
43
+ await scheduleMessage(user.id, "Veuillez choisir votre langue / TΓ nnal sa lΓ kk:\n1. FranΓ§ais πŸ‡«πŸ‡·\n2. Wolof πŸ‡ΈπŸ‡³");
44
+ return;
45
+ }
46
+ }
47
+
48
+ if (!user.activity) {
49
+ // Whatever they type now (or dictated via audio) is their activity
50
+ user = await prisma.user.update({
51
+ where: { id: user.id },
52
+ data: { activity: text.trim() }
53
+ });
54
+
55
+ const welcomeMsg = user.language === 'FR'
56
+ ? `Merci ! Nous avons notΓ© votre secteur : *${user.activity}*.\nJe vous inscris maintenant Γ  notre formation d'introduction !`
57
+ : `JΓ«rΓ«jΓ«f ! Bind nanu la ci: *${user.activity}*.\nLΓ©egi dinanu la dugal ci njΓ ng mi !`;
58
+
59
+ await scheduleMessage(user.id, welcomeMsg);
60
+
61
+ // Find default track
62
+ const defaultTrack = await prisma.track.findFirst({
63
+ where: { title: "Business Model Express" } // Assuming this exists from seed
64
+ });
65
+
66
+ if (defaultTrack) {
67
+ // Delegate to worker which handles premium vs free checks!
68
+ await enrollUser(user.id, defaultTrack.id);
69
+ }
70
+ return;
71
+ }
72
+
73
+ // 3. Check Active Enrollment
74
  const activeEnrollment = await prisma.enrollment.findFirst({
75
  where: { userId: user.id, status: 'ACTIVE' },
76
  include: { track: true }
 
90
  }
91
  });
92
 
93
+ await scheduleMessage(user.id, "βœ… RΓ©ponse enregistrΓ©e ! Γ€ demain pour la suite.");
 
 
 
94
  return;
95
  }
96
 
97
+ // 4. Default Handle Commands
98
  if (normalizedText === 'INSCRIPTION') {
99
+ await scheduleMessage(user.id, "Vous avez dΓ©jΓ  complΓ©tΓ© l'inscription. Consultez le portail ou attendez nos prochaines formations !");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  } else {
101
+ console.log('Unknown command from fully onboarded user without active enrollment.');
 
102
  }
103
  }
104
  }
apps/whatsapp-worker/src/index.ts CHANGED
@@ -9,6 +9,8 @@ const prisma = new PrismaClient();
9
  const connection = {
10
  host: process.env.REDIS_HOST || 'localhost',
11
  port: parseInt(process.env.REDIS_PORT || '6379'),
 
 
12
  };
13
 
14
  const worker = new Worker('whatsapp-queue', async (job: Job) => {
@@ -67,13 +69,60 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
67
  else if (job.name === 'send-content') {
68
  const { userId, trackId, dayNumber } = job.data;
69
 
 
70
  const trackDay = await prisma.trackDay.findFirst({
71
  where: { trackId, dayNumber }
72
  });
73
 
74
  if (trackDay && trackDay.textContent) {
75
- // TODO: Call WhatsApp Cloud API
76
- console.log(`[MOCK SEND CONTENT] To User ${userId}: "${trackDay.textContent}"`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  // Update enrollment progress
79
  await prisma.enrollment.updateMany({
@@ -96,9 +145,8 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
96
  // 🌟 Trigger AI Document Generation 🌟
97
  console.log(`[WORKER] Triggering AI Document Generation for User ${userId}...`);
98
  try {
99
- // In a real scenario, we'd fetch the user's responses from the DB to build the context.
100
- // For now, we pass a dummy userContext to trigger the generation flow.
101
- const userContext = `User ${userId} completed the Business Pitch track. They want to build an AgTech SaaS.`;
102
 
103
  const API_URL = process.env.VITE_API_URL || 'http://localhost:3001';
104
 
 
9
  const connection = {
10
  host: process.env.REDIS_HOST || 'localhost',
11
  port: parseInt(process.env.REDIS_PORT || '6379'),
12
+ password: process.env.REDIS_PASSWORD || undefined,
13
+ tls: process.env.REDIS_TLS === 'true' ? {} : undefined,
14
  };
15
 
16
  const worker = new Worker('whatsapp-queue', async (job: Job) => {
 
69
  else if (job.name === 'send-content') {
70
  const { userId, trackId, dayNumber } = job.data;
71
 
72
+ const user = await prisma.user.findUnique({ where: { id: userId } });
73
  const trackDay = await prisma.trackDay.findFirst({
74
  where: { trackId, dayNumber }
75
  });
76
 
77
  if (trackDay && trackDay.textContent) {
78
+ let finalContent = trackDay.textContent;
79
+
80
+ // 🌟 Personalize Lesson Content 🌟
81
+ if (user && user.activity) {
82
+ try {
83
+ console.log(`[WORKER] Personalizing lesson for User ${userId}'s activity: ${user.activity}`);
84
+ const API_URL = process.env.VITE_API_URL || 'http://localhost:3001';
85
+ const personalizeRes = await fetch(`${API_URL}/v1/ai/personalize-lesson`, {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ lessonText: trackDay.textContent, userActivity: user.activity })
89
+ });
90
+
91
+ if (personalizeRes.ok) {
92
+ const personalizeData = await personalizeRes.json();
93
+ if (personalizeData.text) {
94
+ finalContent = personalizeData.text;
95
+ }
96
+ }
97
+ } catch (err) {
98
+ console.error('[WORKER] Failed to personalize lesson:', err);
99
+ }
100
+ }
101
+
102
+ // 🌟 Generate TTS Audio (Multi-language Support) 🌟
103
+ let audioUrl = null;
104
+ try {
105
+ console.log(`[WORKER] Generating TTS Audio for User ${userId}...`);
106
+ const API_URL = process.env.VITE_API_URL || 'http://localhost:3001';
107
+ const ttsRes = await fetch(`${API_URL}/v1/ai/tts`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ text: finalContent })
111
+ });
112
+
113
+ if (ttsRes.ok) {
114
+ const ttsData = await ttsRes.json();
115
+ audioUrl = ttsData.url;
116
+ }
117
+ } catch (err) {
118
+ console.error('[WORKER] Failed to generate TTS:', err);
119
+ }
120
+
121
+ // TODO: Call WhatsApp Cloud API with finalContent and audioUrl
122
+ console.log(`[MOCK SEND CONTENT] To User ${userId}: "${finalContent}"`);
123
+ if (audioUrl) {
124
+ console.log(`[MOCK SEND AUDIO] To User ${userId}: 🎡 ${audioUrl}`);
125
+ }
126
 
127
  // Update enrollment progress
128
  await prisma.enrollment.updateMany({
 
145
  // 🌟 Trigger AI Document Generation 🌟
146
  console.log(`[WORKER] Triggering AI Document Generation for User ${userId}...`);
147
  try {
148
+ // Update userContext to explicitly reference the user's sector/activity
149
+ const userContext = `User ${userId} completed the Business Pitch track. Their business activity/sector is: ${user?.activity || 'Unknown'}. They want to build a business in this sector using the concepts learned in the track.`;
 
150
 
151
  const API_URL = process.env.VITE_API_URL || 'http://localhost:3001';
152
 
scripts/start-backend.sh CHANGED
@@ -1,13 +1,25 @@
1
- #!/bin/sh
 
2
 
3
- # Migrate database
4
  echo "Running database migrations..."
5
  pnpm --filter @repo/database db:push
6
 
7
- # Start Worker in background
8
  echo "Starting WhatsApp Worker..."
9
- node apps/whatsapp-worker/dist/index.js &
 
 
 
 
 
 
 
10
 
11
- # Start API in foreground
12
  echo "Starting API..."
13
- node apps/api/dist/index.js
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -ex
3
 
 
4
  echo "Running database migrations..."
5
  pnpm --filter @repo/database db:push
6
 
 
7
  echo "Starting WhatsApp Worker..."
8
+ # The TypeScript build might output to dist/src/index.js or dist/index.js depending on rootDir
9
+ if [ -f "apps/whatsapp-worker/dist/src/index.js" ]; then
10
+ node apps/whatsapp-worker/dist/src/index.js &
11
+ elif [ -f "apps/whatsapp-worker/dist/index.js" ]; then
12
+ node apps/whatsapp-worker/dist/index.js &
13
+ else
14
+ echo "ERROR: WhatsApp Worker build output missing (dist/index.js or dist/src/index.js)"
15
+ fi
16
 
 
17
  echo "Starting API..."
18
+ if [ -f "apps/api/dist/src/index.js" ]; then
19
+ exec node apps/api/dist/src/index.js
20
+ elif [ -f "apps/api/dist/index.js" ]; then
21
+ exec node apps/api/dist/index.js
22
+ else
23
+ echo "ERROR: API build output missing (dist/index.js or dist/src/index.js)!"
24
+ exit 1
25
+ fi