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 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.0 🌟
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 (trackDay.exercisePrompt) {
175
  if (trackDay.exerciseType === 'BUTTON' && trackDay.buttonsJson) {
176
  const buttons = trackDay.buttonsJson as Array<{ id: string; title: string }>;
177
- await sendInteractiveButtonMessage(user.phone, trackDay.exercisePrompt, buttons);
178
  } else {
179
- await sendTextMessage(user.phone, trackDay.exercisePrompt);
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 : ÑAN ngay jàppalè, 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,7 +344,7 @@
344
  {
345
  "dayNumber": 11,
346
  "title": "Kooluté",
347
- "lessonText": "Ci businessu Senegaal, kooluté 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": {
 
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 liggeeyal keneen.",
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": "naka la kalkulé prix bi",
68
  "weight": 5
69
  }
70
  ],
@@ -128,7 +128,7 @@
128
  {
129
  "dayNumber": 6,
130
  "title": "Investir walla Faye",
131
- "lessonText": "Jënd machine ngir liggeey mu gën a gaaw mooy investir. Jënd portable bu bees ngir sa bopp, loolu faye la.",
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": "tiitaange, kooluté, mbégte walla plaisir",
22
  "weight": 5
23
  }
24
  ],
@@ -72,7 +72,7 @@
72
  "mustInclude": [
73
  {
74
  "id": "OBJECTION",
75
- "desc": "jafe-jafe (prix, jamono, kooluté)",
76
  "weight": 5
77
  }
78
  ],
@@ -146,7 +146,7 @@
146
  {
147
  "dayNumber": 6,
148
  "title": "Pitch 30s",
149
- "lessonText": "Naka ngay convaincre kiliifa bi ngay guiss ci taxi walla ci marché 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,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 concurrent dafa yomb' ?",
224
- "exercisePrompt": "Yónnee ma audio ngir convaincre ma ma jënd ci yaw :",
225
  "exerciseType": "AUDIO",
226
  "exerciseCriteria": {
227
  "version": "1.0",
@@ -236,7 +236,7 @@
236
  },
237
  {
238
  "id": "TRUST",
239
- "desc": "kooluté",
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": "Kooluté 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,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 xaalis bopp ak xaalisou 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": {
 
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 kooluté ne mën nga fay sa bor. Wone ko ne nga xam sa chiffre yi.",
111
- "exercisePrompt": "Yónnee ma audio ngir convaincre ma ma abal la xaalis :",
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 contentDir = path.resolve(__dirname, '../content/tracks');
 
11
 
12
- if (!fs.existsSync(contentDir)) {
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
- if (files.length === 0) {
19
- return { seeded: false, message: "No track files found in content/tracks." };
 
 
 
 
 
 
20
  }
21
 
22
- console.log(`[SEED] Found ${files.length} track files.`);
23
-
24
- for (const file of files) {
25
- try {
26
- const filePath = path.join(contentDir, file);
27
- const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
 
 
 
28
 
29
- if (!content.trackId || !Array.isArray(content.days)) {
30
- console.warn(`[SEED] Skipping invalid track file: ${file} (missing trackId or days)`);
31
- continue;
32
- }
33
 
34
- console.log(`[SEED] Syncing Track: ${content.title} (${content.language})...`);
 
 
35
 
36
- // 1. Upsert Track
37
- const track = await prisma.track.upsert({
38
- where: { id: content.trackId },
39
- update: {
40
- title: content.title,
41
- description: content.description || '',
42
- duration: Math.ceil(content.days.reduce((max: number, d: any) => Math.max(max, d.dayNumber), 0)),
43
- language: content.language,
44
- },
45
- create: {
46
- id: content.trackId,
47
- title: content.title,
48
- description: content.description || '',
49
- duration: Math.ceil(content.days.reduce((max: number, d: any) => Math.max(max, d.dayNumber), 0)),
50
- language: content.language,
51
- }
52
- });
53
 
54
- // 2. Upsert TrackDays
55
- for (const day of content.days) {
56
- const existing = await prisma.trackDay.findFirst({
57
- where: { trackId: track.id, dayNumber: day.dayNumber }
58
- });
 
 
 
 
 
 
 
 
59
 
60
- const dayData = {
61
- trackId: track.id,
62
- dayNumber: day.dayNumber,
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
- if (existing) {
74
- await prisma.trackDay.update({
75
- where: { id: existing.id },
76
- data: dayData
77
- });
78
- } else {
79
- await prisma.trackDay.create({
80
- data: dayData
81
- });
82
- }
83
- }
84
- } catch (fileErr: any) {
85
- console.error(`[SEED] Failed to sync file ${file}:`, fileErr.message);
 
86
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
 
 
 
 
 
 
88
 
89
- return { seeded: true, message: `✅ ${files.length} modules synchronized successfully.` };
 
 
 
 
 
 
 
 
 
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
  }