@lessonkit/react 0.9.3 → 1.0.1
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/README.md +40 -88
- package/block-catalog.v1.json +39 -1
- package/dist/index.cjs +650 -428
- package/dist/index.d.cts +256 -45
- package/dist/index.d.ts +256 -45
- package/dist/index.js +638 -395
- package/package.json +19 -6
package/dist/index.cjs
CHANGED
|
@@ -42,9 +42,15 @@ __export(index_exports, {
|
|
|
42
42
|
ThemeProvider: () => ThemeProvider,
|
|
43
43
|
blockCatalogVersion: () => blockCatalogVersion,
|
|
44
44
|
buildBlockCatalog: () => buildBlockCatalog,
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
buildTelemetryEvent: () => import_core10.buildTelemetryEvent,
|
|
46
|
+
createLessonkitRuntime: () => import_core10.createLessonkitRuntime,
|
|
47
|
+
createPluginRegistry: () => import_core10.createPluginRegistry,
|
|
48
|
+
createTelemetryPipeline: () => import_core10.createTelemetryPipeline,
|
|
49
|
+
defineAssessmentPlugin: () => import_core10.defineAssessmentPlugin,
|
|
50
|
+
defineLifecyclePlugin: () => import_core10.defineLifecyclePlugin,
|
|
51
|
+
defineTelemetryPlugin: () => import_core10.defineTelemetryPlugin,
|
|
47
52
|
getBlockCatalogEntry: () => getBlockCatalogEntry,
|
|
53
|
+
resetQuizWarningsForTests: () => resetQuizWarningsForTests,
|
|
48
54
|
useCompletion: () => useCompletion,
|
|
49
55
|
useLessonkit: () => useLessonkit,
|
|
50
56
|
useProgress: () => useProgress,
|
|
@@ -55,240 +61,95 @@ __export(index_exports, {
|
|
|
55
61
|
module.exports = __toCommonJS(index_exports);
|
|
56
62
|
|
|
57
63
|
// src/components.tsx
|
|
58
|
-
var
|
|
64
|
+
var import_react5 = require("react");
|
|
59
65
|
var import_accessibility = require("@lessonkit/accessibility");
|
|
60
66
|
|
|
61
67
|
// src/context.tsx
|
|
68
|
+
var import_react2 = require("react");
|
|
69
|
+
|
|
70
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
62
71
|
var import_react = require("react");
|
|
63
|
-
var
|
|
64
|
-
var import_xapi3 = require("@lessonkit/xapi");
|
|
72
|
+
var import_core8 = require("@lessonkit/core");
|
|
65
73
|
var import_xapi4 = require("@lessonkit/xapi");
|
|
74
|
+
var import_xapi5 = require("@lessonkit/xapi");
|
|
66
75
|
|
|
67
76
|
// src/runtime/emitTelemetry.ts
|
|
77
|
+
var import_core2 = require("@lessonkit/core");
|
|
78
|
+
|
|
79
|
+
// src/runtime/telemetryPipeline.ts
|
|
68
80
|
var import_core = require("@lessonkit/core");
|
|
69
81
|
var import_xapi = require("@lessonkit/xapi");
|
|
70
82
|
|
|
71
83
|
// src/runtime/lxpackBridge.ts
|
|
72
84
|
var import_bridge = require("@lessonkit/lxpack/bridge");
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
if (fromSdk) return fromSdk;
|
|
76
|
-
if (typeof window === "undefined") return null;
|
|
77
|
-
const parent = window.parent;
|
|
78
|
-
if (!parent || parent === window) return null;
|
|
79
|
-
return parent.lxpack ?? null;
|
|
85
|
+
function forwardTelemetryToLxpack(event, mode = "auto") {
|
|
86
|
+
(0, import_bridge.forwardTelemetryToBridge)(event, mode);
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return;
|
|
87
|
-
case "completeCourse":
|
|
88
|
-
bridge.completeCourse?.();
|
|
89
|
-
return;
|
|
90
|
-
case "submitAssessment": {
|
|
91
|
-
const scaled = (0, import_bridge.normalizeScore)({
|
|
92
|
-
score: action.score,
|
|
93
|
-
maxScore: action.maxScore
|
|
94
|
-
});
|
|
95
|
-
if (scaled === null) return;
|
|
96
|
-
bridge.submitAssessment?.({
|
|
97
|
-
id: action.id,
|
|
98
|
-
score: scaled,
|
|
99
|
-
passingScore: (0, import_bridge.normalizePassingThreshold)({
|
|
100
|
-
passingScore: action.passingScore,
|
|
101
|
-
maxScore: action.maxScore
|
|
102
|
-
}),
|
|
103
|
-
maxScore: action.maxScore
|
|
104
|
-
});
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
case "track":
|
|
108
|
-
bridge.track?.(action.event);
|
|
109
|
-
return;
|
|
110
|
-
default:
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
88
|
+
|
|
89
|
+
// src/runtime/telemetryPipeline.ts
|
|
90
|
+
function isDevEnvironment() {
|
|
91
|
+
const g = globalThis;
|
|
92
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
113
93
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
94
|
+
function createLegacyPipeline(opts, extraSinks = []) {
|
|
95
|
+
return (0, import_core.createTelemetryPipeline)([
|
|
96
|
+
(0, import_core.createTrackingPipelineSink)("tracking", (event) => opts.tracking.track(event)),
|
|
97
|
+
{
|
|
98
|
+
id: "xapi",
|
|
99
|
+
emit(event) {
|
|
100
|
+
try {
|
|
101
|
+
const statement = (0, import_xapi.telemetryEventToXAPIStatement)(event);
|
|
102
|
+
if (statement) opts.xapi?.send(statement);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (isDevEnvironment()) {
|
|
105
|
+
console.warn(
|
|
106
|
+
"[lessonkit] xAPI mapping skipped:",
|
|
107
|
+
err instanceof Error ? err.message : err
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "lxpack-bridge",
|
|
115
|
+
emit(event) {
|
|
116
|
+
forwardTelemetryToLxpack(event, opts.lxpackBridge);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
...extraSinks
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
function emitThroughPipeline(event, opts, extraSinks) {
|
|
123
|
+
createLegacyPipeline(opts, extraSinks).emit(event);
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
// src/runtime/emitTelemetry.ts
|
|
125
127
|
var warnedMissingCourseId = false;
|
|
126
|
-
|
|
127
|
-
function isDevEnvironment() {
|
|
128
|
+
function isDevEnvironment2() {
|
|
128
129
|
const g = globalThis;
|
|
129
130
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
130
131
|
}
|
|
131
132
|
function emitTelemetry(tracking, xapi, event, opts) {
|
|
132
133
|
if (!event.courseId) {
|
|
133
|
-
if (
|
|
134
|
+
if (isDevEnvironment2() && !warnedMissingCourseId) {
|
|
134
135
|
warnedMissingCourseId = true;
|
|
135
136
|
console.warn("[lessonkit] telemetry event missing courseId");
|
|
136
137
|
}
|
|
137
138
|
return;
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
} catch (err) {
|
|
144
|
-
if (isDevEnvironment()) {
|
|
145
|
-
console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
forwardTelemetryToLxpack(event, opts?.lxpackBridge ?? "auto");
|
|
149
|
-
}
|
|
150
|
-
function buildTrackEvent(opts) {
|
|
151
|
-
const base = {
|
|
152
|
-
timestamp: (0, import_core.nowIso)(),
|
|
153
|
-
courseId: opts.courseId,
|
|
154
|
-
sessionId: opts.sessionId,
|
|
155
|
-
attemptId: opts.attemptId,
|
|
156
|
-
user: opts.user
|
|
140
|
+
const legacy = {
|
|
141
|
+
tracking,
|
|
142
|
+
xapi,
|
|
143
|
+
lxpackBridge: opts?.lxpackBridge ?? "auto"
|
|
157
144
|
};
|
|
158
|
-
|
|
159
|
-
case "course_started":
|
|
160
|
-
return { name: "course_started", ...base };
|
|
161
|
-
case "course_completed":
|
|
162
|
-
return { name: "course_completed", ...base };
|
|
163
|
-
case "lesson_started": {
|
|
164
|
-
const data = opts.data;
|
|
165
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
166
|
-
if (!lessonId) throw new Error("lesson_started requires lessonId");
|
|
167
|
-
return {
|
|
168
|
-
name: "lesson_started",
|
|
169
|
-
...base,
|
|
170
|
-
lessonId,
|
|
171
|
-
data: { ...data, lessonId }
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
case "lesson_completed":
|
|
175
|
-
case "lesson_time_on_task": {
|
|
176
|
-
const data = opts.data;
|
|
177
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
178
|
-
if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
|
|
179
|
-
return {
|
|
180
|
-
name: opts.name,
|
|
181
|
-
...base,
|
|
182
|
-
lessonId,
|
|
183
|
-
data: { ...data, lessonId }
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
case "quiz_answered": {
|
|
187
|
-
const data = opts.data;
|
|
188
|
-
const lessonId = opts.lessonId;
|
|
189
|
-
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
190
|
-
return { name: "quiz_answered", ...base, lessonId, data };
|
|
191
|
-
}
|
|
192
|
-
case "quiz_completed": {
|
|
193
|
-
const data = opts.data;
|
|
194
|
-
const lessonId = opts.lessonId;
|
|
195
|
-
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
196
|
-
return { name: "quiz_completed", ...base, lessonId, data };
|
|
197
|
-
}
|
|
198
|
-
case "interaction":
|
|
199
|
-
return {
|
|
200
|
-
name: "interaction",
|
|
201
|
-
...base,
|
|
202
|
-
lessonId: opts.lessonId,
|
|
203
|
-
data: opts.data
|
|
204
|
-
};
|
|
205
|
-
default:
|
|
206
|
-
return { name: opts.name, ...base };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
function tryBuildTrackEvent(opts) {
|
|
210
|
-
const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
|
|
211
|
-
if (isQuiz && !opts.lessonId) {
|
|
212
|
-
if (isDevEnvironment() && !warnedMissingQuizLesson) {
|
|
213
|
-
warnedMissingQuizLesson = true;
|
|
214
|
-
console.warn(
|
|
215
|
-
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
return buildTrackEvent(opts);
|
|
145
|
+
emitThroughPipeline(event, legacy, opts?.extraSinks);
|
|
221
146
|
}
|
|
222
147
|
|
|
223
148
|
// src/runtime/ports.ts
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
getItem: () => null,
|
|
227
|
-
setItem: () => {
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
function createSessionStoragePort() {
|
|
232
|
-
if (typeof sessionStorage === "undefined") return createNoopStorage();
|
|
233
|
-
return {
|
|
234
|
-
getItem: (key) => {
|
|
235
|
-
try {
|
|
236
|
-
return sessionStorage.getItem(key);
|
|
237
|
-
} catch {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
|
-
setItem: (key, value) => {
|
|
242
|
-
try {
|
|
243
|
-
sessionStorage.setItem(key, value);
|
|
244
|
-
} catch {
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
removeItem: (key) => {
|
|
248
|
-
try {
|
|
249
|
-
sessionStorage.removeItem(key);
|
|
250
|
-
} catch {
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
}
|
|
149
|
+
var import_core3 = require("@lessonkit/core");
|
|
255
150
|
|
|
256
151
|
// src/runtime/progress.ts
|
|
257
|
-
|
|
258
|
-
let activeLessonId;
|
|
259
|
-
let completedLessonIds = /* @__PURE__ */ new Set();
|
|
260
|
-
let courseCompleted = false;
|
|
261
|
-
const lessonStartTimes = /* @__PURE__ */ new Map();
|
|
262
|
-
return {
|
|
263
|
-
getState: () => ({
|
|
264
|
-
activeLessonId,
|
|
265
|
-
completedLessonIds: new Set(completedLessonIds),
|
|
266
|
-
courseCompleted
|
|
267
|
-
}),
|
|
268
|
-
setActiveLesson: (lessonId, startedAtMs) => {
|
|
269
|
-
const previousLessonId = activeLessonId;
|
|
270
|
-
activeLessonId = lessonId;
|
|
271
|
-
lessonStartTimes.set(lessonId, startedAtMs);
|
|
272
|
-
return { previousLessonId };
|
|
273
|
-
},
|
|
274
|
-
completeLesson: (lessonId, completedAtMs) => {
|
|
275
|
-
if (completedLessonIds.has(lessonId)) return { didComplete: false };
|
|
276
|
-
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
277
|
-
if (activeLessonId === lessonId) {
|
|
278
|
-
activeLessonId = void 0;
|
|
279
|
-
}
|
|
280
|
-
const startedAt = lessonStartTimes.get(lessonId);
|
|
281
|
-
lessonStartTimes.delete(lessonId);
|
|
282
|
-
const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
|
|
283
|
-
return { durationMs, didComplete: true };
|
|
284
|
-
},
|
|
285
|
-
completeCourse: () => {
|
|
286
|
-
if (courseCompleted) return { didComplete: false };
|
|
287
|
-
courseCompleted = true;
|
|
288
|
-
return { didComplete: true };
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
}
|
|
152
|
+
var import_core4 = require("@lessonkit/core");
|
|
292
153
|
|
|
293
154
|
// src/runtime/xapi.ts
|
|
294
155
|
var import_xapi2 = require("@lessonkit/xapi");
|
|
@@ -306,64 +167,60 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
306
167
|
}
|
|
307
168
|
|
|
308
169
|
// src/runtime/session.ts
|
|
309
|
-
var
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return id;
|
|
322
|
-
}
|
|
323
|
-
function courseStartedStorageKey(sessionId, courseId) {
|
|
324
|
-
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
325
|
-
}
|
|
326
|
-
function hasCourseStarted(storage, sessionId, courseId) {
|
|
327
|
-
if (!courseId) return false;
|
|
328
|
-
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
329
|
-
}
|
|
330
|
-
function markCourseStarted(storage, sessionId, courseId) {
|
|
331
|
-
if (!courseId) return;
|
|
332
|
-
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
333
|
-
}
|
|
334
|
-
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
335
|
-
if (!courseId || fromSessionId === toSessionId) return;
|
|
336
|
-
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
337
|
-
markCourseStarted(storage, toSessionId, courseId);
|
|
338
|
-
storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
|
|
170
|
+
var import_core5 = require("@lessonkit/core");
|
|
171
|
+
|
|
172
|
+
// src/runtime/courseStartedPipeline.ts
|
|
173
|
+
var import_xapi3 = require("@lessonkit/xapi");
|
|
174
|
+
function emitCourseStartedNonTrackingPipeline(opts) {
|
|
175
|
+
let xapiStatementSent = false;
|
|
176
|
+
if (!opts.skipXapi && opts.xapi) {
|
|
177
|
+
const statement = (0, import_xapi3.telemetryEventToXAPIStatement)(opts.event);
|
|
178
|
+
if (statement) {
|
|
179
|
+
opts.xapi.send(statement);
|
|
180
|
+
xapiStatementSent = true;
|
|
181
|
+
}
|
|
339
182
|
}
|
|
183
|
+
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
|
|
184
|
+
const emitCtx = {
|
|
185
|
+
courseId: opts.event.courseId,
|
|
186
|
+
sessionId: opts.event.sessionId,
|
|
187
|
+
attemptId: opts.event.attemptId
|
|
188
|
+
};
|
|
189
|
+
for (const sink of opts.extraSinks ?? []) {
|
|
190
|
+
sink.emit(opts.event, emitCtx);
|
|
191
|
+
}
|
|
192
|
+
return { xapiStatementSent };
|
|
340
193
|
}
|
|
341
194
|
|
|
342
195
|
// src/runtime/plugins.ts
|
|
343
|
-
var
|
|
196
|
+
var import_core6 = require("@lessonkit/core");
|
|
344
197
|
function createReactPluginHost(plugins) {
|
|
345
198
|
if (!plugins?.length) return null;
|
|
346
|
-
return (0,
|
|
199
|
+
return (0, import_core6.createPluginRegistry)(plugins);
|
|
347
200
|
}
|
|
348
201
|
function buildPluginContext(opts) {
|
|
349
202
|
return {
|
|
350
203
|
courseId: opts.courseId,
|
|
351
204
|
sessionId: opts.sessionId,
|
|
352
|
-
attemptId: opts.attemptId
|
|
205
|
+
attemptId: opts.attemptId,
|
|
206
|
+
user: opts.user
|
|
353
207
|
};
|
|
354
208
|
}
|
|
355
209
|
function emitTelemetryWithPlugins(opts) {
|
|
356
210
|
const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
|
|
357
211
|
if (next === null) return;
|
|
358
|
-
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
212
|
+
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
213
|
+
lxpackBridge: opts.lxpackBridge ?? "auto",
|
|
214
|
+
extraSinks: opts.extraSinks
|
|
215
|
+
});
|
|
359
216
|
}
|
|
360
217
|
|
|
361
218
|
// src/runtime/telemetry.ts
|
|
362
|
-
var
|
|
219
|
+
var import_core7 = require("@lessonkit/core");
|
|
363
220
|
function createTrackingClientFromConfig(config) {
|
|
364
|
-
if (config.tracking?.enabled === false) return (0,
|
|
221
|
+
if (config.tracking?.enabled === false) return (0, import_core7.createTrackingClient)();
|
|
365
222
|
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
366
|
-
return (0,
|
|
223
|
+
return (0, import_core7.createTrackingClient)({
|
|
367
224
|
sink: config.tracking?.sink,
|
|
368
225
|
batchSink: config.tracking?.batchSink,
|
|
369
226
|
batch: config.tracking?.batch
|
|
@@ -380,90 +237,236 @@ async function disposeTrackingClient(client) {
|
|
|
380
237
|
}
|
|
381
238
|
}
|
|
382
239
|
|
|
383
|
-
// src/
|
|
384
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
385
|
-
var LessonkitContext = (0, import_react.createContext)(null);
|
|
240
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
386
241
|
var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
|
|
387
|
-
var defaultStorage = createSessionStoragePort();
|
|
242
|
+
var defaultStorage = (0, import_core3.createSessionStoragePort)();
|
|
388
243
|
function isTrackingActive(tracking) {
|
|
389
244
|
return tracking?.enabled !== false;
|
|
390
245
|
}
|
|
391
|
-
function
|
|
246
|
+
function isCourseStartedSinkSettled(result) {
|
|
247
|
+
return result === "emitted";
|
|
248
|
+
}
|
|
249
|
+
function buildCourseStartedEvent(opts) {
|
|
392
250
|
const pluginCtx = buildPluginContext({
|
|
393
251
|
courseId: opts.courseId,
|
|
394
252
|
sessionId: opts.sessionId,
|
|
395
|
-
attemptId: opts.attemptId
|
|
253
|
+
attemptId: opts.attemptId,
|
|
254
|
+
user: opts.user
|
|
255
|
+
});
|
|
256
|
+
const built = (0, import_core2.buildTelemetryEvent)({
|
|
257
|
+
name: "course_started",
|
|
258
|
+
courseId: opts.courseId,
|
|
259
|
+
sessionId: opts.sessionId,
|
|
260
|
+
attemptId: opts.attemptId,
|
|
261
|
+
user: opts.user
|
|
396
262
|
});
|
|
263
|
+
return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
|
|
264
|
+
}
|
|
265
|
+
function emitCourseStartedPipelineOnly(opts) {
|
|
397
266
|
try {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
tracking: opts.tracking,
|
|
267
|
+
const { xapiStatementSent } = emitCourseStartedNonTrackingPipeline({
|
|
268
|
+
event: opts.event,
|
|
401
269
|
xapi: opts.xapi,
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
sessionId: opts.sessionId,
|
|
406
|
-
attemptId: opts.attemptId,
|
|
407
|
-
user: opts.user
|
|
408
|
-
}),
|
|
409
|
-
pluginCtx,
|
|
410
|
-
lxpackBridge: opts.lxpackBridge
|
|
270
|
+
lxpackBridge: opts.lxpackBridge,
|
|
271
|
+
extraSinks: opts.extraSinks,
|
|
272
|
+
skipXapi: opts.skipXapi
|
|
411
273
|
});
|
|
412
|
-
markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
413
|
-
|
|
274
|
+
(0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
275
|
+
(0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
|
|
276
|
+
if (xapiStatementSent) {
|
|
277
|
+
opts.onXapiStatementSent?.();
|
|
278
|
+
}
|
|
279
|
+
return "emitted";
|
|
414
280
|
} catch {
|
|
415
|
-
return
|
|
281
|
+
return "failed";
|
|
416
282
|
}
|
|
417
283
|
}
|
|
418
|
-
function
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
284
|
+
function emitCourseStarted(opts) {
|
|
285
|
+
const event = buildCourseStartedEvent(opts);
|
|
286
|
+
if (event === null) return "filtered";
|
|
287
|
+
const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
288
|
+
opts.storage,
|
|
289
|
+
opts.sessionId,
|
|
290
|
+
opts.courseId
|
|
291
|
+
);
|
|
292
|
+
if (!trackingAlreadyEmitted) {
|
|
293
|
+
try {
|
|
294
|
+
opts.tracking.track(event);
|
|
295
|
+
(0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
|
|
296
|
+
} catch {
|
|
297
|
+
return "failed";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return emitCourseStartedPipelineOnly({
|
|
301
|
+
...opts,
|
|
302
|
+
event,
|
|
303
|
+
skipXapi: opts.skipXapi,
|
|
304
|
+
onXapiStatementSent: opts.onXapiStatementSent
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function emitCourseStartedToTrackingOnly(opts) {
|
|
308
|
+
const event = buildCourseStartedEvent(opts);
|
|
309
|
+
if (event === null) return "filtered";
|
|
310
|
+
const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
311
|
+
opts.storage,
|
|
312
|
+
opts.sessionId,
|
|
313
|
+
opts.courseId
|
|
314
|
+
);
|
|
315
|
+
if (!trackingAlreadyEmitted) {
|
|
316
|
+
try {
|
|
317
|
+
opts.tracking.track(event);
|
|
318
|
+
(0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
|
|
319
|
+
} catch {
|
|
320
|
+
return "failed";
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
emitCourseStartedNonTrackingPipeline({
|
|
325
|
+
event,
|
|
326
|
+
xapi: null,
|
|
327
|
+
lxpackBridge: opts.lxpackBridge,
|
|
328
|
+
extraSinks: opts.extraSinks,
|
|
329
|
+
skipXapi: true
|
|
330
|
+
});
|
|
331
|
+
(0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
|
|
332
|
+
return "emitted";
|
|
333
|
+
} catch {
|
|
334
|
+
return "failed";
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function emitPendingCourseStarted(opts) {
|
|
338
|
+
const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
339
|
+
opts.storage,
|
|
340
|
+
opts.sessionId,
|
|
341
|
+
opts.courseId
|
|
342
|
+
);
|
|
343
|
+
const sessionStarted = (0, import_core5.hasCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
344
|
+
if (sessionStarted && !trackingEmitted) {
|
|
345
|
+
return emitCourseStartedToTrackingOnly(opts);
|
|
346
|
+
}
|
|
347
|
+
if (trackingEmitted && !sessionStarted) {
|
|
348
|
+
const event = buildCourseStartedEvent(opts);
|
|
349
|
+
if (event === null) return "filtered";
|
|
350
|
+
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
351
|
+
}
|
|
352
|
+
if (!trackingEmitted && !sessionStarted) {
|
|
353
|
+
return emitCourseStarted(opts);
|
|
354
|
+
}
|
|
355
|
+
const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
|
|
356
|
+
opts.storage,
|
|
357
|
+
opts.sessionId,
|
|
358
|
+
opts.courseId
|
|
359
|
+
);
|
|
360
|
+
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
361
|
+
const event = buildCourseStartedEvent(opts);
|
|
362
|
+
if (event === null) return "filtered";
|
|
363
|
+
return emitCourseStartedPipelineOnly({
|
|
364
|
+
...opts,
|
|
365
|
+
event,
|
|
366
|
+
skipXapi: opts.skipXapi,
|
|
367
|
+
onXapiStatementSent: opts.onXapiStatementSent
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return "emitted";
|
|
371
|
+
}
|
|
372
|
+
function assertTrackingSinkConfig(tracking) {
|
|
373
|
+
if (!tracking?.sink || !tracking?.batchSink) return;
|
|
374
|
+
throw new Error(
|
|
375
|
+
"[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
function useLessonkitProviderRuntime(config) {
|
|
379
|
+
const normalizedCourseId = (0, import_react.useMemo)(
|
|
380
|
+
() => (0, import_core8.assertValidId)(config.courseId, "courseId"),
|
|
381
|
+
[config.courseId]
|
|
382
|
+
);
|
|
383
|
+
const normalizedConfig = (0, import_react.useMemo)(
|
|
384
|
+
() => ({ ...config, courseId: normalizedCourseId }),
|
|
385
|
+
[config, normalizedCourseId]
|
|
386
|
+
);
|
|
387
|
+
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
388
|
+
const extraSinksRef = (0, import_react.useRef)(normalizedConfig.sinks);
|
|
389
|
+
extraSinksRef.current = normalizedConfig.sinks;
|
|
390
|
+
const headlessRef = (0, import_react.useRef)(null);
|
|
391
|
+
const sessionIdRef = (0, import_react.useRef)((0, import_core5.resolveSessionId)(defaultStorage, normalizedConfig.session?.sessionId));
|
|
392
|
+
const prevConfiguredSessionIdRef = (0, import_react.useRef)(normalizedConfig.session?.sessionId);
|
|
393
|
+
if (normalizedConfig.session?.sessionId) {
|
|
394
|
+
sessionIdRef.current = normalizedConfig.session.sessionId;
|
|
424
395
|
} else if (prevConfiguredSessionIdRef.current) {
|
|
425
|
-
sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
|
|
396
|
+
sessionIdRef.current = (0, import_core5.resolveSessionId)(defaultStorage, void 0);
|
|
426
397
|
}
|
|
427
|
-
const attemptIdRef = (0, import_react.useRef)(
|
|
428
|
-
const userRef = (0, import_react.useRef)(
|
|
429
|
-
attemptIdRef.current =
|
|
430
|
-
userRef.current =
|
|
431
|
-
const courseIdRef = (0, import_react.useRef)(
|
|
432
|
-
courseIdRef.current =
|
|
433
|
-
const lxpackBridgeModeRef = (0, import_react.useRef)(
|
|
434
|
-
lxpackBridgeModeRef.current =
|
|
435
|
-
const pluginHost = (0, import_react.useMemo)(() => createReactPluginHost(
|
|
398
|
+
const attemptIdRef = (0, import_react.useRef)(normalizedConfig.session?.attemptId);
|
|
399
|
+
const userRef = (0, import_react.useRef)(normalizedConfig.session?.user);
|
|
400
|
+
attemptIdRef.current = normalizedConfig.session?.attemptId;
|
|
401
|
+
userRef.current = normalizedConfig.session?.user;
|
|
402
|
+
const courseIdRef = (0, import_react.useRef)(normalizedCourseId);
|
|
403
|
+
courseIdRef.current = normalizedCourseId;
|
|
404
|
+
const lxpackBridgeModeRef = (0, import_react.useRef)(normalizedConfig.lxpack?.bridge ?? "auto");
|
|
405
|
+
lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
|
|
406
|
+
const pluginHost = (0, import_react.useMemo)(() => createReactPluginHost(normalizedConfig.plugins), [normalizedConfig.plugins]);
|
|
436
407
|
const pluginHostRef = (0, import_react.useRef)(pluginHost);
|
|
437
408
|
pluginHostRef.current = pluginHost;
|
|
438
|
-
const progressRef = (0, import_react.useRef)(createProgressController());
|
|
409
|
+
const progressRef = (0, import_react.useRef)((0, import_core4.createProgressController)());
|
|
439
410
|
const courseStartedEmittedToSinkRef = (0, import_react.useRef)(false);
|
|
440
|
-
const prevCourseIdForProgressRef = (0, import_react.useRef)(
|
|
411
|
+
const prevCourseIdForProgressRef = (0, import_react.useRef)(normalizedCourseId);
|
|
441
412
|
const pendingCourseIdResetRef = (0, import_react.useRef)(false);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
413
|
+
const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
|
|
414
|
+
const xapiCourseStartedSentOnClientRef = (0, import_react.useRef)(false);
|
|
415
|
+
if (prevUseV2RuntimeRef.current !== useV2Runtime) {
|
|
416
|
+
prevUseV2RuntimeRef.current = useV2Runtime;
|
|
417
|
+
if (useV2Runtime) {
|
|
418
|
+
headlessRef.current = (0, import_core8.createLessonkitRuntime)({
|
|
419
|
+
courseId: normalizedCourseId,
|
|
420
|
+
runtimeVersion: "v2",
|
|
421
|
+
session: normalizedConfig.session
|
|
422
|
+
});
|
|
423
|
+
progressRef.current = headlessRef.current.progress;
|
|
424
|
+
} else {
|
|
425
|
+
headlessRef.current = null;
|
|
426
|
+
progressRef.current = (0, import_core4.createProgressController)();
|
|
427
|
+
}
|
|
428
|
+
pendingCourseIdResetRef.current = true;
|
|
429
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
430
|
+
} else if (useV2Runtime && !headlessRef.current) {
|
|
431
|
+
headlessRef.current = (0, import_core8.createLessonkitRuntime)({
|
|
432
|
+
courseId: normalizedCourseId,
|
|
433
|
+
runtimeVersion: "v2",
|
|
434
|
+
session: normalizedConfig.session
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
438
|
+
prevCourseIdForProgressRef.current = normalizedCourseId;
|
|
439
|
+
if (useV2Runtime && headlessRef.current) {
|
|
440
|
+
headlessRef.current.resetForCourseChange(normalizedCourseId);
|
|
441
|
+
progressRef.current = headlessRef.current.progress;
|
|
442
|
+
} else {
|
|
443
|
+
progressRef.current = (0, import_core4.createProgressController)();
|
|
444
|
+
}
|
|
445
445
|
pendingCourseIdResetRef.current = true;
|
|
446
446
|
courseStartedEmittedToSinkRef.current = false;
|
|
447
447
|
}
|
|
448
|
+
if (useV2Runtime && headlessRef.current) {
|
|
449
|
+
progressRef.current = headlessRef.current.progress;
|
|
450
|
+
}
|
|
448
451
|
const [progress, setProgress] = (0, import_react.useState)(() => progressRef.current.getState());
|
|
449
452
|
const syncProgress = (0, import_react.useCallback)(() => {
|
|
450
453
|
setProgress(progressRef.current.getState());
|
|
451
454
|
}, []);
|
|
452
455
|
const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
|
|
453
456
|
activeLessonIdRef.current = progress.activeLessonId;
|
|
454
|
-
const xapiQueueRef = (0, import_react.useRef)((0,
|
|
457
|
+
const xapiQueueRef = (0, import_react.useRef)((0, import_xapi4.createInMemoryXAPIQueue)());
|
|
455
458
|
const xapiRef = (0, import_react.useRef)(null);
|
|
456
459
|
const [xapi, setXapi] = (0, import_react.useState)(null);
|
|
457
|
-
const prevXapiCourseIdRef = (0, import_react.useRef)(
|
|
458
|
-
const xapiEnabled =
|
|
459
|
-
const xapiClient =
|
|
460
|
-
const xapiTransport =
|
|
461
|
-
const courseId =
|
|
462
|
-
const trackingEnabled =
|
|
460
|
+
const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
|
|
461
|
+
const xapiEnabled = normalizedConfig.xapi?.enabled;
|
|
462
|
+
const xapiClient = normalizedConfig.xapi?.client;
|
|
463
|
+
const xapiTransport = normalizedConfig.xapi?.transport;
|
|
464
|
+
const courseId = normalizedCourseId;
|
|
465
|
+
const trackingEnabled = normalizedConfig.tracking?.enabled;
|
|
463
466
|
useIsoLayoutEffect(() => {
|
|
464
467
|
const courseChanged = prevXapiCourseIdRef.current !== courseId;
|
|
465
468
|
if (courseChanged) {
|
|
466
|
-
if (
|
|
469
|
+
if (normalizedConfig.xapi?.client) {
|
|
467
470
|
const g = globalThis;
|
|
468
471
|
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
|
|
469
472
|
console.warn(
|
|
@@ -472,32 +475,42 @@ function LessonkitProvider(props) {
|
|
|
472
475
|
}
|
|
473
476
|
void xapiRef.current?.flush();
|
|
474
477
|
}
|
|
475
|
-
xapiQueueRef.current = (0,
|
|
478
|
+
xapiQueueRef.current = (0, import_xapi4.createInMemoryXAPIQueue)();
|
|
476
479
|
prevXapiCourseIdRef.current = courseId;
|
|
480
|
+
xapiCourseStartedSentOnClientRef.current = false;
|
|
477
481
|
}
|
|
478
482
|
const prev = xapiRef.current;
|
|
479
|
-
const next = createXapiClientFromConfig(
|
|
483
|
+
const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
|
|
480
484
|
xapiRef.current = next;
|
|
481
485
|
setXapi(next);
|
|
482
|
-
if (next
|
|
486
|
+
if (next) {
|
|
483
487
|
const sessionId = sessionIdRef.current;
|
|
484
488
|
const cid = courseIdRef.current;
|
|
485
|
-
const trackingActive = isTrackingActive(
|
|
486
|
-
const alreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
|
|
487
|
-
|
|
489
|
+
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
490
|
+
const alreadyStarted = (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid);
|
|
491
|
+
const clientChanged = !prev || prev !== next;
|
|
492
|
+
const skipBootstrap = trackingActive && !alreadyStarted;
|
|
493
|
+
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
|
|
494
|
+
if (needsBootstrap) {
|
|
488
495
|
try {
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
496
|
+
const event = buildCourseStartedEvent({
|
|
497
|
+
pluginHost: pluginHostRef.current,
|
|
498
|
+
courseId: cid,
|
|
499
|
+
sessionId,
|
|
500
|
+
attemptId: attemptIdRef.current,
|
|
501
|
+
user: userRef.current,
|
|
502
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
503
|
+
});
|
|
504
|
+
if (event === null) {
|
|
505
|
+
} else {
|
|
506
|
+
const statement = (0, import_xapi5.telemetryEventToXAPIStatement)(event);
|
|
507
|
+
if (statement) {
|
|
508
|
+
next.send(statement);
|
|
509
|
+
if (!alreadyStarted) {
|
|
510
|
+
(0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
|
|
511
|
+
}
|
|
512
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
513
|
+
}
|
|
501
514
|
}
|
|
502
515
|
} catch {
|
|
503
516
|
}
|
|
@@ -522,46 +535,56 @@ function LessonkitProvider(props) {
|
|
|
522
535
|
void prev?.flush();
|
|
523
536
|
};
|
|
524
537
|
}, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
|
|
525
|
-
const trackingRef = (0, import_react.useRef)((0,
|
|
538
|
+
const trackingRef = (0, import_react.useRef)((0, import_core8.createTrackingClient)());
|
|
526
539
|
const trackingClientForUnmountRef = (0, import_react.useRef)(trackingRef.current);
|
|
527
540
|
const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
|
|
528
|
-
const trackingSink =
|
|
529
|
-
const trackingBatchSink =
|
|
530
|
-
const batchEnabled =
|
|
531
|
-
const batchFlushIntervalMs =
|
|
532
|
-
const batchMaxBatchSize =
|
|
541
|
+
const trackingSink = normalizedConfig.tracking?.sink;
|
|
542
|
+
const trackingBatchSink = normalizedConfig.tracking?.batchSink;
|
|
543
|
+
const batchEnabled = normalizedConfig.tracking?.batch?.enabled;
|
|
544
|
+
const batchFlushIntervalMs = normalizedConfig.tracking?.batch?.flushIntervalMs;
|
|
545
|
+
const batchMaxBatchSize = normalizedConfig.tracking?.batch?.maxBatchSize;
|
|
533
546
|
const buildCurrentPluginCtx = (0, import_react.useCallback)(
|
|
534
547
|
() => buildPluginContext({
|
|
535
548
|
courseId: courseIdRef.current,
|
|
536
549
|
sessionId: sessionIdRef.current,
|
|
537
|
-
attemptId: attemptIdRef.current
|
|
550
|
+
attemptId: attemptIdRef.current,
|
|
551
|
+
user: userRef.current
|
|
538
552
|
}),
|
|
539
553
|
[]
|
|
540
554
|
);
|
|
541
555
|
useIsoLayoutEffect(() => {
|
|
542
556
|
const prev = trackingRef.current;
|
|
543
|
-
const baseSink =
|
|
557
|
+
const baseSink = normalizedConfig.tracking?.sink;
|
|
558
|
+
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
559
|
+
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
544
560
|
const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
|
|
545
|
-
const batchSink = pluginHostRef.current &&
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
561
|
+
const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
|
|
562
|
+
const host = pluginHostRef.current;
|
|
563
|
+
const ctx = buildCurrentPluginCtx();
|
|
564
|
+
const delivered = host.deliverTelemetryBatch(events, ctx);
|
|
565
|
+
const perEventForBatch = [];
|
|
566
|
+
const collector = (event) => {
|
|
567
|
+
perEventForBatch.push(event);
|
|
568
|
+
};
|
|
569
|
+
const composedPerEvent = host.composeTrackingSink(collector, buildCurrentPluginCtx) ?? collector;
|
|
570
|
+
for (const event of delivered) {
|
|
571
|
+
await Promise.resolve(composedPerEvent(event));
|
|
572
|
+
}
|
|
573
|
+
return userBatchSink(perEventForBatch);
|
|
574
|
+
} : userBatchSink;
|
|
552
575
|
const next = createTrackingClientFromConfig({
|
|
553
|
-
tracking: { ...
|
|
576
|
+
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
554
577
|
});
|
|
555
578
|
trackingRef.current = next;
|
|
556
579
|
trackingClientForUnmountRef.current = next;
|
|
557
580
|
setTracking(next);
|
|
558
581
|
const sessionId = sessionIdRef.current;
|
|
559
582
|
const cid = courseIdRef.current;
|
|
560
|
-
const trackingActive = isTrackingActive(
|
|
583
|
+
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
561
584
|
if (!trackingActive) {
|
|
562
585
|
courseStartedEmittedToSinkRef.current = false;
|
|
563
|
-
} else if (!courseStartedEmittedToSinkRef.current
|
|
564
|
-
const
|
|
586
|
+
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
587
|
+
const result = emitPendingCourseStarted({
|
|
565
588
|
pluginHost: pluginHostRef.current,
|
|
566
589
|
tracking: next,
|
|
567
590
|
xapi: xapiRef.current,
|
|
@@ -570,9 +593,14 @@ function LessonkitProvider(props) {
|
|
|
570
593
|
courseId: cid,
|
|
571
594
|
attemptId: attemptIdRef.current,
|
|
572
595
|
user: userRef.current,
|
|
573
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
596
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
597
|
+
extraSinks: extraSinksRef.current,
|
|
598
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
599
|
+
onXapiStatementSent: () => {
|
|
600
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
601
|
+
}
|
|
574
602
|
});
|
|
575
|
-
courseStartedEmittedToSinkRef.current =
|
|
603
|
+
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
576
604
|
} else if (trackingActive) {
|
|
577
605
|
courseStartedEmittedToSinkRef.current = true;
|
|
578
606
|
}
|
|
@@ -588,8 +616,8 @@ function LessonkitProvider(props) {
|
|
|
588
616
|
batchEnabled,
|
|
589
617
|
batchFlushIntervalMs,
|
|
590
618
|
batchMaxBatchSize,
|
|
591
|
-
|
|
592
|
-
|
|
619
|
+
normalizedConfig.plugins,
|
|
620
|
+
normalizedCourseId,
|
|
593
621
|
buildCurrentPluginCtx
|
|
594
622
|
]);
|
|
595
623
|
const emitWithBridge = (0, import_react.useCallback)((trackingClient, event) => {
|
|
@@ -601,14 +629,32 @@ function LessonkitProvider(props) {
|
|
|
601
629
|
pluginCtx: buildPluginContext({
|
|
602
630
|
courseId: courseIdRef.current,
|
|
603
631
|
sessionId: sessionIdRef.current,
|
|
604
|
-
attemptId: attemptIdRef.current
|
|
632
|
+
attemptId: attemptIdRef.current,
|
|
633
|
+
user: userRef.current
|
|
605
634
|
}),
|
|
606
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
635
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
636
|
+
extraSinks: extraSinksRef.current
|
|
607
637
|
});
|
|
608
638
|
}, []);
|
|
639
|
+
const emitLifecycleEvent = (0, import_react.useCallback)(
|
|
640
|
+
(name, data, lessonId) => {
|
|
641
|
+
const event = (0, import_core2.tryBuildTelemetryEvent)({
|
|
642
|
+
name,
|
|
643
|
+
courseId: courseIdRef.current,
|
|
644
|
+
lessonId: lessonId ?? activeLessonIdRef.current,
|
|
645
|
+
sessionId: sessionIdRef.current,
|
|
646
|
+
attemptId: attemptIdRef.current,
|
|
647
|
+
user: userRef.current,
|
|
648
|
+
data
|
|
649
|
+
});
|
|
650
|
+
if (!event) return;
|
|
651
|
+
emitWithBridge(trackingRef.current, event);
|
|
652
|
+
},
|
|
653
|
+
[emitWithBridge]
|
|
654
|
+
);
|
|
609
655
|
const track = (0, import_react.useCallback)(
|
|
610
656
|
(name, data, opts) => {
|
|
611
|
-
const event =
|
|
657
|
+
const event = (0, import_core2.tryBuildTelemetryEvent)({
|
|
612
658
|
name,
|
|
613
659
|
courseId: courseIdRef.current,
|
|
614
660
|
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
@@ -626,7 +672,7 @@ function LessonkitProvider(props) {
|
|
|
626
672
|
if (!pendingCourseIdResetRef.current) return;
|
|
627
673
|
pendingCourseIdResetRef.current = false;
|
|
628
674
|
syncProgress();
|
|
629
|
-
if (!isTrackingActive(
|
|
675
|
+
if (!isTrackingActive(normalizedConfig.tracking)) return;
|
|
630
676
|
const sessionId = sessionIdRef.current;
|
|
631
677
|
const cid = courseIdRef.current;
|
|
632
678
|
void (async () => {
|
|
@@ -634,8 +680,8 @@ function LessonkitProvider(props) {
|
|
|
634
680
|
await trackingRef.current?.flush?.();
|
|
635
681
|
} catch {
|
|
636
682
|
}
|
|
637
|
-
if (!courseStartedEmittedToSinkRef.current
|
|
638
|
-
const
|
|
683
|
+
if (!courseStartedEmittedToSinkRef.current) {
|
|
684
|
+
const result = emitPendingCourseStarted({
|
|
639
685
|
pluginHost: pluginHostRef.current,
|
|
640
686
|
tracking: trackingRef.current,
|
|
641
687
|
xapi: xapiRef.current,
|
|
@@ -644,12 +690,13 @@ function LessonkitProvider(props) {
|
|
|
644
690
|
courseId: cid,
|
|
645
691
|
attemptId: attemptIdRef.current,
|
|
646
692
|
user: userRef.current,
|
|
647
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
693
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
694
|
+
extraSinks: extraSinksRef.current
|
|
648
695
|
});
|
|
649
|
-
courseStartedEmittedToSinkRef.current =
|
|
696
|
+
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
650
697
|
}
|
|
651
698
|
})();
|
|
652
|
-
}, [
|
|
699
|
+
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
|
|
653
700
|
const emitLessonCompleted = (0, import_react.useCallback)(
|
|
654
701
|
(lessonId, durationMs) => {
|
|
655
702
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -661,13 +708,19 @@ function LessonkitProvider(props) {
|
|
|
661
708
|
);
|
|
662
709
|
const completeLesson = (0, import_react.useCallback)(
|
|
663
710
|
(lessonId) => {
|
|
711
|
+
if (useV2Runtime && headlessRef.current) {
|
|
712
|
+
headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
|
|
713
|
+
syncProgress();
|
|
714
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
664
717
|
const result = progressRef.current.completeLesson(lessonId, Date.now());
|
|
665
718
|
if (!result.didComplete) return;
|
|
666
719
|
syncProgress();
|
|
667
720
|
emitLessonCompleted(lessonId, result.durationMs);
|
|
668
721
|
void Promise.resolve(trackingRef.current?.flush?.());
|
|
669
722
|
},
|
|
670
|
-
[syncProgress, emitLessonCompleted]
|
|
723
|
+
[syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
671
724
|
);
|
|
672
725
|
(0, import_react.useEffect)(() => {
|
|
673
726
|
return () => {
|
|
@@ -691,8 +744,19 @@ function LessonkitProvider(props) {
|
|
|
691
744
|
}, []);
|
|
692
745
|
const setActiveLesson = (0, import_react.useCallback)(
|
|
693
746
|
(lessonId) => {
|
|
747
|
+
if (useV2Runtime && headlessRef.current) {
|
|
748
|
+
headlessRef.current.setActiveLesson(lessonId, emitLifecycleEvent);
|
|
749
|
+
syncProgress();
|
|
750
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
694
753
|
const current = progressRef.current.getState();
|
|
695
754
|
if (current.activeLessonId === lessonId) return;
|
|
755
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
756
|
+
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
757
|
+
syncProgress();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
696
760
|
const previous = current.activeLessonId;
|
|
697
761
|
if (previous && previous !== lessonId) {
|
|
698
762
|
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
@@ -705,9 +769,15 @@ function LessonkitProvider(props) {
|
|
|
705
769
|
syncProgress();
|
|
706
770
|
track("lesson_started", { lessonId }, { lessonId });
|
|
707
771
|
},
|
|
708
|
-
[track, syncProgress, emitLessonCompleted]
|
|
772
|
+
[track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
709
773
|
);
|
|
710
774
|
const completeCourse = (0, import_react.useCallback)(() => {
|
|
775
|
+
if (useV2Runtime && headlessRef.current) {
|
|
776
|
+
headlessRef.current.completeCourse(emitLifecycleEvent);
|
|
777
|
+
syncProgress();
|
|
778
|
+
void trackingRef.current?.flush?.();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
711
781
|
const current = progressRef.current.getState();
|
|
712
782
|
if (current.activeLessonId) {
|
|
713
783
|
const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
|
|
@@ -720,24 +790,37 @@ function LessonkitProvider(props) {
|
|
|
720
790
|
syncProgress();
|
|
721
791
|
track("course_completed");
|
|
722
792
|
void trackingRef.current?.flush?.();
|
|
723
|
-
}, [track, syncProgress, emitLessonCompleted]);
|
|
724
|
-
const sessionUser =
|
|
725
|
-
const
|
|
726
|
-
|
|
793
|
+
}, [track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]);
|
|
794
|
+
const sessionUser = normalizedConfig.session?.user;
|
|
795
|
+
const sessionUserKey = (0, import_react.useMemo)(
|
|
796
|
+
() => sessionUser ? JSON.stringify(sessionUser) : "",
|
|
797
|
+
[sessionUser]
|
|
798
|
+
);
|
|
799
|
+
const sessionAttemptId = normalizedConfig.session?.attemptId;
|
|
800
|
+
const sessionConfiguredId = normalizedConfig.session?.sessionId;
|
|
801
|
+
(0, import_react.useEffect)(() => {
|
|
802
|
+
if (useV2Runtime && headlessRef.current) {
|
|
803
|
+
headlessRef.current.updateConfig({
|
|
804
|
+
courseId: normalizedCourseId,
|
|
805
|
+
session: normalizedConfig.session
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}, [useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey, normalizedConfig.session]);
|
|
727
809
|
(0, import_react.useEffect)(() => {
|
|
728
810
|
if (!pluginHost) return;
|
|
729
811
|
const ctx = buildPluginContext({
|
|
730
812
|
courseId: courseIdRef.current,
|
|
731
813
|
sessionId: sessionIdRef.current,
|
|
732
|
-
attemptId: attemptIdRef.current
|
|
814
|
+
attemptId: attemptIdRef.current,
|
|
815
|
+
user: userRef.current
|
|
733
816
|
});
|
|
734
817
|
pluginHost.setupAll(ctx);
|
|
735
818
|
return () => {
|
|
736
819
|
pluginHost.disposeAll();
|
|
737
820
|
};
|
|
738
|
-
}, [pluginHost,
|
|
821
|
+
}, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
739
822
|
(0, import_react.useEffect)(() => {
|
|
740
|
-
const nextConfigured =
|
|
823
|
+
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
741
824
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
742
825
|
if (nextConfigured === prevConfigured) return;
|
|
743
826
|
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
@@ -745,23 +828,23 @@ function LessonkitProvider(props) {
|
|
|
745
828
|
if (nextConfigured) {
|
|
746
829
|
const fromIds = /* @__PURE__ */ new Set();
|
|
747
830
|
if (prevConfigured) fromIds.add(prevConfigured);
|
|
748
|
-
const tabId = getTabSessionId(defaultStorage);
|
|
831
|
+
const tabId = (0, import_core5.getTabSessionId)(defaultStorage);
|
|
749
832
|
if (tabId) fromIds.add(tabId);
|
|
750
833
|
for (const fromId of fromIds) {
|
|
751
834
|
if (fromId !== nextConfigured) {
|
|
752
|
-
migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
|
|
835
|
+
(0, import_core5.migrateCourseStartedMark)(defaultStorage, fromId, nextConfigured, cid);
|
|
753
836
|
}
|
|
754
837
|
}
|
|
755
838
|
sessionIdRef.current = nextConfigured;
|
|
756
839
|
} else if (prevConfigured) {
|
|
757
|
-
const nextAuto = resolveSessionId(defaultStorage, void 0);
|
|
758
|
-
migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
|
|
840
|
+
const nextAuto = (0, import_core5.resolveSessionId)(defaultStorage, void 0);
|
|
841
|
+
(0, import_core5.migrateCourseStartedMark)(defaultStorage, prevConfigured, nextAuto, cid);
|
|
759
842
|
sessionIdRef.current = nextAuto;
|
|
760
843
|
}
|
|
761
|
-
}, [sessionConfiguredId,
|
|
844
|
+
}, [sessionConfiguredId, normalizedCourseId]);
|
|
762
845
|
const runtime = (0, import_react.useMemo)(
|
|
763
846
|
() => ({
|
|
764
|
-
config,
|
|
847
|
+
config: normalizedConfig,
|
|
765
848
|
tracking,
|
|
766
849
|
xapi,
|
|
767
850
|
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
@@ -773,7 +856,7 @@ function LessonkitProvider(props) {
|
|
|
773
856
|
plugins: pluginHost
|
|
774
857
|
}),
|
|
775
858
|
[
|
|
776
|
-
|
|
859
|
+
normalizedConfig,
|
|
777
860
|
tracking,
|
|
778
861
|
xapi,
|
|
779
862
|
progress,
|
|
@@ -787,13 +870,21 @@ function LessonkitProvider(props) {
|
|
|
787
870
|
sessionConfiguredId
|
|
788
871
|
]
|
|
789
872
|
);
|
|
873
|
+
return runtime;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/context.tsx
|
|
877
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
878
|
+
var LessonkitContext = (0, import_react2.createContext)(null);
|
|
879
|
+
function LessonkitProvider(props) {
|
|
880
|
+
const runtime = useLessonkitProviderRuntime(props.config);
|
|
790
881
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LessonkitContext.Provider, { value: runtime, children: props.children });
|
|
791
882
|
}
|
|
792
883
|
|
|
793
884
|
// src/hooks.ts
|
|
794
|
-
var
|
|
885
|
+
var import_react3 = require("react");
|
|
795
886
|
function useLessonkit() {
|
|
796
|
-
const ctx = (0,
|
|
887
|
+
const ctx = (0, import_react3.useContext)(LessonkitContext);
|
|
797
888
|
if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
|
|
798
889
|
return ctx;
|
|
799
890
|
}
|
|
@@ -803,52 +894,84 @@ function useProgress() {
|
|
|
803
894
|
}
|
|
804
895
|
function useTracking() {
|
|
805
896
|
const { track } = useLessonkit();
|
|
806
|
-
return (0,
|
|
897
|
+
return (0, import_react3.useMemo)(() => ({ track }), [track]);
|
|
807
898
|
}
|
|
808
899
|
function useCompletion() {
|
|
809
900
|
const { completeLesson, completeCourse } = useLessonkit();
|
|
810
|
-
return (0,
|
|
901
|
+
return (0, import_react3.useMemo)(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
811
902
|
}
|
|
812
|
-
function useQuizState() {
|
|
903
|
+
function useQuizState(enclosingLessonId) {
|
|
813
904
|
const { track } = useLessonkit();
|
|
814
|
-
|
|
905
|
+
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
906
|
+
return (0, import_react3.useMemo)(
|
|
815
907
|
() => ({
|
|
816
908
|
answer: (opts) => {
|
|
817
|
-
track("quiz_answered", opts);
|
|
909
|
+
track("quiz_answered", opts, trackOpts);
|
|
818
910
|
},
|
|
819
911
|
complete: (opts) => {
|
|
820
|
-
track("quiz_completed", opts);
|
|
912
|
+
track("quiz_completed", opts, trackOpts);
|
|
821
913
|
}
|
|
822
914
|
}),
|
|
823
|
-
[track]
|
|
915
|
+
[track, enclosingLessonId]
|
|
824
916
|
);
|
|
825
917
|
}
|
|
826
918
|
|
|
919
|
+
// src/lessonContext.tsx
|
|
920
|
+
var import_react4 = require("react");
|
|
921
|
+
var LessonContext = (0, import_react4.createContext)(void 0);
|
|
922
|
+
function useEnclosingLessonId() {
|
|
923
|
+
return (0, import_react4.useContext)(LessonContext);
|
|
924
|
+
}
|
|
925
|
+
|
|
827
926
|
// src/runtime/validateComponentId.ts
|
|
828
|
-
var
|
|
829
|
-
|
|
830
|
-
function isDevEnvironment2() {
|
|
927
|
+
var import_core9 = require("@lessonkit/core");
|
|
928
|
+
function isDevEnvironment3() {
|
|
831
929
|
const g = globalThis;
|
|
832
930
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
833
931
|
}
|
|
834
|
-
function
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
932
|
+
function normalizeComponentId(id, path) {
|
|
933
|
+
if (path === "courseId") return (0, import_core9.assertValidId)(id, "courseId");
|
|
934
|
+
if (path === "lessonId") return (0, import_core9.assertValidId)(id, "lessonId");
|
|
935
|
+
if (path === "checkId") return (0, import_core9.assertValidId)(id, "checkId");
|
|
936
|
+
if (path === "blockId") return (0, import_core9.assertValidId)(id, "blockId");
|
|
937
|
+
return (0, import_core9.assertValidId)(id, path);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/runtime/lessonMountRegistry.ts
|
|
941
|
+
var mountCounts = /* @__PURE__ */ new Map();
|
|
942
|
+
var warnedConcurrentLessons = false;
|
|
943
|
+
function registerLessonMount(lessonId) {
|
|
944
|
+
if (isDevEnvironment3() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
|
|
945
|
+
warnedConcurrentLessons = true;
|
|
946
|
+
console.warn(
|
|
947
|
+
"[lessonkit] Multiple <Lesson> components are mounted; only one should be active at a time. Set autoCompleteOnUnmount={false} on routed lessons or unmount the previous lesson before showing the next."
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
mountCounts.set(lessonId, (mountCounts.get(lessonId) ?? 0) + 1);
|
|
951
|
+
return () => {
|
|
952
|
+
const next = (mountCounts.get(lessonId) ?? 1) - 1;
|
|
953
|
+
if (next <= 0) {
|
|
954
|
+
mountCounts.delete(lessonId);
|
|
955
|
+
} else {
|
|
956
|
+
mountCounts.set(lessonId, next);
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function getLessonMountCount(lessonId) {
|
|
961
|
+
return mountCounts.get(lessonId) ?? 0;
|
|
843
962
|
}
|
|
844
963
|
|
|
845
964
|
// src/components.tsx
|
|
846
965
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
966
|
+
var warnedQuizOutsideLesson = false;
|
|
967
|
+
function resetQuizWarningsForTests() {
|
|
968
|
+
warnedQuizOutsideLesson = false;
|
|
969
|
+
}
|
|
847
970
|
function Course(props) {
|
|
848
|
-
|
|
849
|
-
const providerConfig = (0,
|
|
850
|
-
() => ({ ...props.config, courseId
|
|
851
|
-
[props.config,
|
|
971
|
+
const courseId = (0, import_react5.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
972
|
+
const providerConfig = (0, import_react5.useMemo)(
|
|
973
|
+
() => ({ ...props.config, courseId }),
|
|
974
|
+
[props.config, courseId]
|
|
852
975
|
);
|
|
853
976
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": props.title, children: [
|
|
854
977
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { children: props.title }),
|
|
@@ -856,41 +979,64 @@ function Course(props) {
|
|
|
856
979
|
] }) });
|
|
857
980
|
}
|
|
858
981
|
function Lesson(props) {
|
|
859
|
-
|
|
982
|
+
const lessonId = (0, import_react5.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
983
|
+
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
860
984
|
const { setActiveLesson, config } = useLessonkit();
|
|
861
985
|
const { completeLesson } = useCompletion();
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
986
|
+
const lessonMountGenerationRef = (0, import_react5.useRef)(0);
|
|
987
|
+
(0, import_react5.useEffect)(() => {
|
|
988
|
+
const unregister = registerLessonMount(lessonId);
|
|
865
989
|
const generation = ++lessonMountGenerationRef.current;
|
|
866
|
-
setActiveLesson(
|
|
990
|
+
setActiveLesson(lessonId);
|
|
867
991
|
return () => {
|
|
868
|
-
|
|
992
|
+
unregister();
|
|
993
|
+
if (getLessonMountCount(lessonId) > 0) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (!autoComplete) return;
|
|
869
997
|
queueMicrotask(() => {
|
|
870
998
|
if (lessonMountGenerationRef.current !== generation) return;
|
|
871
999
|
completeLesson(lessonId);
|
|
872
1000
|
});
|
|
873
1001
|
};
|
|
874
|
-
}, [
|
|
875
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { "aria-label": props.title, children: [
|
|
1002
|
+
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1003
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { "aria-label": props.title, children: [
|
|
876
1004
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: props.title }),
|
|
877
1005
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: props.children })
|
|
878
|
-
] });
|
|
1006
|
+
] }) });
|
|
879
1007
|
}
|
|
880
1008
|
function Scenario(props) {
|
|
881
|
-
|
|
882
|
-
|
|
1009
|
+
const blockId = (0, import_react5.useMemo)(
|
|
1010
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1011
|
+
[props.blockId]
|
|
1012
|
+
);
|
|
1013
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
883
1014
|
}
|
|
884
1015
|
function Reflection(props) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1016
|
+
const blockId = (0, import_react5.useMemo)(
|
|
1017
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1018
|
+
[props.blockId]
|
|
1019
|
+
);
|
|
1020
|
+
const promptId = (0, import_react5.useId)();
|
|
1021
|
+
const hintId = (0, import_react5.useId)();
|
|
1022
|
+
const [internalValue, setInternalValue] = (0, import_react5.useState)("");
|
|
1023
|
+
const isControlled = props.value !== void 0;
|
|
1024
|
+
const value = isControlled ? props.value : internalValue;
|
|
1025
|
+
const handleChange = (event) => {
|
|
1026
|
+
if (!isControlled) setInternalValue(event.target.value);
|
|
1027
|
+
props.onChange?.(event.target.value);
|
|
1028
|
+
};
|
|
1029
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
888
1030
|
props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: promptId, children: props.prompt }) : null,
|
|
1031
|
+
props.hint ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: hintId, style: import_accessibility.visuallyHiddenStyle, children: props.hint }) : null,
|
|
889
1032
|
props.children,
|
|
890
1033
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
891
1034
|
"textarea",
|
|
892
1035
|
{
|
|
1036
|
+
value,
|
|
1037
|
+
onChange: handleChange,
|
|
893
1038
|
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
1039
|
+
"aria-describedby": props.hint ? hintId : void 0,
|
|
894
1040
|
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
895
1041
|
}
|
|
896
1042
|
)
|
|
@@ -909,18 +1055,35 @@ function KnowledgeCheck(props) {
|
|
|
909
1055
|
);
|
|
910
1056
|
}
|
|
911
1057
|
function Quiz(props) {
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1058
|
+
const checkId = (0, import_react5.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1059
|
+
const enclosingLessonId = useEnclosingLessonId();
|
|
1060
|
+
const missingLesson = enclosingLessonId === void 0;
|
|
1061
|
+
(0, import_react5.useEffect)(() => {
|
|
1062
|
+
if (!missingLesson || isDevEnvironment3()) return;
|
|
1063
|
+
if (!warnedQuizOutsideLesson) {
|
|
1064
|
+
warnedQuizOutsideLesson = true;
|
|
1065
|
+
console.error(
|
|
1066
|
+
"[lessonkit] <Quiz> must be wrapped in <Lesson>; quiz telemetry will not be emitted."
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
}, [missingLesson]);
|
|
1070
|
+
if (missingLesson && isDevEnvironment3()) {
|
|
1071
|
+
throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
|
|
1072
|
+
}
|
|
1073
|
+
const quiz = useQuizState(enclosingLessonId);
|
|
1074
|
+
const { plugins, config, session } = useLessonkit();
|
|
1075
|
+
const [selected, setSelected] = (0, import_react5.useState)(null);
|
|
1076
|
+
const [selectionCorrect, setSelectionCorrect] = (0, import_react5.useState)(null);
|
|
1077
|
+
const [quizPassed, setQuizPassed] = (0, import_react5.useState)(false);
|
|
1078
|
+
const completedRef = (0, import_react5.useRef)(false);
|
|
1079
|
+
const questionId = (0, import_react5.useId)();
|
|
1080
|
+
const choicesKey = props.choices.join("\0");
|
|
1081
|
+
(0, import_react5.useEffect)(() => {
|
|
920
1082
|
completedRef.current = false;
|
|
1083
|
+
setQuizPassed(false);
|
|
921
1084
|
setSelected(null);
|
|
922
1085
|
setSelectionCorrect(null);
|
|
923
|
-
}, [
|
|
1086
|
+
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
|
|
924
1087
|
const isChoiceCorrect = (choice, custom) => {
|
|
925
1088
|
if (!custom) return choice === props.answer;
|
|
926
1089
|
if (custom.passed !== void 0) return custom.passed;
|
|
@@ -929,7 +1092,11 @@ function Quiz(props) {
|
|
|
929
1092
|
}
|
|
930
1093
|
return choice === props.answer;
|
|
931
1094
|
};
|
|
932
|
-
|
|
1095
|
+
if (missingLesson) {
|
|
1096
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1097
|
+
}
|
|
1098
|
+
const passed = quizPassed;
|
|
1099
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
933
1100
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
|
|
934
1101
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
|
|
935
1102
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
|
|
@@ -941,17 +1108,21 @@ function Quiz(props) {
|
|
|
941
1108
|
name: questionId,
|
|
942
1109
|
value: c,
|
|
943
1110
|
checked: selected === c,
|
|
1111
|
+
disabled: passed,
|
|
1112
|
+
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
944
1113
|
onChange: () => {
|
|
1114
|
+
if (passed) return;
|
|
945
1115
|
setSelected(c);
|
|
946
1116
|
const pluginCtx = buildPluginContext({
|
|
947
1117
|
courseId: config.courseId,
|
|
948
1118
|
sessionId: session.sessionId,
|
|
949
|
-
attemptId: session.attemptId
|
|
1119
|
+
attemptId: session.attemptId,
|
|
1120
|
+
user: session.user
|
|
950
1121
|
});
|
|
951
1122
|
const custom = plugins?.scoreAssessment(
|
|
952
1123
|
{
|
|
953
|
-
checkId
|
|
954
|
-
lessonId:
|
|
1124
|
+
checkId,
|
|
1125
|
+
lessonId: enclosingLessonId,
|
|
955
1126
|
response: c
|
|
956
1127
|
},
|
|
957
1128
|
pluginCtx
|
|
@@ -959,18 +1130,20 @@ function Quiz(props) {
|
|
|
959
1130
|
const correct = isChoiceCorrect(c, custom);
|
|
960
1131
|
setSelectionCorrect(correct);
|
|
961
1132
|
quiz.answer({
|
|
962
|
-
checkId
|
|
1133
|
+
checkId,
|
|
963
1134
|
question: props.question,
|
|
964
1135
|
choice: c,
|
|
965
1136
|
correct
|
|
966
1137
|
});
|
|
967
1138
|
if (correct && !completedRef.current) {
|
|
968
1139
|
completedRef.current = true;
|
|
1140
|
+
setQuizPassed(true);
|
|
1141
|
+
const maxScore = custom?.maxScore ?? 1;
|
|
969
1142
|
quiz.complete({
|
|
970
|
-
checkId
|
|
1143
|
+
checkId,
|
|
971
1144
|
score: custom?.score ?? 1,
|
|
972
|
-
maxScore
|
|
973
|
-
passingScore: props.passingScore ??
|
|
1145
|
+
maxScore,
|
|
1146
|
+
passingScore: props.passingScore ?? maxScore
|
|
974
1147
|
});
|
|
975
1148
|
}
|
|
976
1149
|
}
|
|
@@ -982,20 +1155,40 @@ function Quiz(props) {
|
|
|
982
1155
|
selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
983
1156
|
] });
|
|
984
1157
|
}
|
|
985
|
-
function ProgressTracker() {
|
|
1158
|
+
function ProgressTracker(props) {
|
|
986
1159
|
const { progress } = useLessonkit();
|
|
987
1160
|
const completed = progress.completedLessonIds.size;
|
|
988
|
-
|
|
1161
|
+
if (props.totalLessons != null) {
|
|
1162
|
+
const total = props.totalLessons;
|
|
1163
|
+
const displayed = Math.min(completed, total);
|
|
1164
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1165
|
+
"div",
|
|
1166
|
+
{
|
|
1167
|
+
role: "progressbar",
|
|
1168
|
+
"aria-valuemin": 0,
|
|
1169
|
+
"aria-valuemax": total,
|
|
1170
|
+
"aria-valuenow": displayed,
|
|
1171
|
+
"aria-label": "Lessons completed",
|
|
1172
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
|
|
1173
|
+
"Lessons completed: ",
|
|
1174
|
+
displayed,
|
|
1175
|
+
" of ",
|
|
1176
|
+
total
|
|
1177
|
+
] })
|
|
1178
|
+
}
|
|
1179
|
+
) });
|
|
1180
|
+
}
|
|
1181
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
|
|
989
1182
|
"Lessons completed: ",
|
|
990
1183
|
completed
|
|
991
1184
|
] }) });
|
|
992
1185
|
}
|
|
993
1186
|
|
|
994
1187
|
// src/index.tsx
|
|
995
|
-
var
|
|
1188
|
+
var import_core10 = require("@lessonkit/core");
|
|
996
1189
|
|
|
997
1190
|
// src/theme/ThemeProvider.tsx
|
|
998
|
-
var
|
|
1191
|
+
var import_react6 = __toESM(require("react"), 1);
|
|
999
1192
|
var import_themes = require("@lessonkit/themes");
|
|
1000
1193
|
|
|
1001
1194
|
// src/theme/applyCssVariables.ts
|
|
@@ -1015,8 +1208,8 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
1015
1208
|
|
|
1016
1209
|
// src/theme/ThemeProvider.tsx
|
|
1017
1210
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1018
|
-
var ThemeContext = (0,
|
|
1019
|
-
var useIsoLayoutEffect2 = typeof window !== "undefined" ?
|
|
1211
|
+
var ThemeContext = (0, import_react6.createContext)(null);
|
|
1212
|
+
var useIsoLayoutEffect2 = typeof window !== "undefined" ? import_react6.useLayoutEffect : import_react6.default.useEffect;
|
|
1020
1213
|
function getSystemMode() {
|
|
1021
1214
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
1022
1215
|
return "light";
|
|
@@ -1034,7 +1227,7 @@ function ThemeProvider(props) {
|
|
|
1034
1227
|
const preset = props.preset ?? "default";
|
|
1035
1228
|
const mode = props.mode ?? "light";
|
|
1036
1229
|
const targetKind = props.target ?? "document";
|
|
1037
|
-
const [resolvedMode, setResolvedMode] = (0,
|
|
1230
|
+
const [resolvedMode, setResolvedMode] = (0, import_react6.useState)(
|
|
1038
1231
|
() => mode === "system" ? getSystemMode() : mode
|
|
1039
1232
|
);
|
|
1040
1233
|
useIsoLayoutEffect2(() => {
|
|
@@ -1050,20 +1243,20 @@ function ThemeProvider(props) {
|
|
|
1050
1243
|
return () => mq.removeEventListener("change", onChange);
|
|
1051
1244
|
}, [mode]);
|
|
1052
1245
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
1053
|
-
const effectiveTheme = (0,
|
|
1246
|
+
const effectiveTheme = (0, import_react6.useMemo)(() => {
|
|
1054
1247
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
1055
1248
|
const base = preset === "default" ? modeBase : preset === "brand" ? (0, import_themes.mergeThemes)(modeBase, import_themes.brandThemeOverrides) : (0, import_themes.mergeThemes)(modeBase, (0, import_themes.getPresetTheme)(preset));
|
|
1056
1249
|
return (0, import_themes.mergeThemes)(base, props.theme ?? {});
|
|
1057
1250
|
}, [preset, mode, dataTheme, props.theme]);
|
|
1058
|
-
const hostRef = (0,
|
|
1059
|
-
const appliedKeysRef = (0,
|
|
1251
|
+
const hostRef = (0, import_react6.useRef)(null);
|
|
1252
|
+
const appliedKeysRef = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
|
|
1060
1253
|
useIsoLayoutEffect2(() => {
|
|
1061
1254
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
1062
1255
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
1063
1256
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
1064
1257
|
}
|
|
1065
1258
|
}, [targetKind, dataTheme]);
|
|
1066
|
-
const inject = (0,
|
|
1259
|
+
const inject = (0, import_react6.useCallback)(() => {
|
|
1067
1260
|
const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
|
|
1068
1261
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
1069
1262
|
if (!el) return;
|
|
@@ -1080,7 +1273,7 @@ function ThemeProvider(props) {
|
|
|
1080
1273
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
1081
1274
|
};
|
|
1082
1275
|
}, [inject, targetKind]);
|
|
1083
|
-
const value = (0,
|
|
1276
|
+
const value = (0, import_react6.useMemo)(
|
|
1084
1277
|
() => ({
|
|
1085
1278
|
theme: effectiveTheme,
|
|
1086
1279
|
preset,
|
|
@@ -1095,7 +1288,7 @@ function ThemeProvider(props) {
|
|
|
1095
1288
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
1096
1289
|
}
|
|
1097
1290
|
function useTheme() {
|
|
1098
|
-
const ctx = (0,
|
|
1291
|
+
const ctx = (0, import_react6.useContext)(ThemeContext);
|
|
1099
1292
|
if (!ctx) {
|
|
1100
1293
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
1101
1294
|
}
|
|
@@ -1142,6 +1335,12 @@ var BLOCK_CATALOG = [
|
|
|
1142
1335
|
props: [
|
|
1143
1336
|
{ name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
|
|
1144
1337
|
{ name: "lessonId", type: "LessonId", required: true, description: "Stable lesson identifier for telemetry and packaging." },
|
|
1338
|
+
{
|
|
1339
|
+
name: "autoCompleteOnUnmount",
|
|
1340
|
+
type: "boolean",
|
|
1341
|
+
required: false,
|
|
1342
|
+
description: "When false, unmount does not emit lesson_completed (default true)."
|
|
1343
|
+
},
|
|
1145
1344
|
{ name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
|
|
1146
1345
|
],
|
|
1147
1346
|
requiredIds: ["lessonId"],
|
|
@@ -1194,6 +1393,9 @@ var BLOCK_CATALOG = [
|
|
|
1194
1393
|
props: [
|
|
1195
1394
|
{ name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
|
|
1196
1395
|
{ name: "prompt", type: "string", required: false, description: "Reflection question or instruction." },
|
|
1396
|
+
{ name: "hint", type: "string", required: false, description: "Optional hint linked via aria-describedby." },
|
|
1397
|
+
{ name: "value", type: "string", required: false, description: "Controlled textarea value." },
|
|
1398
|
+
{ name: "onChange", type: "(value: string) => void", required: false, description: "Called when the learner edits the textarea." },
|
|
1197
1399
|
{ name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
|
|
1198
1400
|
],
|
|
1199
1401
|
requiredIds: [],
|
|
@@ -1212,6 +1414,7 @@ var BLOCK_CATALOG = [
|
|
|
1212
1414
|
},
|
|
1213
1415
|
telemetry: {
|
|
1214
1416
|
emits: [],
|
|
1417
|
+
requiresActiveLesson: true,
|
|
1215
1418
|
manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
|
|
1216
1419
|
}
|
|
1217
1420
|
},
|
|
@@ -1224,7 +1427,13 @@ var BLOCK_CATALOG = [
|
|
|
1224
1427
|
{ name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
|
|
1225
1428
|
{ name: "question", type: "string", required: true, description: "Question text shown above choices." },
|
|
1226
1429
|
{ name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
|
|
1227
|
-
{ name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
|
|
1430
|
+
{ name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." },
|
|
1431
|
+
{
|
|
1432
|
+
name: "passingScore",
|
|
1433
|
+
type: "number",
|
|
1434
|
+
required: false,
|
|
1435
|
+
description: "Minimum score required to pass (defaults to maxScore when omitted)."
|
|
1436
|
+
}
|
|
1228
1437
|
],
|
|
1229
1438
|
requiredIds: ["checkId"],
|
|
1230
1439
|
parentConstraints: ["Lesson"],
|
|
@@ -1249,7 +1458,14 @@ var BLOCK_CATALOG = [
|
|
|
1249
1458
|
type: "ProgressTracker",
|
|
1250
1459
|
category: "chrome",
|
|
1251
1460
|
description: "Displays count of completed lessons from runtime progress state.",
|
|
1252
|
-
props: [
|
|
1461
|
+
props: [
|
|
1462
|
+
{
|
|
1463
|
+
name: "totalLessons",
|
|
1464
|
+
type: "number",
|
|
1465
|
+
required: false,
|
|
1466
|
+
description: "When set, renders role=progressbar with aria-valuenow/max."
|
|
1467
|
+
}
|
|
1468
|
+
],
|
|
1253
1469
|
requiredIds: [],
|
|
1254
1470
|
parentConstraints: ["Course"],
|
|
1255
1471
|
a11y: {
|
|
@@ -1302,9 +1518,15 @@ function getBlockCatalogEntry(type) {
|
|
|
1302
1518
|
ThemeProvider,
|
|
1303
1519
|
blockCatalogVersion,
|
|
1304
1520
|
buildBlockCatalog,
|
|
1305
|
-
|
|
1306
|
-
|
|
1521
|
+
buildTelemetryEvent,
|
|
1522
|
+
createLessonkitRuntime,
|
|
1523
|
+
createPluginRegistry,
|
|
1524
|
+
createTelemetryPipeline,
|
|
1525
|
+
defineAssessmentPlugin,
|
|
1526
|
+
defineLifecyclePlugin,
|
|
1527
|
+
defineTelemetryPlugin,
|
|
1307
1528
|
getBlockCatalogEntry,
|
|
1529
|
+
resetQuizWarningsForTests,
|
|
1308
1530
|
useCompletion,
|
|
1309
1531
|
useLessonkit,
|
|
1310
1532
|
useProgress,
|