@lessonkit/react 0.8.1 → 0.9.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 +1 -1
- package/dist/index.cjs +153 -66
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +149 -61
- package/package.json +6 -6
package/README.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,8 @@ __export(index_exports, {
|
|
|
42
42
|
ThemeProvider: () => ThemeProvider,
|
|
43
43
|
blockCatalogVersion: () => blockCatalogVersion,
|
|
44
44
|
buildBlockCatalog: () => buildBlockCatalog,
|
|
45
|
+
createPluginHost: () => import_core7.createPluginHost,
|
|
46
|
+
defineLessonkitPlugin: () => import_core7.defineLessonkitPlugin,
|
|
45
47
|
getBlockCatalogEntry: () => getBlockCatalogEntry,
|
|
46
48
|
useCompletion: () => useCompletion,
|
|
47
49
|
useLessonkit: () => useLessonkit,
|
|
@@ -58,7 +60,7 @@ var import_accessibility = require("@lessonkit/accessibility");
|
|
|
58
60
|
|
|
59
61
|
// src/context.tsx
|
|
60
62
|
var import_react = require("react");
|
|
61
|
-
var
|
|
63
|
+
var import_core5 = require("@lessonkit/core");
|
|
62
64
|
var import_xapi3 = require("@lessonkit/xapi");
|
|
63
65
|
var import_xapi4 = require("@lessonkit/xapi");
|
|
64
66
|
|
|
@@ -69,46 +71,55 @@ var import_xapi = require("@lessonkit/xapi");
|
|
|
69
71
|
// src/runtime/lxpackBridge.ts
|
|
70
72
|
var import_bridge = require("@lessonkit/lxpack/bridge");
|
|
71
73
|
function getBridge() {
|
|
74
|
+
const fromSdk = (0, import_bridge.getLxpackBridge)();
|
|
75
|
+
if (fromSdk) return fromSdk;
|
|
72
76
|
if (typeof window === "undefined") return null;
|
|
73
77
|
const parent = window.parent;
|
|
74
78
|
if (!parent || parent === window) return null;
|
|
75
|
-
return parent.
|
|
79
|
+
return parent.lxpack ?? null;
|
|
76
80
|
}
|
|
77
|
-
function
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
case "lesson_completed": {
|
|
83
|
-
const lessonId = event.lessonId;
|
|
84
|
-
if (lessonId) bridge.completeLesson?.(lessonId);
|
|
81
|
+
function applyBridgeAction(bridge, action) {
|
|
82
|
+
if (!action) return;
|
|
83
|
+
switch (action.kind) {
|
|
84
|
+
case "completeLesson":
|
|
85
|
+
bridge.completeLesson?.(action.lessonId);
|
|
85
86
|
return;
|
|
86
|
-
|
|
87
|
-
case "course_completed":
|
|
87
|
+
case "completeCourse":
|
|
88
88
|
bridge.completeCourse?.();
|
|
89
89
|
return;
|
|
90
|
-
case "
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
score: data.score,
|
|
95
|
-
maxScore: data.maxScore
|
|
90
|
+
case "submitAssessment": {
|
|
91
|
+
const scaled = (0, import_bridge.normalizeScore)({
|
|
92
|
+
score: action.score,
|
|
93
|
+
maxScore: action.maxScore
|
|
96
94
|
});
|
|
97
95
|
if (scaled === null) return;
|
|
98
96
|
bridge.submitAssessment?.({
|
|
99
|
-
id:
|
|
97
|
+
id: action.id,
|
|
100
98
|
score: scaled,
|
|
101
|
-
passingScore: (0, import_bridge.
|
|
102
|
-
passingScore:
|
|
103
|
-
maxScore:
|
|
104
|
-
})
|
|
99
|
+
passingScore: (0, import_bridge.normalizePassingThreshold)({
|
|
100
|
+
passingScore: action.passingScore,
|
|
101
|
+
maxScore: action.maxScore
|
|
102
|
+
}),
|
|
103
|
+
maxScore: action.maxScore
|
|
105
104
|
});
|
|
106
105
|
return;
|
|
107
106
|
}
|
|
107
|
+
case "track":
|
|
108
|
+
bridge.track?.(action.event);
|
|
109
|
+
return;
|
|
108
110
|
default:
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
113
|
}
|
|
114
|
+
function forwardTelemetryToLxpack(event, mode = "auto") {
|
|
115
|
+
if (mode === "off") return;
|
|
116
|
+
const bridge = getBridge();
|
|
117
|
+
if (!bridge) return;
|
|
118
|
+
const lessonkitEvent = (0, import_bridge.telemetryEventToLessonkit)(event);
|
|
119
|
+
if (!lessonkitEvent) return;
|
|
120
|
+
const action = (0, import_bridge.mapLessonkitTelemetryToBridgeAction)(lessonkitEvent);
|
|
121
|
+
applyBridgeAction(bridge, action);
|
|
122
|
+
}
|
|
112
123
|
|
|
113
124
|
// src/runtime/emitTelemetry.ts
|
|
114
125
|
var warnedMissingCourseId = false;
|
|
@@ -328,12 +339,31 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
|
|
|
328
339
|
}
|
|
329
340
|
}
|
|
330
341
|
|
|
331
|
-
// src/runtime/
|
|
342
|
+
// src/runtime/plugins.ts
|
|
332
343
|
var import_core3 = require("@lessonkit/core");
|
|
344
|
+
function createReactPluginHost(plugins) {
|
|
345
|
+
if (!plugins?.length) return null;
|
|
346
|
+
return (0, import_core3.createPluginHost)(plugins);
|
|
347
|
+
}
|
|
348
|
+
function buildPluginContext(opts) {
|
|
349
|
+
return {
|
|
350
|
+
courseId: opts.courseId,
|
|
351
|
+
sessionId: opts.sessionId,
|
|
352
|
+
attemptId: opts.attemptId
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function emitTelemetryWithPlugins(opts) {
|
|
356
|
+
const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
|
|
357
|
+
if (next === null) return;
|
|
358
|
+
emitTelemetry(opts.tracking, opts.xapi, next, { lxpackBridge: opts.lxpackBridge ?? "auto" });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/runtime/telemetry.ts
|
|
362
|
+
var import_core4 = require("@lessonkit/core");
|
|
333
363
|
function createTrackingClientFromConfig(config) {
|
|
334
|
-
if (config.tracking?.enabled === false) return (0,
|
|
364
|
+
if (config.tracking?.enabled === false) return (0, import_core4.createTrackingClient)();
|
|
335
365
|
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
336
|
-
return (0,
|
|
366
|
+
return (0, import_core4.createTrackingClient)({
|
|
337
367
|
sink: config.tracking?.sink,
|
|
338
368
|
batchSink: config.tracking?.batchSink,
|
|
339
369
|
batch: config.tracking?.batch
|
|
@@ -369,6 +399,9 @@ function LessonkitProvider(props) {
|
|
|
369
399
|
courseIdRef.current = config.courseId;
|
|
370
400
|
const lxpackBridgeModeRef = (0, import_react.useRef)(config.lxpack?.bridge ?? "auto");
|
|
371
401
|
lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
|
|
402
|
+
const pluginHost = (0, import_react.useMemo)(() => createReactPluginHost(config.plugins), [config.plugins]);
|
|
403
|
+
const pluginHostRef = (0, import_react.useRef)(pluginHost);
|
|
404
|
+
pluginHostRef.current = pluginHost;
|
|
372
405
|
const progressRef = (0, import_react.useRef)(createProgressController());
|
|
373
406
|
const courseStartedEmittedToSinkRef = (0, import_react.useRef)(false);
|
|
374
407
|
const prevCourseIdForProgressRef = (0, import_react.useRef)(config.courseId);
|
|
@@ -393,6 +426,7 @@ function LessonkitProvider(props) {
|
|
|
393
426
|
const xapiClient = config.xapi?.client;
|
|
394
427
|
const xapiTransport = config.xapi?.transport;
|
|
395
428
|
const courseId = config.courseId;
|
|
429
|
+
const trackingEnabled = config.tracking?.enabled;
|
|
396
430
|
useIsoLayoutEffect(() => {
|
|
397
431
|
if (prevXapiCourseIdRef.current !== courseId) {
|
|
398
432
|
xapiQueueRef.current = (0, import_xapi3.createInMemoryXAPIQueue)();
|
|
@@ -405,18 +439,22 @@ function LessonkitProvider(props) {
|
|
|
405
439
|
if (next && !prev) {
|
|
406
440
|
const sessionId = sessionIdRef.current;
|
|
407
441
|
const cid = courseIdRef.current;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
442
|
+
const trackingActive = isTrackingActive(config.tracking);
|
|
443
|
+
const alreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
|
|
444
|
+
if (!trackingActive || alreadyStarted) {
|
|
445
|
+
try {
|
|
446
|
+
const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
|
|
447
|
+
buildTrackEvent({
|
|
448
|
+
name: "course_started",
|
|
449
|
+
courseId: cid,
|
|
450
|
+
sessionId,
|
|
451
|
+
attemptId: attemptIdRef.current,
|
|
452
|
+
user: userRef.current
|
|
453
|
+
})
|
|
454
|
+
);
|
|
455
|
+
if (statement) next.send(statement);
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
420
458
|
}
|
|
421
459
|
}
|
|
422
460
|
let cancelled = false;
|
|
@@ -437,11 +475,10 @@ function LessonkitProvider(props) {
|
|
|
437
475
|
cancelled = true;
|
|
438
476
|
void prev?.flush();
|
|
439
477
|
};
|
|
440
|
-
}, [xapiEnabled, xapiClient, xapiTransport, courseId]);
|
|
441
|
-
const trackingRef = (0, import_react.useRef)((0,
|
|
478
|
+
}, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
|
|
479
|
+
const trackingRef = (0, import_react.useRef)((0, import_core5.createTrackingClient)());
|
|
442
480
|
const trackingClientForUnmountRef = (0, import_react.useRef)(trackingRef.current);
|
|
443
481
|
const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
|
|
444
|
-
const trackingEnabled = config.tracking?.enabled;
|
|
445
482
|
const trackingSink = config.tracking?.sink;
|
|
446
483
|
const trackingBatchSink = config.tracking?.batchSink;
|
|
447
484
|
const batchEnabled = config.tracking?.batch?.enabled;
|
|
@@ -449,7 +486,19 @@ function LessonkitProvider(props) {
|
|
|
449
486
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
450
487
|
useIsoLayoutEffect(() => {
|
|
451
488
|
const prev = trackingRef.current;
|
|
452
|
-
const
|
|
489
|
+
const pluginCtx = buildPluginContext({
|
|
490
|
+
courseId: courseIdRef.current,
|
|
491
|
+
sessionId: sessionIdRef.current,
|
|
492
|
+
attemptId: attemptIdRef.current
|
|
493
|
+
});
|
|
494
|
+
const sink = pluginHostRef.current?.composeTrackingSink(config.tracking?.sink, pluginCtx) ?? config.tracking?.sink;
|
|
495
|
+
const batchSink = pluginHostRef.current && config.tracking?.batchSink ? (events) => {
|
|
496
|
+
const filtered = pluginHostRef.current.runTelemetryBatch(events, pluginCtx);
|
|
497
|
+
return config.tracking.batchSink(filtered);
|
|
498
|
+
} : config.tracking?.batchSink;
|
|
499
|
+
const next = createTrackingClientFromConfig({
|
|
500
|
+
tracking: { ...config.tracking, sink, batchSink }
|
|
501
|
+
});
|
|
453
502
|
trackingRef.current = next;
|
|
454
503
|
trackingClientForUnmountRef.current = next;
|
|
455
504
|
setTracking(next);
|
|
@@ -460,18 +509,24 @@ function LessonkitProvider(props) {
|
|
|
460
509
|
courseStartedEmittedToSinkRef.current = false;
|
|
461
510
|
} else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
462
511
|
markCourseStarted(defaultStorage, sessionId, cid);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
512
|
+
emitTelemetryWithPlugins({
|
|
513
|
+
pluginHost: pluginHostRef.current,
|
|
514
|
+
tracking: next,
|
|
515
|
+
xapi: xapiRef.current,
|
|
516
|
+
event: buildTrackEvent({
|
|
467
517
|
name: "course_started",
|
|
468
518
|
courseId: cid,
|
|
469
519
|
sessionId,
|
|
470
520
|
attemptId: attemptIdRef.current,
|
|
471
521
|
user: userRef.current
|
|
472
522
|
}),
|
|
473
|
-
|
|
474
|
-
|
|
523
|
+
pluginCtx: buildPluginContext({
|
|
524
|
+
courseId: cid,
|
|
525
|
+
sessionId,
|
|
526
|
+
attemptId: attemptIdRef.current
|
|
527
|
+
}),
|
|
528
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
529
|
+
});
|
|
475
530
|
courseStartedEmittedToSinkRef.current = true;
|
|
476
531
|
} else if (trackingActive) {
|
|
477
532
|
courseStartedEmittedToSinkRef.current = true;
|
|
@@ -487,16 +542,23 @@ function LessonkitProvider(props) {
|
|
|
487
542
|
trackingBatchSink,
|
|
488
543
|
batchEnabled,
|
|
489
544
|
batchFlushIntervalMs,
|
|
490
|
-
batchMaxBatchSize
|
|
545
|
+
batchMaxBatchSize,
|
|
546
|
+
config.plugins
|
|
491
547
|
]);
|
|
492
|
-
const emitWithBridge = (0, import_react.useCallback)(
|
|
493
|
-
(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
548
|
+
const emitWithBridge = (0, import_react.useCallback)((trackingClient, event) => {
|
|
549
|
+
emitTelemetryWithPlugins({
|
|
550
|
+
pluginHost: pluginHostRef.current,
|
|
551
|
+
tracking: trackingClient,
|
|
552
|
+
xapi: xapiRef.current,
|
|
553
|
+
event,
|
|
554
|
+
pluginCtx: buildPluginContext({
|
|
555
|
+
courseId: courseIdRef.current,
|
|
556
|
+
sessionId: sessionIdRef.current,
|
|
557
|
+
attemptId: attemptIdRef.current
|
|
558
|
+
}),
|
|
559
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
560
|
+
});
|
|
561
|
+
}, []);
|
|
500
562
|
const track = (0, import_react.useCallback)(
|
|
501
563
|
(name, data, opts) => {
|
|
502
564
|
const event = tryBuildTrackEvent({
|
|
@@ -522,18 +584,24 @@ function LessonkitProvider(props) {
|
|
|
522
584
|
const cid = courseIdRef.current;
|
|
523
585
|
if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
524
586
|
markCourseStarted(defaultStorage, sessionId, cid);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
587
|
+
emitTelemetryWithPlugins({
|
|
588
|
+
pluginHost: pluginHostRef.current,
|
|
589
|
+
tracking: trackingRef.current,
|
|
590
|
+
xapi: xapiRef.current,
|
|
591
|
+
event: buildTrackEvent({
|
|
529
592
|
name: "course_started",
|
|
530
593
|
courseId: cid,
|
|
531
594
|
sessionId,
|
|
532
595
|
attemptId: attemptIdRef.current,
|
|
533
596
|
user: userRef.current
|
|
534
597
|
}),
|
|
535
|
-
|
|
536
|
-
|
|
598
|
+
pluginCtx: buildPluginContext({
|
|
599
|
+
courseId: cid,
|
|
600
|
+
sessionId,
|
|
601
|
+
attemptId: attemptIdRef.current
|
|
602
|
+
}),
|
|
603
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
604
|
+
});
|
|
537
605
|
courseStartedEmittedToSinkRef.current = true;
|
|
538
606
|
}
|
|
539
607
|
}, [config.courseId, config.tracking?.enabled, syncProgress]);
|
|
@@ -600,6 +668,18 @@ function LessonkitProvider(props) {
|
|
|
600
668
|
const sessionUser = config.session?.user;
|
|
601
669
|
const sessionAttemptId = config.session?.attemptId;
|
|
602
670
|
const sessionConfiguredId = config.session?.sessionId;
|
|
671
|
+
(0, import_react.useEffect)(() => {
|
|
672
|
+
if (!pluginHost) return;
|
|
673
|
+
const ctx = buildPluginContext({
|
|
674
|
+
courseId: courseIdRef.current,
|
|
675
|
+
sessionId: sessionIdRef.current,
|
|
676
|
+
attemptId: attemptIdRef.current
|
|
677
|
+
});
|
|
678
|
+
pluginHost.setupAll(ctx);
|
|
679
|
+
return () => {
|
|
680
|
+
pluginHost.disposeAll();
|
|
681
|
+
};
|
|
682
|
+
}, [pluginHost, config.courseId, sessionAttemptId, sessionConfiguredId]);
|
|
603
683
|
(0, import_react.useEffect)(() => {
|
|
604
684
|
const nextConfigured = config.session?.sessionId;
|
|
605
685
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
@@ -633,7 +713,8 @@ function LessonkitProvider(props) {
|
|
|
633
713
|
setActiveLesson,
|
|
634
714
|
completeLesson,
|
|
635
715
|
completeCourse,
|
|
636
|
-
track
|
|
716
|
+
track,
|
|
717
|
+
plugins: pluginHost
|
|
637
718
|
}),
|
|
638
719
|
[
|
|
639
720
|
config,
|
|
@@ -644,6 +725,7 @@ function LessonkitProvider(props) {
|
|
|
644
725
|
completeLesson,
|
|
645
726
|
completeCourse,
|
|
646
727
|
track,
|
|
728
|
+
pluginHost,
|
|
647
729
|
sessionUser,
|
|
648
730
|
sessionAttemptId,
|
|
649
731
|
sessionConfiguredId
|
|
@@ -687,7 +769,7 @@ function useQuizState() {
|
|
|
687
769
|
}
|
|
688
770
|
|
|
689
771
|
// src/runtime/validateComponentId.ts
|
|
690
|
-
var
|
|
772
|
+
var import_core6 = require("@lessonkit/core");
|
|
691
773
|
var warnedPaths = /* @__PURE__ */ new Set();
|
|
692
774
|
function isDevEnvironment2() {
|
|
693
775
|
const g = globalThis;
|
|
@@ -697,7 +779,7 @@ function warnInvalidComponentId(id, path) {
|
|
|
697
779
|
if (!isDevEnvironment2()) return;
|
|
698
780
|
const key = `${path}:${String(id)}`;
|
|
699
781
|
if (warnedPaths.has(key)) return;
|
|
700
|
-
const result = (0,
|
|
782
|
+
const result = (0, import_core6.validateId)(id, path);
|
|
701
783
|
if (result.ok) return;
|
|
702
784
|
warnedPaths.add(key);
|
|
703
785
|
const detail = result.issues.map((i) => `${i.path}: ${i.message}`).join("; ");
|
|
@@ -822,6 +904,9 @@ function ProgressTracker() {
|
|
|
822
904
|
] }) });
|
|
823
905
|
}
|
|
824
906
|
|
|
907
|
+
// src/index.tsx
|
|
908
|
+
var import_core7 = require("@lessonkit/core");
|
|
909
|
+
|
|
825
910
|
// src/theme/ThemeProvider.tsx
|
|
826
911
|
var import_react4 = __toESM(require("react"), 1);
|
|
827
912
|
var import_themes = require("@lessonkit/themes");
|
|
@@ -1130,6 +1215,8 @@ function getBlockCatalogEntry(type) {
|
|
|
1130
1215
|
ThemeProvider,
|
|
1131
1216
|
blockCatalogVersion,
|
|
1132
1217
|
buildBlockCatalog,
|
|
1218
|
+
createPluginHost,
|
|
1219
|
+
defineLessonkitPlugin,
|
|
1133
1220
|
getBlockCatalogEntry,
|
|
1134
1221
|
useCompletion,
|
|
1135
1222
|
useLessonkit,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import * as _lessonkit_core from '@lessonkit/core';
|
|
4
|
-
import { LessonId, CourseId, TelemetryUser, TrackingClient, TelemetryEventName, CheckId, BlockId } from '@lessonkit/core';
|
|
4
|
+
import { LessonId, CourseId, TelemetryUser, TrackingClient, LessonkitPlugin, TelemetryEventName, PluginHost, CheckId, BlockId } from '@lessonkit/core';
|
|
5
|
+
export { AssessmentScoreInput, AssessmentScoreResult, InteractionBlockRegistration, LessonkitPlugin, LessonkitPluginContext, LessonkitPluginKind, PluginHost, createPluginHost, defineLessonkitPlugin } from '@lessonkit/core';
|
|
5
6
|
import { XAPITransport, XAPIClient } from '@lessonkit/xapi';
|
|
6
7
|
import { LessonkitThemeV1, ThemePresetName, PartialLessonkitThemeV1 } from '@lessonkit/themes';
|
|
7
8
|
export { ThemePresetName } from '@lessonkit/themes';
|
|
@@ -38,6 +39,8 @@ type LessonkitConfig = {
|
|
|
38
39
|
/** Forward completion events to `window.parent.lxpackBridge.v1` when embedded (default `auto`). */
|
|
39
40
|
bridge?: "auto" | "off";
|
|
40
41
|
};
|
|
42
|
+
/** Framework plugins (analytics, LMS, assessment, interaction, AI). */
|
|
43
|
+
plugins?: LessonkitPlugin[];
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
type LessonkitRuntime = {
|
|
@@ -56,6 +59,7 @@ type LessonkitRuntime = {
|
|
|
56
59
|
track: (name: TelemetryEventName, data?: unknown, opts?: {
|
|
57
60
|
lessonId?: LessonId;
|
|
58
61
|
}) => void;
|
|
62
|
+
plugins: PluginHost | null;
|
|
59
63
|
};
|
|
60
64
|
declare function LessonkitProvider(props: {
|
|
61
65
|
config: LessonkitConfig;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import * as _lessonkit_core from '@lessonkit/core';
|
|
4
|
-
import { LessonId, CourseId, TelemetryUser, TrackingClient, TelemetryEventName, CheckId, BlockId } from '@lessonkit/core';
|
|
4
|
+
import { LessonId, CourseId, TelemetryUser, TrackingClient, LessonkitPlugin, TelemetryEventName, PluginHost, CheckId, BlockId } from '@lessonkit/core';
|
|
5
|
+
export { AssessmentScoreInput, AssessmentScoreResult, InteractionBlockRegistration, LessonkitPlugin, LessonkitPluginContext, LessonkitPluginKind, PluginHost, createPluginHost, defineLessonkitPlugin } from '@lessonkit/core';
|
|
5
6
|
import { XAPITransport, XAPIClient } from '@lessonkit/xapi';
|
|
6
7
|
import { LessonkitThemeV1, ThemePresetName, PartialLessonkitThemeV1 } from '@lessonkit/themes';
|
|
7
8
|
export { ThemePresetName } from '@lessonkit/themes';
|
|
@@ -38,6 +39,8 @@ type LessonkitConfig = {
|
|
|
38
39
|
/** Forward completion events to `window.parent.lxpackBridge.v1` when embedded (default `auto`). */
|
|
39
40
|
bridge?: "auto" | "off";
|
|
40
41
|
};
|
|
42
|
+
/** Framework plugins (analytics, LMS, assessment, interaction, AI). */
|
|
43
|
+
plugins?: LessonkitPlugin[];
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
type LessonkitRuntime = {
|
|
@@ -56,6 +59,7 @@ type LessonkitRuntime = {
|
|
|
56
59
|
track: (name: TelemetryEventName, data?: unknown, opts?: {
|
|
57
60
|
lessonId?: LessonId;
|
|
58
61
|
}) => void;
|
|
62
|
+
plugins: PluginHost | null;
|
|
59
63
|
};
|
|
60
64
|
declare function LessonkitProvider(props: {
|
|
61
65
|
config: LessonkitConfig;
|
package/dist/index.js
CHANGED
|
@@ -22,50 +22,62 @@ import { telemetryEventToXAPIStatement } from "@lessonkit/xapi";
|
|
|
22
22
|
|
|
23
23
|
// src/runtime/lxpackBridge.ts
|
|
24
24
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
getLxpackBridge as getLxpackBridgeFromSdk,
|
|
26
|
+
mapLessonkitTelemetryToBridgeAction,
|
|
27
|
+
normalizePassingThreshold,
|
|
28
|
+
normalizeScore,
|
|
29
|
+
telemetryEventToLessonkit
|
|
27
30
|
} from "@lessonkit/lxpack/bridge";
|
|
28
31
|
function getBridge() {
|
|
32
|
+
const fromSdk = getLxpackBridgeFromSdk();
|
|
33
|
+
if (fromSdk) return fromSdk;
|
|
29
34
|
if (typeof window === "undefined") return null;
|
|
30
35
|
const parent = window.parent;
|
|
31
36
|
if (!parent || parent === window) return null;
|
|
32
|
-
return parent.
|
|
37
|
+
return parent.lxpack ?? null;
|
|
33
38
|
}
|
|
34
|
-
function
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
case "lesson_completed": {
|
|
40
|
-
const lessonId = event.lessonId;
|
|
41
|
-
if (lessonId) bridge.completeLesson?.(lessonId);
|
|
39
|
+
function applyBridgeAction(bridge, action) {
|
|
40
|
+
if (!action) return;
|
|
41
|
+
switch (action.kind) {
|
|
42
|
+
case "completeLesson":
|
|
43
|
+
bridge.completeLesson?.(action.lessonId);
|
|
42
44
|
return;
|
|
43
|
-
|
|
44
|
-
case "course_completed":
|
|
45
|
+
case "completeCourse":
|
|
45
46
|
bridge.completeCourse?.();
|
|
46
47
|
return;
|
|
47
|
-
case "
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
score: data.score,
|
|
52
|
-
maxScore: data.maxScore
|
|
48
|
+
case "submitAssessment": {
|
|
49
|
+
const scaled = normalizeScore({
|
|
50
|
+
score: action.score,
|
|
51
|
+
maxScore: action.maxScore
|
|
53
52
|
});
|
|
54
53
|
if (scaled === null) return;
|
|
55
54
|
bridge.submitAssessment?.({
|
|
56
|
-
id:
|
|
55
|
+
id: action.id,
|
|
57
56
|
score: scaled,
|
|
58
|
-
passingScore:
|
|
59
|
-
passingScore:
|
|
60
|
-
maxScore:
|
|
61
|
-
})
|
|
57
|
+
passingScore: normalizePassingThreshold({
|
|
58
|
+
passingScore: action.passingScore,
|
|
59
|
+
maxScore: action.maxScore
|
|
60
|
+
}),
|
|
61
|
+
maxScore: action.maxScore
|
|
62
62
|
});
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
+
case "track":
|
|
66
|
+
bridge.track?.(action.event);
|
|
67
|
+
return;
|
|
65
68
|
default:
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
68
71
|
}
|
|
72
|
+
function forwardTelemetryToLxpack(event, mode = "auto") {
|
|
73
|
+
if (mode === "off") return;
|
|
74
|
+
const bridge = getBridge();
|
|
75
|
+
if (!bridge) return;
|
|
76
|
+
const lessonkitEvent = telemetryEventToLessonkit(event);
|
|
77
|
+
if (!lessonkitEvent) return;
|
|
78
|
+
const action = mapLessonkitTelemetryToBridgeAction(lessonkitEvent);
|
|
79
|
+
applyBridgeAction(bridge, action);
|
|
80
|
+
}
|
|
69
81
|
|
|
70
82
|
// src/runtime/emitTelemetry.ts
|
|
71
83
|
var warnedMissingCourseId = false;
|
|
@@ -285,6 +297,25 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
|
|
|
285
297
|
}
|
|
286
298
|
}
|
|
287
299
|
|
|
300
|
+
// src/runtime/plugins.ts
|
|
301
|
+
import { createPluginHost } from "@lessonkit/core";
|
|
302
|
+
function createReactPluginHost(plugins) {
|
|
303
|
+
if (!plugins?.length) return null;
|
|
304
|
+
return createPluginHost(plugins);
|
|
305
|
+
}
|
|
306
|
+
function buildPluginContext(opts) {
|
|
307
|
+
return {
|
|
308
|
+
courseId: opts.courseId,
|
|
309
|
+
sessionId: opts.sessionId,
|
|
310
|
+
attemptId: opts.attemptId
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function emitTelemetryWithPlugins(opts) {
|
|
314
|
+
const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
|
|
315
|
+
if (next === null) return;
|
|
316
|
+
emitTelemetry(opts.tracking, opts.xapi, next, { lxpackBridge: opts.lxpackBridge ?? "auto" });
|
|
317
|
+
}
|
|
318
|
+
|
|
288
319
|
// src/runtime/telemetry.ts
|
|
289
320
|
import { createTrackingClient } from "@lessonkit/core";
|
|
290
321
|
function createTrackingClientFromConfig(config) {
|
|
@@ -326,6 +357,9 @@ function LessonkitProvider(props) {
|
|
|
326
357
|
courseIdRef.current = config.courseId;
|
|
327
358
|
const lxpackBridgeModeRef = useRef(config.lxpack?.bridge ?? "auto");
|
|
328
359
|
lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
|
|
360
|
+
const pluginHost = useMemo(() => createReactPluginHost(config.plugins), [config.plugins]);
|
|
361
|
+
const pluginHostRef = useRef(pluginHost);
|
|
362
|
+
pluginHostRef.current = pluginHost;
|
|
329
363
|
const progressRef = useRef(createProgressController());
|
|
330
364
|
const courseStartedEmittedToSinkRef = useRef(false);
|
|
331
365
|
const prevCourseIdForProgressRef = useRef(config.courseId);
|
|
@@ -350,6 +384,7 @@ function LessonkitProvider(props) {
|
|
|
350
384
|
const xapiClient = config.xapi?.client;
|
|
351
385
|
const xapiTransport = config.xapi?.transport;
|
|
352
386
|
const courseId = config.courseId;
|
|
387
|
+
const trackingEnabled = config.tracking?.enabled;
|
|
353
388
|
useIsoLayoutEffect(() => {
|
|
354
389
|
if (prevXapiCourseIdRef.current !== courseId) {
|
|
355
390
|
xapiQueueRef.current = createInMemoryXAPIQueue();
|
|
@@ -362,18 +397,22 @@ function LessonkitProvider(props) {
|
|
|
362
397
|
if (next && !prev) {
|
|
363
398
|
const sessionId = sessionIdRef.current;
|
|
364
399
|
const cid = courseIdRef.current;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
400
|
+
const trackingActive = isTrackingActive(config.tracking);
|
|
401
|
+
const alreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
|
|
402
|
+
if (!trackingActive || alreadyStarted) {
|
|
403
|
+
try {
|
|
404
|
+
const statement = telemetryEventToXAPIStatement2(
|
|
405
|
+
buildTrackEvent({
|
|
406
|
+
name: "course_started",
|
|
407
|
+
courseId: cid,
|
|
408
|
+
sessionId,
|
|
409
|
+
attemptId: attemptIdRef.current,
|
|
410
|
+
user: userRef.current
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
if (statement) next.send(statement);
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
377
416
|
}
|
|
378
417
|
}
|
|
379
418
|
let cancelled = false;
|
|
@@ -394,11 +433,10 @@ function LessonkitProvider(props) {
|
|
|
394
433
|
cancelled = true;
|
|
395
434
|
void prev?.flush();
|
|
396
435
|
};
|
|
397
|
-
}, [xapiEnabled, xapiClient, xapiTransport, courseId]);
|
|
436
|
+
}, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
|
|
398
437
|
const trackingRef = useRef(createTrackingClient2());
|
|
399
438
|
const trackingClientForUnmountRef = useRef(trackingRef.current);
|
|
400
439
|
const [tracking, setTracking] = useState(() => trackingRef.current);
|
|
401
|
-
const trackingEnabled = config.tracking?.enabled;
|
|
402
440
|
const trackingSink = config.tracking?.sink;
|
|
403
441
|
const trackingBatchSink = config.tracking?.batchSink;
|
|
404
442
|
const batchEnabled = config.tracking?.batch?.enabled;
|
|
@@ -406,7 +444,19 @@ function LessonkitProvider(props) {
|
|
|
406
444
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
407
445
|
useIsoLayoutEffect(() => {
|
|
408
446
|
const prev = trackingRef.current;
|
|
409
|
-
const
|
|
447
|
+
const pluginCtx = buildPluginContext({
|
|
448
|
+
courseId: courseIdRef.current,
|
|
449
|
+
sessionId: sessionIdRef.current,
|
|
450
|
+
attemptId: attemptIdRef.current
|
|
451
|
+
});
|
|
452
|
+
const sink = pluginHostRef.current?.composeTrackingSink(config.tracking?.sink, pluginCtx) ?? config.tracking?.sink;
|
|
453
|
+
const batchSink = pluginHostRef.current && config.tracking?.batchSink ? (events) => {
|
|
454
|
+
const filtered = pluginHostRef.current.runTelemetryBatch(events, pluginCtx);
|
|
455
|
+
return config.tracking.batchSink(filtered);
|
|
456
|
+
} : config.tracking?.batchSink;
|
|
457
|
+
const next = createTrackingClientFromConfig({
|
|
458
|
+
tracking: { ...config.tracking, sink, batchSink }
|
|
459
|
+
});
|
|
410
460
|
trackingRef.current = next;
|
|
411
461
|
trackingClientForUnmountRef.current = next;
|
|
412
462
|
setTracking(next);
|
|
@@ -417,18 +467,24 @@ function LessonkitProvider(props) {
|
|
|
417
467
|
courseStartedEmittedToSinkRef.current = false;
|
|
418
468
|
} else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
419
469
|
markCourseStarted(defaultStorage, sessionId, cid);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
470
|
+
emitTelemetryWithPlugins({
|
|
471
|
+
pluginHost: pluginHostRef.current,
|
|
472
|
+
tracking: next,
|
|
473
|
+
xapi: xapiRef.current,
|
|
474
|
+
event: buildTrackEvent({
|
|
424
475
|
name: "course_started",
|
|
425
476
|
courseId: cid,
|
|
426
477
|
sessionId,
|
|
427
478
|
attemptId: attemptIdRef.current,
|
|
428
479
|
user: userRef.current
|
|
429
480
|
}),
|
|
430
|
-
|
|
431
|
-
|
|
481
|
+
pluginCtx: buildPluginContext({
|
|
482
|
+
courseId: cid,
|
|
483
|
+
sessionId,
|
|
484
|
+
attemptId: attemptIdRef.current
|
|
485
|
+
}),
|
|
486
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
487
|
+
});
|
|
432
488
|
courseStartedEmittedToSinkRef.current = true;
|
|
433
489
|
} else if (trackingActive) {
|
|
434
490
|
courseStartedEmittedToSinkRef.current = true;
|
|
@@ -444,16 +500,23 @@ function LessonkitProvider(props) {
|
|
|
444
500
|
trackingBatchSink,
|
|
445
501
|
batchEnabled,
|
|
446
502
|
batchFlushIntervalMs,
|
|
447
|
-
batchMaxBatchSize
|
|
503
|
+
batchMaxBatchSize,
|
|
504
|
+
config.plugins
|
|
448
505
|
]);
|
|
449
|
-
const emitWithBridge = useCallback(
|
|
450
|
-
(
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
506
|
+
const emitWithBridge = useCallback((trackingClient, event) => {
|
|
507
|
+
emitTelemetryWithPlugins({
|
|
508
|
+
pluginHost: pluginHostRef.current,
|
|
509
|
+
tracking: trackingClient,
|
|
510
|
+
xapi: xapiRef.current,
|
|
511
|
+
event,
|
|
512
|
+
pluginCtx: buildPluginContext({
|
|
513
|
+
courseId: courseIdRef.current,
|
|
514
|
+
sessionId: sessionIdRef.current,
|
|
515
|
+
attemptId: attemptIdRef.current
|
|
516
|
+
}),
|
|
517
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
518
|
+
});
|
|
519
|
+
}, []);
|
|
457
520
|
const track = useCallback(
|
|
458
521
|
(name, data, opts) => {
|
|
459
522
|
const event = tryBuildTrackEvent({
|
|
@@ -479,18 +542,24 @@ function LessonkitProvider(props) {
|
|
|
479
542
|
const cid = courseIdRef.current;
|
|
480
543
|
if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
481
544
|
markCourseStarted(defaultStorage, sessionId, cid);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
545
|
+
emitTelemetryWithPlugins({
|
|
546
|
+
pluginHost: pluginHostRef.current,
|
|
547
|
+
tracking: trackingRef.current,
|
|
548
|
+
xapi: xapiRef.current,
|
|
549
|
+
event: buildTrackEvent({
|
|
486
550
|
name: "course_started",
|
|
487
551
|
courseId: cid,
|
|
488
552
|
sessionId,
|
|
489
553
|
attemptId: attemptIdRef.current,
|
|
490
554
|
user: userRef.current
|
|
491
555
|
}),
|
|
492
|
-
|
|
493
|
-
|
|
556
|
+
pluginCtx: buildPluginContext({
|
|
557
|
+
courseId: cid,
|
|
558
|
+
sessionId,
|
|
559
|
+
attemptId: attemptIdRef.current
|
|
560
|
+
}),
|
|
561
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
562
|
+
});
|
|
494
563
|
courseStartedEmittedToSinkRef.current = true;
|
|
495
564
|
}
|
|
496
565
|
}, [config.courseId, config.tracking?.enabled, syncProgress]);
|
|
@@ -557,6 +626,18 @@ function LessonkitProvider(props) {
|
|
|
557
626
|
const sessionUser = config.session?.user;
|
|
558
627
|
const sessionAttemptId = config.session?.attemptId;
|
|
559
628
|
const sessionConfiguredId = config.session?.sessionId;
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
if (!pluginHost) return;
|
|
631
|
+
const ctx = buildPluginContext({
|
|
632
|
+
courseId: courseIdRef.current,
|
|
633
|
+
sessionId: sessionIdRef.current,
|
|
634
|
+
attemptId: attemptIdRef.current
|
|
635
|
+
});
|
|
636
|
+
pluginHost.setupAll(ctx);
|
|
637
|
+
return () => {
|
|
638
|
+
pluginHost.disposeAll();
|
|
639
|
+
};
|
|
640
|
+
}, [pluginHost, config.courseId, sessionAttemptId, sessionConfiguredId]);
|
|
560
641
|
useEffect(() => {
|
|
561
642
|
const nextConfigured = config.session?.sessionId;
|
|
562
643
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
@@ -590,7 +671,8 @@ function LessonkitProvider(props) {
|
|
|
590
671
|
setActiveLesson,
|
|
591
672
|
completeLesson,
|
|
592
673
|
completeCourse,
|
|
593
|
-
track
|
|
674
|
+
track,
|
|
675
|
+
plugins: pluginHost
|
|
594
676
|
}),
|
|
595
677
|
[
|
|
596
678
|
config,
|
|
@@ -601,6 +683,7 @@ function LessonkitProvider(props) {
|
|
|
601
683
|
completeLesson,
|
|
602
684
|
completeCourse,
|
|
603
685
|
track,
|
|
686
|
+
pluginHost,
|
|
604
687
|
sessionUser,
|
|
605
688
|
sessionAttemptId,
|
|
606
689
|
sessionConfiguredId
|
|
@@ -779,6 +862,9 @@ function ProgressTracker() {
|
|
|
779
862
|
] }) });
|
|
780
863
|
}
|
|
781
864
|
|
|
865
|
+
// src/index.tsx
|
|
866
|
+
import { createPluginHost as createPluginHost2, defineLessonkitPlugin } from "@lessonkit/core";
|
|
867
|
+
|
|
782
868
|
// src/theme/ThemeProvider.tsx
|
|
783
869
|
import React3, {
|
|
784
870
|
createContext as createContext2,
|
|
@@ -1101,6 +1187,8 @@ export {
|
|
|
1101
1187
|
ThemeProvider,
|
|
1102
1188
|
blockCatalogVersion,
|
|
1103
1189
|
buildBlockCatalog,
|
|
1190
|
+
createPluginHost2 as createPluginHost,
|
|
1191
|
+
defineLessonkitPlugin,
|
|
1104
1192
|
getBlockCatalogEntry,
|
|
1105
1193
|
useCompletion,
|
|
1106
1194
|
useLessonkit,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "React components and hooks for building learning experiences with LessonKit.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -54,11 +54,11 @@
|
|
|
54
54
|
"react-dom": ">=18"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@lessonkit/accessibility": "0.
|
|
58
|
-
"@lessonkit/core": "0.
|
|
59
|
-
"@lessonkit/lxpack": "0.
|
|
60
|
-
"@lessonkit/themes": "0.
|
|
61
|
-
"@lessonkit/xapi": "0.
|
|
57
|
+
"@lessonkit/accessibility": "0.9.1",
|
|
58
|
+
"@lessonkit/core": "0.9.1",
|
|
59
|
+
"@lessonkit/lxpack": "0.9.1",
|
|
60
|
+
"@lessonkit/themes": "0.9.1",
|
|
61
|
+
"@lessonkit/xapi": "0.9.1"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@testing-library/react": "^16.3.0",
|