@lessonkit/react 0.5.0 → 0.6.0
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 +4 -2
- package/dist/index.cjs +166 -37
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +169 -37
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@lessonkit/react`
|
|
2
2
|
|
|
3
|
-
[](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@lessonkit/react)
|
|
5
5
|
[](../../LICENSE)
|
|
6
6
|
|
|
@@ -56,7 +56,7 @@ export default function App() {
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
## API (0.
|
|
59
|
+
## API (0.6.0)
|
|
60
60
|
|
|
61
61
|
### Components
|
|
62
62
|
|
|
@@ -87,6 +87,8 @@ export default function App() {
|
|
|
87
87
|
- `Course` accepts a `config` prop that is passed through to `LessonkitProvider` (tracking sink,
|
|
88
88
|
optional `xapi.transport` or custom `xapi.client`, session metadata). Hoist `config` with `useMemo`
|
|
89
89
|
so tracking/xAPI clients are not recreated every render.
|
|
90
|
+
- xAPI is enabled by default unless `xapi.enabled: false`. Provide `xapi.transport` or `xapi.client`
|
|
91
|
+
or statements are queued in memory and never sent (dev warns once).
|
|
90
92
|
- A lesson is marked complete when its `<Lesson>` unmounts (for example, wizard navigation) or when
|
|
91
93
|
another lesson becomes active via `setActiveLesson`. Use stable `lessonId` values so completion and
|
|
92
94
|
time-on-task telemetry stay consistent.
|
package/dist/index.cjs
CHANGED
|
@@ -56,16 +56,61 @@ var import_accessibility = require("@lessonkit/accessibility");
|
|
|
56
56
|
var import_react = require("react");
|
|
57
57
|
var import_core3 = require("@lessonkit/core");
|
|
58
58
|
var import_xapi3 = require("@lessonkit/xapi");
|
|
59
|
+
var import_xapi4 = require("@lessonkit/xapi");
|
|
59
60
|
|
|
60
61
|
// src/runtime/emitTelemetry.ts
|
|
61
62
|
var import_core = require("@lessonkit/core");
|
|
62
63
|
var import_xapi = require("@lessonkit/xapi");
|
|
64
|
+
|
|
65
|
+
// src/runtime/lxpackBridge.ts
|
|
66
|
+
var import_bridge = require("@lessonkit/lxpack/bridge");
|
|
67
|
+
function getBridge() {
|
|
68
|
+
if (typeof window === "undefined") return null;
|
|
69
|
+
const parent = window.parent;
|
|
70
|
+
if (!parent || parent === window) return null;
|
|
71
|
+
return parent.lxpackBridge?.v1 ?? parent.lxpack ?? null;
|
|
72
|
+
}
|
|
73
|
+
function forwardTelemetryToLxpack(event, mode = "auto") {
|
|
74
|
+
if (mode === "off") return;
|
|
75
|
+
const bridge = getBridge();
|
|
76
|
+
if (!bridge) return;
|
|
77
|
+
switch (event.name) {
|
|
78
|
+
case "lesson_completed": {
|
|
79
|
+
const lessonId = event.lessonId;
|
|
80
|
+
if (lessonId) bridge.completeLesson?.(lessonId);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
case "course_completed":
|
|
84
|
+
bridge.completeCourse?.();
|
|
85
|
+
return;
|
|
86
|
+
case "quiz_completed": {
|
|
87
|
+
const data = event.data;
|
|
88
|
+
if (!data?.checkId) return;
|
|
89
|
+
const scaled = (0, import_bridge.normalizeAssessmentScore)({
|
|
90
|
+
score: data.score,
|
|
91
|
+
maxScore: data.maxScore
|
|
92
|
+
});
|
|
93
|
+
if (scaled === null) return;
|
|
94
|
+
bridge.submitAssessment?.({
|
|
95
|
+
id: data.checkId,
|
|
96
|
+
score: scaled,
|
|
97
|
+
passingScore: (0, import_bridge.normalizeAssessmentPassingScore)(data.passingScore)
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/runtime/emitTelemetry.ts
|
|
63
107
|
var warnedMissingCourseId = false;
|
|
108
|
+
var warnedMissingQuizLesson = false;
|
|
64
109
|
function isDevEnvironment() {
|
|
65
110
|
const g = globalThis;
|
|
66
111
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
67
112
|
}
|
|
68
|
-
function emitTelemetry(tracking, xapi, event) {
|
|
113
|
+
function emitTelemetry(tracking, xapi, event, opts) {
|
|
69
114
|
if (!event.courseId) {
|
|
70
115
|
if (isDevEnvironment() && !warnedMissingCourseId) {
|
|
71
116
|
warnedMissingCourseId = true;
|
|
@@ -82,6 +127,7 @@ function emitTelemetry(tracking, xapi, event) {
|
|
|
82
127
|
console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
|
|
83
128
|
}
|
|
84
129
|
}
|
|
130
|
+
forwardTelemetryToLxpack(event, opts?.lxpackBridge ?? "auto");
|
|
85
131
|
}
|
|
86
132
|
function buildTrackEvent(opts) {
|
|
87
133
|
const base = {
|
|
@@ -104,7 +150,7 @@ function buildTrackEvent(opts) {
|
|
|
104
150
|
name: "lesson_started",
|
|
105
151
|
...base,
|
|
106
152
|
lessonId,
|
|
107
|
-
data: {
|
|
153
|
+
data: { ...data, lessonId }
|
|
108
154
|
};
|
|
109
155
|
}
|
|
110
156
|
case "lesson_completed":
|
|
@@ -116,7 +162,7 @@ function buildTrackEvent(opts) {
|
|
|
116
162
|
name: opts.name,
|
|
117
163
|
...base,
|
|
118
164
|
lessonId,
|
|
119
|
-
data: {
|
|
165
|
+
data: { ...data, lessonId }
|
|
120
166
|
};
|
|
121
167
|
}
|
|
122
168
|
case "quiz_answered": {
|
|
@@ -142,6 +188,19 @@ function buildTrackEvent(opts) {
|
|
|
142
188
|
return { name: opts.name, ...base };
|
|
143
189
|
}
|
|
144
190
|
}
|
|
191
|
+
function tryBuildTrackEvent(opts) {
|
|
192
|
+
const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
|
|
193
|
+
if (isQuiz && !opts.lessonId) {
|
|
194
|
+
if (isDevEnvironment() && !warnedMissingQuizLesson) {
|
|
195
|
+
warnedMissingQuizLesson = true;
|
|
196
|
+
console.warn(
|
|
197
|
+
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return buildTrackEvent(opts);
|
|
203
|
+
}
|
|
145
204
|
|
|
146
205
|
// src/runtime/ports.ts
|
|
147
206
|
function createNoopStorage() {
|
|
@@ -270,6 +329,8 @@ function LessonkitProvider(props) {
|
|
|
270
329
|
userRef.current = config.session?.user;
|
|
271
330
|
const courseIdRef = (0, import_react.useRef)(config.courseId);
|
|
272
331
|
courseIdRef.current = config.courseId;
|
|
332
|
+
const lxpackBridgeModeRef = (0, import_react.useRef)(config.lxpack?.bridge ?? "auto");
|
|
333
|
+
lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
|
|
273
334
|
const progressRef = (0, import_react.useRef)(createProgressController());
|
|
274
335
|
const [progress, setProgress] = (0, import_react.useState)(() => progressRef.current.getState());
|
|
275
336
|
const syncProgress = (0, import_react.useCallback)(() => {
|
|
@@ -277,6 +338,53 @@ function LessonkitProvider(props) {
|
|
|
277
338
|
}, []);
|
|
278
339
|
const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
|
|
279
340
|
activeLessonIdRef.current = progress.activeLessonId;
|
|
341
|
+
const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
|
|
342
|
+
const xapiRef = (0, import_react.useRef)(null);
|
|
343
|
+
const [xapi, setXapi] = (0, import_react.useState)(null);
|
|
344
|
+
const xapiEnabled = config.xapi?.enabled;
|
|
345
|
+
const xapiClient = config.xapi?.client;
|
|
346
|
+
const xapiTransport = config.xapi?.transport;
|
|
347
|
+
const courseId = config.courseId;
|
|
348
|
+
useIsoLayoutEffect(() => {
|
|
349
|
+
const prev = xapiRef.current;
|
|
350
|
+
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
351
|
+
xapiRef.current = next;
|
|
352
|
+
setXapi(next);
|
|
353
|
+
if (next && !prev) {
|
|
354
|
+
const sessionId = sessionIdRef.current;
|
|
355
|
+
const cid = courseIdRef.current;
|
|
356
|
+
if (hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
357
|
+
try {
|
|
358
|
+
const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
|
|
359
|
+
buildTrackEvent({
|
|
360
|
+
name: "course_started",
|
|
361
|
+
courseId: cid,
|
|
362
|
+
sessionId,
|
|
363
|
+
attemptId: attemptIdRef.current,
|
|
364
|
+
user: userRef.current
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
if (statement) next.send(statement);
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
void (async () => {
|
|
373
|
+
if (prev) {
|
|
374
|
+
try {
|
|
375
|
+
await prev.flush();
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
await next?.flush();
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
})();
|
|
384
|
+
return () => {
|
|
385
|
+
void prev?.flush();
|
|
386
|
+
};
|
|
387
|
+
}, [xapiEnabled, xapiClient, xapiTransport, courseId]);
|
|
280
388
|
const trackingRef = (0, import_react.useRef)((0, import_core3.createTrackingClient)());
|
|
281
389
|
const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
|
|
282
390
|
const trackingEnabled = config.tracking?.enabled;
|
|
@@ -303,7 +411,8 @@ function LessonkitProvider(props) {
|
|
|
303
411
|
sessionId,
|
|
304
412
|
attemptId: attemptIdRef.current,
|
|
305
413
|
user: userRef.current
|
|
306
|
-
})
|
|
414
|
+
}),
|
|
415
|
+
{ lxpackBridge: lxpackBridgeModeRef.current }
|
|
307
416
|
);
|
|
308
417
|
}
|
|
309
418
|
return () => {
|
|
@@ -317,37 +426,17 @@ function LessonkitProvider(props) {
|
|
|
317
426
|
batchFlushIntervalMs,
|
|
318
427
|
batchMaxBatchSize
|
|
319
428
|
]);
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const prev = xapiRef.current;
|
|
329
|
-
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
330
|
-
xapiRef.current = next;
|
|
331
|
-
setXapi(next);
|
|
332
|
-
void (async () => {
|
|
333
|
-
if (prev) {
|
|
334
|
-
try {
|
|
335
|
-
await prev.flush();
|
|
336
|
-
} catch {
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
try {
|
|
340
|
-
await next?.flush();
|
|
341
|
-
} catch {
|
|
342
|
-
}
|
|
343
|
-
})();
|
|
344
|
-
return () => {
|
|
345
|
-
void prev?.flush();
|
|
346
|
-
};
|
|
347
|
-
}, [xapiEnabled, xapiClient, xapiTransport, courseId]);
|
|
429
|
+
const emitWithBridge = (0, import_react.useCallback)(
|
|
430
|
+
(trackingClient, event) => {
|
|
431
|
+
emitTelemetry(trackingClient, xapiRef.current, event, {
|
|
432
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
[]
|
|
436
|
+
);
|
|
348
437
|
const track = (0, import_react.useCallback)(
|
|
349
438
|
(name, data, opts) => {
|
|
350
|
-
const event =
|
|
439
|
+
const event = tryBuildTrackEvent({
|
|
351
440
|
name,
|
|
352
441
|
courseId: courseIdRef.current,
|
|
353
442
|
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
@@ -356,10 +445,35 @@ function LessonkitProvider(props) {
|
|
|
356
445
|
user: userRef.current,
|
|
357
446
|
data
|
|
358
447
|
});
|
|
359
|
-
|
|
448
|
+
if (!event) return;
|
|
449
|
+
emitWithBridge(trackingRef.current, event);
|
|
360
450
|
},
|
|
361
|
-
[]
|
|
451
|
+
[emitWithBridge]
|
|
362
452
|
);
|
|
453
|
+
const prevCourseIdRef = (0, import_react.useRef)(config.courseId);
|
|
454
|
+
(0, import_react.useEffect)(() => {
|
|
455
|
+
if (prevCourseIdRef.current === config.courseId) return;
|
|
456
|
+
prevCourseIdRef.current = config.courseId;
|
|
457
|
+
progressRef.current = createProgressController();
|
|
458
|
+
syncProgress();
|
|
459
|
+
const sessionId = sessionIdRef.current;
|
|
460
|
+
const cid = config.courseId;
|
|
461
|
+
if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
462
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
463
|
+
emitTelemetry(
|
|
464
|
+
trackingRef.current,
|
|
465
|
+
xapiRef.current,
|
|
466
|
+
buildTrackEvent({
|
|
467
|
+
name: "course_started",
|
|
468
|
+
courseId: cid,
|
|
469
|
+
sessionId,
|
|
470
|
+
attemptId: attemptIdRef.current,
|
|
471
|
+
user: userRef.current
|
|
472
|
+
}),
|
|
473
|
+
{ lxpackBridge: lxpackBridgeModeRef.current }
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}, [config.courseId, syncProgress]);
|
|
363
477
|
(0, import_react.useEffect)(() => {
|
|
364
478
|
return () => {
|
|
365
479
|
trackingRef.current?.flush?.();
|
|
@@ -407,6 +521,9 @@ function LessonkitProvider(props) {
|
|
|
407
521
|
syncProgress();
|
|
408
522
|
track("course_completed");
|
|
409
523
|
}, [track, syncProgress]);
|
|
524
|
+
const sessionUser = config.session?.user;
|
|
525
|
+
const sessionAttemptId = config.session?.attemptId;
|
|
526
|
+
const sessionConfiguredId = config.session?.sessionId;
|
|
410
527
|
const runtime = (0, import_react.useMemo)(
|
|
411
528
|
() => ({
|
|
412
529
|
config,
|
|
@@ -419,7 +536,19 @@ function LessonkitProvider(props) {
|
|
|
419
536
|
completeCourse,
|
|
420
537
|
track
|
|
421
538
|
}),
|
|
422
|
-
[
|
|
539
|
+
[
|
|
540
|
+
config,
|
|
541
|
+
tracking,
|
|
542
|
+
xapi,
|
|
543
|
+
progress,
|
|
544
|
+
setActiveLesson,
|
|
545
|
+
completeLesson,
|
|
546
|
+
completeCourse,
|
|
547
|
+
track,
|
|
548
|
+
sessionUser,
|
|
549
|
+
sessionAttemptId,
|
|
550
|
+
sessionConfiguredId
|
|
551
|
+
]
|
|
423
552
|
);
|
|
424
553
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LessonkitContext.Provider, { value: runtime, children: props.children });
|
|
425
554
|
}
|
|
@@ -573,7 +702,7 @@ function Quiz(props) {
|
|
|
573
702
|
});
|
|
574
703
|
if (correct && !completedRef.current) {
|
|
575
704
|
completedRef.current = true;
|
|
576
|
-
quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1 });
|
|
705
|
+
quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1, passingScore: 1 });
|
|
577
706
|
}
|
|
578
707
|
}
|
|
579
708
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -34,6 +34,10 @@ type LessonkitConfig = {
|
|
|
34
34
|
transport?: XAPITransport;
|
|
35
35
|
client?: XAPIClient;
|
|
36
36
|
};
|
|
37
|
+
lxpack?: {
|
|
38
|
+
/** Forward completion events to `window.parent.lxpackBridge.v1` when embedded (default `auto`). */
|
|
39
|
+
bridge?: "auto" | "off";
|
|
40
|
+
};
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
type LessonkitRuntime = {
|
|
@@ -114,6 +118,7 @@ declare function useQuizState(): {
|
|
|
114
118
|
checkId: CheckId;
|
|
115
119
|
score?: number;
|
|
116
120
|
maxScore?: number;
|
|
121
|
+
passingScore?: number;
|
|
117
122
|
}) => void;
|
|
118
123
|
};
|
|
119
124
|
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,10 @@ type LessonkitConfig = {
|
|
|
34
34
|
transport?: XAPITransport;
|
|
35
35
|
client?: XAPIClient;
|
|
36
36
|
};
|
|
37
|
+
lxpack?: {
|
|
38
|
+
/** Forward completion events to `window.parent.lxpackBridge.v1` when embedded (default `auto`). */
|
|
39
|
+
bridge?: "auto" | "off";
|
|
40
|
+
};
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
type LessonkitRuntime = {
|
|
@@ -114,6 +118,7 @@ declare function useQuizState(): {
|
|
|
114
118
|
checkId: CheckId;
|
|
115
119
|
score?: number;
|
|
116
120
|
maxScore?: number;
|
|
121
|
+
passingScore?: number;
|
|
117
122
|
}) => void;
|
|
118
123
|
};
|
|
119
124
|
|
package/dist/index.js
CHANGED
|
@@ -14,16 +14,64 @@ import {
|
|
|
14
14
|
} from "react";
|
|
15
15
|
import { createTrackingClient } from "@lessonkit/core";
|
|
16
16
|
import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
|
|
17
|
+
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
|
|
17
18
|
|
|
18
19
|
// src/runtime/emitTelemetry.ts
|
|
19
20
|
import { nowIso } from "@lessonkit/core";
|
|
20
21
|
import { telemetryEventToXAPIStatement } from "@lessonkit/xapi";
|
|
22
|
+
|
|
23
|
+
// src/runtime/lxpackBridge.ts
|
|
24
|
+
import {
|
|
25
|
+
normalizeAssessmentPassingScore,
|
|
26
|
+
normalizeAssessmentScore
|
|
27
|
+
} from "@lessonkit/lxpack/bridge";
|
|
28
|
+
function getBridge() {
|
|
29
|
+
if (typeof window === "undefined") return null;
|
|
30
|
+
const parent = window.parent;
|
|
31
|
+
if (!parent || parent === window) return null;
|
|
32
|
+
return parent.lxpackBridge?.v1 ?? parent.lxpack ?? null;
|
|
33
|
+
}
|
|
34
|
+
function forwardTelemetryToLxpack(event, mode = "auto") {
|
|
35
|
+
if (mode === "off") return;
|
|
36
|
+
const bridge = getBridge();
|
|
37
|
+
if (!bridge) return;
|
|
38
|
+
switch (event.name) {
|
|
39
|
+
case "lesson_completed": {
|
|
40
|
+
const lessonId = event.lessonId;
|
|
41
|
+
if (lessonId) bridge.completeLesson?.(lessonId);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
case "course_completed":
|
|
45
|
+
bridge.completeCourse?.();
|
|
46
|
+
return;
|
|
47
|
+
case "quiz_completed": {
|
|
48
|
+
const data = event.data;
|
|
49
|
+
if (!data?.checkId) return;
|
|
50
|
+
const scaled = normalizeAssessmentScore({
|
|
51
|
+
score: data.score,
|
|
52
|
+
maxScore: data.maxScore
|
|
53
|
+
});
|
|
54
|
+
if (scaled === null) return;
|
|
55
|
+
bridge.submitAssessment?.({
|
|
56
|
+
id: data.checkId,
|
|
57
|
+
score: scaled,
|
|
58
|
+
passingScore: normalizeAssessmentPassingScore(data.passingScore)
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/runtime/emitTelemetry.ts
|
|
21
68
|
var warnedMissingCourseId = false;
|
|
69
|
+
var warnedMissingQuizLesson = false;
|
|
22
70
|
function isDevEnvironment() {
|
|
23
71
|
const g = globalThis;
|
|
24
72
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
25
73
|
}
|
|
26
|
-
function emitTelemetry(tracking, xapi, event) {
|
|
74
|
+
function emitTelemetry(tracking, xapi, event, opts) {
|
|
27
75
|
if (!event.courseId) {
|
|
28
76
|
if (isDevEnvironment() && !warnedMissingCourseId) {
|
|
29
77
|
warnedMissingCourseId = true;
|
|
@@ -40,6 +88,7 @@ function emitTelemetry(tracking, xapi, event) {
|
|
|
40
88
|
console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
|
|
41
89
|
}
|
|
42
90
|
}
|
|
91
|
+
forwardTelemetryToLxpack(event, opts?.lxpackBridge ?? "auto");
|
|
43
92
|
}
|
|
44
93
|
function buildTrackEvent(opts) {
|
|
45
94
|
const base = {
|
|
@@ -62,7 +111,7 @@ function buildTrackEvent(opts) {
|
|
|
62
111
|
name: "lesson_started",
|
|
63
112
|
...base,
|
|
64
113
|
lessonId,
|
|
65
|
-
data: {
|
|
114
|
+
data: { ...data, lessonId }
|
|
66
115
|
};
|
|
67
116
|
}
|
|
68
117
|
case "lesson_completed":
|
|
@@ -74,7 +123,7 @@ function buildTrackEvent(opts) {
|
|
|
74
123
|
name: opts.name,
|
|
75
124
|
...base,
|
|
76
125
|
lessonId,
|
|
77
|
-
data: {
|
|
126
|
+
data: { ...data, lessonId }
|
|
78
127
|
};
|
|
79
128
|
}
|
|
80
129
|
case "quiz_answered": {
|
|
@@ -100,6 +149,19 @@ function buildTrackEvent(opts) {
|
|
|
100
149
|
return { name: opts.name, ...base };
|
|
101
150
|
}
|
|
102
151
|
}
|
|
152
|
+
function tryBuildTrackEvent(opts) {
|
|
153
|
+
const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
|
|
154
|
+
if (isQuiz && !opts.lessonId) {
|
|
155
|
+
if (isDevEnvironment() && !warnedMissingQuizLesson) {
|
|
156
|
+
warnedMissingQuizLesson = true;
|
|
157
|
+
console.warn(
|
|
158
|
+
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return buildTrackEvent(opts);
|
|
164
|
+
}
|
|
103
165
|
|
|
104
166
|
// src/runtime/ports.ts
|
|
105
167
|
function createNoopStorage() {
|
|
@@ -228,6 +290,8 @@ function LessonkitProvider(props) {
|
|
|
228
290
|
userRef.current = config.session?.user;
|
|
229
291
|
const courseIdRef = useRef(config.courseId);
|
|
230
292
|
courseIdRef.current = config.courseId;
|
|
293
|
+
const lxpackBridgeModeRef = useRef(config.lxpack?.bridge ?? "auto");
|
|
294
|
+
lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
|
|
231
295
|
const progressRef = useRef(createProgressController());
|
|
232
296
|
const [progress, setProgress] = useState(() => progressRef.current.getState());
|
|
233
297
|
const syncProgress = useCallback(() => {
|
|
@@ -235,6 +299,53 @@ function LessonkitProvider(props) {
|
|
|
235
299
|
}, []);
|
|
236
300
|
const activeLessonIdRef = useRef(progress.activeLessonId);
|
|
237
301
|
activeLessonIdRef.current = progress.activeLessonId;
|
|
302
|
+
const xapiQueueRef = useRef(createInMemoryXAPIQueue());
|
|
303
|
+
const xapiRef = useRef(null);
|
|
304
|
+
const [xapi, setXapi] = useState(null);
|
|
305
|
+
const xapiEnabled = config.xapi?.enabled;
|
|
306
|
+
const xapiClient = config.xapi?.client;
|
|
307
|
+
const xapiTransport = config.xapi?.transport;
|
|
308
|
+
const courseId = config.courseId;
|
|
309
|
+
useIsoLayoutEffect(() => {
|
|
310
|
+
const prev = xapiRef.current;
|
|
311
|
+
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
312
|
+
xapiRef.current = next;
|
|
313
|
+
setXapi(next);
|
|
314
|
+
if (next && !prev) {
|
|
315
|
+
const sessionId = sessionIdRef.current;
|
|
316
|
+
const cid = courseIdRef.current;
|
|
317
|
+
if (hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
318
|
+
try {
|
|
319
|
+
const statement = telemetryEventToXAPIStatement2(
|
|
320
|
+
buildTrackEvent({
|
|
321
|
+
name: "course_started",
|
|
322
|
+
courseId: cid,
|
|
323
|
+
sessionId,
|
|
324
|
+
attemptId: attemptIdRef.current,
|
|
325
|
+
user: userRef.current
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
if (statement) next.send(statement);
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
void (async () => {
|
|
334
|
+
if (prev) {
|
|
335
|
+
try {
|
|
336
|
+
await prev.flush();
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
await next?.flush();
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
})();
|
|
345
|
+
return () => {
|
|
346
|
+
void prev?.flush();
|
|
347
|
+
};
|
|
348
|
+
}, [xapiEnabled, xapiClient, xapiTransport, courseId]);
|
|
238
349
|
const trackingRef = useRef(createTrackingClient());
|
|
239
350
|
const [tracking, setTracking] = useState(() => trackingRef.current);
|
|
240
351
|
const trackingEnabled = config.tracking?.enabled;
|
|
@@ -261,7 +372,8 @@ function LessonkitProvider(props) {
|
|
|
261
372
|
sessionId,
|
|
262
373
|
attemptId: attemptIdRef.current,
|
|
263
374
|
user: userRef.current
|
|
264
|
-
})
|
|
375
|
+
}),
|
|
376
|
+
{ lxpackBridge: lxpackBridgeModeRef.current }
|
|
265
377
|
);
|
|
266
378
|
}
|
|
267
379
|
return () => {
|
|
@@ -275,37 +387,17 @@ function LessonkitProvider(props) {
|
|
|
275
387
|
batchFlushIntervalMs,
|
|
276
388
|
batchMaxBatchSize
|
|
277
389
|
]);
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const prev = xapiRef.current;
|
|
287
|
-
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
288
|
-
xapiRef.current = next;
|
|
289
|
-
setXapi(next);
|
|
290
|
-
void (async () => {
|
|
291
|
-
if (prev) {
|
|
292
|
-
try {
|
|
293
|
-
await prev.flush();
|
|
294
|
-
} catch {
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
await next?.flush();
|
|
299
|
-
} catch {
|
|
300
|
-
}
|
|
301
|
-
})();
|
|
302
|
-
return () => {
|
|
303
|
-
void prev?.flush();
|
|
304
|
-
};
|
|
305
|
-
}, [xapiEnabled, xapiClient, xapiTransport, courseId]);
|
|
390
|
+
const emitWithBridge = useCallback(
|
|
391
|
+
(trackingClient, event) => {
|
|
392
|
+
emitTelemetry(trackingClient, xapiRef.current, event, {
|
|
393
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
[]
|
|
397
|
+
);
|
|
306
398
|
const track = useCallback(
|
|
307
399
|
(name, data, opts) => {
|
|
308
|
-
const event =
|
|
400
|
+
const event = tryBuildTrackEvent({
|
|
309
401
|
name,
|
|
310
402
|
courseId: courseIdRef.current,
|
|
311
403
|
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
@@ -314,10 +406,35 @@ function LessonkitProvider(props) {
|
|
|
314
406
|
user: userRef.current,
|
|
315
407
|
data
|
|
316
408
|
});
|
|
317
|
-
|
|
409
|
+
if (!event) return;
|
|
410
|
+
emitWithBridge(trackingRef.current, event);
|
|
318
411
|
},
|
|
319
|
-
[]
|
|
412
|
+
[emitWithBridge]
|
|
320
413
|
);
|
|
414
|
+
const prevCourseIdRef = useRef(config.courseId);
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
if (prevCourseIdRef.current === config.courseId) return;
|
|
417
|
+
prevCourseIdRef.current = config.courseId;
|
|
418
|
+
progressRef.current = createProgressController();
|
|
419
|
+
syncProgress();
|
|
420
|
+
const sessionId = sessionIdRef.current;
|
|
421
|
+
const cid = config.courseId;
|
|
422
|
+
if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
423
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
424
|
+
emitTelemetry(
|
|
425
|
+
trackingRef.current,
|
|
426
|
+
xapiRef.current,
|
|
427
|
+
buildTrackEvent({
|
|
428
|
+
name: "course_started",
|
|
429
|
+
courseId: cid,
|
|
430
|
+
sessionId,
|
|
431
|
+
attemptId: attemptIdRef.current,
|
|
432
|
+
user: userRef.current
|
|
433
|
+
}),
|
|
434
|
+
{ lxpackBridge: lxpackBridgeModeRef.current }
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}, [config.courseId, syncProgress]);
|
|
321
438
|
useEffect(() => {
|
|
322
439
|
return () => {
|
|
323
440
|
trackingRef.current?.flush?.();
|
|
@@ -365,6 +482,9 @@ function LessonkitProvider(props) {
|
|
|
365
482
|
syncProgress();
|
|
366
483
|
track("course_completed");
|
|
367
484
|
}, [track, syncProgress]);
|
|
485
|
+
const sessionUser = config.session?.user;
|
|
486
|
+
const sessionAttemptId = config.session?.attemptId;
|
|
487
|
+
const sessionConfiguredId = config.session?.sessionId;
|
|
368
488
|
const runtime = useMemo(
|
|
369
489
|
() => ({
|
|
370
490
|
config,
|
|
@@ -377,7 +497,19 @@ function LessonkitProvider(props) {
|
|
|
377
497
|
completeCourse,
|
|
378
498
|
track
|
|
379
499
|
}),
|
|
380
|
-
[
|
|
500
|
+
[
|
|
501
|
+
config,
|
|
502
|
+
tracking,
|
|
503
|
+
xapi,
|
|
504
|
+
progress,
|
|
505
|
+
setActiveLesson,
|
|
506
|
+
completeLesson,
|
|
507
|
+
completeCourse,
|
|
508
|
+
track,
|
|
509
|
+
sessionUser,
|
|
510
|
+
sessionAttemptId,
|
|
511
|
+
sessionConfiguredId
|
|
512
|
+
]
|
|
381
513
|
);
|
|
382
514
|
return /* @__PURE__ */ jsx(LessonkitContext.Provider, { value: runtime, children: props.children });
|
|
383
515
|
}
|
|
@@ -531,7 +663,7 @@ function Quiz(props) {
|
|
|
531
663
|
});
|
|
532
664
|
if (correct && !completedRef.current) {
|
|
533
665
|
completedRef.current = true;
|
|
534
|
-
quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1 });
|
|
666
|
+
quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1, passingScore: 1 });
|
|
535
667
|
}
|
|
536
668
|
}
|
|
537
669
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "React components and hooks for building learning experiences with LessonKit.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"dist"
|
|
38
38
|
],
|
|
39
39
|
"scripts": {
|
|
40
|
-
"build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/themes",
|
|
41
|
-
"dev": "tsup src/index.tsx --format esm,cjs --dts --watch --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/themes",
|
|
40
|
+
"build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/lxpack --external @lessonkit/themes",
|
|
41
|
+
"dev": "tsup src/index.tsx --format esm,cjs --dts --watch --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/lxpack --external @lessonkit/themes",
|
|
42
42
|
"prepublishOnly": "npm run build",
|
|
43
43
|
"typecheck": "tsc -p tsconfig.json",
|
|
44
44
|
"test": "vitest run --passWithNoTests",
|
|
@@ -50,10 +50,11 @@
|
|
|
50
50
|
"react-dom": ">=18"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lessonkit/accessibility": "0.
|
|
54
|
-
"@lessonkit/core": "0.
|
|
55
|
-
"@lessonkit/
|
|
56
|
-
"@lessonkit/
|
|
53
|
+
"@lessonkit/accessibility": "0.6.0",
|
|
54
|
+
"@lessonkit/core": "0.6.0",
|
|
55
|
+
"@lessonkit/lxpack": "0.6.0",
|
|
56
|
+
"@lessonkit/themes": "0.6.0",
|
|
57
|
+
"@lessonkit/xapi": "0.6.0"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@testing-library/react": "^16.3.0",
|