@lessonkit/react 0.9.1 → 0.9.2
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 +146 -65
- package/dist/index.js +146 -65
- 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,27 @@ 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 ? (event) => {
|
|
545
|
+
const composed = pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx()) ?? baseSink;
|
|
546
|
+
return composed(event);
|
|
547
|
+
} : baseSink;
|
|
495
548
|
const batchSink = pluginHostRef.current && config.tracking?.batchSink ? (events) => {
|
|
496
|
-
const
|
|
497
|
-
|
|
549
|
+
const delivered = pluginHostRef.current.deliverTelemetryBatch(
|
|
550
|
+
events,
|
|
551
|
+
buildCurrentPluginCtx()
|
|
552
|
+
);
|
|
553
|
+
return config.tracking.batchSink(delivered);
|
|
498
554
|
} : config.tracking?.batchSink;
|
|
499
555
|
const next = createTrackingClientFromConfig({
|
|
500
556
|
tracking: { ...config.tracking, sink, batchSink }
|
|
@@ -508,32 +564,24 @@ function LessonkitProvider(props) {
|
|
|
508
564
|
if (!trackingActive) {
|
|
509
565
|
courseStartedEmittedToSinkRef.current = false;
|
|
510
566
|
} else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
511
|
-
|
|
512
|
-
emitTelemetryWithPlugins({
|
|
567
|
+
const emitted = emitCourseStarted({
|
|
513
568
|
pluginHost: pluginHostRef.current,
|
|
514
569
|
tracking: next,
|
|
515
570
|
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
|
-
}),
|
|
571
|
+
storage: defaultStorage,
|
|
572
|
+
sessionId,
|
|
573
|
+
courseId: cid,
|
|
574
|
+
attemptId: attemptIdRef.current,
|
|
575
|
+
user: userRef.current,
|
|
528
576
|
lxpackBridge: lxpackBridgeModeRef.current
|
|
529
577
|
});
|
|
530
|
-
courseStartedEmittedToSinkRef.current =
|
|
578
|
+
courseStartedEmittedToSinkRef.current = emitted;
|
|
531
579
|
} else if (trackingActive) {
|
|
532
580
|
courseStartedEmittedToSinkRef.current = true;
|
|
533
581
|
}
|
|
534
582
|
return () => {
|
|
535
583
|
if (prev !== trackingRef.current) {
|
|
536
|
-
disposeTrackingClient(prev);
|
|
584
|
+
void disposeTrackingClient(prev);
|
|
537
585
|
}
|
|
538
586
|
};
|
|
539
587
|
}, [
|
|
@@ -543,7 +591,9 @@ function LessonkitProvider(props) {
|
|
|
543
591
|
batchEnabled,
|
|
544
592
|
batchFlushIntervalMs,
|
|
545
593
|
batchMaxBatchSize,
|
|
546
|
-
config.plugins
|
|
594
|
+
config.plugins,
|
|
595
|
+
config.courseId,
|
|
596
|
+
buildCurrentPluginCtx
|
|
547
597
|
]);
|
|
548
598
|
const emitWithBridge = (0, import_react.useCallback)((trackingClient, event) => {
|
|
549
599
|
emitTelemetryWithPlugins({
|
|
@@ -582,28 +632,26 @@ function LessonkitProvider(props) {
|
|
|
582
632
|
if (!isTrackingActive(config.tracking)) return;
|
|
583
633
|
const sessionId = sessionIdRef.current;
|
|
584
634
|
const cid = courseIdRef.current;
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
635
|
+
void (async () => {
|
|
636
|
+
try {
|
|
637
|
+
await trackingRef.current?.flush?.();
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
640
|
+
if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
641
|
+
const emitted = emitCourseStarted({
|
|
642
|
+
pluginHost: pluginHostRef.current,
|
|
643
|
+
tracking: trackingRef.current,
|
|
644
|
+
xapi: xapiRef.current,
|
|
645
|
+
storage: defaultStorage,
|
|
594
646
|
sessionId,
|
|
595
|
-
attemptId: attemptIdRef.current,
|
|
596
|
-
user: userRef.current
|
|
597
|
-
}),
|
|
598
|
-
pluginCtx: buildPluginContext({
|
|
599
647
|
courseId: cid,
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
648
|
+
attemptId: attemptIdRef.current,
|
|
649
|
+
user: userRef.current,
|
|
650
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
651
|
+
});
|
|
652
|
+
courseStartedEmittedToSinkRef.current = emitted;
|
|
653
|
+
}
|
|
654
|
+
})();
|
|
607
655
|
}, [config.courseId, config.tracking?.enabled, syncProgress]);
|
|
608
656
|
const emitLessonCompleted = (0, import_react.useCallback)(
|
|
609
657
|
(lessonId, durationMs) => {
|
|
@@ -620,25 +668,27 @@ function LessonkitProvider(props) {
|
|
|
620
668
|
if (!result.didComplete) return;
|
|
621
669
|
syncProgress();
|
|
622
670
|
emitLessonCompleted(lessonId, result.durationMs);
|
|
623
|
-
void trackingRef.current?.flush?.();
|
|
671
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
624
672
|
},
|
|
625
673
|
[syncProgress, emitLessonCompleted]
|
|
626
674
|
);
|
|
627
|
-
const unmountTimerIdsRef = (0, import_react.useRef)([]);
|
|
628
675
|
(0, import_react.useEffect)(() => {
|
|
629
676
|
return () => {
|
|
630
|
-
for (const id of unmountTimerIdsRef.current) clearTimeout(id);
|
|
631
|
-
unmountTimerIdsRef.current = [];
|
|
632
677
|
const client = trackingClientForUnmountRef.current;
|
|
633
|
-
void
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
678
|
+
void (async () => {
|
|
679
|
+
try {
|
|
680
|
+
await xapiRef.current?.flush();
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
await client?.flush?.();
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
await client?.dispose?.();
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
})();
|
|
642
692
|
};
|
|
643
693
|
}, []);
|
|
644
694
|
const setActiveLesson = (0, import_react.useCallback)(
|
|
@@ -650,6 +700,7 @@ function LessonkitProvider(props) {
|
|
|
650
700
|
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
651
701
|
if (completed.didComplete) {
|
|
652
702
|
emitLessonCompleted(previous, completed.durationMs);
|
|
703
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
653
704
|
}
|
|
654
705
|
}
|
|
655
706
|
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
@@ -854,13 +905,24 @@ function KnowledgeCheck(props) {
|
|
|
854
905
|
function Quiz(props) {
|
|
855
906
|
warnInvalidComponentId(props.checkId, "checkId");
|
|
856
907
|
const quiz = useQuizState();
|
|
908
|
+
const { plugins, config, progress, session } = useLessonkit();
|
|
857
909
|
const [selected, setSelected] = (0, import_react3.useState)(null);
|
|
910
|
+
const [selectionCorrect, setSelectionCorrect] = (0, import_react3.useState)(null);
|
|
858
911
|
const completedRef = (0, import_react3.useRef)(false);
|
|
859
912
|
const questionId = (0, import_react3.useId)();
|
|
860
913
|
(0, import_react3.useEffect)(() => {
|
|
861
914
|
completedRef.current = false;
|
|
862
915
|
setSelected(null);
|
|
916
|
+
setSelectionCorrect(null);
|
|
863
917
|
}, [props.checkId, props.answer, props.question]);
|
|
918
|
+
const isChoiceCorrect = (choice, custom) => {
|
|
919
|
+
if (!custom) return choice === props.answer;
|
|
920
|
+
if (custom.passed !== void 0) return custom.passed;
|
|
921
|
+
if (custom.maxScore != null && custom.maxScore > 0) {
|
|
922
|
+
return custom.score / custom.maxScore >= 1;
|
|
923
|
+
}
|
|
924
|
+
return choice === props.answer;
|
|
925
|
+
};
|
|
864
926
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
|
|
865
927
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
|
|
866
928
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
|
|
@@ -875,7 +937,21 @@ function Quiz(props) {
|
|
|
875
937
|
checked: selected === c,
|
|
876
938
|
onChange: () => {
|
|
877
939
|
setSelected(c);
|
|
878
|
-
const
|
|
940
|
+
const pluginCtx = buildPluginContext({
|
|
941
|
+
courseId: config.courseId,
|
|
942
|
+
sessionId: session.sessionId,
|
|
943
|
+
attemptId: session.attemptId
|
|
944
|
+
});
|
|
945
|
+
const custom = plugins?.scoreAssessment(
|
|
946
|
+
{
|
|
947
|
+
checkId: props.checkId,
|
|
948
|
+
lessonId: progress.activeLessonId,
|
|
949
|
+
response: c
|
|
950
|
+
},
|
|
951
|
+
pluginCtx
|
|
952
|
+
) ?? null;
|
|
953
|
+
const correct = isChoiceCorrect(c, custom);
|
|
954
|
+
setSelectionCorrect(correct);
|
|
879
955
|
quiz.answer({
|
|
880
956
|
checkId: props.checkId,
|
|
881
957
|
question: props.question,
|
|
@@ -884,7 +960,12 @@ function Quiz(props) {
|
|
|
884
960
|
});
|
|
885
961
|
if (correct && !completedRef.current) {
|
|
886
962
|
completedRef.current = true;
|
|
887
|
-
quiz.complete({
|
|
963
|
+
quiz.complete({
|
|
964
|
+
checkId: props.checkId,
|
|
965
|
+
score: custom?.score ?? 1,
|
|
966
|
+
maxScore: custom?.maxScore ?? 1,
|
|
967
|
+
passingScore: 1
|
|
968
|
+
});
|
|
888
969
|
}
|
|
889
970
|
}
|
|
890
971
|
}
|
|
@@ -892,7 +973,7 @@ function Quiz(props) {
|
|
|
892
973
|
c
|
|
893
974
|
] }, `${questionId}-${i}`))
|
|
894
975
|
] }),
|
|
895
|
-
selected ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children:
|
|
976
|
+
selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
896
977
|
] });
|
|
897
978
|
}
|
|
898
979
|
function ProgressTracker() {
|
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,27 @@ 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 ? (event) => {
|
|
503
|
+
const composed = pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx()) ?? baseSink;
|
|
504
|
+
return composed(event);
|
|
505
|
+
} : baseSink;
|
|
453
506
|
const batchSink = pluginHostRef.current && config.tracking?.batchSink ? (events) => {
|
|
454
|
-
const
|
|
455
|
-
|
|
507
|
+
const delivered = pluginHostRef.current.deliverTelemetryBatch(
|
|
508
|
+
events,
|
|
509
|
+
buildCurrentPluginCtx()
|
|
510
|
+
);
|
|
511
|
+
return config.tracking.batchSink(delivered);
|
|
456
512
|
} : config.tracking?.batchSink;
|
|
457
513
|
const next = createTrackingClientFromConfig({
|
|
458
514
|
tracking: { ...config.tracking, sink, batchSink }
|
|
@@ -466,32 +522,24 @@ function LessonkitProvider(props) {
|
|
|
466
522
|
if (!trackingActive) {
|
|
467
523
|
courseStartedEmittedToSinkRef.current = false;
|
|
468
524
|
} else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
469
|
-
|
|
470
|
-
emitTelemetryWithPlugins({
|
|
525
|
+
const emitted = emitCourseStarted({
|
|
471
526
|
pluginHost: pluginHostRef.current,
|
|
472
527
|
tracking: next,
|
|
473
528
|
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
|
-
}),
|
|
529
|
+
storage: defaultStorage,
|
|
530
|
+
sessionId,
|
|
531
|
+
courseId: cid,
|
|
532
|
+
attemptId: attemptIdRef.current,
|
|
533
|
+
user: userRef.current,
|
|
486
534
|
lxpackBridge: lxpackBridgeModeRef.current
|
|
487
535
|
});
|
|
488
|
-
courseStartedEmittedToSinkRef.current =
|
|
536
|
+
courseStartedEmittedToSinkRef.current = emitted;
|
|
489
537
|
} else if (trackingActive) {
|
|
490
538
|
courseStartedEmittedToSinkRef.current = true;
|
|
491
539
|
}
|
|
492
540
|
return () => {
|
|
493
541
|
if (prev !== trackingRef.current) {
|
|
494
|
-
disposeTrackingClient(prev);
|
|
542
|
+
void disposeTrackingClient(prev);
|
|
495
543
|
}
|
|
496
544
|
};
|
|
497
545
|
}, [
|
|
@@ -501,7 +549,9 @@ function LessonkitProvider(props) {
|
|
|
501
549
|
batchEnabled,
|
|
502
550
|
batchFlushIntervalMs,
|
|
503
551
|
batchMaxBatchSize,
|
|
504
|
-
config.plugins
|
|
552
|
+
config.plugins,
|
|
553
|
+
config.courseId,
|
|
554
|
+
buildCurrentPluginCtx
|
|
505
555
|
]);
|
|
506
556
|
const emitWithBridge = useCallback((trackingClient, event) => {
|
|
507
557
|
emitTelemetryWithPlugins({
|
|
@@ -540,28 +590,26 @@ function LessonkitProvider(props) {
|
|
|
540
590
|
if (!isTrackingActive(config.tracking)) return;
|
|
541
591
|
const sessionId = sessionIdRef.current;
|
|
542
592
|
const cid = courseIdRef.current;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
593
|
+
void (async () => {
|
|
594
|
+
try {
|
|
595
|
+
await trackingRef.current?.flush?.();
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
599
|
+
const emitted = emitCourseStarted({
|
|
600
|
+
pluginHost: pluginHostRef.current,
|
|
601
|
+
tracking: trackingRef.current,
|
|
602
|
+
xapi: xapiRef.current,
|
|
603
|
+
storage: defaultStorage,
|
|
552
604
|
sessionId,
|
|
553
|
-
attemptId: attemptIdRef.current,
|
|
554
|
-
user: userRef.current
|
|
555
|
-
}),
|
|
556
|
-
pluginCtx: buildPluginContext({
|
|
557
605
|
courseId: cid,
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
606
|
+
attemptId: attemptIdRef.current,
|
|
607
|
+
user: userRef.current,
|
|
608
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
609
|
+
});
|
|
610
|
+
courseStartedEmittedToSinkRef.current = emitted;
|
|
611
|
+
}
|
|
612
|
+
})();
|
|
565
613
|
}, [config.courseId, config.tracking?.enabled, syncProgress]);
|
|
566
614
|
const emitLessonCompleted = useCallback(
|
|
567
615
|
(lessonId, durationMs) => {
|
|
@@ -578,25 +626,27 @@ function LessonkitProvider(props) {
|
|
|
578
626
|
if (!result.didComplete) return;
|
|
579
627
|
syncProgress();
|
|
580
628
|
emitLessonCompleted(lessonId, result.durationMs);
|
|
581
|
-
void trackingRef.current?.flush?.();
|
|
629
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
582
630
|
},
|
|
583
631
|
[syncProgress, emitLessonCompleted]
|
|
584
632
|
);
|
|
585
|
-
const unmountTimerIdsRef = useRef([]);
|
|
586
633
|
useEffect(() => {
|
|
587
634
|
return () => {
|
|
588
|
-
for (const id of unmountTimerIdsRef.current) clearTimeout(id);
|
|
589
|
-
unmountTimerIdsRef.current = [];
|
|
590
635
|
const client = trackingClientForUnmountRef.current;
|
|
591
|
-
void
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
636
|
+
void (async () => {
|
|
637
|
+
try {
|
|
638
|
+
await xapiRef.current?.flush();
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
await client?.flush?.();
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
await client?.dispose?.();
|
|
647
|
+
} catch {
|
|
648
|
+
}
|
|
649
|
+
})();
|
|
600
650
|
};
|
|
601
651
|
}, []);
|
|
602
652
|
const setActiveLesson = useCallback(
|
|
@@ -608,6 +658,7 @@ function LessonkitProvider(props) {
|
|
|
608
658
|
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
609
659
|
if (completed.didComplete) {
|
|
610
660
|
emitLessonCompleted(previous, completed.durationMs);
|
|
661
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
611
662
|
}
|
|
612
663
|
}
|
|
613
664
|
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
@@ -812,13 +863,24 @@ function KnowledgeCheck(props) {
|
|
|
812
863
|
function Quiz(props) {
|
|
813
864
|
warnInvalidComponentId(props.checkId, "checkId");
|
|
814
865
|
const quiz = useQuizState();
|
|
866
|
+
const { plugins, config, progress, session } = useLessonkit();
|
|
815
867
|
const [selected, setSelected] = useState2(null);
|
|
868
|
+
const [selectionCorrect, setSelectionCorrect] = useState2(null);
|
|
816
869
|
const completedRef = useRef2(false);
|
|
817
870
|
const questionId = useId();
|
|
818
871
|
useEffect2(() => {
|
|
819
872
|
completedRef.current = false;
|
|
820
873
|
setSelected(null);
|
|
874
|
+
setSelectionCorrect(null);
|
|
821
875
|
}, [props.checkId, props.answer, props.question]);
|
|
876
|
+
const isChoiceCorrect = (choice, custom) => {
|
|
877
|
+
if (!custom) return choice === props.answer;
|
|
878
|
+
if (custom.passed !== void 0) return custom.passed;
|
|
879
|
+
if (custom.maxScore != null && custom.maxScore > 0) {
|
|
880
|
+
return custom.score / custom.maxScore >= 1;
|
|
881
|
+
}
|
|
882
|
+
return choice === props.answer;
|
|
883
|
+
};
|
|
822
884
|
return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
|
|
823
885
|
/* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
|
|
824
886
|
/* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
|
|
@@ -833,7 +895,21 @@ function Quiz(props) {
|
|
|
833
895
|
checked: selected === c,
|
|
834
896
|
onChange: () => {
|
|
835
897
|
setSelected(c);
|
|
836
|
-
const
|
|
898
|
+
const pluginCtx = buildPluginContext({
|
|
899
|
+
courseId: config.courseId,
|
|
900
|
+
sessionId: session.sessionId,
|
|
901
|
+
attemptId: session.attemptId
|
|
902
|
+
});
|
|
903
|
+
const custom = plugins?.scoreAssessment(
|
|
904
|
+
{
|
|
905
|
+
checkId: props.checkId,
|
|
906
|
+
lessonId: progress.activeLessonId,
|
|
907
|
+
response: c
|
|
908
|
+
},
|
|
909
|
+
pluginCtx
|
|
910
|
+
) ?? null;
|
|
911
|
+
const correct = isChoiceCorrect(c, custom);
|
|
912
|
+
setSelectionCorrect(correct);
|
|
837
913
|
quiz.answer({
|
|
838
914
|
checkId: props.checkId,
|
|
839
915
|
question: props.question,
|
|
@@ -842,7 +918,12 @@ function Quiz(props) {
|
|
|
842
918
|
});
|
|
843
919
|
if (correct && !completedRef.current) {
|
|
844
920
|
completedRef.current = true;
|
|
845
|
-
quiz.complete({
|
|
921
|
+
quiz.complete({
|
|
922
|
+
checkId: props.checkId,
|
|
923
|
+
score: custom?.score ?? 1,
|
|
924
|
+
maxScore: custom?.maxScore ?? 1,
|
|
925
|
+
passingScore: 1
|
|
926
|
+
});
|
|
846
927
|
}
|
|
847
928
|
}
|
|
848
929
|
}
|
|
@@ -850,7 +931,7 @@ function Quiz(props) {
|
|
|
850
931
|
c
|
|
851
932
|
] }, `${questionId}-${i}`))
|
|
852
933
|
] }),
|
|
853
|
-
selected ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children:
|
|
934
|
+
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
854
935
|
] });
|
|
855
936
|
}
|
|
856
937
|
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.2",
|
|
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.2",
|
|
58
|
+
"@lessonkit/core": "0.9.2",
|
|
59
|
+
"@lessonkit/lxpack": "0.9.2",
|
|
60
|
+
"@lessonkit/themes": "0.9.2",
|
|
61
|
+
"@lessonkit/xapi": "0.9.2"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@testing-library/react": "^16.3.0",
|