@contractspec/module.learning-journey 3.7.16 → 3.7.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/contracts/index.js +1 -578
- package/dist/browser/contracts/models.js +1 -193
- package/dist/browser/contracts/onboarding.js +1 -417
- package/dist/browser/contracts/operations.js +1 -326
- package/dist/browser/contracts/shared.js +1 -5
- package/dist/browser/docs/index.js +7 -51
- package/dist/browser/docs/learning-journey.docblock.js +7 -51
- package/dist/browser/engines/index.js +1 -675
- package/dist/browser/engines/srs.js +1 -198
- package/dist/browser/engines/streak.js +1 -159
- package/dist/browser/engines/xp.js +1 -320
- package/dist/browser/entities/ai.js +1 -343
- package/dist/browser/entities/course.js +1 -276
- package/dist/browser/entities/flashcard.js +1 -222
- package/dist/browser/entities/gamification.js +1 -340
- package/dist/browser/entities/index.js +1 -2140
- package/dist/browser/entities/learner.js +1 -333
- package/dist/browser/entities/onboarding.js +1 -301
- package/dist/browser/entities/quiz.js +1 -304
- package/dist/browser/events.js +1 -423
- package/dist/browser/i18n/catalogs/en.js +1 -43
- package/dist/browser/i18n/catalogs/es.js +1 -43
- package/dist/browser/i18n/catalogs/fr.js +1 -43
- package/dist/browser/i18n/catalogs/index.js +1 -127
- package/dist/browser/i18n/index.js +1 -169
- package/dist/browser/i18n/keys.js +1 -16
- package/dist/browser/i18n/locale.js +1 -13
- package/dist/browser/i18n/messages.js +1 -139
- package/dist/browser/index.js +7 -3914
- package/dist/browser/learning-journey.capability.js +1 -43
- package/dist/browser/learning-journey.feature.js +1 -56
- package/dist/contracts/index.js +1 -578
- package/dist/contracts/models.js +1 -193
- package/dist/contracts/onboarding.js +1 -417
- package/dist/contracts/operations.js +1 -326
- package/dist/contracts/shared.js +1 -5
- package/dist/docs/index.js +7 -51
- package/dist/docs/learning-journey.docblock.js +7 -51
- package/dist/engines/index.js +1 -675
- package/dist/engines/srs.js +1 -198
- package/dist/engines/streak.js +1 -159
- package/dist/engines/xp.js +1 -320
- package/dist/entities/ai.js +1 -343
- package/dist/entities/course.js +1 -276
- package/dist/entities/flashcard.js +1 -222
- package/dist/entities/gamification.js +1 -340
- package/dist/entities/index.js +1 -2140
- package/dist/entities/learner.js +1 -333
- package/dist/entities/onboarding.js +1 -301
- package/dist/entities/quiz.js +1 -304
- package/dist/events.js +1 -423
- package/dist/i18n/catalogs/en.js +1 -43
- package/dist/i18n/catalogs/es.js +1 -43
- package/dist/i18n/catalogs/fr.js +1 -43
- package/dist/i18n/catalogs/index.js +1 -127
- package/dist/i18n/index.js +1 -169
- package/dist/i18n/keys.js +1 -16
- package/dist/i18n/locale.js +1 -13
- package/dist/i18n/messages.js +1 -139
- package/dist/index.js +7 -3914
- package/dist/learning-journey.capability.js +1 -43
- package/dist/learning-journey.feature.js +1 -56
- package/dist/node/contracts/index.js +1 -578
- package/dist/node/contracts/models.js +1 -193
- package/dist/node/contracts/onboarding.js +1 -417
- package/dist/node/contracts/operations.js +1 -326
- package/dist/node/contracts/shared.js +1 -5
- package/dist/node/docs/index.js +7 -51
- package/dist/node/docs/learning-journey.docblock.js +7 -51
- package/dist/node/engines/index.js +1 -675
- package/dist/node/engines/srs.js +1 -198
- package/dist/node/engines/streak.js +1 -159
- package/dist/node/engines/xp.js +1 -320
- package/dist/node/entities/ai.js +1 -343
- package/dist/node/entities/course.js +1 -276
- package/dist/node/entities/flashcard.js +1 -222
- package/dist/node/entities/gamification.js +1 -340
- package/dist/node/entities/index.js +1 -2140
- package/dist/node/entities/learner.js +1 -333
- package/dist/node/entities/onboarding.js +1 -301
- package/dist/node/entities/quiz.js +1 -304
- package/dist/node/events.js +1 -423
- package/dist/node/i18n/catalogs/en.js +1 -43
- package/dist/node/i18n/catalogs/es.js +1 -43
- package/dist/node/i18n/catalogs/fr.js +1 -43
- package/dist/node/i18n/catalogs/index.js +1 -127
- package/dist/node/i18n/index.js +1 -169
- package/dist/node/i18n/keys.js +1 -16
- package/dist/node/i18n/locale.js +1 -13
- package/dist/node/i18n/messages.js +1 -139
- package/dist/node/index.js +7 -3914
- package/dist/node/learning-journey.capability.js +1 -43
- package/dist/node/learning-journey.feature.js +1 -56
- package/package.json +5 -5
|
@@ -1,675 +1 @@
|
|
|
1
|
-
// src/engines/srs.ts
|
|
2
|
-
var DEFAULT_SRS_CONFIG = {
|
|
3
|
-
learningSteps: [1, 10],
|
|
4
|
-
graduatingInterval: 1,
|
|
5
|
-
easyInterval: 4,
|
|
6
|
-
relearningSteps: [10],
|
|
7
|
-
minEaseFactor: 1.3,
|
|
8
|
-
maxInterval: 365,
|
|
9
|
-
intervalModifier: 1,
|
|
10
|
-
newIntervalModifier: 0.5,
|
|
11
|
-
hardIntervalModifier: 1.2,
|
|
12
|
-
easyBonus: 1.3
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
class SRSEngine {
|
|
16
|
-
config;
|
|
17
|
-
constructor(config = {}) {
|
|
18
|
-
this.config = { ...DEFAULT_SRS_CONFIG, ...config };
|
|
19
|
-
}
|
|
20
|
-
calculateNextReview(state, rating, now = new Date) {
|
|
21
|
-
if (!state.isGraduated && !state.isRelearning) {
|
|
22
|
-
return this.handleLearningCard(state, rating, now);
|
|
23
|
-
}
|
|
24
|
-
if (state.isRelearning) {
|
|
25
|
-
return this.handleRelearningCard(state, rating, now);
|
|
26
|
-
}
|
|
27
|
-
return this.handleReviewCard(state, rating, now);
|
|
28
|
-
}
|
|
29
|
-
getInitialState() {
|
|
30
|
-
return {
|
|
31
|
-
interval: 0,
|
|
32
|
-
easeFactor: 2.5,
|
|
33
|
-
repetitions: 0,
|
|
34
|
-
learningStep: 0,
|
|
35
|
-
isGraduated: false,
|
|
36
|
-
isRelearning: false,
|
|
37
|
-
lapses: 0
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
isDue(nextReviewAt, now = new Date) {
|
|
41
|
-
return nextReviewAt <= now;
|
|
42
|
-
}
|
|
43
|
-
getOverdueDays(nextReviewAt, now = new Date) {
|
|
44
|
-
const diff = now.getTime() - nextReviewAt.getTime();
|
|
45
|
-
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
46
|
-
}
|
|
47
|
-
handleLearningCard(state, rating, now) {
|
|
48
|
-
const steps = this.config.learningSteps;
|
|
49
|
-
let newStep = state.learningStep;
|
|
50
|
-
let isGraduated = false;
|
|
51
|
-
let interval = 0;
|
|
52
|
-
let nextReviewAt;
|
|
53
|
-
switch (rating) {
|
|
54
|
-
case "AGAIN":
|
|
55
|
-
newStep = 0;
|
|
56
|
-
interval = steps[0] ?? 1;
|
|
57
|
-
nextReviewAt = this.addMinutes(now, interval);
|
|
58
|
-
break;
|
|
59
|
-
case "HARD":
|
|
60
|
-
interval = steps[newStep] ?? steps[0] ?? 1;
|
|
61
|
-
nextReviewAt = this.addMinutes(now, interval);
|
|
62
|
-
break;
|
|
63
|
-
case "GOOD":
|
|
64
|
-
newStep++;
|
|
65
|
-
if (newStep >= steps.length) {
|
|
66
|
-
isGraduated = true;
|
|
67
|
-
interval = this.config.graduatingInterval;
|
|
68
|
-
nextReviewAt = this.addDays(now, interval);
|
|
69
|
-
} else {
|
|
70
|
-
interval = steps[newStep] ?? 10;
|
|
71
|
-
nextReviewAt = this.addMinutes(now, interval);
|
|
72
|
-
}
|
|
73
|
-
break;
|
|
74
|
-
case "EASY":
|
|
75
|
-
isGraduated = true;
|
|
76
|
-
interval = this.config.easyInterval;
|
|
77
|
-
nextReviewAt = this.addDays(now, interval);
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
interval: isGraduated ? interval : 0,
|
|
82
|
-
easeFactor: state.easeFactor,
|
|
83
|
-
repetitions: isGraduated ? 1 : 0,
|
|
84
|
-
nextReviewAt,
|
|
85
|
-
learningStep: newStep,
|
|
86
|
-
isGraduated,
|
|
87
|
-
isRelearning: false,
|
|
88
|
-
lapses: state.lapses
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
handleRelearningCard(state, rating, now) {
|
|
92
|
-
const steps = this.config.relearningSteps;
|
|
93
|
-
let newStep = state.learningStep;
|
|
94
|
-
let isRelearning = true;
|
|
95
|
-
let interval = 0;
|
|
96
|
-
let nextReviewAt;
|
|
97
|
-
switch (rating) {
|
|
98
|
-
case "AGAIN":
|
|
99
|
-
newStep = 0;
|
|
100
|
-
interval = steps[0] ?? 10;
|
|
101
|
-
nextReviewAt = this.addMinutes(now, interval);
|
|
102
|
-
break;
|
|
103
|
-
case "HARD":
|
|
104
|
-
interval = steps[newStep] ?? steps[0] ?? 10;
|
|
105
|
-
nextReviewAt = this.addMinutes(now, interval);
|
|
106
|
-
break;
|
|
107
|
-
case "GOOD":
|
|
108
|
-
newStep++;
|
|
109
|
-
if (newStep >= steps.length) {
|
|
110
|
-
isRelearning = false;
|
|
111
|
-
interval = Math.max(1, Math.floor(state.interval * this.config.newIntervalModifier));
|
|
112
|
-
nextReviewAt = this.addDays(now, interval);
|
|
113
|
-
} else {
|
|
114
|
-
interval = steps[newStep] ?? 10;
|
|
115
|
-
nextReviewAt = this.addMinutes(now, interval);
|
|
116
|
-
}
|
|
117
|
-
break;
|
|
118
|
-
case "EASY":
|
|
119
|
-
isRelearning = false;
|
|
120
|
-
interval = Math.max(1, Math.floor(state.interval * this.config.newIntervalModifier * 1.5));
|
|
121
|
-
nextReviewAt = this.addDays(now, interval);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
interval: isRelearning ? state.interval : interval,
|
|
126
|
-
easeFactor: state.easeFactor,
|
|
127
|
-
repetitions: isRelearning ? state.repetitions : state.repetitions + 1,
|
|
128
|
-
nextReviewAt,
|
|
129
|
-
learningStep: newStep,
|
|
130
|
-
isGraduated: true,
|
|
131
|
-
isRelearning,
|
|
132
|
-
lapses: state.lapses
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
handleReviewCard(state, rating, now) {
|
|
136
|
-
let newInterval;
|
|
137
|
-
let newEaseFactor = state.easeFactor;
|
|
138
|
-
let repetitions = state.repetitions;
|
|
139
|
-
let isRelearning = false;
|
|
140
|
-
let learningStep = 0;
|
|
141
|
-
let lapses = state.lapses;
|
|
142
|
-
switch (rating) {
|
|
143
|
-
case "AGAIN":
|
|
144
|
-
lapses++;
|
|
145
|
-
isRelearning = true;
|
|
146
|
-
learningStep = 0;
|
|
147
|
-
newEaseFactor = Math.max(this.config.minEaseFactor, newEaseFactor - 0.2);
|
|
148
|
-
newInterval = state.interval;
|
|
149
|
-
return {
|
|
150
|
-
interval: newInterval,
|
|
151
|
-
easeFactor: newEaseFactor,
|
|
152
|
-
repetitions,
|
|
153
|
-
nextReviewAt: this.addMinutes(now, this.config.relearningSteps[0] ?? 10),
|
|
154
|
-
learningStep,
|
|
155
|
-
isGraduated: true,
|
|
156
|
-
isRelearning: true,
|
|
157
|
-
lapses
|
|
158
|
-
};
|
|
159
|
-
case "HARD":
|
|
160
|
-
newEaseFactor = Math.max(this.config.minEaseFactor, newEaseFactor - 0.15);
|
|
161
|
-
newInterval = Math.max(state.interval + 1, state.interval * this.config.hardIntervalModifier);
|
|
162
|
-
break;
|
|
163
|
-
case "GOOD":
|
|
164
|
-
newInterval = state.interval * newEaseFactor * this.config.intervalModifier;
|
|
165
|
-
repetitions++;
|
|
166
|
-
break;
|
|
167
|
-
case "EASY":
|
|
168
|
-
newEaseFactor = newEaseFactor + 0.15;
|
|
169
|
-
newInterval = state.interval * newEaseFactor * this.config.easyBonus * this.config.intervalModifier;
|
|
170
|
-
repetitions++;
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
newInterval = Math.min(Math.round(newInterval), this.config.maxInterval);
|
|
174
|
-
newInterval = Math.max(1, newInterval);
|
|
175
|
-
return {
|
|
176
|
-
interval: newInterval,
|
|
177
|
-
easeFactor: newEaseFactor,
|
|
178
|
-
repetitions,
|
|
179
|
-
nextReviewAt: this.addDays(now, newInterval),
|
|
180
|
-
learningStep,
|
|
181
|
-
isGraduated: true,
|
|
182
|
-
isRelearning,
|
|
183
|
-
lapses
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
addMinutes(date, minutes) {
|
|
187
|
-
return new Date(date.getTime() + minutes * 60 * 1000);
|
|
188
|
-
}
|
|
189
|
-
addDays(date, days) {
|
|
190
|
-
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
var srsEngine = new SRSEngine;
|
|
194
|
-
|
|
195
|
-
// src/engines/streak.ts
|
|
196
|
-
var DEFAULT_STREAK_CONFIG = {
|
|
197
|
-
timezone: "UTC",
|
|
198
|
-
freezesPerMonth: 2,
|
|
199
|
-
maxFreezes: 5,
|
|
200
|
-
gracePeriodHours: 4
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
class StreakEngine {
|
|
204
|
-
config;
|
|
205
|
-
constructor(config = {}) {
|
|
206
|
-
this.config = { ...DEFAULT_STREAK_CONFIG, ...config };
|
|
207
|
-
}
|
|
208
|
-
update(state, now = new Date) {
|
|
209
|
-
const todayDate = this.getDateString(now);
|
|
210
|
-
const result = {
|
|
211
|
-
state: { ...state },
|
|
212
|
-
streakMaintained: false,
|
|
213
|
-
streakLost: false,
|
|
214
|
-
freezeUsed: false,
|
|
215
|
-
newStreak: false,
|
|
216
|
-
daysMissed: 0
|
|
217
|
-
};
|
|
218
|
-
if (!state.lastActivityDate) {
|
|
219
|
-
result.state.currentStreak = 1;
|
|
220
|
-
result.state.longestStreak = Math.max(1, state.longestStreak);
|
|
221
|
-
result.state.lastActivityAt = now;
|
|
222
|
-
result.state.lastActivityDate = todayDate;
|
|
223
|
-
result.newStreak = true;
|
|
224
|
-
result.streakMaintained = true;
|
|
225
|
-
return result;
|
|
226
|
-
}
|
|
227
|
-
if (state.lastActivityDate === todayDate) {
|
|
228
|
-
result.state.lastActivityAt = now;
|
|
229
|
-
result.streakMaintained = true;
|
|
230
|
-
return result;
|
|
231
|
-
}
|
|
232
|
-
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
233
|
-
if (daysSinceActivity === 1) {
|
|
234
|
-
result.state.currentStreak = state.currentStreak + 1;
|
|
235
|
-
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
236
|
-
result.state.lastActivityAt = now;
|
|
237
|
-
result.state.lastActivityDate = todayDate;
|
|
238
|
-
result.streakMaintained = true;
|
|
239
|
-
return result;
|
|
240
|
-
}
|
|
241
|
-
result.daysMissed = daysSinceActivity - 1;
|
|
242
|
-
const freezesNeeded = result.daysMissed;
|
|
243
|
-
if (freezesNeeded <= state.freezesRemaining) {
|
|
244
|
-
result.state.freezesRemaining = state.freezesRemaining - freezesNeeded;
|
|
245
|
-
result.state.freezeUsedAt = now;
|
|
246
|
-
result.state.currentStreak = state.currentStreak + 1;
|
|
247
|
-
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
248
|
-
result.state.lastActivityAt = now;
|
|
249
|
-
result.state.lastActivityDate = todayDate;
|
|
250
|
-
result.freezeUsed = true;
|
|
251
|
-
result.streakMaintained = true;
|
|
252
|
-
return result;
|
|
253
|
-
}
|
|
254
|
-
result.streakLost = true;
|
|
255
|
-
result.state.currentStreak = 1;
|
|
256
|
-
result.state.lastActivityAt = now;
|
|
257
|
-
result.state.lastActivityDate = todayDate;
|
|
258
|
-
result.newStreak = true;
|
|
259
|
-
return result;
|
|
260
|
-
}
|
|
261
|
-
checkStatus(state, now = new Date) {
|
|
262
|
-
if (!state.lastActivityDate) {
|
|
263
|
-
return {
|
|
264
|
-
isActive: false,
|
|
265
|
-
willExpireAt: null,
|
|
266
|
-
canUseFreeze: false,
|
|
267
|
-
daysUntilExpiry: 0
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
const todayDate = this.getDateString(now);
|
|
271
|
-
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
272
|
-
if (daysSinceActivity === 0) {
|
|
273
|
-
const tomorrow = this.addDays(now, 1);
|
|
274
|
-
tomorrow.setHours(23, 59, 59, 999);
|
|
275
|
-
return {
|
|
276
|
-
isActive: true,
|
|
277
|
-
willExpireAt: tomorrow,
|
|
278
|
-
canUseFreeze: state.freezesRemaining > 0,
|
|
279
|
-
daysUntilExpiry: 1
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
if (daysSinceActivity === 1) {
|
|
283
|
-
const endOfDay = new Date(now);
|
|
284
|
-
endOfDay.setHours(23 + this.config.gracePeriodHours, 59, 59, 999);
|
|
285
|
-
return {
|
|
286
|
-
isActive: true,
|
|
287
|
-
willExpireAt: endOfDay,
|
|
288
|
-
canUseFreeze: state.freezesRemaining > 0,
|
|
289
|
-
daysUntilExpiry: 0
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
const missedDays = daysSinceActivity - 1;
|
|
293
|
-
return {
|
|
294
|
-
isActive: missedDays <= state.freezesRemaining,
|
|
295
|
-
willExpireAt: null,
|
|
296
|
-
canUseFreeze: missedDays <= state.freezesRemaining,
|
|
297
|
-
daysUntilExpiry: -missedDays
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
useFreeze(state, now = new Date) {
|
|
301
|
-
if (state.freezesRemaining <= 0) {
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
return {
|
|
305
|
-
...state,
|
|
306
|
-
freezesRemaining: state.freezesRemaining - 1,
|
|
307
|
-
freezeUsedAt: now
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
awardMonthlyFreezes(state) {
|
|
311
|
-
return {
|
|
312
|
-
...state,
|
|
313
|
-
freezesRemaining: Math.min(state.freezesRemaining + this.config.freezesPerMonth, this.config.maxFreezes)
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
getInitialState() {
|
|
317
|
-
return {
|
|
318
|
-
currentStreak: 0,
|
|
319
|
-
longestStreak: 0,
|
|
320
|
-
lastActivityAt: null,
|
|
321
|
-
lastActivityDate: null,
|
|
322
|
-
freezesRemaining: this.config.freezesPerMonth,
|
|
323
|
-
freezeUsedAt: null
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
getMilestones(currentStreak) {
|
|
327
|
-
const milestones = [3, 7, 14, 30, 60, 90, 180, 365, 500, 1000];
|
|
328
|
-
const achieved = milestones.filter((m) => currentStreak >= m);
|
|
329
|
-
const next = milestones.find((m) => currentStreak < m) ?? null;
|
|
330
|
-
return { achieved, next };
|
|
331
|
-
}
|
|
332
|
-
getDateString(date) {
|
|
333
|
-
const year = date.getFullYear();
|
|
334
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
335
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
336
|
-
return `${year}-${month}-${day}`;
|
|
337
|
-
}
|
|
338
|
-
getDaysBetween(dateStr1, dateStr2) {
|
|
339
|
-
const date1 = new Date(dateStr1);
|
|
340
|
-
const date2 = new Date(dateStr2);
|
|
341
|
-
const diffTime = date2.getTime() - date1.getTime();
|
|
342
|
-
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
343
|
-
}
|
|
344
|
-
addDays(date, days) {
|
|
345
|
-
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
var streakEngine = new StreakEngine;
|
|
349
|
-
|
|
350
|
-
// src/i18n/catalogs/en.ts
|
|
351
|
-
import { defineTranslation } from "@contractspec/lib.contracts-spec/translations";
|
|
352
|
-
var enMessages = defineTranslation({
|
|
353
|
-
meta: {
|
|
354
|
-
key: "learning-journey.messages",
|
|
355
|
-
version: "1.0.0",
|
|
356
|
-
domain: "learning-journey",
|
|
357
|
-
description: "XP source labels for the learning-journey module",
|
|
358
|
-
owners: ["platform"],
|
|
359
|
-
stability: "experimental"
|
|
360
|
-
},
|
|
361
|
-
locale: "en",
|
|
362
|
-
fallback: "en",
|
|
363
|
-
messages: {
|
|
364
|
-
"xp.source.base": {
|
|
365
|
-
value: "Base",
|
|
366
|
-
description: "XP breakdown label for base XP"
|
|
367
|
-
},
|
|
368
|
-
"xp.source.scoreBonus": {
|
|
369
|
-
value: "Score Bonus",
|
|
370
|
-
description: "XP breakdown label for score-based bonus"
|
|
371
|
-
},
|
|
372
|
-
"xp.source.perfectScore": {
|
|
373
|
-
value: "Perfect Score",
|
|
374
|
-
description: "XP breakdown label for perfect score bonus"
|
|
375
|
-
},
|
|
376
|
-
"xp.source.firstAttempt": {
|
|
377
|
-
value: "First Attempt",
|
|
378
|
-
description: "XP breakdown label for first attempt bonus"
|
|
379
|
-
},
|
|
380
|
-
"xp.source.retryPenalty": {
|
|
381
|
-
value: "Retry Penalty",
|
|
382
|
-
description: "XP breakdown label for retry penalty"
|
|
383
|
-
},
|
|
384
|
-
"xp.source.streakBonus": {
|
|
385
|
-
value: "Streak Bonus",
|
|
386
|
-
description: "XP breakdown label for streak bonus"
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
// src/i18n/catalogs/es.ts
|
|
392
|
-
import { defineTranslation as defineTranslation2 } from "@contractspec/lib.contracts-spec/translations";
|
|
393
|
-
var esMessages = defineTranslation2({
|
|
394
|
-
meta: {
|
|
395
|
-
key: "learning-journey.messages",
|
|
396
|
-
version: "1.0.0",
|
|
397
|
-
domain: "learning-journey",
|
|
398
|
-
description: "XP source labels (Spanish)",
|
|
399
|
-
owners: ["platform"],
|
|
400
|
-
stability: "experimental"
|
|
401
|
-
},
|
|
402
|
-
locale: "es",
|
|
403
|
-
fallback: "en",
|
|
404
|
-
messages: {
|
|
405
|
-
"xp.source.base": {
|
|
406
|
-
value: "Base",
|
|
407
|
-
description: "XP breakdown label for base XP"
|
|
408
|
-
},
|
|
409
|
-
"xp.source.scoreBonus": {
|
|
410
|
-
value: "Bonificación por puntuación",
|
|
411
|
-
description: "XP breakdown label for score-based bonus"
|
|
412
|
-
},
|
|
413
|
-
"xp.source.perfectScore": {
|
|
414
|
-
value: "Puntuación perfecta",
|
|
415
|
-
description: "XP breakdown label for perfect score bonus"
|
|
416
|
-
},
|
|
417
|
-
"xp.source.firstAttempt": {
|
|
418
|
-
value: "Primer intento",
|
|
419
|
-
description: "XP breakdown label for first attempt bonus"
|
|
420
|
-
},
|
|
421
|
-
"xp.source.retryPenalty": {
|
|
422
|
-
value: "Penalización por reintento",
|
|
423
|
-
description: "XP breakdown label for retry penalty"
|
|
424
|
-
},
|
|
425
|
-
"xp.source.streakBonus": {
|
|
426
|
-
value: "Bonificación por racha",
|
|
427
|
-
description: "XP breakdown label for streak bonus"
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
// src/i18n/catalogs/fr.ts
|
|
433
|
-
import { defineTranslation as defineTranslation3 } from "@contractspec/lib.contracts-spec/translations";
|
|
434
|
-
var frMessages = defineTranslation3({
|
|
435
|
-
meta: {
|
|
436
|
-
key: "learning-journey.messages",
|
|
437
|
-
version: "1.0.0",
|
|
438
|
-
domain: "learning-journey",
|
|
439
|
-
description: "XP source labels (French)",
|
|
440
|
-
owners: ["platform"],
|
|
441
|
-
stability: "experimental"
|
|
442
|
-
},
|
|
443
|
-
locale: "fr",
|
|
444
|
-
fallback: "en",
|
|
445
|
-
messages: {
|
|
446
|
-
"xp.source.base": {
|
|
447
|
-
value: "Base",
|
|
448
|
-
description: "XP breakdown label for base XP"
|
|
449
|
-
},
|
|
450
|
-
"xp.source.scoreBonus": {
|
|
451
|
-
value: "Bonus de score",
|
|
452
|
-
description: "XP breakdown label for score-based bonus"
|
|
453
|
-
},
|
|
454
|
-
"xp.source.perfectScore": {
|
|
455
|
-
value: "Score parfait",
|
|
456
|
-
description: "XP breakdown label for perfect score bonus"
|
|
457
|
-
},
|
|
458
|
-
"xp.source.firstAttempt": {
|
|
459
|
-
value: "Premier essai",
|
|
460
|
-
description: "XP breakdown label for first attempt bonus"
|
|
461
|
-
},
|
|
462
|
-
"xp.source.retryPenalty": {
|
|
463
|
-
value: "Pénalité de réessai",
|
|
464
|
-
description: "XP breakdown label for retry penalty"
|
|
465
|
-
},
|
|
466
|
-
"xp.source.streakBonus": {
|
|
467
|
-
value: "Bonus de série",
|
|
468
|
-
description: "XP breakdown label for streak bonus"
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
// src/i18n/messages.ts
|
|
474
|
-
import {
|
|
475
|
-
createI18nFactory
|
|
476
|
-
} from "@contractspec/lib.contracts-spec/translations";
|
|
477
|
-
var factory = createI18nFactory({
|
|
478
|
-
specKey: "learning-journey.messages",
|
|
479
|
-
catalogs: [enMessages, frMessages, esMessages]
|
|
480
|
-
});
|
|
481
|
-
var createLearningJourneyI18n = factory.create;
|
|
482
|
-
var getDefaultI18n = factory.getDefault;
|
|
483
|
-
var resetI18nRegistry = factory.resetRegistry;
|
|
484
|
-
|
|
485
|
-
// src/engines/xp.ts
|
|
486
|
-
var DEFAULT_XP_CONFIG = {
|
|
487
|
-
baseValues: {
|
|
488
|
-
lesson_complete: 10,
|
|
489
|
-
quiz_pass: 20,
|
|
490
|
-
quiz_perfect: 50,
|
|
491
|
-
flashcard_review: 1,
|
|
492
|
-
course_complete: 200,
|
|
493
|
-
module_complete: 50,
|
|
494
|
-
streak_bonus: 5,
|
|
495
|
-
achievement_unlock: 0,
|
|
496
|
-
daily_goal_complete: 15,
|
|
497
|
-
first_lesson: 25,
|
|
498
|
-
onboarding_step: 5,
|
|
499
|
-
onboarding_complete: 50
|
|
500
|
-
},
|
|
501
|
-
scoreThresholds: [
|
|
502
|
-
{ min: 90, multiplier: 1.5 },
|
|
503
|
-
{ min: 80, multiplier: 1.25 },
|
|
504
|
-
{ min: 70, multiplier: 1 },
|
|
505
|
-
{ min: 60, multiplier: 0.75 },
|
|
506
|
-
{ min: 0, multiplier: 0.5 }
|
|
507
|
-
],
|
|
508
|
-
streakTiers: [
|
|
509
|
-
{ days: 365, bonus: 50 },
|
|
510
|
-
{ days: 180, bonus: 30 },
|
|
511
|
-
{ days: 90, bonus: 20 },
|
|
512
|
-
{ days: 30, bonus: 15 },
|
|
513
|
-
{ days: 14, bonus: 10 },
|
|
514
|
-
{ days: 7, bonus: 5 },
|
|
515
|
-
{ days: 3, bonus: 2 },
|
|
516
|
-
{ days: 1, bonus: 0 }
|
|
517
|
-
],
|
|
518
|
-
perfectScoreMultiplier: 1.5,
|
|
519
|
-
firstAttemptBonus: 10,
|
|
520
|
-
retryPenalty: 0.5,
|
|
521
|
-
speedBonusMultiplier: 1.2,
|
|
522
|
-
speedBonusThreshold: 0.8
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
class XPEngine {
|
|
526
|
-
config;
|
|
527
|
-
constructor(config = {}) {
|
|
528
|
-
this.config = {
|
|
529
|
-
...DEFAULT_XP_CONFIG,
|
|
530
|
-
...config,
|
|
531
|
-
baseValues: { ...DEFAULT_XP_CONFIG.baseValues, ...config.baseValues },
|
|
532
|
-
scoreThresholds: config.scoreThresholds || DEFAULT_XP_CONFIG.scoreThresholds,
|
|
533
|
-
streakTiers: config.streakTiers || DEFAULT_XP_CONFIG.streakTiers
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
calculate(input) {
|
|
537
|
-
const breakdown = [];
|
|
538
|
-
const baseXp = input.baseXp ?? this.config.baseValues[input.activity];
|
|
539
|
-
let totalXp = baseXp;
|
|
540
|
-
breakdown.push({
|
|
541
|
-
source: "base",
|
|
542
|
-
amount: baseXp
|
|
543
|
-
});
|
|
544
|
-
if (input.score !== undefined) {
|
|
545
|
-
const scoreMultiplier = this.getScoreMultiplier(input.score);
|
|
546
|
-
if (scoreMultiplier !== 1) {
|
|
547
|
-
const scoreBonus = Math.round(baseXp * (scoreMultiplier - 1));
|
|
548
|
-
totalXp += scoreBonus;
|
|
549
|
-
breakdown.push({
|
|
550
|
-
source: "score_bonus",
|
|
551
|
-
amount: scoreBonus,
|
|
552
|
-
multiplier: scoreMultiplier
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
if (input.score === 100) {
|
|
556
|
-
const perfectBonus = Math.round(baseXp * (this.config.perfectScoreMultiplier - 1));
|
|
557
|
-
totalXp += perfectBonus;
|
|
558
|
-
breakdown.push({
|
|
559
|
-
source: "perfect_score",
|
|
560
|
-
amount: perfectBonus,
|
|
561
|
-
multiplier: this.config.perfectScoreMultiplier
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
if (input.attemptNumber === 1 && !input.isRetry) {
|
|
566
|
-
totalXp += this.config.firstAttemptBonus;
|
|
567
|
-
breakdown.push({
|
|
568
|
-
source: "first_attempt",
|
|
569
|
-
amount: this.config.firstAttemptBonus
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
if (input.isRetry) {
|
|
573
|
-
const penalty = Math.round(totalXp * (1 - this.config.retryPenalty));
|
|
574
|
-
totalXp -= penalty;
|
|
575
|
-
breakdown.push({
|
|
576
|
-
source: "retry_penalty",
|
|
577
|
-
amount: -penalty,
|
|
578
|
-
multiplier: this.config.retryPenalty
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
if (input.currentStreak && input.currentStreak > 0) {
|
|
582
|
-
const streakBonus = this.getStreakBonus(input.currentStreak);
|
|
583
|
-
if (streakBonus > 0) {
|
|
584
|
-
totalXp += streakBonus;
|
|
585
|
-
breakdown.push({
|
|
586
|
-
source: "streak_bonus",
|
|
587
|
-
amount: streakBonus
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (baseXp > 0) {
|
|
592
|
-
totalXp = Math.max(1, totalXp);
|
|
593
|
-
}
|
|
594
|
-
return {
|
|
595
|
-
totalXp: Math.round(totalXp),
|
|
596
|
-
baseXp,
|
|
597
|
-
breakdown
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
calculateStreakBonus(currentStreak) {
|
|
601
|
-
const bonus = this.getStreakBonus(currentStreak);
|
|
602
|
-
return {
|
|
603
|
-
totalXp: bonus,
|
|
604
|
-
baseXp: bonus,
|
|
605
|
-
breakdown: [
|
|
606
|
-
{
|
|
607
|
-
source: "streak_bonus",
|
|
608
|
-
amount: bonus
|
|
609
|
-
}
|
|
610
|
-
]
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
getXpForLevel(level) {
|
|
614
|
-
if (level <= 1)
|
|
615
|
-
return 0;
|
|
616
|
-
return Math.round(100 * Math.pow(level - 1, 1.5));
|
|
617
|
-
}
|
|
618
|
-
getLevelFromXp(totalXp) {
|
|
619
|
-
let level = 1;
|
|
620
|
-
let xpRequired = this.getXpForLevel(level + 1);
|
|
621
|
-
while (totalXp >= xpRequired && level < 1000) {
|
|
622
|
-
level++;
|
|
623
|
-
xpRequired = this.getXpForLevel(level + 1);
|
|
624
|
-
}
|
|
625
|
-
const xpForCurrentLevel = this.getXpForLevel(level);
|
|
626
|
-
const xpForNextLevel = this.getXpForLevel(level + 1);
|
|
627
|
-
return {
|
|
628
|
-
level,
|
|
629
|
-
xpInLevel: totalXp - xpForCurrentLevel,
|
|
630
|
-
xpForNextLevel: xpForNextLevel - xpForCurrentLevel
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
getScoreMultiplier(score) {
|
|
634
|
-
for (const threshold of this.config.scoreThresholds) {
|
|
635
|
-
if (score >= threshold.min) {
|
|
636
|
-
return threshold.multiplier;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return 1;
|
|
640
|
-
}
|
|
641
|
-
getStreakBonus(streak) {
|
|
642
|
-
for (const tier of this.config.streakTiers) {
|
|
643
|
-
if (streak >= tier.days) {
|
|
644
|
-
return tier.bonus;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
return 0;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
var SOURCE_KEY_MAP = {
|
|
651
|
-
base: "xp.source.base",
|
|
652
|
-
score_bonus: "xp.source.scoreBonus",
|
|
653
|
-
perfect_score: "xp.source.perfectScore",
|
|
654
|
-
first_attempt: "xp.source.firstAttempt",
|
|
655
|
-
retry_penalty: "xp.source.retryPenalty",
|
|
656
|
-
streak_bonus: "xp.source.streakBonus"
|
|
657
|
-
};
|
|
658
|
-
function getXpSourceLabel(source, locale) {
|
|
659
|
-
const i18n = createLearningJourneyI18n(locale);
|
|
660
|
-
const i18nKey = SOURCE_KEY_MAP[source];
|
|
661
|
-
return i18nKey ? i18n.t(i18nKey) : source;
|
|
662
|
-
}
|
|
663
|
-
var xpEngine = new XPEngine;
|
|
664
|
-
export {
|
|
665
|
-
xpEngine,
|
|
666
|
-
streakEngine,
|
|
667
|
-
srsEngine,
|
|
668
|
-
getXpSourceLabel,
|
|
669
|
-
XPEngine,
|
|
670
|
-
StreakEngine,
|
|
671
|
-
SRSEngine,
|
|
672
|
-
DEFAULT_XP_CONFIG,
|
|
673
|
-
DEFAULT_STREAK_CONFIG,
|
|
674
|
-
DEFAULT_SRS_CONFIG
|
|
675
|
-
};
|
|
1
|
+
var N={learningSteps:[1,10],graduatingInterval:1,easyInterval:4,relearningSteps:[10],minEaseFactor:1.3,maxInterval:365,intervalModifier:1,newIntervalModifier:0.5,hardIntervalModifier:1.2,easyBonus:1.3};class K{config;constructor(j={}){this.config={...N,...j}}calculateNextReview(j,Q,W=new Date){if(!j.isGraduated&&!j.isRelearning)return this.handleLearningCard(j,Q,W);if(j.isRelearning)return this.handleRelearningCard(j,Q,W);return this.handleReviewCard(j,Q,W)}getInitialState(){return{interval:0,easeFactor:2.5,repetitions:0,learningStep:0,isGraduated:!1,isRelearning:!1,lapses:0}}isDue(j,Q=new Date){return j<=Q}getOverdueDays(j,Q=new Date){let W=Q.getTime()-j.getTime();return Math.floor(W/86400000)}handleLearningCard(j,Q,W){let H=this.config.learningSteps,V=j.learningStep,Z=!1,$=0,J;switch(Q){case"AGAIN":V=0,$=H[0]??1,J=this.addMinutes(W,$);break;case"HARD":$=H[V]??H[0]??1,J=this.addMinutes(W,$);break;case"GOOD":if(V++,V>=H.length)Z=!0,$=this.config.graduatingInterval,J=this.addDays(W,$);else $=H[V]??10,J=this.addMinutes(W,$);break;case"EASY":Z=!0,$=this.config.easyInterval,J=this.addDays(W,$);break}return{interval:Z?$:0,easeFactor:j.easeFactor,repetitions:Z?1:0,nextReviewAt:J,learningStep:V,isGraduated:Z,isRelearning:!1,lapses:j.lapses}}handleRelearningCard(j,Q,W){let H=this.config.relearningSteps,V=j.learningStep,Z=!0,$=0,J;switch(Q){case"AGAIN":V=0,$=H[0]??10,J=this.addMinutes(W,$);break;case"HARD":$=H[V]??H[0]??10,J=this.addMinutes(W,$);break;case"GOOD":if(V++,V>=H.length)Z=!1,$=Math.max(1,Math.floor(j.interval*this.config.newIntervalModifier)),J=this.addDays(W,$);else $=H[V]??10,J=this.addMinutes(W,$);break;case"EASY":Z=!1,$=Math.max(1,Math.floor(j.interval*this.config.newIntervalModifier*1.5)),J=this.addDays(W,$);break}return{interval:Z?j.interval:$,easeFactor:j.easeFactor,repetitions:Z?j.repetitions:j.repetitions+1,nextReviewAt:J,learningStep:V,isGraduated:!0,isRelearning:Z,lapses:j.lapses}}handleReviewCard(j,Q,W){let H,V=j.easeFactor,Z=j.repetitions,$=!1,J=0,q=j.lapses;switch(Q){case"AGAIN":return q++,$=!0,J=0,V=Math.max(this.config.minEaseFactor,V-0.2),H=j.interval,{interval:H,easeFactor:V,repetitions:Z,nextReviewAt:this.addMinutes(W,this.config.relearningSteps[0]??10),learningStep:J,isGraduated:!0,isRelearning:!0,lapses:q};case"HARD":V=Math.max(this.config.minEaseFactor,V-0.15),H=Math.max(j.interval+1,j.interval*this.config.hardIntervalModifier);break;case"GOOD":H=j.interval*V*this.config.intervalModifier,Z++;break;case"EASY":V=V+0.15,H=j.interval*V*this.config.easyBonus*this.config.intervalModifier,Z++;break}return H=Math.min(Math.round(H),this.config.maxInterval),H=Math.max(1,H),{interval:H,easeFactor:V,repetitions:Z,nextReviewAt:this.addDays(W,H),learningStep:J,isGraduated:!0,isRelearning:$,lapses:q}}addMinutes(j,Q){return new Date(j.getTime()+Q*60*1000)}addDays(j,Q){return new Date(j.getTime()+Q*24*60*60*1000)}}var I=new K;var O={timezone:"UTC",freezesPerMonth:2,maxFreezes:5,gracePeriodHours:4};class h{config;constructor(j={}){this.config={...O,...j}}update(j,Q=new Date){let W=this.getDateString(Q),H={state:{...j},streakMaintained:!1,streakLost:!1,freezeUsed:!1,newStreak:!1,daysMissed:0};if(!j.lastActivityDate)return H.state.currentStreak=1,H.state.longestStreak=Math.max(1,j.longestStreak),H.state.lastActivityAt=Q,H.state.lastActivityDate=W,H.newStreak=!0,H.streakMaintained=!0,H;if(j.lastActivityDate===W)return H.state.lastActivityAt=Q,H.streakMaintained=!0,H;let V=this.getDaysBetween(j.lastActivityDate,W);if(V===1)return H.state.currentStreak=j.currentStreak+1,H.state.longestStreak=Math.max(H.state.currentStreak,j.longestStreak),H.state.lastActivityAt=Q,H.state.lastActivityDate=W,H.streakMaintained=!0,H;H.daysMissed=V-1;let Z=H.daysMissed;if(Z<=j.freezesRemaining)return H.state.freezesRemaining=j.freezesRemaining-Z,H.state.freezeUsedAt=Q,H.state.currentStreak=j.currentStreak+1,H.state.longestStreak=Math.max(H.state.currentStreak,j.longestStreak),H.state.lastActivityAt=Q,H.state.lastActivityDate=W,H.freezeUsed=!0,H.streakMaintained=!0,H;return H.streakLost=!0,H.state.currentStreak=1,H.state.lastActivityAt=Q,H.state.lastActivityDate=W,H.newStreak=!0,H}checkStatus(j,Q=new Date){if(!j.lastActivityDate)return{isActive:!1,willExpireAt:null,canUseFreeze:!1,daysUntilExpiry:0};let W=this.getDateString(Q),H=this.getDaysBetween(j.lastActivityDate,W);if(H===0){let Z=this.addDays(Q,1);return Z.setHours(23,59,59,999),{isActive:!0,willExpireAt:Z,canUseFreeze:j.freezesRemaining>0,daysUntilExpiry:1}}if(H===1){let Z=new Date(Q);return Z.setHours(23+this.config.gracePeriodHours,59,59,999),{isActive:!0,willExpireAt:Z,canUseFreeze:j.freezesRemaining>0,daysUntilExpiry:0}}let V=H-1;return{isActive:V<=j.freezesRemaining,willExpireAt:null,canUseFreeze:V<=j.freezesRemaining,daysUntilExpiry:-V}}useFreeze(j,Q=new Date){if(j.freezesRemaining<=0)return null;return{...j,freezesRemaining:j.freezesRemaining-1,freezeUsedAt:Q}}awardMonthlyFreezes(j){return{...j,freezesRemaining:Math.min(j.freezesRemaining+this.config.freezesPerMonth,this.config.maxFreezes)}}getInitialState(){return{currentStreak:0,longestStreak:0,lastActivityAt:null,lastActivityDate:null,freezesRemaining:this.config.freezesPerMonth,freezeUsedAt:null}}getMilestones(j){let Q=[3,7,14,30,60,90,180,365,500,1000],W=Q.filter((V)=>j>=V),H=Q.find((V)=>j<V)??null;return{achieved:W,next:H}}getDateString(j){let Q=j.getFullYear(),W=String(j.getMonth()+1).padStart(2,"0"),H=String(j.getDate()).padStart(2,"0");return`${Q}-${W}-${H}`}getDaysBetween(j,Q){let W=new Date(j),V=new Date(Q).getTime()-W.getTime();return Math.floor(V/86400000)}addDays(j,Q){return new Date(j.getTime()+Q*24*60*60*1000)}}var x=new h;import{defineTranslation as L}from"@contractspec/lib.contracts-spec/translations";var B=L({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels for the learning-journey module",owners:["platform"],stability:"experimental"},locale:"en",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Score Bonus",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Perfect Score",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"First Attempt",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Retry Penalty",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Streak Bonus",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as G}from"@contractspec/lib.contracts-spec/translations";var M=G({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (Spanish)",owners:["platform"],stability:"experimental"},locale:"es",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonificación por puntuación",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Puntuación perfecta",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Primer intento",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Penalización por reintento",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonificación por racha",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as T}from"@contractspec/lib.contracts-spec/translations";var U=T({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (French)",owners:["platform"],stability:"experimental"},locale:"fr",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonus de score",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Score parfait",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Premier essai",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Pénalité de réessai",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonus de série",description:"XP breakdown label for streak bonus"}}});import{createI18nFactory as m}from"@contractspec/lib.contracts-spec/translations";var z=m({specKey:"learning-journey.messages",catalogs:[B,U,M]}),P=z.create,v=z.getDefault,d=z.resetRegistry;var Y={baseValues:{lesson_complete:10,quiz_pass:20,quiz_perfect:50,flashcard_review:1,course_complete:200,module_complete:50,streak_bonus:5,achievement_unlock:0,daily_goal_complete:15,first_lesson:25,onboarding_step:5,onboarding_complete:50},scoreThresholds:[{min:90,multiplier:1.5},{min:80,multiplier:1.25},{min:70,multiplier:1},{min:60,multiplier:0.75},{min:0,multiplier:0.5}],streakTiers:[{days:365,bonus:50},{days:180,bonus:30},{days:90,bonus:20},{days:30,bonus:15},{days:14,bonus:10},{days:7,bonus:5},{days:3,bonus:2},{days:1,bonus:0}],perfectScoreMultiplier:1.5,firstAttemptBonus:10,retryPenalty:0.5,speedBonusMultiplier:1.2,speedBonusThreshold:0.8};class C{config;constructor(j={}){this.config={...Y,...j,baseValues:{...Y.baseValues,...j.baseValues},scoreThresholds:j.scoreThresholds||Y.scoreThresholds,streakTiers:j.streakTiers||Y.streakTiers}}calculate(j){let Q=[],W=j.baseXp??this.config.baseValues[j.activity],H=W;if(Q.push({source:"base",amount:W}),j.score!==void 0){let V=this.getScoreMultiplier(j.score);if(V!==1){let Z=Math.round(W*(V-1));H+=Z,Q.push({source:"score_bonus",amount:Z,multiplier:V})}if(j.score===100){let Z=Math.round(W*(this.config.perfectScoreMultiplier-1));H+=Z,Q.push({source:"perfect_score",amount:Z,multiplier:this.config.perfectScoreMultiplier})}}if(j.attemptNumber===1&&!j.isRetry)H+=this.config.firstAttemptBonus,Q.push({source:"first_attempt",amount:this.config.firstAttemptBonus});if(j.isRetry){let V=Math.round(H*(1-this.config.retryPenalty));H-=V,Q.push({source:"retry_penalty",amount:-V,multiplier:this.config.retryPenalty})}if(j.currentStreak&&j.currentStreak>0){let V=this.getStreakBonus(j.currentStreak);if(V>0)H+=V,Q.push({source:"streak_bonus",amount:V})}if(W>0)H=Math.max(1,H);return{totalXp:Math.round(H),baseXp:W,breakdown:Q}}calculateStreakBonus(j){let Q=this.getStreakBonus(j);return{totalXp:Q,baseXp:Q,breakdown:[{source:"streak_bonus",amount:Q}]}}getXpForLevel(j){if(j<=1)return 0;return Math.round(100*Math.pow(j-1,1.5))}getLevelFromXp(j){let Q=1,W=this.getXpForLevel(Q+1);while(j>=W&&Q<1000)Q++,W=this.getXpForLevel(Q+1);let H=this.getXpForLevel(Q),V=this.getXpForLevel(Q+1);return{level:Q,xpInLevel:j-H,xpForNextLevel:V-H}}getScoreMultiplier(j){for(let Q of this.config.scoreThresholds)if(j>=Q.min)return Q.multiplier;return 1}getStreakBonus(j){for(let Q of this.config.streakTiers)if(j>=Q.days)return Q.bonus;return 0}}var _={base:"xp.source.base",score_bonus:"xp.source.scoreBonus",perfect_score:"xp.source.perfectScore",first_attempt:"xp.source.firstAttempt",retry_penalty:"xp.source.retryPenalty",streak_bonus:"xp.source.streakBonus"};function u(j,Q){let W=P(Q),H=_[j];return H?W.t(H):j}var w=new C;export{w as xpEngine,x as streakEngine,I as srsEngine,u as getXpSourceLabel,C as XPEngine,h as StreakEngine,K as SRSEngine,Y as DEFAULT_XP_CONFIG,O as DEFAULT_STREAK_CONFIG,N as DEFAULT_SRS_CONFIG};
|