@lessonkit/react 0.8.0 → 0.8.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 +132 -51
- package/dist/index.js +130 -49
- package/package.json +6 -6
package/README.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -58,7 +58,7 @@ var import_accessibility = require("@lessonkit/accessibility");
|
|
|
58
58
|
|
|
59
59
|
// src/context.tsx
|
|
60
60
|
var import_react = require("react");
|
|
61
|
-
var
|
|
61
|
+
var import_core4 = require("@lessonkit/core");
|
|
62
62
|
var import_xapi3 = require("@lessonkit/xapi");
|
|
63
63
|
var import_xapi4 = require("@lessonkit/xapi");
|
|
64
64
|
|
|
@@ -98,7 +98,10 @@ function forwardTelemetryToLxpack(event, mode = "auto") {
|
|
|
98
98
|
bridge.submitAssessment?.({
|
|
99
99
|
id: data.checkId,
|
|
100
100
|
score: scaled,
|
|
101
|
-
passingScore: (0, import_bridge.normalizeAssessmentPassingScore)(
|
|
101
|
+
passingScore: (0, import_bridge.normalizeAssessmentPassingScore)({
|
|
102
|
+
passingScore: data.passingScore,
|
|
103
|
+
maxScore: data.maxScore
|
|
104
|
+
})
|
|
102
105
|
});
|
|
103
106
|
return;
|
|
104
107
|
}
|
|
@@ -229,6 +232,12 @@ function createSessionStoragePort() {
|
|
|
229
232
|
sessionStorage.setItem(key, value);
|
|
230
233
|
} catch {
|
|
231
234
|
}
|
|
235
|
+
},
|
|
236
|
+
removeItem: (key) => {
|
|
237
|
+
try {
|
|
238
|
+
sessionStorage.removeItem(key);
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
232
241
|
}
|
|
233
242
|
};
|
|
234
243
|
}
|
|
@@ -276,6 +285,8 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
276
285
|
if (config.xapi?.enabled === false) return null;
|
|
277
286
|
if (config.xapi?.client) return config.xapi.client;
|
|
278
287
|
if (!config.courseId) return null;
|
|
288
|
+
const hasTransport = typeof config.xapi?.transport === "function";
|
|
289
|
+
if (!hasTransport && config.xapi?.enabled !== true) return null;
|
|
279
290
|
return (0, import_xapi2.createXAPIClient)({
|
|
280
291
|
courseId: config.courseId,
|
|
281
292
|
transport: config.xapi?.transport,
|
|
@@ -286,6 +297,9 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
286
297
|
// src/runtime/session.ts
|
|
287
298
|
var import_core2 = require("@lessonkit/core");
|
|
288
299
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
300
|
+
function getTabSessionId(storage) {
|
|
301
|
+
return storage.getItem(SESSION_STORAGE_KEY);
|
|
302
|
+
}
|
|
289
303
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
290
304
|
function resolveSessionId(storage, provided) {
|
|
291
305
|
if (provided) return provided;
|
|
@@ -306,30 +320,47 @@ function markCourseStarted(storage, sessionId, courseId) {
|
|
|
306
320
|
if (!courseId) return;
|
|
307
321
|
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
308
322
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
client?.flush?.();
|
|
316
|
-
client?.dispose?.();
|
|
323
|
+
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
324
|
+
if (!courseId || fromSessionId === toSessionId) return;
|
|
325
|
+
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
326
|
+
markCourseStarted(storage, toSessionId, courseId);
|
|
327
|
+
storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
|
|
328
|
+
}
|
|
317
329
|
}
|
|
318
|
-
|
|
330
|
+
|
|
331
|
+
// src/runtime/telemetry.ts
|
|
332
|
+
var import_core3 = require("@lessonkit/core");
|
|
319
333
|
function createTrackingClientFromConfig(config) {
|
|
320
|
-
if (config.tracking?.enabled === false)
|
|
321
|
-
|
|
322
|
-
}
|
|
334
|
+
if (config.tracking?.enabled === false) return (0, import_core3.createTrackingClient)();
|
|
335
|
+
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
323
336
|
return (0, import_core3.createTrackingClient)({
|
|
324
337
|
sink: config.tracking?.sink,
|
|
325
338
|
batchSink: config.tracking?.batchSink,
|
|
326
339
|
batch: config.tracking?.batch
|
|
327
340
|
});
|
|
328
341
|
}
|
|
342
|
+
function disposeTrackingClient(client) {
|
|
343
|
+
client?.flush?.();
|
|
344
|
+
client?.dispose?.();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/context.tsx
|
|
348
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
349
|
+
var LessonkitContext = (0, import_react.createContext)(null);
|
|
350
|
+
var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
|
|
351
|
+
var defaultStorage = createSessionStoragePort();
|
|
352
|
+
function isTrackingActive(tracking) {
|
|
353
|
+
return tracking?.enabled !== false;
|
|
354
|
+
}
|
|
329
355
|
function LessonkitProvider(props) {
|
|
330
356
|
const config = props.config;
|
|
331
357
|
const sessionIdRef = (0, import_react.useRef)(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
332
|
-
|
|
358
|
+
const prevConfiguredSessionIdRef = (0, import_react.useRef)(config.session?.sessionId);
|
|
359
|
+
if (config.session?.sessionId) {
|
|
360
|
+
sessionIdRef.current = config.session.sessionId;
|
|
361
|
+
} else if (prevConfiguredSessionIdRef.current) {
|
|
362
|
+
sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
|
|
363
|
+
}
|
|
333
364
|
const attemptIdRef = (0, import_react.useRef)(config.session?.attemptId);
|
|
334
365
|
const userRef = (0, import_react.useRef)(config.session?.user);
|
|
335
366
|
attemptIdRef.current = config.session?.attemptId;
|
|
@@ -339,6 +370,15 @@ function LessonkitProvider(props) {
|
|
|
339
370
|
const lxpackBridgeModeRef = (0, import_react.useRef)(config.lxpack?.bridge ?? "auto");
|
|
340
371
|
lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
|
|
341
372
|
const progressRef = (0, import_react.useRef)(createProgressController());
|
|
373
|
+
const courseStartedEmittedToSinkRef = (0, import_react.useRef)(false);
|
|
374
|
+
const prevCourseIdForProgressRef = (0, import_react.useRef)(config.courseId);
|
|
375
|
+
const pendingCourseIdResetRef = (0, import_react.useRef)(false);
|
|
376
|
+
if (prevCourseIdForProgressRef.current !== config.courseId) {
|
|
377
|
+
prevCourseIdForProgressRef.current = config.courseId;
|
|
378
|
+
progressRef.current = createProgressController();
|
|
379
|
+
pendingCourseIdResetRef.current = true;
|
|
380
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
381
|
+
}
|
|
342
382
|
const [progress, setProgress] = (0, import_react.useState)(() => progressRef.current.getState());
|
|
343
383
|
const syncProgress = (0, import_react.useCallback)(() => {
|
|
344
384
|
setProgress(progressRef.current.getState());
|
|
@@ -348,11 +388,16 @@ function LessonkitProvider(props) {
|
|
|
348
388
|
const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
|
|
349
389
|
const xapiRef = (0, import_react.useRef)(null);
|
|
350
390
|
const [xapi, setXapi] = (0, import_react.useState)(null);
|
|
391
|
+
const prevXapiCourseIdRef = (0, import_react.useRef)(config.courseId);
|
|
351
392
|
const xapiEnabled = config.xapi?.enabled;
|
|
352
393
|
const xapiClient = config.xapi?.client;
|
|
353
394
|
const xapiTransport = config.xapi?.transport;
|
|
354
395
|
const courseId = config.courseId;
|
|
355
396
|
useIsoLayoutEffect(() => {
|
|
397
|
+
if (prevXapiCourseIdRef.current !== courseId) {
|
|
398
|
+
xapiQueueRef.current = (0, import_xapi3.createInMemoryXAPIQueue)();
|
|
399
|
+
prevXapiCourseIdRef.current = courseId;
|
|
400
|
+
}
|
|
356
401
|
const prev = xapiRef.current;
|
|
357
402
|
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
358
403
|
xapiRef.current = next;
|
|
@@ -360,22 +405,21 @@ function LessonkitProvider(props) {
|
|
|
360
405
|
if (next && !prev) {
|
|
361
406
|
const sessionId = sessionIdRef.current;
|
|
362
407
|
const cid = courseIdRef.current;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
} catch {
|
|
376
|
-
}
|
|
408
|
+
try {
|
|
409
|
+
const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
|
|
410
|
+
buildTrackEvent({
|
|
411
|
+
name: "course_started",
|
|
412
|
+
courseId: cid,
|
|
413
|
+
sessionId,
|
|
414
|
+
attemptId: attemptIdRef.current,
|
|
415
|
+
user: userRef.current
|
|
416
|
+
})
|
|
417
|
+
);
|
|
418
|
+
if (statement) next.send(statement);
|
|
419
|
+
} catch {
|
|
377
420
|
}
|
|
378
421
|
}
|
|
422
|
+
let cancelled = false;
|
|
379
423
|
void (async () => {
|
|
380
424
|
if (prev) {
|
|
381
425
|
try {
|
|
@@ -383,16 +427,19 @@ function LessonkitProvider(props) {
|
|
|
383
427
|
} catch {
|
|
384
428
|
}
|
|
385
429
|
}
|
|
430
|
+
if (cancelled) return;
|
|
386
431
|
try {
|
|
387
432
|
await next?.flush();
|
|
388
433
|
} catch {
|
|
389
434
|
}
|
|
390
435
|
})();
|
|
391
436
|
return () => {
|
|
437
|
+
cancelled = true;
|
|
392
438
|
void prev?.flush();
|
|
393
439
|
};
|
|
394
440
|
}, [xapiEnabled, xapiClient, xapiTransport, courseId]);
|
|
395
|
-
const trackingRef = (0, import_react.useRef)((0,
|
|
441
|
+
const trackingRef = (0, import_react.useRef)((0, import_core4.createTrackingClient)());
|
|
442
|
+
const trackingClientForUnmountRef = (0, import_react.useRef)(trackingRef.current);
|
|
396
443
|
const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
|
|
397
444
|
const trackingEnabled = config.tracking?.enabled;
|
|
398
445
|
const trackingSink = config.tracking?.sink;
|
|
@@ -402,12 +449,16 @@ function LessonkitProvider(props) {
|
|
|
402
449
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
403
450
|
useIsoLayoutEffect(() => {
|
|
404
451
|
const prev = trackingRef.current;
|
|
405
|
-
const next = createTrackingClientFromConfig(config);
|
|
452
|
+
const next = createTrackingClientFromConfig({ tracking: config.tracking });
|
|
406
453
|
trackingRef.current = next;
|
|
454
|
+
trackingClientForUnmountRef.current = next;
|
|
407
455
|
setTracking(next);
|
|
408
456
|
const sessionId = sessionIdRef.current;
|
|
409
457
|
const cid = courseIdRef.current;
|
|
410
|
-
|
|
458
|
+
const trackingActive = isTrackingActive(config.tracking);
|
|
459
|
+
if (!trackingActive) {
|
|
460
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
461
|
+
} else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
411
462
|
markCourseStarted(defaultStorage, sessionId, cid);
|
|
412
463
|
emitTelemetry(
|
|
413
464
|
next,
|
|
@@ -421,6 +472,9 @@ function LessonkitProvider(props) {
|
|
|
421
472
|
}),
|
|
422
473
|
{ lxpackBridge: lxpackBridgeModeRef.current }
|
|
423
474
|
);
|
|
475
|
+
courseStartedEmittedToSinkRef.current = true;
|
|
476
|
+
} else if (trackingActive) {
|
|
477
|
+
courseStartedEmittedToSinkRef.current = true;
|
|
424
478
|
}
|
|
425
479
|
return () => {
|
|
426
480
|
if (prev !== trackingRef.current) {
|
|
@@ -459,21 +513,14 @@ function LessonkitProvider(props) {
|
|
|
459
513
|
},
|
|
460
514
|
[emitWithBridge]
|
|
461
515
|
);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const previousActiveLesson = progressRef.current.getState().activeLessonId;
|
|
466
|
-
prevCourseIdRef.current = config.courseId;
|
|
467
|
-
progressRef.current = createProgressController();
|
|
516
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
517
|
+
if (!pendingCourseIdResetRef.current) return;
|
|
518
|
+
pendingCourseIdResetRef.current = false;
|
|
468
519
|
syncProgress();
|
|
469
|
-
if (
|
|
470
|
-
progressRef.current.setActiveLesson(previousActiveLesson, Date.now());
|
|
471
|
-
syncProgress();
|
|
472
|
-
track("lesson_started", { lessonId: previousActiveLesson }, { lessonId: previousActiveLesson });
|
|
473
|
-
}
|
|
520
|
+
if (!isTrackingActive(config.tracking)) return;
|
|
474
521
|
const sessionId = sessionIdRef.current;
|
|
475
|
-
const cid =
|
|
476
|
-
if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
522
|
+
const cid = courseIdRef.current;
|
|
523
|
+
if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
477
524
|
markCourseStarted(defaultStorage, sessionId, cid);
|
|
478
525
|
emitTelemetry(
|
|
479
526
|
trackingRef.current,
|
|
@@ -487,8 +534,9 @@ function LessonkitProvider(props) {
|
|
|
487
534
|
}),
|
|
488
535
|
{ lxpackBridge: lxpackBridgeModeRef.current }
|
|
489
536
|
);
|
|
537
|
+
courseStartedEmittedToSinkRef.current = true;
|
|
490
538
|
}
|
|
491
|
-
}, [config.courseId,
|
|
539
|
+
}, [config.courseId, config.tracking?.enabled, syncProgress]);
|
|
492
540
|
const emitLessonCompleted = (0, import_react.useCallback)(
|
|
493
541
|
(lessonId, durationMs) => {
|
|
494
542
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -508,16 +556,21 @@ function LessonkitProvider(props) {
|
|
|
508
556
|
},
|
|
509
557
|
[syncProgress, emitLessonCompleted]
|
|
510
558
|
);
|
|
559
|
+
const unmountTimerIdsRef = (0, import_react.useRef)([]);
|
|
511
560
|
(0, import_react.useEffect)(() => {
|
|
512
561
|
return () => {
|
|
513
|
-
const
|
|
562
|
+
for (const id of unmountTimerIdsRef.current) clearTimeout(id);
|
|
563
|
+
unmountTimerIdsRef.current = [];
|
|
564
|
+
const client = trackingClientForUnmountRef.current;
|
|
514
565
|
void xapiRef.current?.flush();
|
|
515
|
-
setTimeout(() => {
|
|
566
|
+
const flushTimer = setTimeout(() => {
|
|
516
567
|
client?.flush?.();
|
|
517
|
-
setTimeout(() => {
|
|
568
|
+
const disposeTimer = setTimeout(() => {
|
|
518
569
|
client?.dispose?.();
|
|
519
570
|
}, 0);
|
|
571
|
+
unmountTimerIdsRef.current.push(disposeTimer);
|
|
520
572
|
}, 0);
|
|
573
|
+
unmountTimerIdsRef.current.push(flushTimer);
|
|
521
574
|
};
|
|
522
575
|
}, []);
|
|
523
576
|
const setActiveLesson = (0, import_react.useCallback)(
|
|
@@ -542,10 +595,34 @@ function LessonkitProvider(props) {
|
|
|
542
595
|
if (!result.didComplete) return;
|
|
543
596
|
syncProgress();
|
|
544
597
|
track("course_completed");
|
|
598
|
+
void trackingRef.current?.flush?.();
|
|
545
599
|
}, [track, syncProgress]);
|
|
546
600
|
const sessionUser = config.session?.user;
|
|
547
601
|
const sessionAttemptId = config.session?.attemptId;
|
|
548
602
|
const sessionConfiguredId = config.session?.sessionId;
|
|
603
|
+
(0, import_react.useEffect)(() => {
|
|
604
|
+
const nextConfigured = config.session?.sessionId;
|
|
605
|
+
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
606
|
+
if (nextConfigured === prevConfigured) return;
|
|
607
|
+
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
608
|
+
const cid = courseIdRef.current;
|
|
609
|
+
if (nextConfigured) {
|
|
610
|
+
const fromIds = /* @__PURE__ */ new Set();
|
|
611
|
+
if (prevConfigured) fromIds.add(prevConfigured);
|
|
612
|
+
const tabId = getTabSessionId(defaultStorage);
|
|
613
|
+
if (tabId) fromIds.add(tabId);
|
|
614
|
+
for (const fromId of fromIds) {
|
|
615
|
+
if (fromId !== nextConfigured) {
|
|
616
|
+
migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
sessionIdRef.current = nextConfigured;
|
|
620
|
+
} else if (prevConfigured) {
|
|
621
|
+
const nextAuto = resolveSessionId(defaultStorage, void 0);
|
|
622
|
+
migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
|
|
623
|
+
sessionIdRef.current = nextAuto;
|
|
624
|
+
}
|
|
625
|
+
}, [sessionConfiguredId, config.courseId]);
|
|
549
626
|
const runtime = (0, import_react.useMemo)(
|
|
550
627
|
() => ({
|
|
551
628
|
config,
|
|
@@ -610,7 +687,7 @@ function useQuizState() {
|
|
|
610
687
|
}
|
|
611
688
|
|
|
612
689
|
// src/runtime/validateComponentId.ts
|
|
613
|
-
var
|
|
690
|
+
var import_core5 = require("@lessonkit/core");
|
|
614
691
|
var warnedPaths = /* @__PURE__ */ new Set();
|
|
615
692
|
function isDevEnvironment2() {
|
|
616
693
|
const g = globalThis;
|
|
@@ -620,7 +697,7 @@ function warnInvalidComponentId(id, path) {
|
|
|
620
697
|
if (!isDevEnvironment2()) return;
|
|
621
698
|
const key = `${path}:${String(id)}`;
|
|
622
699
|
if (warnedPaths.has(key)) return;
|
|
623
|
-
const result = (0,
|
|
700
|
+
const result = (0, import_core5.validateId)(id, path);
|
|
624
701
|
if (result.ok) return;
|
|
625
702
|
warnedPaths.add(key);
|
|
626
703
|
const detail = result.issues.map((i) => `${i.path}: ${i.message}`).join("; ");
|
|
@@ -698,6 +775,10 @@ function Quiz(props) {
|
|
|
698
775
|
const [selected, setSelected] = (0, import_react3.useState)(null);
|
|
699
776
|
const completedRef = (0, import_react3.useRef)(false);
|
|
700
777
|
const questionId = (0, import_react3.useId)();
|
|
778
|
+
(0, import_react3.useEffect)(() => {
|
|
779
|
+
completedRef.current = false;
|
|
780
|
+
setSelected(null);
|
|
781
|
+
}, [props.checkId, props.answer, props.question]);
|
|
701
782
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
|
|
702
783
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
|
|
703
784
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
useRef,
|
|
13
13
|
useState
|
|
14
14
|
} from "react";
|
|
15
|
-
import { createTrackingClient } from "@lessonkit/core";
|
|
15
|
+
import { createTrackingClient as createTrackingClient2 } from "@lessonkit/core";
|
|
16
16
|
import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
|
|
17
17
|
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
|
|
18
18
|
|
|
@@ -55,7 +55,10 @@ function forwardTelemetryToLxpack(event, mode = "auto") {
|
|
|
55
55
|
bridge.submitAssessment?.({
|
|
56
56
|
id: data.checkId,
|
|
57
57
|
score: scaled,
|
|
58
|
-
passingScore: normalizeAssessmentPassingScore(
|
|
58
|
+
passingScore: normalizeAssessmentPassingScore({
|
|
59
|
+
passingScore: data.passingScore,
|
|
60
|
+
maxScore: data.maxScore
|
|
61
|
+
})
|
|
59
62
|
});
|
|
60
63
|
return;
|
|
61
64
|
}
|
|
@@ -186,6 +189,12 @@ function createSessionStoragePort() {
|
|
|
186
189
|
sessionStorage.setItem(key, value);
|
|
187
190
|
} catch {
|
|
188
191
|
}
|
|
192
|
+
},
|
|
193
|
+
removeItem: (key) => {
|
|
194
|
+
try {
|
|
195
|
+
sessionStorage.removeItem(key);
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
189
198
|
}
|
|
190
199
|
};
|
|
191
200
|
}
|
|
@@ -233,6 +242,8 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
233
242
|
if (config.xapi?.enabled === false) return null;
|
|
234
243
|
if (config.xapi?.client) return config.xapi.client;
|
|
235
244
|
if (!config.courseId) return null;
|
|
245
|
+
const hasTransport = typeof config.xapi?.transport === "function";
|
|
246
|
+
if (!hasTransport && config.xapi?.enabled !== true) return null;
|
|
236
247
|
return createXAPIClient({
|
|
237
248
|
courseId: config.courseId,
|
|
238
249
|
transport: config.xapi?.transport,
|
|
@@ -243,6 +254,9 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
243
254
|
// src/runtime/session.ts
|
|
244
255
|
import { createSessionId } from "@lessonkit/core";
|
|
245
256
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
257
|
+
function getTabSessionId(storage) {
|
|
258
|
+
return storage.getItem(SESSION_STORAGE_KEY);
|
|
259
|
+
}
|
|
246
260
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
247
261
|
function resolveSessionId(storage, provided) {
|
|
248
262
|
if (provided) return provided;
|
|
@@ -263,30 +277,47 @@ function markCourseStarted(storage, sessionId, courseId) {
|
|
|
263
277
|
if (!courseId) return;
|
|
264
278
|
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
265
279
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
client?.flush?.();
|
|
273
|
-
client?.dispose?.();
|
|
280
|
+
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
281
|
+
if (!courseId || fromSessionId === toSessionId) return;
|
|
282
|
+
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
283
|
+
markCourseStarted(storage, toSessionId, courseId);
|
|
284
|
+
storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
|
|
285
|
+
}
|
|
274
286
|
}
|
|
275
|
-
|
|
287
|
+
|
|
288
|
+
// src/runtime/telemetry.ts
|
|
289
|
+
import { createTrackingClient } from "@lessonkit/core";
|
|
276
290
|
function createTrackingClientFromConfig(config) {
|
|
277
|
-
if (config.tracking?.enabled === false)
|
|
278
|
-
|
|
279
|
-
}
|
|
291
|
+
if (config.tracking?.enabled === false) return createTrackingClient();
|
|
292
|
+
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
280
293
|
return createTrackingClient({
|
|
281
294
|
sink: config.tracking?.sink,
|
|
282
295
|
batchSink: config.tracking?.batchSink,
|
|
283
296
|
batch: config.tracking?.batch
|
|
284
297
|
});
|
|
285
298
|
}
|
|
299
|
+
function disposeTrackingClient(client) {
|
|
300
|
+
client?.flush?.();
|
|
301
|
+
client?.dispose?.();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/context.tsx
|
|
305
|
+
import { jsx } from "react/jsx-runtime";
|
|
306
|
+
var LessonkitContext = createContext(null);
|
|
307
|
+
var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
308
|
+
var defaultStorage = createSessionStoragePort();
|
|
309
|
+
function isTrackingActive(tracking) {
|
|
310
|
+
return tracking?.enabled !== false;
|
|
311
|
+
}
|
|
286
312
|
function LessonkitProvider(props) {
|
|
287
313
|
const config = props.config;
|
|
288
314
|
const sessionIdRef = useRef(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
289
|
-
|
|
315
|
+
const prevConfiguredSessionIdRef = useRef(config.session?.sessionId);
|
|
316
|
+
if (config.session?.sessionId) {
|
|
317
|
+
sessionIdRef.current = config.session.sessionId;
|
|
318
|
+
} else if (prevConfiguredSessionIdRef.current) {
|
|
319
|
+
sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
|
|
320
|
+
}
|
|
290
321
|
const attemptIdRef = useRef(config.session?.attemptId);
|
|
291
322
|
const userRef = useRef(config.session?.user);
|
|
292
323
|
attemptIdRef.current = config.session?.attemptId;
|
|
@@ -296,6 +327,15 @@ function LessonkitProvider(props) {
|
|
|
296
327
|
const lxpackBridgeModeRef = useRef(config.lxpack?.bridge ?? "auto");
|
|
297
328
|
lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
|
|
298
329
|
const progressRef = useRef(createProgressController());
|
|
330
|
+
const courseStartedEmittedToSinkRef = useRef(false);
|
|
331
|
+
const prevCourseIdForProgressRef = useRef(config.courseId);
|
|
332
|
+
const pendingCourseIdResetRef = useRef(false);
|
|
333
|
+
if (prevCourseIdForProgressRef.current !== config.courseId) {
|
|
334
|
+
prevCourseIdForProgressRef.current = config.courseId;
|
|
335
|
+
progressRef.current = createProgressController();
|
|
336
|
+
pendingCourseIdResetRef.current = true;
|
|
337
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
338
|
+
}
|
|
299
339
|
const [progress, setProgress] = useState(() => progressRef.current.getState());
|
|
300
340
|
const syncProgress = useCallback(() => {
|
|
301
341
|
setProgress(progressRef.current.getState());
|
|
@@ -305,11 +345,16 @@ function LessonkitProvider(props) {
|
|
|
305
345
|
const xapiQueueRef = useRef(createInMemoryXAPIQueue());
|
|
306
346
|
const xapiRef = useRef(null);
|
|
307
347
|
const [xapi, setXapi] = useState(null);
|
|
348
|
+
const prevXapiCourseIdRef = useRef(config.courseId);
|
|
308
349
|
const xapiEnabled = config.xapi?.enabled;
|
|
309
350
|
const xapiClient = config.xapi?.client;
|
|
310
351
|
const xapiTransport = config.xapi?.transport;
|
|
311
352
|
const courseId = config.courseId;
|
|
312
353
|
useIsoLayoutEffect(() => {
|
|
354
|
+
if (prevXapiCourseIdRef.current !== courseId) {
|
|
355
|
+
xapiQueueRef.current = createInMemoryXAPIQueue();
|
|
356
|
+
prevXapiCourseIdRef.current = courseId;
|
|
357
|
+
}
|
|
313
358
|
const prev = xapiRef.current;
|
|
314
359
|
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
315
360
|
xapiRef.current = next;
|
|
@@ -317,22 +362,21 @@ function LessonkitProvider(props) {
|
|
|
317
362
|
if (next && !prev) {
|
|
318
363
|
const sessionId = sessionIdRef.current;
|
|
319
364
|
const cid = courseIdRef.current;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
} catch {
|
|
333
|
-
}
|
|
365
|
+
try {
|
|
366
|
+
const statement = telemetryEventToXAPIStatement2(
|
|
367
|
+
buildTrackEvent({
|
|
368
|
+
name: "course_started",
|
|
369
|
+
courseId: cid,
|
|
370
|
+
sessionId,
|
|
371
|
+
attemptId: attemptIdRef.current,
|
|
372
|
+
user: userRef.current
|
|
373
|
+
})
|
|
374
|
+
);
|
|
375
|
+
if (statement) next.send(statement);
|
|
376
|
+
} catch {
|
|
334
377
|
}
|
|
335
378
|
}
|
|
379
|
+
let cancelled = false;
|
|
336
380
|
void (async () => {
|
|
337
381
|
if (prev) {
|
|
338
382
|
try {
|
|
@@ -340,16 +384,19 @@ function LessonkitProvider(props) {
|
|
|
340
384
|
} catch {
|
|
341
385
|
}
|
|
342
386
|
}
|
|
387
|
+
if (cancelled) return;
|
|
343
388
|
try {
|
|
344
389
|
await next?.flush();
|
|
345
390
|
} catch {
|
|
346
391
|
}
|
|
347
392
|
})();
|
|
348
393
|
return () => {
|
|
394
|
+
cancelled = true;
|
|
349
395
|
void prev?.flush();
|
|
350
396
|
};
|
|
351
397
|
}, [xapiEnabled, xapiClient, xapiTransport, courseId]);
|
|
352
|
-
const trackingRef = useRef(
|
|
398
|
+
const trackingRef = useRef(createTrackingClient2());
|
|
399
|
+
const trackingClientForUnmountRef = useRef(trackingRef.current);
|
|
353
400
|
const [tracking, setTracking] = useState(() => trackingRef.current);
|
|
354
401
|
const trackingEnabled = config.tracking?.enabled;
|
|
355
402
|
const trackingSink = config.tracking?.sink;
|
|
@@ -359,12 +406,16 @@ function LessonkitProvider(props) {
|
|
|
359
406
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
360
407
|
useIsoLayoutEffect(() => {
|
|
361
408
|
const prev = trackingRef.current;
|
|
362
|
-
const next = createTrackingClientFromConfig(config);
|
|
409
|
+
const next = createTrackingClientFromConfig({ tracking: config.tracking });
|
|
363
410
|
trackingRef.current = next;
|
|
411
|
+
trackingClientForUnmountRef.current = next;
|
|
364
412
|
setTracking(next);
|
|
365
413
|
const sessionId = sessionIdRef.current;
|
|
366
414
|
const cid = courseIdRef.current;
|
|
367
|
-
|
|
415
|
+
const trackingActive = isTrackingActive(config.tracking);
|
|
416
|
+
if (!trackingActive) {
|
|
417
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
418
|
+
} else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
368
419
|
markCourseStarted(defaultStorage, sessionId, cid);
|
|
369
420
|
emitTelemetry(
|
|
370
421
|
next,
|
|
@@ -378,6 +429,9 @@ function LessonkitProvider(props) {
|
|
|
378
429
|
}),
|
|
379
430
|
{ lxpackBridge: lxpackBridgeModeRef.current }
|
|
380
431
|
);
|
|
432
|
+
courseStartedEmittedToSinkRef.current = true;
|
|
433
|
+
} else if (trackingActive) {
|
|
434
|
+
courseStartedEmittedToSinkRef.current = true;
|
|
381
435
|
}
|
|
382
436
|
return () => {
|
|
383
437
|
if (prev !== trackingRef.current) {
|
|
@@ -416,21 +470,14 @@ function LessonkitProvider(props) {
|
|
|
416
470
|
},
|
|
417
471
|
[emitWithBridge]
|
|
418
472
|
);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const previousActiveLesson = progressRef.current.getState().activeLessonId;
|
|
423
|
-
prevCourseIdRef.current = config.courseId;
|
|
424
|
-
progressRef.current = createProgressController();
|
|
473
|
+
useLayoutEffect(() => {
|
|
474
|
+
if (!pendingCourseIdResetRef.current) return;
|
|
475
|
+
pendingCourseIdResetRef.current = false;
|
|
425
476
|
syncProgress();
|
|
426
|
-
if (
|
|
427
|
-
progressRef.current.setActiveLesson(previousActiveLesson, Date.now());
|
|
428
|
-
syncProgress();
|
|
429
|
-
track("lesson_started", { lessonId: previousActiveLesson }, { lessonId: previousActiveLesson });
|
|
430
|
-
}
|
|
477
|
+
if (!isTrackingActive(config.tracking)) return;
|
|
431
478
|
const sessionId = sessionIdRef.current;
|
|
432
|
-
const cid =
|
|
433
|
-
if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
479
|
+
const cid = courseIdRef.current;
|
|
480
|
+
if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
|
|
434
481
|
markCourseStarted(defaultStorage, sessionId, cid);
|
|
435
482
|
emitTelemetry(
|
|
436
483
|
trackingRef.current,
|
|
@@ -444,8 +491,9 @@ function LessonkitProvider(props) {
|
|
|
444
491
|
}),
|
|
445
492
|
{ lxpackBridge: lxpackBridgeModeRef.current }
|
|
446
493
|
);
|
|
494
|
+
courseStartedEmittedToSinkRef.current = true;
|
|
447
495
|
}
|
|
448
|
-
}, [config.courseId,
|
|
496
|
+
}, [config.courseId, config.tracking?.enabled, syncProgress]);
|
|
449
497
|
const emitLessonCompleted = useCallback(
|
|
450
498
|
(lessonId, durationMs) => {
|
|
451
499
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -465,16 +513,21 @@ function LessonkitProvider(props) {
|
|
|
465
513
|
},
|
|
466
514
|
[syncProgress, emitLessonCompleted]
|
|
467
515
|
);
|
|
516
|
+
const unmountTimerIdsRef = useRef([]);
|
|
468
517
|
useEffect(() => {
|
|
469
518
|
return () => {
|
|
470
|
-
const
|
|
519
|
+
for (const id of unmountTimerIdsRef.current) clearTimeout(id);
|
|
520
|
+
unmountTimerIdsRef.current = [];
|
|
521
|
+
const client = trackingClientForUnmountRef.current;
|
|
471
522
|
void xapiRef.current?.flush();
|
|
472
|
-
setTimeout(() => {
|
|
523
|
+
const flushTimer = setTimeout(() => {
|
|
473
524
|
client?.flush?.();
|
|
474
|
-
setTimeout(() => {
|
|
525
|
+
const disposeTimer = setTimeout(() => {
|
|
475
526
|
client?.dispose?.();
|
|
476
527
|
}, 0);
|
|
528
|
+
unmountTimerIdsRef.current.push(disposeTimer);
|
|
477
529
|
}, 0);
|
|
530
|
+
unmountTimerIdsRef.current.push(flushTimer);
|
|
478
531
|
};
|
|
479
532
|
}, []);
|
|
480
533
|
const setActiveLesson = useCallback(
|
|
@@ -499,10 +552,34 @@ function LessonkitProvider(props) {
|
|
|
499
552
|
if (!result.didComplete) return;
|
|
500
553
|
syncProgress();
|
|
501
554
|
track("course_completed");
|
|
555
|
+
void trackingRef.current?.flush?.();
|
|
502
556
|
}, [track, syncProgress]);
|
|
503
557
|
const sessionUser = config.session?.user;
|
|
504
558
|
const sessionAttemptId = config.session?.attemptId;
|
|
505
559
|
const sessionConfiguredId = config.session?.sessionId;
|
|
560
|
+
useEffect(() => {
|
|
561
|
+
const nextConfigured = config.session?.sessionId;
|
|
562
|
+
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
563
|
+
if (nextConfigured === prevConfigured) return;
|
|
564
|
+
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
565
|
+
const cid = courseIdRef.current;
|
|
566
|
+
if (nextConfigured) {
|
|
567
|
+
const fromIds = /* @__PURE__ */ new Set();
|
|
568
|
+
if (prevConfigured) fromIds.add(prevConfigured);
|
|
569
|
+
const tabId = getTabSessionId(defaultStorage);
|
|
570
|
+
if (tabId) fromIds.add(tabId);
|
|
571
|
+
for (const fromId of fromIds) {
|
|
572
|
+
if (fromId !== nextConfigured) {
|
|
573
|
+
migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
sessionIdRef.current = nextConfigured;
|
|
577
|
+
} else if (prevConfigured) {
|
|
578
|
+
const nextAuto = resolveSessionId(defaultStorage, void 0);
|
|
579
|
+
migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
|
|
580
|
+
sessionIdRef.current = nextAuto;
|
|
581
|
+
}
|
|
582
|
+
}, [sessionConfiguredId, config.courseId]);
|
|
506
583
|
const runtime = useMemo(
|
|
507
584
|
() => ({
|
|
508
585
|
config,
|
|
@@ -655,6 +732,10 @@ function Quiz(props) {
|
|
|
655
732
|
const [selected, setSelected] = useState2(null);
|
|
656
733
|
const completedRef = useRef2(false);
|
|
657
734
|
const questionId = useId();
|
|
735
|
+
useEffect2(() => {
|
|
736
|
+
completedRef.current = false;
|
|
737
|
+
setSelected(null);
|
|
738
|
+
}, [props.checkId, props.answer, props.question]);
|
|
658
739
|
return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
|
|
659
740
|
/* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
|
|
660
741
|
/* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/react",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.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.8.
|
|
58
|
-
"@lessonkit/core": "0.8.
|
|
59
|
-
"@lessonkit/lxpack": "0.8.
|
|
60
|
-
"@lessonkit/themes": "0.8.
|
|
61
|
-
"@lessonkit/xapi": "0.8.
|
|
57
|
+
"@lessonkit/accessibility": "0.8.1",
|
|
58
|
+
"@lessonkit/core": "0.8.1",
|
|
59
|
+
"@lessonkit/lxpack": "0.8.1",
|
|
60
|
+
"@lessonkit/themes": "0.8.1",
|
|
61
|
+
"@lessonkit/xapi": "0.8.1"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@testing-library/react": "^16.3.0",
|