@lessonkit/react 0.9.1 → 0.9.3
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 +155 -68
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +155 -68
- package/package.json +6 -6
package/README.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -369,9 +369,15 @@ function createTrackingClientFromConfig(config) {
|
|
|
369
369
|
batch: config.tracking?.batch
|
|
370
370
|
});
|
|
371
371
|
}
|
|
372
|
-
function disposeTrackingClient(client) {
|
|
373
|
-
|
|
374
|
-
|
|
372
|
+
async function disposeTrackingClient(client) {
|
|
373
|
+
try {
|
|
374
|
+
await client?.flush?.();
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
await client?.dispose?.();
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
375
381
|
}
|
|
376
382
|
|
|
377
383
|
// src/context.tsx
|
|
@@ -382,6 +388,33 @@ var defaultStorage = createSessionStoragePort();
|
|
|
382
388
|
function isTrackingActive(tracking) {
|
|
383
389
|
return tracking?.enabled !== false;
|
|
384
390
|
}
|
|
391
|
+
function emitCourseStarted(opts) {
|
|
392
|
+
const pluginCtx = buildPluginContext({
|
|
393
|
+
courseId: opts.courseId,
|
|
394
|
+
sessionId: opts.sessionId,
|
|
395
|
+
attemptId: opts.attemptId
|
|
396
|
+
});
|
|
397
|
+
try {
|
|
398
|
+
emitTelemetryWithPlugins({
|
|
399
|
+
pluginHost: opts.pluginHost,
|
|
400
|
+
tracking: opts.tracking,
|
|
401
|
+
xapi: opts.xapi,
|
|
402
|
+
event: buildTrackEvent({
|
|
403
|
+
name: "course_started",
|
|
404
|
+
courseId: opts.courseId,
|
|
405
|
+
sessionId: opts.sessionId,
|
|
406
|
+
attemptId: opts.attemptId,
|
|
407
|
+
user: opts.user
|
|
408
|
+
}),
|
|
409
|
+
pluginCtx,
|
|
410
|
+
lxpackBridge: opts.lxpackBridge
|
|
411
|
+
});
|
|
412
|
+
markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
413
|
+
return true;
|
|
414
|
+
} catch {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
385
418
|
function LessonkitProvider(props) {
|
|
386
419
|
const config = props.config;
|
|
387
420
|
const sessionIdRef = (0, import_react.useRef)(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
@@ -428,7 +461,17 @@ function LessonkitProvider(props) {
|
|
|
428
461
|
const courseId = config.courseId;
|
|
429
462
|
const trackingEnabled = config.tracking?.enabled;
|
|
430
463
|
useIsoLayoutEffect(() => {
|
|
431
|
-
|
|
464
|
+
const courseChanged = prevXapiCourseIdRef.current !== courseId;
|
|
465
|
+
if (courseChanged) {
|
|
466
|
+
if (config.xapi?.client) {
|
|
467
|
+
const g = globalThis;
|
|
468
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
|
|
469
|
+
console.warn(
|
|
470
|
+
"[lessonkit] courseId changed while using config.xapi.client; flush the client between courses or use config.xapi.transport so the provider can manage the queue."
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
void xapiRef.current?.flush();
|
|
474
|
+
}
|
|
432
475
|
xapiQueueRef.current = (0, import_xapi3.createInMemoryXAPIQueue)();
|
|
433
476
|
prevXapiCourseIdRef.current = courseId;
|
|
434
477
|
}
|
|
@@ -452,7 +495,10 @@ function LessonkitProvider(props) {
|
|
|
452
495
|
user: userRef.current
|
|
453
496
|
})
|
|
454
497
|
);
|
|
455
|
-
if (statement)
|
|
498
|
+
if (statement) {
|
|
499
|
+
next.send(statement);
|
|
500
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
501
|
+
}
|
|
456
502
|
} catch {
|
|
457
503
|
}
|
|
458
504
|
}
|
|
@@ -484,17 +530,24 @@ function LessonkitProvider(props) {
|
|
|
484
530
|
const batchEnabled = config.tracking?.batch?.enabled;
|
|
485
531
|
const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
|
|
486
532
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const pluginCtx = buildPluginContext({
|
|
533
|
+
const buildCurrentPluginCtx = (0, import_react.useCallback)(
|
|
534
|
+
() => buildPluginContext({
|
|
490
535
|
courseId: courseIdRef.current,
|
|
491
536
|
sessionId: sessionIdRef.current,
|
|
492
537
|
attemptId: attemptIdRef.current
|
|
493
|
-
})
|
|
494
|
-
|
|
538
|
+
}),
|
|
539
|
+
[]
|
|
540
|
+
);
|
|
541
|
+
useIsoLayoutEffect(() => {
|
|
542
|
+
const prev = trackingRef.current;
|
|
543
|
+
const baseSink = config.tracking?.sink;
|
|
544
|
+
const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
|
|
495
545
|
const batchSink = pluginHostRef.current && config.tracking?.batchSink ? (events) => {
|
|
496
|
-
const
|
|
497
|
-
|
|
546
|
+
const delivered = pluginHostRef.current.deliverTelemetryBatch(
|
|
547
|
+
events,
|
|
548
|
+
buildCurrentPluginCtx()
|
|
549
|
+
);
|
|
550
|
+
return config.tracking.batchSink(delivered);
|
|
498
551
|
} : config.tracking?.batchSink;
|
|
499
552
|
const next = createTrackingClientFromConfig({
|
|
500
553
|
tracking: { ...config.tracking, sink, batchSink }
|
|
@@ -508,32 +561,24 @@ function LessonkitProvider(props) {
|
|
|
508
561
|
if (!trackingActive) {
|
|
509
562
|
courseStartedEmittedToSinkRef.current = false;
|
|
510
563
|
} else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
511
|
-
|
|
512
|
-
emitTelemetryWithPlugins({
|
|
564
|
+
const emitted = emitCourseStarted({
|
|
513
565
|
pluginHost: pluginHostRef.current,
|
|
514
566
|
tracking: next,
|
|
515
567
|
xapi: xapiRef.current,
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
user: userRef.current
|
|
522
|
-
}),
|
|
523
|
-
pluginCtx: buildPluginContext({
|
|
524
|
-
courseId: cid,
|
|
525
|
-
sessionId,
|
|
526
|
-
attemptId: attemptIdRef.current
|
|
527
|
-
}),
|
|
568
|
+
storage: defaultStorage,
|
|
569
|
+
sessionId,
|
|
570
|
+
courseId: cid,
|
|
571
|
+
attemptId: attemptIdRef.current,
|
|
572
|
+
user: userRef.current,
|
|
528
573
|
lxpackBridge: lxpackBridgeModeRef.current
|
|
529
574
|
});
|
|
530
|
-
courseStartedEmittedToSinkRef.current =
|
|
575
|
+
courseStartedEmittedToSinkRef.current = emitted;
|
|
531
576
|
} else if (trackingActive) {
|
|
532
577
|
courseStartedEmittedToSinkRef.current = true;
|
|
533
578
|
}
|
|
534
579
|
return () => {
|
|
535
580
|
if (prev !== trackingRef.current) {
|
|
536
|
-
disposeTrackingClient(prev);
|
|
581
|
+
void disposeTrackingClient(prev);
|
|
537
582
|
}
|
|
538
583
|
};
|
|
539
584
|
}, [
|
|
@@ -543,7 +588,9 @@ function LessonkitProvider(props) {
|
|
|
543
588
|
batchEnabled,
|
|
544
589
|
batchFlushIntervalMs,
|
|
545
590
|
batchMaxBatchSize,
|
|
546
|
-
config.plugins
|
|
591
|
+
config.plugins,
|
|
592
|
+
config.courseId,
|
|
593
|
+
buildCurrentPluginCtx
|
|
547
594
|
]);
|
|
548
595
|
const emitWithBridge = (0, import_react.useCallback)((trackingClient, event) => {
|
|
549
596
|
emitTelemetryWithPlugins({
|
|
@@ -582,28 +629,26 @@ function LessonkitProvider(props) {
|
|
|
582
629
|
if (!isTrackingActive(config.tracking)) return;
|
|
583
630
|
const sessionId = sessionIdRef.current;
|
|
584
631
|
const cid = courseIdRef.current;
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
632
|
+
void (async () => {
|
|
633
|
+
try {
|
|
634
|
+
await trackingRef.current?.flush?.();
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
638
|
+
const emitted = emitCourseStarted({
|
|
639
|
+
pluginHost: pluginHostRef.current,
|
|
640
|
+
tracking: trackingRef.current,
|
|
641
|
+
xapi: xapiRef.current,
|
|
642
|
+
storage: defaultStorage,
|
|
594
643
|
sessionId,
|
|
595
|
-
attemptId: attemptIdRef.current,
|
|
596
|
-
user: userRef.current
|
|
597
|
-
}),
|
|
598
|
-
pluginCtx: buildPluginContext({
|
|
599
644
|
courseId: cid,
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
645
|
+
attemptId: attemptIdRef.current,
|
|
646
|
+
user: userRef.current,
|
|
647
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
648
|
+
});
|
|
649
|
+
courseStartedEmittedToSinkRef.current = emitted;
|
|
650
|
+
}
|
|
651
|
+
})();
|
|
607
652
|
}, [config.courseId, config.tracking?.enabled, syncProgress]);
|
|
608
653
|
const emitLessonCompleted = (0, import_react.useCallback)(
|
|
609
654
|
(lessonId, durationMs) => {
|
|
@@ -620,25 +665,28 @@ function LessonkitProvider(props) {
|
|
|
620
665
|
if (!result.didComplete) return;
|
|
621
666
|
syncProgress();
|
|
622
667
|
emitLessonCompleted(lessonId, result.durationMs);
|
|
623
|
-
void trackingRef.current?.flush?.();
|
|
668
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
624
669
|
},
|
|
625
670
|
[syncProgress, emitLessonCompleted]
|
|
626
671
|
);
|
|
627
|
-
const unmountTimerIdsRef = (0, import_react.useRef)([]);
|
|
628
672
|
(0, import_react.useEffect)(() => {
|
|
629
673
|
return () => {
|
|
630
|
-
for (const id of unmountTimerIdsRef.current) clearTimeout(id);
|
|
631
|
-
unmountTimerIdsRef.current = [];
|
|
632
674
|
const client = trackingClientForUnmountRef.current;
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
675
|
+
const xapi2 = xapiRef.current;
|
|
676
|
+
void (async () => {
|
|
677
|
+
try {
|
|
678
|
+
await xapi2?.flush();
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
await client?.flush?.();
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
await client?.dispose?.();
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
})();
|
|
642
690
|
};
|
|
643
691
|
}, []);
|
|
644
692
|
const setActiveLesson = (0, import_react.useCallback)(
|
|
@@ -650,6 +698,7 @@ function LessonkitProvider(props) {
|
|
|
650
698
|
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
651
699
|
if (completed.didComplete) {
|
|
652
700
|
emitLessonCompleted(previous, completed.durationMs);
|
|
701
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
653
702
|
}
|
|
654
703
|
}
|
|
655
704
|
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
@@ -659,12 +708,19 @@ function LessonkitProvider(props) {
|
|
|
659
708
|
[track, syncProgress, emitLessonCompleted]
|
|
660
709
|
);
|
|
661
710
|
const completeCourse = (0, import_react.useCallback)(() => {
|
|
711
|
+
const current = progressRef.current.getState();
|
|
712
|
+
if (current.activeLessonId) {
|
|
713
|
+
const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
|
|
714
|
+
if (lessonResult.didComplete) {
|
|
715
|
+
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
662
718
|
const result = progressRef.current.completeCourse();
|
|
663
719
|
if (!result.didComplete) return;
|
|
664
720
|
syncProgress();
|
|
665
721
|
track("course_completed");
|
|
666
722
|
void trackingRef.current?.flush?.();
|
|
667
|
-
}, [track, syncProgress]);
|
|
723
|
+
}, [track, syncProgress, emitLessonCompleted]);
|
|
668
724
|
const sessionUser = config.session?.user;
|
|
669
725
|
const sessionAttemptId = config.session?.attemptId;
|
|
670
726
|
const sessionConfiguredId = config.session?.sessionId;
|
|
@@ -847,20 +903,32 @@ function KnowledgeCheck(props) {
|
|
|
847
903
|
checkId: props.checkId,
|
|
848
904
|
question: props.question,
|
|
849
905
|
choices: props.choices,
|
|
850
|
-
answer: props.answer
|
|
906
|
+
answer: props.answer,
|
|
907
|
+
passingScore: props.passingScore
|
|
851
908
|
}
|
|
852
909
|
);
|
|
853
910
|
}
|
|
854
911
|
function Quiz(props) {
|
|
855
912
|
warnInvalidComponentId(props.checkId, "checkId");
|
|
856
913
|
const quiz = useQuizState();
|
|
914
|
+
const { plugins, config, progress, session } = useLessonkit();
|
|
857
915
|
const [selected, setSelected] = (0, import_react3.useState)(null);
|
|
916
|
+
const [selectionCorrect, setSelectionCorrect] = (0, import_react3.useState)(null);
|
|
858
917
|
const completedRef = (0, import_react3.useRef)(false);
|
|
859
918
|
const questionId = (0, import_react3.useId)();
|
|
860
919
|
(0, import_react3.useEffect)(() => {
|
|
861
920
|
completedRef.current = false;
|
|
862
921
|
setSelected(null);
|
|
863
|
-
|
|
922
|
+
setSelectionCorrect(null);
|
|
923
|
+
}, [props.checkId, props.answer, props.question, config.courseId]);
|
|
924
|
+
const isChoiceCorrect = (choice, custom) => {
|
|
925
|
+
if (!custom) return choice === props.answer;
|
|
926
|
+
if (custom.passed !== void 0) return custom.passed;
|
|
927
|
+
if (custom.maxScore != null && custom.maxScore > 0) {
|
|
928
|
+
return custom.score / custom.maxScore >= 1;
|
|
929
|
+
}
|
|
930
|
+
return choice === props.answer;
|
|
931
|
+
};
|
|
864
932
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
|
|
865
933
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
|
|
866
934
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
|
|
@@ -875,7 +943,21 @@ function Quiz(props) {
|
|
|
875
943
|
checked: selected === c,
|
|
876
944
|
onChange: () => {
|
|
877
945
|
setSelected(c);
|
|
878
|
-
const
|
|
946
|
+
const pluginCtx = buildPluginContext({
|
|
947
|
+
courseId: config.courseId,
|
|
948
|
+
sessionId: session.sessionId,
|
|
949
|
+
attemptId: session.attemptId
|
|
950
|
+
});
|
|
951
|
+
const custom = plugins?.scoreAssessment(
|
|
952
|
+
{
|
|
953
|
+
checkId: props.checkId,
|
|
954
|
+
lessonId: progress.activeLessonId,
|
|
955
|
+
response: c
|
|
956
|
+
},
|
|
957
|
+
pluginCtx
|
|
958
|
+
) ?? null;
|
|
959
|
+
const correct = isChoiceCorrect(c, custom);
|
|
960
|
+
setSelectionCorrect(correct);
|
|
879
961
|
quiz.answer({
|
|
880
962
|
checkId: props.checkId,
|
|
881
963
|
question: props.question,
|
|
@@ -884,7 +966,12 @@ function Quiz(props) {
|
|
|
884
966
|
});
|
|
885
967
|
if (correct && !completedRef.current) {
|
|
886
968
|
completedRef.current = true;
|
|
887
|
-
quiz.complete({
|
|
969
|
+
quiz.complete({
|
|
970
|
+
checkId: props.checkId,
|
|
971
|
+
score: custom?.score ?? 1,
|
|
972
|
+
maxScore: custom?.maxScore ?? 1,
|
|
973
|
+
passingScore: props.passingScore ?? 1
|
|
974
|
+
});
|
|
888
975
|
}
|
|
889
976
|
}
|
|
890
977
|
}
|
|
@@ -892,7 +979,7 @@ function Quiz(props) {
|
|
|
892
979
|
c
|
|
893
980
|
] }, `${questionId}-${i}`))
|
|
894
981
|
] }),
|
|
895
|
-
selected ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children:
|
|
982
|
+
selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
896
983
|
] });
|
|
897
984
|
}
|
|
898
985
|
function ProgressTracker() {
|
package/dist/index.d.cts
CHANGED
|
@@ -91,12 +91,14 @@ declare function KnowledgeCheck(props: {
|
|
|
91
91
|
question: string;
|
|
92
92
|
choices: string[];
|
|
93
93
|
answer: string;
|
|
94
|
+
passingScore?: number;
|
|
94
95
|
}): react_jsx_runtime.JSX.Element;
|
|
95
96
|
declare function Quiz(props: {
|
|
96
97
|
checkId: CheckId;
|
|
97
98
|
question: string;
|
|
98
99
|
choices: string[];
|
|
99
100
|
answer: string;
|
|
101
|
+
passingScore?: number;
|
|
100
102
|
}): react_jsx_runtime.JSX.Element;
|
|
101
103
|
declare function ProgressTracker(): react_jsx_runtime.JSX.Element;
|
|
102
104
|
|
package/dist/index.d.ts
CHANGED
|
@@ -91,12 +91,14 @@ declare function KnowledgeCheck(props: {
|
|
|
91
91
|
question: string;
|
|
92
92
|
choices: string[];
|
|
93
93
|
answer: string;
|
|
94
|
+
passingScore?: number;
|
|
94
95
|
}): react_jsx_runtime.JSX.Element;
|
|
95
96
|
declare function Quiz(props: {
|
|
96
97
|
checkId: CheckId;
|
|
97
98
|
question: string;
|
|
98
99
|
choices: string[];
|
|
99
100
|
answer: string;
|
|
101
|
+
passingScore?: number;
|
|
100
102
|
}): react_jsx_runtime.JSX.Element;
|
|
101
103
|
declare function ProgressTracker(): react_jsx_runtime.JSX.Element;
|
|
102
104
|
|
package/dist/index.js
CHANGED
|
@@ -327,9 +327,15 @@ function createTrackingClientFromConfig(config) {
|
|
|
327
327
|
batch: config.tracking?.batch
|
|
328
328
|
});
|
|
329
329
|
}
|
|
330
|
-
function disposeTrackingClient(client) {
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
async function disposeTrackingClient(client) {
|
|
331
|
+
try {
|
|
332
|
+
await client?.flush?.();
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
await client?.dispose?.();
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
333
339
|
}
|
|
334
340
|
|
|
335
341
|
// src/context.tsx
|
|
@@ -340,6 +346,33 @@ var defaultStorage = createSessionStoragePort();
|
|
|
340
346
|
function isTrackingActive(tracking) {
|
|
341
347
|
return tracking?.enabled !== false;
|
|
342
348
|
}
|
|
349
|
+
function emitCourseStarted(opts) {
|
|
350
|
+
const pluginCtx = buildPluginContext({
|
|
351
|
+
courseId: opts.courseId,
|
|
352
|
+
sessionId: opts.sessionId,
|
|
353
|
+
attemptId: opts.attemptId
|
|
354
|
+
});
|
|
355
|
+
try {
|
|
356
|
+
emitTelemetryWithPlugins({
|
|
357
|
+
pluginHost: opts.pluginHost,
|
|
358
|
+
tracking: opts.tracking,
|
|
359
|
+
xapi: opts.xapi,
|
|
360
|
+
event: buildTrackEvent({
|
|
361
|
+
name: "course_started",
|
|
362
|
+
courseId: opts.courseId,
|
|
363
|
+
sessionId: opts.sessionId,
|
|
364
|
+
attemptId: opts.attemptId,
|
|
365
|
+
user: opts.user
|
|
366
|
+
}),
|
|
367
|
+
pluginCtx,
|
|
368
|
+
lxpackBridge: opts.lxpackBridge
|
|
369
|
+
});
|
|
370
|
+
markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
371
|
+
return true;
|
|
372
|
+
} catch {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
343
376
|
function LessonkitProvider(props) {
|
|
344
377
|
const config = props.config;
|
|
345
378
|
const sessionIdRef = useRef(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
@@ -386,7 +419,17 @@ function LessonkitProvider(props) {
|
|
|
386
419
|
const courseId = config.courseId;
|
|
387
420
|
const trackingEnabled = config.tracking?.enabled;
|
|
388
421
|
useIsoLayoutEffect(() => {
|
|
389
|
-
|
|
422
|
+
const courseChanged = prevXapiCourseIdRef.current !== courseId;
|
|
423
|
+
if (courseChanged) {
|
|
424
|
+
if (config.xapi?.client) {
|
|
425
|
+
const g = globalThis;
|
|
426
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
|
|
427
|
+
console.warn(
|
|
428
|
+
"[lessonkit] courseId changed while using config.xapi.client; flush the client between courses or use config.xapi.transport so the provider can manage the queue."
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
void xapiRef.current?.flush();
|
|
432
|
+
}
|
|
390
433
|
xapiQueueRef.current = createInMemoryXAPIQueue();
|
|
391
434
|
prevXapiCourseIdRef.current = courseId;
|
|
392
435
|
}
|
|
@@ -410,7 +453,10 @@ function LessonkitProvider(props) {
|
|
|
410
453
|
user: userRef.current
|
|
411
454
|
})
|
|
412
455
|
);
|
|
413
|
-
if (statement)
|
|
456
|
+
if (statement) {
|
|
457
|
+
next.send(statement);
|
|
458
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
459
|
+
}
|
|
414
460
|
} catch {
|
|
415
461
|
}
|
|
416
462
|
}
|
|
@@ -442,17 +488,24 @@ function LessonkitProvider(props) {
|
|
|
442
488
|
const batchEnabled = config.tracking?.batch?.enabled;
|
|
443
489
|
const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
|
|
444
490
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const pluginCtx = buildPluginContext({
|
|
491
|
+
const buildCurrentPluginCtx = useCallback(
|
|
492
|
+
() => buildPluginContext({
|
|
448
493
|
courseId: courseIdRef.current,
|
|
449
494
|
sessionId: sessionIdRef.current,
|
|
450
495
|
attemptId: attemptIdRef.current
|
|
451
|
-
})
|
|
452
|
-
|
|
496
|
+
}),
|
|
497
|
+
[]
|
|
498
|
+
);
|
|
499
|
+
useIsoLayoutEffect(() => {
|
|
500
|
+
const prev = trackingRef.current;
|
|
501
|
+
const baseSink = config.tracking?.sink;
|
|
502
|
+
const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
|
|
453
503
|
const batchSink = pluginHostRef.current && config.tracking?.batchSink ? (events) => {
|
|
454
|
-
const
|
|
455
|
-
|
|
504
|
+
const delivered = pluginHostRef.current.deliverTelemetryBatch(
|
|
505
|
+
events,
|
|
506
|
+
buildCurrentPluginCtx()
|
|
507
|
+
);
|
|
508
|
+
return config.tracking.batchSink(delivered);
|
|
456
509
|
} : config.tracking?.batchSink;
|
|
457
510
|
const next = createTrackingClientFromConfig({
|
|
458
511
|
tracking: { ...config.tracking, sink, batchSink }
|
|
@@ -466,32 +519,24 @@ function LessonkitProvider(props) {
|
|
|
466
519
|
if (!trackingActive) {
|
|
467
520
|
courseStartedEmittedToSinkRef.current = false;
|
|
468
521
|
} else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
469
|
-
|
|
470
|
-
emitTelemetryWithPlugins({
|
|
522
|
+
const emitted = emitCourseStarted({
|
|
471
523
|
pluginHost: pluginHostRef.current,
|
|
472
524
|
tracking: next,
|
|
473
525
|
xapi: xapiRef.current,
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
user: userRef.current
|
|
480
|
-
}),
|
|
481
|
-
pluginCtx: buildPluginContext({
|
|
482
|
-
courseId: cid,
|
|
483
|
-
sessionId,
|
|
484
|
-
attemptId: attemptIdRef.current
|
|
485
|
-
}),
|
|
526
|
+
storage: defaultStorage,
|
|
527
|
+
sessionId,
|
|
528
|
+
courseId: cid,
|
|
529
|
+
attemptId: attemptIdRef.current,
|
|
530
|
+
user: userRef.current,
|
|
486
531
|
lxpackBridge: lxpackBridgeModeRef.current
|
|
487
532
|
});
|
|
488
|
-
courseStartedEmittedToSinkRef.current =
|
|
533
|
+
courseStartedEmittedToSinkRef.current = emitted;
|
|
489
534
|
} else if (trackingActive) {
|
|
490
535
|
courseStartedEmittedToSinkRef.current = true;
|
|
491
536
|
}
|
|
492
537
|
return () => {
|
|
493
538
|
if (prev !== trackingRef.current) {
|
|
494
|
-
disposeTrackingClient(prev);
|
|
539
|
+
void disposeTrackingClient(prev);
|
|
495
540
|
}
|
|
496
541
|
};
|
|
497
542
|
}, [
|
|
@@ -501,7 +546,9 @@ function LessonkitProvider(props) {
|
|
|
501
546
|
batchEnabled,
|
|
502
547
|
batchFlushIntervalMs,
|
|
503
548
|
batchMaxBatchSize,
|
|
504
|
-
config.plugins
|
|
549
|
+
config.plugins,
|
|
550
|
+
config.courseId,
|
|
551
|
+
buildCurrentPluginCtx
|
|
505
552
|
]);
|
|
506
553
|
const emitWithBridge = useCallback((trackingClient, event) => {
|
|
507
554
|
emitTelemetryWithPlugins({
|
|
@@ -540,28 +587,26 @@ function LessonkitProvider(props) {
|
|
|
540
587
|
if (!isTrackingActive(config.tracking)) return;
|
|
541
588
|
const sessionId = sessionIdRef.current;
|
|
542
589
|
const cid = courseIdRef.current;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
590
|
+
void (async () => {
|
|
591
|
+
try {
|
|
592
|
+
await trackingRef.current?.flush?.();
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
596
|
+
const emitted = emitCourseStarted({
|
|
597
|
+
pluginHost: pluginHostRef.current,
|
|
598
|
+
tracking: trackingRef.current,
|
|
599
|
+
xapi: xapiRef.current,
|
|
600
|
+
storage: defaultStorage,
|
|
552
601
|
sessionId,
|
|
553
|
-
attemptId: attemptIdRef.current,
|
|
554
|
-
user: userRef.current
|
|
555
|
-
}),
|
|
556
|
-
pluginCtx: buildPluginContext({
|
|
557
602
|
courseId: cid,
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
603
|
+
attemptId: attemptIdRef.current,
|
|
604
|
+
user: userRef.current,
|
|
605
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
606
|
+
});
|
|
607
|
+
courseStartedEmittedToSinkRef.current = emitted;
|
|
608
|
+
}
|
|
609
|
+
})();
|
|
565
610
|
}, [config.courseId, config.tracking?.enabled, syncProgress]);
|
|
566
611
|
const emitLessonCompleted = useCallback(
|
|
567
612
|
(lessonId, durationMs) => {
|
|
@@ -578,25 +623,28 @@ function LessonkitProvider(props) {
|
|
|
578
623
|
if (!result.didComplete) return;
|
|
579
624
|
syncProgress();
|
|
580
625
|
emitLessonCompleted(lessonId, result.durationMs);
|
|
581
|
-
void trackingRef.current?.flush?.();
|
|
626
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
582
627
|
},
|
|
583
628
|
[syncProgress, emitLessonCompleted]
|
|
584
629
|
);
|
|
585
|
-
const unmountTimerIdsRef = useRef([]);
|
|
586
630
|
useEffect(() => {
|
|
587
631
|
return () => {
|
|
588
|
-
for (const id of unmountTimerIdsRef.current) clearTimeout(id);
|
|
589
|
-
unmountTimerIdsRef.current = [];
|
|
590
632
|
const client = trackingClientForUnmountRef.current;
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
633
|
+
const xapi2 = xapiRef.current;
|
|
634
|
+
void (async () => {
|
|
635
|
+
try {
|
|
636
|
+
await xapi2?.flush();
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
await client?.flush?.();
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
await client?.dispose?.();
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
})();
|
|
600
648
|
};
|
|
601
649
|
}, []);
|
|
602
650
|
const setActiveLesson = useCallback(
|
|
@@ -608,6 +656,7 @@ function LessonkitProvider(props) {
|
|
|
608
656
|
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
609
657
|
if (completed.didComplete) {
|
|
610
658
|
emitLessonCompleted(previous, completed.durationMs);
|
|
659
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
611
660
|
}
|
|
612
661
|
}
|
|
613
662
|
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
@@ -617,12 +666,19 @@ function LessonkitProvider(props) {
|
|
|
617
666
|
[track, syncProgress, emitLessonCompleted]
|
|
618
667
|
);
|
|
619
668
|
const completeCourse = useCallback(() => {
|
|
669
|
+
const current = progressRef.current.getState();
|
|
670
|
+
if (current.activeLessonId) {
|
|
671
|
+
const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
|
|
672
|
+
if (lessonResult.didComplete) {
|
|
673
|
+
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
620
676
|
const result = progressRef.current.completeCourse();
|
|
621
677
|
if (!result.didComplete) return;
|
|
622
678
|
syncProgress();
|
|
623
679
|
track("course_completed");
|
|
624
680
|
void trackingRef.current?.flush?.();
|
|
625
|
-
}, [track, syncProgress]);
|
|
681
|
+
}, [track, syncProgress, emitLessonCompleted]);
|
|
626
682
|
const sessionUser = config.session?.user;
|
|
627
683
|
const sessionAttemptId = config.session?.attemptId;
|
|
628
684
|
const sessionConfiguredId = config.session?.sessionId;
|
|
@@ -805,20 +861,32 @@ function KnowledgeCheck(props) {
|
|
|
805
861
|
checkId: props.checkId,
|
|
806
862
|
question: props.question,
|
|
807
863
|
choices: props.choices,
|
|
808
|
-
answer: props.answer
|
|
864
|
+
answer: props.answer,
|
|
865
|
+
passingScore: props.passingScore
|
|
809
866
|
}
|
|
810
867
|
);
|
|
811
868
|
}
|
|
812
869
|
function Quiz(props) {
|
|
813
870
|
warnInvalidComponentId(props.checkId, "checkId");
|
|
814
871
|
const quiz = useQuizState();
|
|
872
|
+
const { plugins, config, progress, session } = useLessonkit();
|
|
815
873
|
const [selected, setSelected] = useState2(null);
|
|
874
|
+
const [selectionCorrect, setSelectionCorrect] = useState2(null);
|
|
816
875
|
const completedRef = useRef2(false);
|
|
817
876
|
const questionId = useId();
|
|
818
877
|
useEffect2(() => {
|
|
819
878
|
completedRef.current = false;
|
|
820
879
|
setSelected(null);
|
|
821
|
-
|
|
880
|
+
setSelectionCorrect(null);
|
|
881
|
+
}, [props.checkId, props.answer, props.question, config.courseId]);
|
|
882
|
+
const isChoiceCorrect = (choice, custom) => {
|
|
883
|
+
if (!custom) return choice === props.answer;
|
|
884
|
+
if (custom.passed !== void 0) return custom.passed;
|
|
885
|
+
if (custom.maxScore != null && custom.maxScore > 0) {
|
|
886
|
+
return custom.score / custom.maxScore >= 1;
|
|
887
|
+
}
|
|
888
|
+
return choice === props.answer;
|
|
889
|
+
};
|
|
822
890
|
return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
|
|
823
891
|
/* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
|
|
824
892
|
/* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
|
|
@@ -833,7 +901,21 @@ function Quiz(props) {
|
|
|
833
901
|
checked: selected === c,
|
|
834
902
|
onChange: () => {
|
|
835
903
|
setSelected(c);
|
|
836
|
-
const
|
|
904
|
+
const pluginCtx = buildPluginContext({
|
|
905
|
+
courseId: config.courseId,
|
|
906
|
+
sessionId: session.sessionId,
|
|
907
|
+
attemptId: session.attemptId
|
|
908
|
+
});
|
|
909
|
+
const custom = plugins?.scoreAssessment(
|
|
910
|
+
{
|
|
911
|
+
checkId: props.checkId,
|
|
912
|
+
lessonId: progress.activeLessonId,
|
|
913
|
+
response: c
|
|
914
|
+
},
|
|
915
|
+
pluginCtx
|
|
916
|
+
) ?? null;
|
|
917
|
+
const correct = isChoiceCorrect(c, custom);
|
|
918
|
+
setSelectionCorrect(correct);
|
|
837
919
|
quiz.answer({
|
|
838
920
|
checkId: props.checkId,
|
|
839
921
|
question: props.question,
|
|
@@ -842,7 +924,12 @@ function Quiz(props) {
|
|
|
842
924
|
});
|
|
843
925
|
if (correct && !completedRef.current) {
|
|
844
926
|
completedRef.current = true;
|
|
845
|
-
quiz.complete({
|
|
927
|
+
quiz.complete({
|
|
928
|
+
checkId: props.checkId,
|
|
929
|
+
score: custom?.score ?? 1,
|
|
930
|
+
maxScore: custom?.maxScore ?? 1,
|
|
931
|
+
passingScore: props.passingScore ?? 1
|
|
932
|
+
});
|
|
846
933
|
}
|
|
847
934
|
}
|
|
848
935
|
}
|
|
@@ -850,7 +937,7 @@ function Quiz(props) {
|
|
|
850
937
|
c
|
|
851
938
|
] }, `${questionId}-${i}`))
|
|
852
939
|
] }),
|
|
853
|
-
selected ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children:
|
|
940
|
+
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
854
941
|
] });
|
|
855
942
|
}
|
|
856
943
|
function ProgressTracker() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/react",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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.9.
|
|
58
|
-
"@lessonkit/core": "0.9.
|
|
59
|
-
"@lessonkit/lxpack": "0.9.
|
|
60
|
-
"@lessonkit/themes": "0.9.
|
|
61
|
-
"@lessonkit/xapi": "0.9.
|
|
57
|
+
"@lessonkit/accessibility": "0.9.3",
|
|
58
|
+
"@lessonkit/core": "0.9.3",
|
|
59
|
+
"@lessonkit/lxpack": "0.9.3",
|
|
60
|
+
"@lessonkit/themes": "0.9.3",
|
|
61
|
+
"@lessonkit/xapi": "0.9.3"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@testing-library/react": "^16.3.0",
|