| | import { prisma } from './prisma'; |
| | import { scheduleMessage, enrollUser, whatsappQueue, scheduleInteractiveButtons, scheduleInteractiveList } from './queue'; |
| |
|
| | export class WhatsAppService { |
| | private static normalizeCommand(text: string): string { |
| | return text |
| | .trim() |
| | .toLowerCase() |
| | .replace(/[.,!?;:]+$/, "") |
| | .toUpperCase(); |
| | } |
| |
|
| | private static detectIntent(text: string): 'YES' | 'NO' | 'UNKNOWN' { |
| | const normalized = text.trim().toLowerCase().replace(/[.,!?;:]+$/, ""); |
| | const yesWords = ['oui', 'ouais', 'wi', 'waaw', 'yes', 'yep', 'ok', 'd’accord', 'daccord', 'da’accord']; |
| | const noWords = ['non', 'déet', 'deet', 'no', 'nah', 'nein']; |
| |
|
| | if (yesWords.some(w => normalized.includes(w))) return 'YES'; |
| | if (noWords.some(w => normalized.includes(w))) return 'NO'; |
| | return 'UNKNOWN'; |
| | } |
| |
|
| | private static levenshteinDistance(a: string, b: string): number { |
| | const matrix: number[][] = []; |
| | for (let i = 0; i <= b.length; i++) matrix[i] = [i]; |
| | for (let j = 0; j <= a.length; j++) matrix[0][j] = j; |
| |
|
| | for (let i = 1; i <= b.length; i++) { |
| | for (let j = 1; j <= a.length; j++) { |
| | if (b.charAt(i - 1) === a.charAt(j - 1)) { |
| | matrix[i][j] = matrix[i - 1][j - 1]; |
| | } else { |
| | matrix[i][j] = Math.min( |
| | matrix[i - 1][j - 1] + 1, |
| | matrix[i][j - 1] + 1, |
| | matrix[i - 1][j] + 1 |
| | ); |
| | } |
| | } |
| | } |
| | return matrix[b.length][a.length]; |
| | } |
| |
|
| | private static isFuzzyMatch(text: string, target: string, threshold = 0.8): boolean { |
| | const normalized = text.trim().toUpperCase(); |
| | const tar = target.toUpperCase(); |
| | if (normalized === tar) return true; |
| | if (normalized.includes(tar) || tar.includes(normalized)) return true; |
| |
|
| | const distance = this.levenshteinDistance(normalized, tar); |
| | const maxLength = Math.max(normalized.length, tar.length); |
| | const similarity = 1 - distance / maxLength; |
| | return similarity >= threshold; |
| | } |
| |
|
| | static async handleIncomingMessage(phone: string, text: string, audioUrl?: string) { |
| | const traceId = audioUrl ? `[STT-FLOW-${phone.slice(-4)}]` : `[TXT-FLOW-${phone.slice(-4)}]`; |
| | const normalizedText = this.normalizeCommand(text); |
| | console.log(`${traceId} Received: ${normalizedText} (Audio: ${audioUrl || 'N/A'})`); |
| |
|
| | |
| | let user = await prisma.user.findUnique({ where: { phone } }); |
| |
|
| | if (!user) { |
| | const isInscription = this.isFuzzyMatch(normalizedText, 'INSCRIPTION') || normalizedText.includes('INSCRI') || normalizedText.includes('INSCRI'); |
| |
|
| | if (isInscription) { |
| | console.log(`${traceId} New user registration triggered for ${phone}`); |
| | user = await prisma.user.create({ data: { phone } }); |
| | await scheduleInteractiveButtons(user.id, |
| | "Dalal jàmm! Xamle ngay tàmbali. ⏳ 30s.\n(FR) Ton cours se prépare (30s).", |
| | [ |
| | { id: 'LANG_FR', title: 'Français 🇫🇷' }, |
| | { id: 'LANG_WO', title: 'Wolof 🇸🇳' } |
| | ] |
| | ); |
| | return; |
| | } else { |
| | console.log(`${traceId} Unregistered user ${phone} sent: "${normalizedText}". Sending instructions.`); |
| | |
| | const { whatsappQueue } = await import('./queue'); |
| | await whatsappQueue.add('send-message-direct', { |
| | phone, |
| | text: "🎓 Bienvenue chez XAMLÉ !\nPour commencer ta formation gratuite, envoie le mot : *INSCRIPTION*\n\n(WO) Dalal jàmm ! Ngir tàmbali sa njàng mburu, bindal : *INSCRIPTION*" |
| | }); |
| | return; |
| | } |
| | } |
| |
|
| | |
| | try { |
| | await prisma.message.create({ |
| | data: { |
| | content: text, |
| | mediaUrl: audioUrl, |
| | direction: 'INBOUND', |
| | userId: user.id |
| | } |
| | }); |
| | } catch (err: unknown) { |
| | console.error('[WhatsAppService] Failed to log incoming message:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))); |
| | } |
| |
|
| | |
| | if (this.isFuzzyMatch(normalizedText, 'INSCRIPTION')) { |
| | await prisma.enrollment.deleteMany({ where: { userId: user.id } }); |
| | await prisma.userProgress.deleteMany({ where: { userId: user.id } }); |
| | await prisma.response.deleteMany({ where: { userId: user.id } }); |
| | |
| | await (prisma as any).businessProfile.deleteMany({ where: { userId: user.id } }); |
| | user = await prisma.user.update({ |
| | where: { id: user.id }, |
| | data: { city: null, activity: null } |
| | }); |
| | await scheduleInteractiveButtons(user.id, |
| | "Réinitialisation réussie – choisissez votre langue / Tànnal sa làkk :", |
| | [ |
| | { id: 'LANG_FR', title: 'Français 🇫🇷' }, |
| | { id: 'LANG_WO', title: 'Wolof 🇸🇳' } |
| | ] |
| | ); |
| | return; |
| | } |
| |
|
| | if (normalizedText === 'TEST_IMAGE') { |
| | await whatsappQueue.add('send-image', { |
| | to: user.phone, |
| | imageUrl: 'https://r2.xamle.sn/branding/branding_xamle.png', |
| | caption: 'Branding XAMLÉ - Industrialisation 2026' |
| | }); |
| | return; |
| | } |
| |
|
| | if (normalizedText.startsWith('TEST_VIDEO')) { |
| | const parts = normalizedText.split(' '); |
| | if (parts.length < 3) { |
| | await scheduleMessage(user.id, "Usage: TEST_VIDEO <TrackId> <DayNumber>"); |
| | return; |
| | } |
| | const trackId = parts[1]; |
| | const dayNumber = parseFloat(parts[2]); |
| |
|
| | await scheduleMessage(user.id, `🧪 Test Video pour ${trackId} J${dayNumber}...`); |
| | await whatsappQueue.add('send-content', { |
| | userId: user.id, |
| | trackId, |
| | dayNumber |
| | }); |
| | return; |
| | } |
| |
|
| | |
| | const wordCount = (text || '').trim().split(/\s+/).length; |
| | const systemCommands = ['1', '2', 'SUITE', 'APPROFONDIR', 'INSCRIPTION', 'SEED']; |
| | const isSystemCommand = systemCommands.some(cmd => this.isFuzzyMatch(normalizedText, cmd)) || normalizedText.includes('INSCRI'); |
| |
|
| | if (wordCount < 3 && !isSystemCommand) { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Dama lay xaar nga wax ma lu gën a yaatu ci sa mbir (mbebetu 3 baat). Waxtaanal ak man !" |
| | : "Je n'ai pas bien compris ton activité. Peux-tu me réexpliquer en quelques mots ce que tu fais ? (Minimum 3 mots)"); |
| | return; |
| | } |
| |
|
| | if (this.isFuzzyMatch(normalizedText, 'SEED')) { |
| | |
| | console.log(`[SEED] Triggered by user ${user.id}`); |
| | try { |
| | |
| | const { seedDatabase } = await import('@repo/database/seed'); |
| | const result = await seedDatabase(prisma); |
| | console.log('[SEED] Result:', result.message); |
| |
|
| | |
| | try { |
| | await (prisma as any).businessProfile.deleteMany({ where: { userId: user.id } }); |
| | await prisma.user.update({ where: { id: user.id }, data: { activity: null } }); |
| | console.log(`[SEED] Cleared cognitive cache for User ${user.id}`); |
| | } catch (cacheErr: unknown) { |
| | console.error('[SEED] Failed to clear cognitive cache:', (cacheErr as Error).message); |
| | } |
| |
|
| | await scheduleMessage(user.id, result.seeded |
| | ? "✅ Seeding terminé ! Le Cache Cognitif a été réinitialisé.\nEnvoie INSCRIPTION pour commencer." |
| | : "ℹ️ Les données existent déjà. Cache Cognitif purgé. Envoie INSCRIPTION." |
| | ); |
| | } catch (err: unknown) { |
| | console.error('[SEED] Error:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))); |
| | await scheduleMessage(user.id, `❌ Erreur seed : ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))?.substring(0, 200)}`); |
| | } |
| | return; |
| | } |
| |
|
| | |
| | |
| | const dayActionMatch = normalizedText.match(/^DAY(\d+)_(EXERCISE|REPLAY|CONTINUE|PROMPT)$/); |
| | if (dayActionMatch) { |
| | const action = dayActionMatch[2]; |
| |
|
| | if (action === 'REPLAY') { |
| | const enrollment = await prisma.enrollment.findFirst({ |
| | where: { userId: user.id, status: 'ACTIVE' } |
| | }); |
| | if (enrollment) { |
| | await whatsappQueue.add('send-content', { |
| | userId: user.id, |
| | trackId: enrollment.trackId, |
| | dayNumber: enrollment.currentDay |
| | }); |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "🔁 Dinanu la yëgël waxtu bi ci kanam..." |
| | : "🔁 Je te renvoie la leçon dans quelques secondes..." |
| | ); |
| | } |
| | return; |
| | } else if (action === 'EXERCISE') { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "🎙️ Yónnee sa tontu (audio walla bind) :" |
| | : "🎙️ Envoie ta réponse (audio ou texte) :" |
| | ); |
| | return; |
| | } else if (action === 'PROMPT') { |
| | const enrollment = await prisma.enrollment.findFirst({ |
| | where: { userId: user.id, status: 'ACTIVE' } |
| | }); |
| | if (enrollment) { |
| | const trackDay = await prisma.trackDay.findFirst({ |
| | where: { trackId: enrollment.trackId, dayNumber: enrollment.currentDay } |
| | }); |
| | if (trackDay?.exercisePrompt) { |
| | await scheduleMessage(user.id, trackDay.exercisePrompt); |
| | } else { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' ? "Amul lëjj" : "Pas d'exercice pour ce jour"); |
| | } |
| | } |
| | return; |
| | } else if (action === 'CONTINUE') { |
| | |
| | const pendingProgress = await prisma.userProgress.findFirst({ |
| | where: { userId: user.id, exerciseStatus: { in: ['PENDING', 'PENDING_REMEDIATION'] } } |
| | }); |
| | if (pendingProgress) { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Danga wara tontu lëjj bi balaa nga dem ci kanam. Tànnal 'Yónni tontu'." |
| | : "Tu dois d'abord répondre à l'exercice avant de continuer. Choisis 'Faire l'exercice' ou 'Répondre'." |
| | ); |
| | } else { |
| | |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Waaw, ñuy dem ci kanam !" |
| | : "C'est noté, on avance !" |
| | ); |
| | |
| | |
| | } |
| | return; |
| | } |
| | } |
| |
|
| | |
| | if (normalizedText === 'LANG_FR' || normalizedText === 'LANG_WO') { |
| | const newLang = normalizedText === 'LANG_FR' ? 'FR' : 'WOLOF'; |
| | user = await prisma.user.update({ |
| | where: { id: user.id }, |
| | data: { language: newLang } |
| | }); |
| |
|
| | const promptText = newLang === 'FR' |
| | ? "Parfait, nous allons continuer en Français ! 🇫🇷\nDans quel domaine d'activité te trouves-tu ?" |
| | : "Baax na, dinanu wéy ci Wolof ! 🇸🇳\nCi ban mbir ngay yëngu ?"; |
| |
|
| | await scheduleInteractiveList( |
| | user.id, |
| | newLang === 'FR' ? "Ton secteur" : "Sa Mbir", |
| | promptText, |
| | newLang === 'FR' ? "Secteurs" : "Tànn", |
| | [{ |
| | title: newLang === 'FR' ? 'Liste' : 'Mbir', |
| | rows: [ |
| | { id: 'SEC_COMMERCE', title: newLang === 'FR' ? 'Commerce / Vente' : 'Njaay' }, |
| | { id: 'SEC_AGRI', title: newLang === 'FR' ? 'Agri / Élevage' : 'Mbay / Samm' }, |
| | { id: 'SEC_FOOD', title: newLang === 'FR' ? 'Alimentation / Rest.' : 'Lekk / Restauration' }, |
| | { id: 'SEC_COUTURE', title: newLang === 'FR' ? 'Couture / Mode' : 'Couture' }, |
| | { id: 'SEC_BEAUTE', title: newLang === 'FR' ? 'Beauté / Bien-être' : 'Rafet' }, |
| | { id: 'SEC_TRANSPORT', title: newLang === 'FR' ? 'Transport / Livr.' : 'Transport / Yëgël' }, |
| | { id: 'SEC_TECH', title: newLang === 'FR' ? 'Tech / Digital' : 'Tech / Digital' }, |
| | { id: 'SEC_AUTRE', title: newLang === 'FR' ? 'Autre secteur' : 'Beneen mbir' } |
| | ] |
| | }] |
| | ); |
| | return; |
| | } |
| |
|
| | |
| | |
| | const SECTOR_LABELS: Record<string, { fr: string; wo: string }> = { |
| | SEC_COMMERCE: { fr: 'Commerce / Vente', wo: 'Njaay' }, |
| | SEC_AGRI: { fr: 'Agriculture / Élevage', wo: 'Mbay' }, |
| | SEC_FOOD: { fr: 'Alimentation / Restauration', wo: 'Lekk / Restauration' }, |
| | SEC_TECH: { fr: 'Tech / Digital', wo: 'Tech / Digital' }, |
| | SEC_BEAUTE: { fr: 'Beauté / Bien-être', wo: 'Rafet' }, |
| | SEC_COUTURE: { fr: 'Couture / Mode', wo: 'Couture' }, |
| | SEC_TRANSPORT: { fr: 'Transport / Livraison', wo: 'Transport / Yëgël' }, |
| | }; |
| |
|
| | if (normalizedText === 'SEC_AUTRE') { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? 'Waaw ! Wax ma ban mbir ngay def ci ab kàddu gatt :' |
| | : 'Parfait ! Décris ton activité en quelques mots :' |
| | ); |
| | return; |
| | } |
| |
|
| | const sectorLabel = SECTOR_LABELS[normalizedText]; |
| |
|
| | |
| | const existingEnrollment = await prisma.enrollment.findFirst({ |
| | where: { userId: user.id, status: 'ACTIVE' } |
| | }); |
| |
|
| | if (existingEnrollment && (sectorLabel || normalizedText.startsWith('SEC_'))) { |
| | console.log(`[IMMUTABILITY] User ${user.id} tried to change sector but is already enrolled.`); |
| | return; |
| | } |
| |
|
| | if (!existingEnrollment && (sectorLabel || (!user.activity && text.length > 2))) { |
| | const activity = sectorLabel |
| | ? (user.language === 'WOLOF' ? sectorLabel.wo : sectorLabel.fr) |
| | : text.trim(); |
| |
|
| | user = await prisma.user.update({ |
| | where: { id: user.id }, |
| | data: { activity } |
| | }); |
| |
|
| | const welcomeMsg = user.language === 'FR' |
| | ? `Parfait ! Secteur noté : *${activity}*.\nJe t'inscris à ta formation personnalisée !` |
| | : `Baax na ! Bind nanu la ci: *${activity}*.\nLéegi dinanu la dugal ci njàng mi !`; |
| |
|
| | await scheduleMessage(user.id, welcomeMsg); |
| |
|
| | const trackId = user.language === 'FR' ? "T1-FR" : "T1-WO"; |
| | const defaultTrack = await prisma.track.findUnique({ where: { id: trackId } }); |
| | if (defaultTrack) await enrollUser(user.id, defaultTrack.id); |
| | return; |
| | } |
| |
|
| | |
| | const activeEnrollment = await prisma.enrollment.findFirst({ |
| | where: { userId: user.id, status: 'ACTIVE' }, |
| | include: { track: true } |
| | }); |
| |
|
| | if (activeEnrollment) { |
| | const intent = this.detectIntent(text); |
| | const isSuite = this.isFuzzyMatch(normalizedText, 'SUITE') || normalizedText === '2'; |
| | const isApprofondir = this.isFuzzyMatch(normalizedText, 'APPROFONDIR') || normalizedText === '1'; |
| |
|
| | |
| | if (isSuite) { |
| | const userProgress = await prisma.userProgress.findUnique({ |
| | where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } } |
| | }); |
| |
|
| | |
| | |
| | if (userProgress?.exerciseStatus !== 'COMPLETED') { |
| | console.log(`[SUITE-BLOCKED] User ${user.id} tried SUITE but status is ${userProgress?.exerciseStatus || 'null'}`); |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Dafa laaj nga tontu laaj bi ci kaw dëbb (audio walla texte) balaa nga dem ci kanam ! 🎙️" |
| | : "Tu dois d'abord répondre à l'exercice ci-dessus pour continuer ! 🎙️" |
| | ); |
| | return; |
| | } |
| |
|
| | console.log(`[SUITE-ALLOWED] User ${user.id} advancing from day ${activeEnrollment.currentDay}`); |
| | const nextDay = activeEnrollment.currentDay % 1 !== 0 |
| | ? Math.floor(activeEnrollment.currentDay) + 1 |
| | : activeEnrollment.currentDay + 1; |
| |
|
| | await prisma.enrollment.update({ where: { id: activeEnrollment.id }, data: { currentDay: nextDay } }); |
| | await prisma.userProgress.update({ |
| | where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } }, |
| | data: { |
| | exerciseStatus: 'PENDING', |
| | iterationCount: 0 |
| | } |
| | }); |
| | await whatsappQueue.add('send-content', { userId: user.id, trackId: activeEnrollment.trackId, dayNumber: nextDay }); |
| | return; |
| | } |
| |
|
| | |
| | if (isApprofondir) { |
| | const userProgress = await prisma.userProgress.findUnique({ |
| | where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } } |
| | }); |
| |
|
| | if (userProgress?.exerciseStatus === 'COMPLETED') { |
| | |
| | await prisma.userProgress.update({ |
| | where: { id: userProgress.id }, |
| | data: { exerciseStatus: 'PENDING_DEEPDIVE' } |
| | }); |
| |
|
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Baax na ! Wax ma ndox mi nga yor ci sa mbir (njëg, jafe-jafe, njëgëndal, njàngat, etc.) ngir ma gën a deesi njàngat bi :" |
| | : "Très bien ! Quelle information précise issue de ton terrain veux-tu ajouter ? (ex: un prix précis, un obstacle, un fournisseur, etc.) :" |
| | ); |
| | return; |
| | } else { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Dafa laaj nga tontu laaj bi ci kaw dëbb balaa nga natt nga tontu !" |
| | : "Réponds d'abord à l'exercice principal avant d'approfondir !" |
| | ); |
| | return; |
| | } |
| | } |
| |
|
| | |
| | if (intent === 'YES' && normalizedText.length < 15) { |
| | const userProgress = await prisma.userProgress.findUnique({ |
| | where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } } |
| | }); |
| | if (userProgress?.exerciseStatus === 'COMPLETED') { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' ? "Waaw ! Lexon bi mu ngi ñëw..." : "C'est parti ! Voici la suite..."); |
| | await whatsappQueue.add('send-content', { userId: user.id, trackId: activeEnrollment.trackId, dayNumber: activeEnrollment.currentDay + 1 }); |
| | return; |
| | } |
| | } |
| |
|
| | if (intent === 'NO' && normalizedText.length < 15) { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' ? "Baax na, bu la neexee nga tontu laaj bi." : "Pas de souci, tu peux répondre à l'exercice quand tu es prêt."); |
| | return; |
| | } |
| |
|
| | |
| | |
| | if (!user.activity) { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Danga wara tànn sa mbiru liggeey balaa ñuy tàmbali coaching bi." |
| | : "Tu dois d'abord définir ton activité avant que le coach AI ne puisse t'aider."); |
| | return; |
| | } |
| |
|
| | const pendingProgress = await prisma.userProgress.findFirst({ |
| | where: { userId: user.id, exerciseStatus: { in: ['PENDING', 'PENDING_REMEDIATION', 'PENDING_DEEPDIVE'] }, trackId: activeEnrollment.trackId }, |
| | }); |
| |
|
| | if (pendingProgress) { |
| | const isDeepDiveAction = pendingProgress.exerciseStatus === 'PENDING_DEEPDIVE'; |
| |
|
| | const trackDay = await prisma.trackDay.findFirst({ |
| | where: { trackId: activeEnrollment.trackId, dayNumber: activeEnrollment.currentDay } |
| | }); |
| |
|
| | if (trackDay) { |
| | await scheduleMessage(user.id, user.language === 'WOLOF' ? "⏳ Defar ak sa tontu..." : "⏳ Analyse de votre réponse..."); |
| |
|
| | |
| | let currentIterationCount = pendingProgress.iterationCount || 0; |
| | if (isDeepDiveAction) { |
| | currentIterationCount += 1; |
| | await prisma.userProgress.update({ |
| | where: { id: pendingProgress.id }, |
| | data: { iterationCount: currentIterationCount } |
| | }); |
| | } |
| |
|
| | |
| | await prisma.response.create({ |
| | data: { |
| | enrollmentId: activeEnrollment.id, |
| | userId: user.id, |
| | dayNumber: activeEnrollment.currentDay, |
| | content: text |
| | } |
| | }); |
| |
|
| | |
| | const previousResponsesData = await prisma.response.findMany({ |
| | where: { userId: user.id, enrollmentId: activeEnrollment.id }, |
| | orderBy: { dayNumber: 'asc' }, |
| | take: 5 |
| | }); |
| | const previousResponses = previousResponsesData.map(r => ({ day: r.dayNumber, response: r.content })); |
| |
|
| | await whatsappQueue.add('generate-feedback', { |
| | userId: user.id, text, trackId: activeEnrollment.trackId, trackDayId: trackDay.id, |
| | exercisePrompt: trackDay.exercisePrompt || '', lessonText: trackDay.lessonText || '', |
| | exerciseCriteria: trackDay.exerciseCriteria, pendingProgressId: pendingProgress.id, |
| | enrollmentId: activeEnrollment.id, currentDay: activeEnrollment.currentDay, |
| | totalDays: activeEnrollment.track.duration, language: user.language, |
| | |
| | userActivity: user.activity, |
| | userRegion: user.city, |
| | previousResponses, |
| | |
| | isDeepDive: isDeepDiveAction, |
| | iterationCount: currentIterationCount |
| | }, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } }); |
| | return; |
| | } |
| | } |
| |
|
| | |
| | console.log(`${traceId} User ${user.id} fallback daily response to day ${activeEnrollment.currentDay}`); |
| | await prisma.response.create({ |
| | data: { |
| | enrollmentId: activeEnrollment.id, |
| | userId: user.id, |
| | dayNumber: activeEnrollment.currentDay, |
| | content: text |
| | } |
| | }); |
| |
|
| | const trackDayFallback = await prisma.trackDay.findFirst({ |
| | where: { trackId: activeEnrollment.trackId, dayNumber: activeEnrollment.currentDay } |
| | }); |
| |
|
| | if (trackDayFallback) { |
| | |
| | const wordCount = text.trim().split(/\s+/).length; |
| | if (wordCount < 3 || text.length < 5) { |
| | console.log(`${traceId} Guardrail: Input too short or potential gibberish: "${text}"`); |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Ma déggul li nga wax mbir mi... Mën nga ko gën a firi ci ab kàddu gatt (3-4 kàddu) ?" |
| | : "Je n'ai pas bien compris ton activité. Peux-tu me réexpliquer en quelques mots ce que tu vends et à qui ?"); |
| | return; |
| | } |
| |
|
| | |
| | if (!user.activity || !user.language) { |
| | console.log(`${traceId} Blocking AI feedback: Enrollment incomplete for User ${user.id}`); |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Baax na, waaye laaj bi des na... Bindal 'INSCRIPTION' ngir tàmbali." |
| | : "C'est noté, mais il faut d'abord terminer ton inscription. Envoie 'INSCRIPTION' pour commencer."); |
| | return; |
| | } |
| |
|
| | |
| | const previousResponsesData = await prisma.response.findMany({ |
| | where: { userId: user.id, enrollmentId: activeEnrollment.id }, |
| | orderBy: { dayNumber: 'asc' }, |
| | take: 5 |
| | }); |
| | const previousResponses = previousResponsesData.map(r => ({ day: r.dayNumber, response: r.content })); |
| |
|
| | await whatsappQueue.add('generate-feedback', { |
| | userId: user.id, text, trackId: activeEnrollment.trackId, trackDayId: trackDayFallback.id, |
| | exercisePrompt: trackDayFallback.exercisePrompt || '', lessonText: trackDayFallback.lessonText || '', |
| | exerciseCriteria: trackDayFallback.exerciseCriteria, |
| | enrollmentId: activeEnrollment.id, currentDay: activeEnrollment.currentDay, |
| | totalDays: activeEnrollment.track.duration, language: user.language, |
| | |
| | userActivity: user.activity, |
| | userRegion: user.city, |
| | previousResponses |
| | }); |
| | return; |
| | } |
| |
|
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Baax na ! Yónnee *SUITE* ngir dem ci kanam walla tontul laaj bi ci kaw." |
| | : "✅ Message reçu ! Envoie *SUITE* pour avancer ou réponds à l'exercice ci-dessus." |
| | ); |
| | return; |
| | } |
| |
|
| | |
| | console.log(`${traceId} Unknown command from user ${user.id}: "${normalizedText}"`); |
| | await scheduleMessage(user.id, user.language === 'WOLOF' |
| | ? "Bañ ma dégg. Yónnee *INSCRIPTION* ngir tàmbalee ci kanam walla bind *SUITE*." |
| | : "Je n'ai pas compris. Envoie *INSCRIPTION* pour recommencer ou *SUITE* pour avancer." |
| | ); |
| | } |
| | } |
| |
|