CognxSafeTrack commited on
Commit ·
b9727b3
1
Parent(s): 26c5d48
feat(wolof): standardized tracks T1-T5 with glossary v4.0 and implemented adaptive multi-sector architecture
Browse files- apps/api/src/services/ai/index.ts +23 -0
- apps/whatsapp-worker/src/index.ts +18 -2
- apps/whatsapp-worker/src/pedagogy.ts +13 -3
- apps/whatsapp-worker/src/scoring.ts +31 -0
- packages/database/content/modules/M-GESTION-STOCK-PARALLEL.json +42 -0
- packages/database/content/modules/M-RESTAURATION-WO.json +90 -0
- packages/database/content/tracks/T1-WO.json +2 -2
- packages/database/content/tracks/T2-WO.json +3 -3
- packages/database/content/tracks/T3-WO.json +6 -6
- packages/database/content/tracks/T4-WO.json +2 -2
- packages/database/content/tracks/T5-WO.json +2 -2
- packages/database/prisma/schema.prisma +1 -0
- packages/database/src/seed.ts +102 -67
apps/api/src/services/ai/index.ts
CHANGED
|
@@ -112,6 +112,18 @@ class AIService {
|
|
| 112 |
- ZÉRO ANGLAIS (NEVER USE ENGLISH).
|
| 113 |
- ZÉRO HALLUCINATION (ne pas inventer de faits non cités).
|
| 114 |
- CONTEXTE : Ne cite jamais "Manga Deaf".
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
`;
|
| 116 |
|
| 117 |
const schema = z.object({
|
|
@@ -152,6 +164,17 @@ class AIService {
|
|
| 152 |
|
| 153 |
LEÇON À RÉÉCRIRE :
|
| 154 |
${lessonText}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
`;
|
| 156 |
|
| 157 |
const result = await this.provider.generateStructuredData(prompt, PersonalizedLessonSchema);
|
|
|
|
| 112 |
- ZÉRO ANGLAIS (NEVER USE ENGLISH).
|
| 113 |
- ZÉRO HALLUCINATION (ne pas inventer de faits non cités).
|
| 114 |
- CONTEXTE : Ne cite jamais "Manga Deaf".
|
| 115 |
+
|
| 116 |
+
RÈGLES D'ADAPTATION SECTORIELLE :
|
| 117 |
+
- Si "RESTAURATION/TANGANA" : Focus sur l'hygiène (set-setal), la gestion des pertes (ñàkk) et la rapidité.
|
| 118 |
+
- Si "TEXTILE/COUTURE" : Focus sur la précision (natt), les finitions (bammeel) et la relation client.
|
| 119 |
+
- Si "AGRO-BUSINESS" : Focus sur la conservation (denc) et la transformation.
|
| 120 |
+
|
| 121 |
+
RÈGLES WOLOF (Version 4.0) :
|
| 122 |
+
- ñ : Utiliser systématiquement ñ (Waññi, Ñaata, Ñàkk).
|
| 123 |
+
- ë : Utiliser ë pour le son “eu” (Jënd, Dëgër, Téggin).
|
| 124 |
+
- é : Utiliser é pour son fermé (Liggéey).
|
| 125 |
+
- Termes : Utiliser 'Wollo' pour confiance, 'Ñàkk' pour perte (éviter Lakk), 'Liggéey' pour travail.
|
| 126 |
+
- FCFA : Toujours préciser FCFA dans les prix.
|
| 127 |
`;
|
| 128 |
|
| 129 |
const schema = z.object({
|
|
|
|
| 164 |
|
| 165 |
LEÇON À RÉÉCRIRE :
|
| 166 |
${lessonText}
|
| 167 |
+
|
| 168 |
+
RÈGLES D'ADAPTATION SECTORIELLE :
|
| 169 |
+
- Si "RESTAURATION/TANGANA" : Focus sur l'hygiène (set-setal), la gestion des pertes (ñàkk) et la rapidité.
|
| 170 |
+
- Si "TEXTILE/COUTURE" : Focus sur la précision (natt), les finitions (bammeel) et la relation client.
|
| 171 |
+
- Si "AGRO-BUSINESS" : Focus sur la conservation (denc) et la transformation.
|
| 172 |
+
|
| 173 |
+
RÈGLES WOLOF (Version 4.0) :
|
| 174 |
+
- ñ : Utiliser systématiquement ñ (Waññi, Ñaata, Ñàkk).
|
| 175 |
+
- ë : Utiliser ë pour le son “eu” (Jënd, Dëgër, Téggin).
|
| 176 |
+
- é : Utiliser é pour son fermé (Liggéey).
|
| 177 |
+
- Termes : Utiliser 'Wollo' pour confiance, 'Ñàkk' pour perte (éviter Lakk), 'Liggéey' pour travail.
|
| 178 |
`;
|
| 179 |
|
| 180 |
const result = await this.provider.generateStructuredData(prompt, PersonalizedLessonSchema);
|
apps/whatsapp-worker/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import dotenv from 'dotenv';
|
|
| 6 |
import { PrismaClient } from '@repo/database';
|
| 7 |
import { sendTextMessage, sendDocumentMessage, downloadMedia, sendInteractiveButtonMessage, sendInteractiveListMessage, sendImageMessage } from './whatsapp-cloud';
|
| 8 |
import { sendLessonDay } from './pedagogy';
|
|
|
|
| 9 |
import { getApiUrl, getAdminApiKey, validateEnvironment, isFeatureEnabled } from './config';
|
| 10 |
|
| 11 |
dotenv.config();
|
|
@@ -147,7 +148,7 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
|
|
| 147 |
}
|
| 148 |
}
|
| 149 |
|
| 150 |
-
// 🌟 Adaptive Pedagogy: Dynamic Remediation Logic v1.
|
| 151 |
let nextDay = currentDay + 1;
|
| 152 |
const currentProgress = await prisma.userProgress.findUnique({
|
| 153 |
where: { userId_trackId: { userId, trackId } }
|
|
@@ -156,6 +157,20 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
|
|
| 156 |
let updatedBadges = [...currentBadges];
|
| 157 |
|
| 158 |
if (feedbackData?.isQualified === false) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
const remediationDay = (exerciseCriteria as any)?.remediation?.dayNumber;
|
| 160 |
if (remediationDay && remediationDay !== currentDay) {
|
| 161 |
console.log(`[WORKER] Dynamic remediation triggered for User ${userId}: Day ${currentDay} -> ${remediationDay}`);
|
|
@@ -185,7 +200,8 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
|
|
| 185 |
data: {
|
| 186 |
exerciseStatus: 'COMPLETED',
|
| 187 |
score: { increment: 1 },
|
| 188 |
-
badges: updatedBadges
|
|
|
|
| 189 |
} as any
|
| 190 |
});
|
| 191 |
|
|
|
|
| 6 |
import { PrismaClient } from '@repo/database';
|
| 7 |
import { sendTextMessage, sendDocumentMessage, downloadMedia, sendInteractiveButtonMessage, sendInteractiveListMessage, sendImageMessage } from './whatsapp-cloud';
|
| 8 |
import { sendLessonDay } from './pedagogy';
|
| 9 |
+
import { updateBehavioralScore } from './scoring';
|
| 10 |
import { getApiUrl, getAdminApiKey, validateEnvironment, isFeatureEnabled } from './config';
|
| 11 |
|
| 12 |
dotenv.config();
|
|
|
|
| 148 |
}
|
| 149 |
}
|
| 150 |
|
| 151 |
+
// 🌟 Adaptive Pedagogy: Dynamic Remediation & Diagnostic Logic v1.1 🌟
|
| 152 |
let nextDay = currentDay + 1;
|
| 153 |
const currentProgress = await prisma.userProgress.findUnique({
|
| 154 |
where: { userId_trackId: { userId, trackId } }
|
|
|
|
| 157 |
let updatedBadges = [...currentBadges];
|
| 158 |
|
| 159 |
if (feedbackData?.isQualified === false) {
|
| 160 |
+
// Check for Adaptive Diagnostic Branching
|
| 161 |
+
const diagnosticTrigger = (exerciseCriteria as any)?.diagnostic?.trigger;
|
| 162 |
+
const adaptiveModuleId = (exerciseCriteria as any)?.diagnostic?.moduleId;
|
| 163 |
+
|
| 164 |
+
if (diagnosticTrigger && feedbackData?.missingElements?.includes(diagnosticTrigger) && adaptiveModuleId) {
|
| 165 |
+
console.log(`[WORKER] Adaptive Diagnostic triggered for User ${userId}: Re-routing to module ${adaptiveModuleId}`);
|
| 166 |
+
// 🚀 Redirect to specific module
|
| 167 |
+
nextDay = 1; // Modules start at day 1
|
| 168 |
+
await prisma.enrollment.updateMany({
|
| 169 |
+
where: { userId, status: 'ACTIVE' },
|
| 170 |
+
data: { trackId: adaptiveModuleId, currentDay: 1 }
|
| 171 |
+
});
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
const remediationDay = (exerciseCriteria as any)?.remediation?.dayNumber;
|
| 175 |
if (remediationDay && remediationDay !== currentDay) {
|
| 176 |
console.log(`[WORKER] Dynamic remediation triggered for User ${userId}: Day ${currentDay} -> ${remediationDay}`);
|
|
|
|
| 200 |
data: {
|
| 201 |
exerciseStatus: 'COMPLETED',
|
| 202 |
score: { increment: 1 },
|
| 203 |
+
badges: updatedBadges,
|
| 204 |
+
behavioralScoring: updateBehavioralScore((currentProgress as any)?.behavioralScoring, (exerciseCriteria as any)?.scoring?.impact_success)
|
| 205 |
} as any
|
| 206 |
});
|
| 207 |
|
apps/whatsapp-worker/src/pedagogy.ts
CHANGED
|
@@ -50,6 +50,16 @@ export async function sendLessonDay(userId: string, trackId: string, dayNumber:
|
|
| 50 |
}
|
| 51 |
|
| 52 |
let lessonText = trackDay.lessonText || '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
// 🌟 Personalize Lesson Content 🌟
|
| 55 |
if (user.activity && lessonText) {
|
|
@@ -171,12 +181,12 @@ export async function sendLessonDay(userId: string, trackId: string, dayNumber:
|
|
| 171 |
}
|
| 172 |
|
| 173 |
// 🌟 2. Send Interactive Exercise 🌟
|
| 174 |
-
if (
|
| 175 |
if (trackDay.exerciseType === 'BUTTON' && trackDay.buttonsJson) {
|
| 176 |
const buttons = trackDay.buttonsJson as Array<{ id: string; title: string }>;
|
| 177 |
-
await sendInteractiveButtonMessage(user.phone,
|
| 178 |
} else {
|
| 179 |
-
await sendTextMessage(user.phone,
|
| 180 |
}
|
| 181 |
}
|
| 182 |
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
let lessonText = trackDay.lessonText || '';
|
| 53 |
+
let exercisePrompt = trackDay.exercisePrompt || '';
|
| 54 |
+
|
| 55 |
+
// 🌍 Parallel Multi-Lang Support v1.0 🌍
|
| 56 |
+
if ((trackDay as any).buttonsJson && (trackDay as any).buttonsJson.content) {
|
| 57 |
+
const langContent = (trackDay as any).buttonsJson.content[user.language];
|
| 58 |
+
if (langContent) {
|
| 59 |
+
lessonText = langContent.lessonText || lessonText;
|
| 60 |
+
exercisePrompt = langContent.exercisePrompt || exercisePrompt;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
|
| 64 |
// 🌟 Personalize Lesson Content 🌟
|
| 65 |
if (user.activity && lessonText) {
|
|
|
|
| 181 |
}
|
| 182 |
|
| 183 |
// 🌟 2. Send Interactive Exercise 🌟
|
| 184 |
+
if (exercisePrompt) {
|
| 185 |
if (trackDay.exerciseType === 'BUTTON' && trackDay.buttonsJson) {
|
| 186 |
const buttons = trackDay.buttonsJson as Array<{ id: string; title: string }>;
|
| 187 |
+
await sendInteractiveButtonMessage(user.phone, exercisePrompt, buttons);
|
| 188 |
} else {
|
| 189 |
+
await sendTextMessage(user.phone, exercisePrompt);
|
| 190 |
}
|
| 191 |
}
|
| 192 |
|
apps/whatsapp-worker/src/scoring.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type BehavioralScoring = {
|
| 2 |
+
discipline_financiere: number;
|
| 3 |
+
organisation: number;
|
| 4 |
+
relation_client: number;
|
| 5 |
+
risque_management: number;
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
export function updateBehavioralScore(current: BehavioralScoring | null, impact: Partial<BehavioralScoring> | undefined): BehavioralScoring {
|
| 9 |
+
const base: BehavioralScoring = current || {
|
| 10 |
+
discipline_financiere: 50,
|
| 11 |
+
organisation: 50,
|
| 12 |
+
relation_client: 50,
|
| 13 |
+
risque_management: 50
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
if (!impact) return base;
|
| 17 |
+
|
| 18 |
+
return {
|
| 19 |
+
discipline_financiere: Math.min(100, Math.max(0, base.discipline_financiere + (impact.discipline_financiere || 0))),
|
| 20 |
+
organisation: Math.min(100, Math.max(0, base.organisation + (impact.organisation || 0))),
|
| 21 |
+
relation_client: Math.min(100, Math.max(0, base.relation_client + (impact.relation_client || 0))),
|
| 22 |
+
risque_management: Math.min(100, Math.max(0, base.risque_management + (impact.risque_management || 0)))
|
| 23 |
+
};
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export function getLevel(score: number): string {
|
| 27 |
+
if (score <= 40) return "Informel instable";
|
| 28 |
+
if (score <= 60) return "Structuration début";
|
| 29 |
+
if (score <= 80) return "Business organisé";
|
| 30 |
+
return "Entrepreneur avancé";
|
| 31 |
+
}
|
packages/database/content/modules/M-GESTION-STOCK-PARALLEL.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"meta": {
|
| 3 |
+
"id": "M-GESTION-STOCK-PARALLEL",
|
| 4 |
+
"version": "1.0",
|
| 5 |
+
"secteur": "all"
|
| 6 |
+
},
|
| 7 |
+
"content": {
|
| 8 |
+
"FR": {
|
| 9 |
+
"title": "Maîtrise des Stocks",
|
| 10 |
+
"lessonText": "Gérer son stock, c'est comme garder son argent. Si le produit pourrit, l'argent s'en va.",
|
| 11 |
+
"exercisePrompt": "Citez deux produits que vous perdez souvent :"
|
| 12 |
+
},
|
| 13 |
+
"WOLOF": {
|
| 14 |
+
"title": "Saytou sa Mbir",
|
| 15 |
+
"lessonText": "Samm sa stock, mooy samm sa xaalis. Su product bi yàquwee, sa xaalis mooy sotti (Ñàkk).",
|
| 16 |
+
"exercisePrompt": "Limal ma ñaari mbir yu la jafe dence :"
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
"rules_engine": {
|
| 20 |
+
"conditions": [
|
| 21 |
+
{
|
| 22 |
+
"if": {
|
| 23 |
+
"risque_detecte": "Ñàkk"
|
| 24 |
+
},
|
| 25 |
+
"then": {
|
| 26 |
+
"priorite": "haute",
|
| 27 |
+
"impact": {
|
| 28 |
+
"organisation": -5
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
"scoring": {
|
| 35 |
+
"impact_success": {
|
| 36 |
+
"discipline_financiere": 10
|
| 37 |
+
},
|
| 38 |
+
"impact_fail": {
|
| 39 |
+
"organisation": -2
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
}
|
packages/database/content/modules/M-RESTAURATION-WO.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"meta": {
|
| 3 |
+
"id": "M-RESTAURATION-WO",
|
| 4 |
+
"version": "1.0",
|
| 5 |
+
"langue": "wolof",
|
| 6 |
+
"secteur": "restauration",
|
| 7 |
+
"niveau_business": 2
|
| 8 |
+
},
|
| 9 |
+
"modules": [
|
| 10 |
+
{
|
| 11 |
+
"id": "GESTION_STOCK_PERISSABLE",
|
| 12 |
+
"focus_vocab": [
|
| 13 |
+
"Denc",
|
| 14 |
+
"Ñàkk",
|
| 15 |
+
"Rot"
|
| 16 |
+
],
|
| 17 |
+
"scenario": {
|
| 18 |
+
"contexte": "Gestion des ingrédients (oignons, huile, viande) dans un Tangana.",
|
| 19 |
+
"lessonText": "Baax na ! Ci sa liggéeyu Tangana, li gën a jafe mooy samm sa stock. Su sooble bi walla yàpp bi yàquwee, sa xaalis mooy sotti. Loolu lañuy woowe Ñàkk.",
|
| 20 |
+
"dialogue": [
|
| 21 |
+
{
|
| 22 |
+
"ia": "Ñata sooble nga jënd tey ?",
|
| 23 |
+
"user": "5 Kg."
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"ia": "Te ñata lañu dooré ci biir waañ ?",
|
| 27 |
+
"user": "4 Kg."
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"ia": "Kon am nga 1 Kg bu des. Naka nga koy dence suba ?"
|
| 31 |
+
}
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
"exercice": {
|
| 35 |
+
"type": "AUDIO",
|
| 36 |
+
"question": "Nettoolil ma naka nga dence sa yàpp suba sa tel ngir mu bañ a yàqu ?",
|
| 37 |
+
"criteria": {
|
| 38 |
+
"mustInclude": [
|
| 39 |
+
{
|
| 40 |
+
"id": "METHOD",
|
| 41 |
+
"desc": "glacière / frigo / denc bu leer",
|
| 42 |
+
"weight": 5
|
| 43 |
+
}
|
| 44 |
+
],
|
| 45 |
+
"threshold": {
|
| 46 |
+
"minScore": 5,
|
| 47 |
+
"minMustPass": 1
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"id": "CALCUL_MARGE_PLAT",
|
| 54 |
+
"focus_vocab": [
|
| 55 |
+
"Coût de revient",
|
| 56 |
+
"Bénéfice",
|
| 57 |
+
"Marge"
|
| 58 |
+
],
|
| 59 |
+
"scenario": {
|
| 60 |
+
"contexte": "Calculer le bénéfice sur un plat de Thieb.",
|
| 61 |
+
"lessonText": "Léegi, nattal sa bénéfice. Su sa cinu Thieb jaralë la 10.000 FCFA te am nga 20 plat, ñata nga koy jaay plat bu ne ?"
|
| 62 |
+
},
|
| 63 |
+
"exercice": {
|
| 64 |
+
"type": "TEXT",
|
| 65 |
+
"question": "Bindal ma sa prix plat :",
|
| 66 |
+
"criteria": {
|
| 67 |
+
"mustInclude": [
|
| 68 |
+
{
|
| 69 |
+
"id": "PRICE",
|
| 70 |
+
"desc": "montant > 500 FCFA",
|
| 71 |
+
"weight": 5
|
| 72 |
+
}
|
| 73 |
+
],
|
| 74 |
+
"threshold": {
|
| 75 |
+
"minScore": 5,
|
| 76 |
+
"minMustPass": 1
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
],
|
| 82 |
+
"simulation": {
|
| 83 |
+
"id": "SIM-RENTABILITE-TANGANA",
|
| 84 |
+
"params": {
|
| 85 |
+
"loyer_place": 2500,
|
| 86 |
+
"gaz_jour": 1500,
|
| 87 |
+
"matiere_moyenne_plat": 600
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
packages/database/content/tracks/T1-WO.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
| 7 |
{
|
| 8 |
"dayNumber": 1,
|
| 9 |
"title": "Kan, Lan, Naka",
|
| 10 |
-
"lessonText": "Dalal jàmm ci XAMLÉ ! Tey danuy tàmbale lu yomb. Ñu bari dañuy wax : 'damay def commerce'. Waaye loolu leeralul sa kiliifa dara. Wax ma bu leer :
|
| 11 |
"exercisePrompt": "Yónnee ma sa phrase (ex: Damay jaay cere biir quartier bi) :",
|
| 12 |
"exerciseType": "AUDIO",
|
| 13 |
"exerciseCriteria": {
|
|
@@ -344,7 +344,7 @@
|
|
| 344 |
{
|
| 345 |
"dayNumber": 11,
|
| 346 |
"title": "Kooluté",
|
| 347 |
-
"lessonText": "Ci businessu Senegaal,
|
| 348 |
"exercisePrompt": "Lan ngay def ngir ñu gën laa woolu ?",
|
| 349 |
"exerciseType": "TEXT",
|
| 350 |
"exerciseCriteria": {
|
|
|
|
| 7 |
{
|
| 8 |
"dayNumber": 1,
|
| 9 |
"title": "Kan, Lan, Naka",
|
| 10 |
+
"lessonText": "Dalal jàmm ci XAMLÉ ! Tey danuy tàmbale lu yomb. Ñu bari dañuy wax : 'damay def commerce'. Waaye loolu leeralul sa kiliifa dara. Wax ma bu leer : Ñan ngay jàppale, Lan ngay def, te Naka ngay amé xaalis ?",
|
| 11 |
"exercisePrompt": "Yónnee ma sa phrase (ex: Damay jaay cere biir quartier bi) :",
|
| 12 |
"exerciseType": "AUDIO",
|
| 13 |
"exerciseCriteria": {
|
|
|
|
| 344 |
{
|
| 345 |
"dayNumber": 11,
|
| 346 |
"title": "Kooluté",
|
| 347 |
+
"lessonText": "Ci businessu Senegaal, woolu mooy lépp. Naka ngay def ngir kiliifa bi dëgërël sa wax ?",
|
| 348 |
"exercisePrompt": "Lan ngay def ngir ñu gën laa woolu ?",
|
| 349 |
"exerciseType": "TEXT",
|
| 350 |
"exerciseCriteria": {
|
packages/database/content/tracks/T2-WO.json
CHANGED
|
@@ -53,7 +53,7 @@
|
|
| 53 |
{
|
| 54 |
"dayNumber": 3,
|
| 55 |
"title": "Prix bu dëggu",
|
| 56 |
-
"lessonText": "Sa prix = Li nga faye + Sa njariñ (marge). Su nga fatte li nga faye ci transport walla courant, dangay
|
| 57 |
"exercisePrompt": "Naka nga faye sa prix tey ?",
|
| 58 |
"exerciseType": "TEXT",
|
| 59 |
"exerciseCriteria": {
|
|
@@ -64,7 +64,7 @@
|
|
| 64 |
"mustInclude": [
|
| 65 |
{
|
| 66 |
"id": "METHOD",
|
| 67 |
-
"desc": "
|
| 68 |
"weight": 5
|
| 69 |
}
|
| 70 |
],
|
|
@@ -128,7 +128,7 @@
|
|
| 128 |
{
|
| 129 |
"dayNumber": 6,
|
| 130 |
"title": "Investir walla Faye",
|
| 131 |
-
"lessonText": "Jënd
|
| 132 |
"exercisePrompt": "Lan mooy sa prochain gros investissement ci sa business ?",
|
| 133 |
"exerciseType": "TEXT",
|
| 134 |
"exerciseCriteria": {
|
|
|
|
| 53 |
{
|
| 54 |
"dayNumber": 3,
|
| 55 |
"title": "Prix bu dëggu",
|
| 56 |
+
"lessonText": "Sa prix = Li nga faye + Sa njariñ (marge). Su nga fatte li nga faye ci transport walla courant, dangay liggéeyal keneen.",
|
| 57 |
"exercisePrompt": "Naka nga faye sa prix tey ?",
|
| 58 |
"exerciseType": "TEXT",
|
| 59 |
"exerciseCriteria": {
|
|
|
|
| 64 |
"mustInclude": [
|
| 65 |
{
|
| 66 |
"id": "METHOD",
|
| 67 |
+
"desc": "nu la limé prix bi",
|
| 68 |
"weight": 5
|
| 69 |
}
|
| 70 |
],
|
|
|
|
| 128 |
{
|
| 129 |
"dayNumber": 6,
|
| 130 |
"title": "Investir walla Faye",
|
| 131 |
+
"lessonText": "Jënd masin ngir liggéey mu gën a gaaw mooy investir. Jënd portabel bu bees ngir sa bopp, loolu faye la.",
|
| 132 |
"exercisePrompt": "Lan mooy sa prochain gros investissement ci sa business ?",
|
| 133 |
"exerciseType": "TEXT",
|
| 134 |
"exerciseCriteria": {
|
packages/database/content/tracks/T3-WO.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
| 18 |
"mustInclude": [
|
| 19 |
{
|
| 20 |
"id": "EMOTION",
|
| 21 |
-
"desc": "
|
| 22 |
"weight": 5
|
| 23 |
}
|
| 24 |
],
|
|
@@ -72,7 +72,7 @@
|
|
| 72 |
"mustInclude": [
|
| 73 |
{
|
| 74 |
"id": "OBJECTION",
|
| 75 |
-
"desc": "jafe-jafe (prix, jamono,
|
| 76 |
"weight": 5
|
| 77 |
}
|
| 78 |
],
|
|
@@ -146,7 +146,7 @@
|
|
| 146 |
{
|
| 147 |
"dayNumber": 6,
|
| 148 |
"title": "Pitch 30s",
|
| 149 |
-
"lessonText": "Naka ngay
|
| 150 |
"exercisePrompt": "Yónnee ma audio sa pitch bu gàtt ci 30 seconde ngir jaay sa mbir :",
|
| 151 |
"exerciseType": "AUDIO",
|
| 152 |
"exerciseCriteria": {
|
|
@@ -220,8 +220,8 @@
|
|
| 220 |
{
|
| 221 |
"dayNumber": 8,
|
| 222 |
"title": "Natt bi",
|
| 223 |
-
"lessonText": "Natt bi tollu na : Naka ngay tontu kiliifa bu la wax 'Sa
|
| 224 |
-
"exercisePrompt": "Yónnee ma audio ngir
|
| 225 |
"exerciseType": "AUDIO",
|
| 226 |
"exerciseCriteria": {
|
| 227 |
"version": "1.0",
|
|
@@ -236,7 +236,7 @@
|
|
| 236 |
},
|
| 237 |
{
|
| 238 |
"id": "TRUST",
|
| 239 |
-
"desc": "
|
| 240 |
"weight": 5
|
| 241 |
}
|
| 242 |
],
|
|
|
|
| 18 |
"mustInclude": [
|
| 19 |
{
|
| 20 |
"id": "EMOTION",
|
| 21 |
+
"desc": "titaange, woolu, mbégte walla plaisir",
|
| 22 |
"weight": 5
|
| 23 |
}
|
| 24 |
],
|
|
|
|
| 72 |
"mustInclude": [
|
| 73 |
{
|
| 74 |
"id": "OBJECTION",
|
| 75 |
+
"desc": "jafe-jafe (prix, jamono, woolu)",
|
| 76 |
"weight": 5
|
| 77 |
}
|
| 78 |
],
|
|
|
|
| 146 |
{
|
| 147 |
"dayNumber": 6,
|
| 148 |
"title": "Pitch 30s",
|
| 149 |
+
"lessonText": "Naka ngay wóoral kiliifa bi ngay guiss ci taxi walla ci marche bi ci 30 seconde ?",
|
| 150 |
"exercisePrompt": "Yónnee ma audio sa pitch bu gàtt ci 30 seconde ngir jaay sa mbir :",
|
| 151 |
"exerciseType": "AUDIO",
|
| 152 |
"exerciseCriteria": {
|
|
|
|
| 220 |
{
|
| 221 |
"dayNumber": 8,
|
| 222 |
"title": "Natt bi",
|
| 223 |
+
"lessonText": "Natt bi tollu na : Naka ngay tontu kiliifa bu la wax 'Sa moroom dafa yomb' ?",
|
| 224 |
+
"exercisePrompt": "Yónnee ma audio ngir wóoral ma ma jënd ci yaw :",
|
| 225 |
"exerciseType": "AUDIO",
|
| 226 |
"exerciseCriteria": {
|
| 227 |
"version": "1.0",
|
|
|
|
| 236 |
},
|
| 237 |
{
|
| 238 |
"id": "TRUST",
|
| 239 |
+
"desc": "woolu",
|
| 240 |
"weight": 5
|
| 241 |
}
|
| 242 |
],
|
packages/database/content/tracks/T4-WO.json
CHANGED
|
@@ -56,7 +56,7 @@
|
|
| 56 |
},
|
| 57 |
{
|
| 58 |
"dayNumber": 3,
|
| 59 |
-
"title": "
|
| 60 |
"lessonText": "Banque yi ak fournisseur yu mag yi, papier lañuy laaj. Su nga amul papier, daggay des commerçant bu ndaw rekk.",
|
| 61 |
"exercisePrompt": "Baax na, yan opportunité nga mas a ñàkk ndax sa mbir bindoowul ?",
|
| 62 |
"exerciseType": "TEXT",
|
|
@@ -128,7 +128,7 @@
|
|
| 128 |
{
|
| 129 |
"dayNumber": 6,
|
| 130 |
"title": "Cahier de Cash",
|
| 131 |
-
"lessonText": "Bindal lépp li duggu ak li génn. Bul jaxasse sa
|
| 132 |
"exercisePrompt": "Su nga bëggoon a séparer sa deux comptes suba, lan mooy jafe jafe bi ?",
|
| 133 |
"exerciseType": "TEXT",
|
| 134 |
"exerciseCriteria": {
|
|
|
|
| 56 |
},
|
| 57 |
{
|
| 58 |
"dayNumber": 3,
|
| 59 |
+
"title": "Wollo bi",
|
| 60 |
"lessonText": "Banque yi ak fournisseur yu mag yi, papier lañuy laaj. Su nga amul papier, daggay des commerçant bu ndaw rekk.",
|
| 61 |
"exercisePrompt": "Baax na, yan opportunité nga mas a ñàkk ndax sa mbir bindoowul ?",
|
| 62 |
"exerciseType": "TEXT",
|
|
|
|
| 128 |
{
|
| 129 |
"dayNumber": 6,
|
| 130 |
"title": "Cahier de Cash",
|
| 131 |
+
"lessonText": "Bindal lépp li duggu ak li génn. Bul jaxasse sa xaalissu bopp ak xaalissu business bi.",
|
| 132 |
"exercisePrompt": "Su nga bëggoon a séparer sa deux comptes suba, lan mooy jafe jafe bi ?",
|
| 133 |
"exerciseType": "TEXT",
|
| 134 |
"exerciseCriteria": {
|
packages/database/content/tracks/T5-WO.json
CHANGED
|
@@ -107,8 +107,8 @@
|
|
| 107 |
{
|
| 108 |
"dayNumber": 5,
|
| 109 |
"title": "Pitch Financeur",
|
| 110 |
-
"lessonText": "Doo jaay banquier bi sa produit, dangay jaay
|
| 111 |
-
"exercisePrompt": "Yónnee ma audio ngir
|
| 112 |
"exerciseType": "AUDIO",
|
| 113 |
"exerciseCriteria": {
|
| 114 |
"version": "1.0",
|
|
|
|
| 107 |
{
|
| 108 |
"dayNumber": 5,
|
| 109 |
"title": "Pitch Financeur",
|
| 110 |
+
"lessonText": "Doo jaay banquier bi sa produit, dangay jaay woolu ne mën nga fay sa bor. Wone ko ne nga xam sa chiffre yi.",
|
| 111 |
+
"exercisePrompt": "Yónnee ma audio ngir wóoral ma ma abal la xaalis :",
|
| 112 |
"exerciseType": "AUDIO",
|
| 113 |
"exerciseCriteria": {
|
| 114 |
"version": "1.0",
|
packages/database/prisma/schema.prisma
CHANGED
|
@@ -97,6 +97,7 @@ model UserProgress {
|
|
| 97 |
lastInteraction DateTime @default(now())
|
| 98 |
exerciseStatus ExerciseStatus @default(PENDING)
|
| 99 |
badges Json? // Array of strings: ["CLARTE", "CONFIANCE"]
|
|
|
|
| 100 |
createdAt DateTime @default(now())
|
| 101 |
updatedAt DateTime @updatedAt
|
| 102 |
|
|
|
|
| 97 |
lastInteraction DateTime @default(now())
|
| 98 |
exerciseStatus ExerciseStatus @default(PENDING)
|
| 99 |
badges Json? // Array of strings: ["CLARTE", "CONFIANCE"]
|
| 100 |
+
behavioralScoring Json? // { discipline_financiere: 0, organisation: 0, ... }
|
| 101 |
createdAt DateTime @default(now())
|
| 102 |
updatedAt DateTime @updatedAt
|
| 103 |
|
packages/database/src/seed.ts
CHANGED
|
@@ -7,84 +7,119 @@ import path from 'path';
|
|
| 7 |
* Safe to call multiple times — checks for existing data before inserting/updating.
|
| 8 |
*/
|
| 9 |
export async function seedDatabase(prisma: PrismaClient): Promise<{ seeded: boolean; message: string }> {
|
| 10 |
-
const
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
return { seeded: false, message: `Content directory not found: ${contentDir}` };
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
const files = fs.readdirSync(contentDir).filter(f => f.endsWith('.json'));
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
continue;
|
| 32 |
-
}
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
});
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
title: day.title || `Jour ${day.dayNumber}`,
|
| 64 |
-
lessonText: day.lessonText,
|
| 65 |
-
exercisePrompt: day.exercisePrompt,
|
| 66 |
-
exerciseType: day.exerciseType || 'TEXT',
|
| 67 |
-
exerciseCriteria: day.exerciseCriteria || null,
|
| 68 |
-
badges: day.badges || null,
|
| 69 |
-
audioUrl: day.audioUrl || null,
|
| 70 |
-
buttonsJson: day.buttonsJson || null,
|
| 71 |
-
};
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
|
|
|
|
| 86 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
|
|
|
| 7 |
* Safe to call multiple times — checks for existing data before inserting/updating.
|
| 8 |
*/
|
| 9 |
export async function seedDatabase(prisma: PrismaClient): Promise<{ seeded: boolean; message: string }> {
|
| 10 |
+
const tracksDir = path.resolve(__dirname, '../content/tracks');
|
| 11 |
+
const modulesDir = path.resolve(__dirname, '../content/modules');
|
| 12 |
|
| 13 |
+
let totalSynced = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
// 1. Sync regular tracks
|
| 16 |
+
if (fs.existsSync(tracksDir)) {
|
| 17 |
+
const trackFiles = fs.readdirSync(tracksDir).filter(f => f.endsWith('.json'));
|
| 18 |
+
console.log(`[SEED] Found ${trackFiles.length} track files.`);
|
| 19 |
+
for (const file of trackFiles) {
|
| 20 |
+
await syncTrackFile(prisma, path.join(tracksDir, file));
|
| 21 |
+
totalSynced++;
|
| 22 |
+
}
|
| 23 |
}
|
| 24 |
|
| 25 |
+
// 2. Sync modular adaptive content
|
| 26 |
+
if (fs.existsSync(modulesDir)) {
|
| 27 |
+
const moduleFiles = fs.readdirSync(modulesDir).filter(f => f.endsWith('.json'));
|
| 28 |
+
console.log(`[SEED] Found ${moduleFiles.length} modular files.`);
|
| 29 |
+
for (const file of moduleFiles) {
|
| 30 |
+
await syncModuleFile(prisma, path.join(modulesDir, file));
|
| 31 |
+
totalSynced++;
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
|
| 35 |
+
return { seeded: true, message: `✅ ${totalSynced} files synchronized successfully.` };
|
| 36 |
+
}
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
async function syncTrackFile(prisma: PrismaClient, filePath: string) {
|
| 39 |
+
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
| 40 |
+
if (!content.trackId || !Array.isArray(content.days)) return;
|
| 41 |
|
| 42 |
+
const track = await prisma.track.upsert({
|
| 43 |
+
where: { id: content.trackId },
|
| 44 |
+
update: {
|
| 45 |
+
title: content.title,
|
| 46 |
+
description: content.description || '',
|
| 47 |
+
duration: Math.ceil(content.days.reduce((max: number, d: any) => Math.max(max, d.dayNumber), 0)),
|
| 48 |
+
language: content.language,
|
| 49 |
+
},
|
| 50 |
+
create: {
|
| 51 |
+
id: content.trackId,
|
| 52 |
+
title: content.title,
|
| 53 |
+
description: content.description || '',
|
| 54 |
+
duration: Math.ceil(content.days.reduce((max: number, d: any) => Math.max(max, d.dayNumber), 0)),
|
| 55 |
+
language: content.language,
|
| 56 |
+
}
|
| 57 |
+
});
|
|
|
|
| 58 |
|
| 59 |
+
for (const day of content.days) {
|
| 60 |
+
await upsertTrackDay(prisma, track.id, day.dayNumber, {
|
| 61 |
+
title: day.title || `Jour ${day.dayNumber}`,
|
| 62 |
+
lessonText: day.lessonText,
|
| 63 |
+
exercisePrompt: day.exercisePrompt,
|
| 64 |
+
exerciseType: day.exerciseType || 'TEXT',
|
| 65 |
+
exerciseCriteria: day.exerciseCriteria || null,
|
| 66 |
+
badges: day.badges || null,
|
| 67 |
+
audioUrl: day.audioUrl || null,
|
| 68 |
+
buttonsJson: day.buttonsJson || null,
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
|
| 73 |
+
async function syncModuleFile(prisma: PrismaClient, filePath: string) {
|
| 74 |
+
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
| 75 |
+
if (!content.meta?.id || !Array.isArray(content.modules)) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
const track = await prisma.track.upsert({
|
| 78 |
+
where: { id: content.meta.id },
|
| 79 |
+
update: {
|
| 80 |
+
title: `Module: ${content.meta.secteur}`,
|
| 81 |
+
description: `Adaptive module for ${content.meta.secteur}`,
|
| 82 |
+
duration: content.modules.length,
|
| 83 |
+
language: content.meta.langue.toUpperCase() as any,
|
| 84 |
+
},
|
| 85 |
+
create: {
|
| 86 |
+
id: content.meta.id,
|
| 87 |
+
title: `Module: ${content.meta.secteur}`,
|
| 88 |
+
description: `Adaptive module for ${content.meta.secteur}`,
|
| 89 |
+
duration: content.modules.length,
|
| 90 |
+
language: content.meta.langue.toUpperCase() as any,
|
| 91 |
}
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
for (let i = 0; i < content.modules.length; i++) {
|
| 95 |
+
const mod = content.modules[i];
|
| 96 |
+
const dayNumber = i + 1; // Modules are mapped to 1, 2, 3...
|
| 97 |
+
await upsertTrackDay(prisma, track.id, dayNumber, {
|
| 98 |
+
title: mod.id,
|
| 99 |
+
lessonText: mod.scenario.lessonText,
|
| 100 |
+
exercisePrompt: mod.exercice.question,
|
| 101 |
+
exerciseType: mod.exercice.type || 'TEXT',
|
| 102 |
+
exerciseCriteria: mod.exercice.criteria || null,
|
| 103 |
+
badges: null,
|
| 104 |
+
audioUrl: null,
|
| 105 |
+
buttonsJson: null,
|
| 106 |
+
});
|
| 107 |
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
async function upsertTrackDay(prisma: PrismaClient, trackId: string, dayNumber: number, data: any) {
|
| 111 |
+
const existing = await prisma.trackDay.findFirst({
|
| 112 |
+
where: { trackId, dayNumber }
|
| 113 |
+
});
|
| 114 |
|
| 115 |
+
if (existing) {
|
| 116 |
+
await prisma.trackDay.update({
|
| 117 |
+
where: { id: existing.id },
|
| 118 |
+
data
|
| 119 |
+
});
|
| 120 |
+
} else {
|
| 121 |
+
await prisma.trackDay.create({
|
| 122 |
+
data: { ...data, trackId, dayNumber }
|
| 123 |
+
});
|
| 124 |
+
}
|
| 125 |
}
|