@lessonkit/react 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -88
- package/block-catalog.v1.json +33 -1
- package/dist/index.cjs +597 -416
- package/dist/index.d.cts +23 -14
- package/dist/index.d.ts +23 -14
- package/dist/index.js +585 -385
- package/package.json +19 -6
package/dist/index.js
CHANGED
|
@@ -3,8 +3,10 @@ import { useEffect as useEffect2, useId, useMemo as useMemo3, useRef as useRef2,
|
|
|
3
3
|
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
4
4
|
|
|
5
5
|
// src/context.tsx
|
|
6
|
+
import { createContext } from "react";
|
|
7
|
+
|
|
8
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
6
9
|
import {
|
|
7
|
-
createContext,
|
|
8
10
|
useCallback,
|
|
9
11
|
useEffect,
|
|
10
12
|
useLayoutEffect,
|
|
@@ -12,241 +14,102 @@ import {
|
|
|
12
14
|
useRef,
|
|
13
15
|
useState
|
|
14
16
|
} from "react";
|
|
15
|
-
import { createTrackingClient as createTrackingClient2 } from "@lessonkit/core";
|
|
17
|
+
import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
|
|
16
18
|
import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
|
|
17
19
|
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
|
|
18
20
|
|
|
19
21
|
// src/runtime/emitTelemetry.ts
|
|
20
|
-
import {
|
|
22
|
+
import { buildTelemetryEvent, tryBuildTelemetryEvent } from "@lessonkit/core";
|
|
23
|
+
|
|
24
|
+
// src/runtime/telemetryPipeline.ts
|
|
25
|
+
import {
|
|
26
|
+
createTelemetryPipeline,
|
|
27
|
+
createTrackingPipelineSink
|
|
28
|
+
} from "@lessonkit/core";
|
|
21
29
|
import { telemetryEventToXAPIStatement } from "@lessonkit/xapi";
|
|
22
30
|
|
|
23
31
|
// src/runtime/lxpackBridge.ts
|
|
24
32
|
import {
|
|
25
|
-
|
|
33
|
+
dispatchBridgeAction,
|
|
34
|
+
forwardTelemetryToBridge,
|
|
35
|
+
getLxpackBridge,
|
|
26
36
|
mapLessonkitTelemetryToBridgeAction,
|
|
27
|
-
normalizePassingThreshold,
|
|
28
|
-
normalizeScore,
|
|
29
37
|
telemetryEventToLessonkit
|
|
30
38
|
} from "@lessonkit/lxpack/bridge";
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
if (fromSdk) return fromSdk;
|
|
34
|
-
if (typeof window === "undefined") return null;
|
|
35
|
-
const parent = window.parent;
|
|
36
|
-
if (!parent || parent === window) return null;
|
|
37
|
-
return parent.lxpack ?? null;
|
|
39
|
+
function forwardTelemetryToLxpack(event, mode = "auto") {
|
|
40
|
+
forwardTelemetryToBridge(event, mode);
|
|
38
41
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return;
|
|
45
|
-
case "completeCourse":
|
|
46
|
-
bridge.completeCourse?.();
|
|
47
|
-
return;
|
|
48
|
-
case "submitAssessment": {
|
|
49
|
-
const scaled = normalizeScore({
|
|
50
|
-
score: action.score,
|
|
51
|
-
maxScore: action.maxScore
|
|
52
|
-
});
|
|
53
|
-
if (scaled === null) return;
|
|
54
|
-
bridge.submitAssessment?.({
|
|
55
|
-
id: action.id,
|
|
56
|
-
score: scaled,
|
|
57
|
-
passingScore: normalizePassingThreshold({
|
|
58
|
-
passingScore: action.passingScore,
|
|
59
|
-
maxScore: action.maxScore
|
|
60
|
-
}),
|
|
61
|
-
maxScore: action.maxScore
|
|
62
|
-
});
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
case "track":
|
|
66
|
-
bridge.track?.(action.event);
|
|
67
|
-
return;
|
|
68
|
-
default:
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
42
|
+
|
|
43
|
+
// src/runtime/telemetryPipeline.ts
|
|
44
|
+
function isDevEnvironment() {
|
|
45
|
+
const g = globalThis;
|
|
46
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
71
47
|
}
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
48
|
+
function createLegacyPipeline(opts, extraSinks = []) {
|
|
49
|
+
return createTelemetryPipeline([
|
|
50
|
+
createTrackingPipelineSink("tracking", (event) => opts.tracking.track(event)),
|
|
51
|
+
{
|
|
52
|
+
id: "xapi",
|
|
53
|
+
emit(event) {
|
|
54
|
+
try {
|
|
55
|
+
const statement = telemetryEventToXAPIStatement(event);
|
|
56
|
+
if (statement) opts.xapi?.send(statement);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (isDevEnvironment()) {
|
|
59
|
+
console.warn(
|
|
60
|
+
"[lessonkit] xAPI mapping skipped:",
|
|
61
|
+
err instanceof Error ? err.message : err
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "lxpack-bridge",
|
|
69
|
+
emit(event) {
|
|
70
|
+
forwardTelemetryToLxpack(event, opts.lxpackBridge);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
...extraSinks
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
function emitThroughPipeline(event, opts, extraSinks) {
|
|
77
|
+
createLegacyPipeline(opts, extraSinks).emit(event);
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
// src/runtime/emitTelemetry.ts
|
|
83
81
|
var warnedMissingCourseId = false;
|
|
84
|
-
|
|
85
|
-
function isDevEnvironment() {
|
|
82
|
+
function isDevEnvironment2() {
|
|
86
83
|
const g = globalThis;
|
|
87
84
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
88
85
|
}
|
|
89
86
|
function emitTelemetry(tracking, xapi, event, opts) {
|
|
90
87
|
if (!event.courseId) {
|
|
91
|
-
if (
|
|
88
|
+
if (isDevEnvironment2() && !warnedMissingCourseId) {
|
|
92
89
|
warnedMissingCourseId = true;
|
|
93
90
|
console.warn("[lessonkit] telemetry event missing courseId");
|
|
94
91
|
}
|
|
95
92
|
return;
|
|
96
93
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} catch (err) {
|
|
102
|
-
if (isDevEnvironment()) {
|
|
103
|
-
console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
forwardTelemetryToLxpack(event, opts?.lxpackBridge ?? "auto");
|
|
107
|
-
}
|
|
108
|
-
function buildTrackEvent(opts) {
|
|
109
|
-
const base = {
|
|
110
|
-
timestamp: nowIso(),
|
|
111
|
-
courseId: opts.courseId,
|
|
112
|
-
sessionId: opts.sessionId,
|
|
113
|
-
attemptId: opts.attemptId,
|
|
114
|
-
user: opts.user
|
|
94
|
+
const legacy = {
|
|
95
|
+
tracking,
|
|
96
|
+
xapi,
|
|
97
|
+
lxpackBridge: opts?.lxpackBridge ?? "auto"
|
|
115
98
|
};
|
|
116
|
-
|
|
117
|
-
case "course_started":
|
|
118
|
-
return { name: "course_started", ...base };
|
|
119
|
-
case "course_completed":
|
|
120
|
-
return { name: "course_completed", ...base };
|
|
121
|
-
case "lesson_started": {
|
|
122
|
-
const data = opts.data;
|
|
123
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
124
|
-
if (!lessonId) throw new Error("lesson_started requires lessonId");
|
|
125
|
-
return {
|
|
126
|
-
name: "lesson_started",
|
|
127
|
-
...base,
|
|
128
|
-
lessonId,
|
|
129
|
-
data: { ...data, lessonId }
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
case "lesson_completed":
|
|
133
|
-
case "lesson_time_on_task": {
|
|
134
|
-
const data = opts.data;
|
|
135
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
136
|
-
if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
|
|
137
|
-
return {
|
|
138
|
-
name: opts.name,
|
|
139
|
-
...base,
|
|
140
|
-
lessonId,
|
|
141
|
-
data: { ...data, lessonId }
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
case "quiz_answered": {
|
|
145
|
-
const data = opts.data;
|
|
146
|
-
const lessonId = opts.lessonId;
|
|
147
|
-
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
148
|
-
return { name: "quiz_answered", ...base, lessonId, data };
|
|
149
|
-
}
|
|
150
|
-
case "quiz_completed": {
|
|
151
|
-
const data = opts.data;
|
|
152
|
-
const lessonId = opts.lessonId;
|
|
153
|
-
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
154
|
-
return { name: "quiz_completed", ...base, lessonId, data };
|
|
155
|
-
}
|
|
156
|
-
case "interaction":
|
|
157
|
-
return {
|
|
158
|
-
name: "interaction",
|
|
159
|
-
...base,
|
|
160
|
-
lessonId: opts.lessonId,
|
|
161
|
-
data: opts.data
|
|
162
|
-
};
|
|
163
|
-
default:
|
|
164
|
-
return { name: opts.name, ...base };
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
function tryBuildTrackEvent(opts) {
|
|
168
|
-
const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
|
|
169
|
-
if (isQuiz && !opts.lessonId) {
|
|
170
|
-
if (isDevEnvironment() && !warnedMissingQuizLesson) {
|
|
171
|
-
warnedMissingQuizLesson = true;
|
|
172
|
-
console.warn(
|
|
173
|
-
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
return buildTrackEvent(opts);
|
|
99
|
+
emitThroughPipeline(event, legacy, opts?.extraSinks);
|
|
179
100
|
}
|
|
180
101
|
|
|
181
102
|
// src/runtime/ports.ts
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
function createSessionStoragePort() {
|
|
190
|
-
if (typeof sessionStorage === "undefined") return createNoopStorage();
|
|
191
|
-
return {
|
|
192
|
-
getItem: (key) => {
|
|
193
|
-
try {
|
|
194
|
-
return sessionStorage.getItem(key);
|
|
195
|
-
} catch {
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
setItem: (key, value) => {
|
|
200
|
-
try {
|
|
201
|
-
sessionStorage.setItem(key, value);
|
|
202
|
-
} catch {
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
removeItem: (key) => {
|
|
206
|
-
try {
|
|
207
|
-
sessionStorage.removeItem(key);
|
|
208
|
-
} catch {
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
}
|
|
103
|
+
import {
|
|
104
|
+
createDefaultClock,
|
|
105
|
+
createGlobalTimer,
|
|
106
|
+
createNoopStorage,
|
|
107
|
+
createSessionStoragePort,
|
|
108
|
+
resetStoragePortForTests
|
|
109
|
+
} from "@lessonkit/core";
|
|
213
110
|
|
|
214
111
|
// src/runtime/progress.ts
|
|
215
|
-
|
|
216
|
-
let activeLessonId;
|
|
217
|
-
let completedLessonIds = /* @__PURE__ */ new Set();
|
|
218
|
-
let courseCompleted = false;
|
|
219
|
-
const lessonStartTimes = /* @__PURE__ */ new Map();
|
|
220
|
-
return {
|
|
221
|
-
getState: () => ({
|
|
222
|
-
activeLessonId,
|
|
223
|
-
completedLessonIds: new Set(completedLessonIds),
|
|
224
|
-
courseCompleted
|
|
225
|
-
}),
|
|
226
|
-
setActiveLesson: (lessonId, startedAtMs) => {
|
|
227
|
-
const previousLessonId = activeLessonId;
|
|
228
|
-
activeLessonId = lessonId;
|
|
229
|
-
lessonStartTimes.set(lessonId, startedAtMs);
|
|
230
|
-
return { previousLessonId };
|
|
231
|
-
},
|
|
232
|
-
completeLesson: (lessonId, completedAtMs) => {
|
|
233
|
-
if (completedLessonIds.has(lessonId)) return { didComplete: false };
|
|
234
|
-
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
235
|
-
if (activeLessonId === lessonId) {
|
|
236
|
-
activeLessonId = void 0;
|
|
237
|
-
}
|
|
238
|
-
const startedAt = lessonStartTimes.get(lessonId);
|
|
239
|
-
lessonStartTimes.delete(lessonId);
|
|
240
|
-
const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
|
|
241
|
-
return { durationMs, didComplete: true };
|
|
242
|
-
},
|
|
243
|
-
completeCourse: () => {
|
|
244
|
-
if (courseCompleted) return { didComplete: false };
|
|
245
|
-
courseCompleted = true;
|
|
246
|
-
return { didComplete: true };
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
112
|
+
import { createProgressController } from "@lessonkit/core";
|
|
250
113
|
|
|
251
114
|
// src/runtime/xapi.ts
|
|
252
115
|
import { createXAPIClient } from "@lessonkit/xapi";
|
|
@@ -264,56 +127,38 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
264
127
|
}
|
|
265
128
|
|
|
266
129
|
// src/runtime/session.ts
|
|
267
|
-
import {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const id = createSessionId();
|
|
278
|
-
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
279
|
-
return id;
|
|
280
|
-
}
|
|
281
|
-
function courseStartedStorageKey(sessionId, courseId) {
|
|
282
|
-
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
283
|
-
}
|
|
284
|
-
function hasCourseStarted(storage, sessionId, courseId) {
|
|
285
|
-
if (!courseId) return false;
|
|
286
|
-
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
287
|
-
}
|
|
288
|
-
function markCourseStarted(storage, sessionId, courseId) {
|
|
289
|
-
if (!courseId) return;
|
|
290
|
-
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
291
|
-
}
|
|
292
|
-
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
293
|
-
if (!courseId || fromSessionId === toSessionId) return;
|
|
294
|
-
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
295
|
-
markCourseStarted(storage, toSessionId, courseId);
|
|
296
|
-
storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
|
|
297
|
-
}
|
|
298
|
-
}
|
|
130
|
+
import {
|
|
131
|
+
SESSION_STORAGE_KEY,
|
|
132
|
+
getTabSessionId,
|
|
133
|
+
resolveSessionId,
|
|
134
|
+
hasCourseStarted,
|
|
135
|
+
markCourseStarted,
|
|
136
|
+
hasCourseStartedEmittedToTracking,
|
|
137
|
+
markCourseStartedEmittedToTracking,
|
|
138
|
+
migrateCourseStartedMark
|
|
139
|
+
} from "@lessonkit/core";
|
|
299
140
|
|
|
300
141
|
// src/runtime/plugins.ts
|
|
301
|
-
import {
|
|
142
|
+
import { createPluginRegistry } from "@lessonkit/core";
|
|
302
143
|
function createReactPluginHost(plugins) {
|
|
303
144
|
if (!plugins?.length) return null;
|
|
304
|
-
return
|
|
145
|
+
return createPluginRegistry(plugins);
|
|
305
146
|
}
|
|
306
147
|
function buildPluginContext(opts) {
|
|
307
148
|
return {
|
|
308
149
|
courseId: opts.courseId,
|
|
309
150
|
sessionId: opts.sessionId,
|
|
310
|
-
attemptId: opts.attemptId
|
|
151
|
+
attemptId: opts.attemptId,
|
|
152
|
+
user: opts.user
|
|
311
153
|
};
|
|
312
154
|
}
|
|
313
155
|
function emitTelemetryWithPlugins(opts) {
|
|
314
156
|
const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
|
|
315
157
|
if (next === null) return;
|
|
316
|
-
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
158
|
+
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
159
|
+
lxpackBridge: opts.lxpackBridge ?? "auto",
|
|
160
|
+
extraSinks: opts.extraSinks
|
|
161
|
+
});
|
|
317
162
|
}
|
|
318
163
|
|
|
319
164
|
// src/runtime/telemetry.ts
|
|
@@ -338,34 +183,46 @@ async function disposeTrackingClient(client) {
|
|
|
338
183
|
}
|
|
339
184
|
}
|
|
340
185
|
|
|
341
|
-
// src/
|
|
342
|
-
import { jsx } from "react/jsx-runtime";
|
|
343
|
-
var LessonkitContext = createContext(null);
|
|
186
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
344
187
|
var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
345
188
|
var defaultStorage = createSessionStoragePort();
|
|
346
189
|
function isTrackingActive(tracking) {
|
|
347
190
|
return tracking?.enabled !== false;
|
|
348
191
|
}
|
|
349
|
-
|
|
192
|
+
var noopTrackingClient = { track: () => {
|
|
193
|
+
} };
|
|
194
|
+
function buildCourseStartedEvent(opts) {
|
|
350
195
|
const pluginCtx = buildPluginContext({
|
|
351
196
|
courseId: opts.courseId,
|
|
352
197
|
sessionId: opts.sessionId,
|
|
353
|
-
attemptId: opts.attemptId
|
|
198
|
+
attemptId: opts.attemptId,
|
|
199
|
+
user: opts.user
|
|
200
|
+
});
|
|
201
|
+
const built = buildTelemetryEvent({
|
|
202
|
+
name: "course_started",
|
|
203
|
+
courseId: opts.courseId,
|
|
204
|
+
sessionId: opts.sessionId,
|
|
205
|
+
attemptId: opts.attemptId,
|
|
206
|
+
user: opts.user
|
|
207
|
+
});
|
|
208
|
+
return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
|
|
209
|
+
}
|
|
210
|
+
function emitCourseStartedPipelineOnly(opts) {
|
|
211
|
+
const pluginCtx = buildPluginContext({
|
|
212
|
+
courseId: opts.courseId,
|
|
213
|
+
sessionId: opts.sessionId,
|
|
214
|
+
attemptId: opts.attemptId,
|
|
215
|
+
user: opts.user
|
|
354
216
|
});
|
|
355
217
|
try {
|
|
356
218
|
emitTelemetryWithPlugins({
|
|
357
|
-
pluginHost:
|
|
358
|
-
tracking:
|
|
219
|
+
pluginHost: null,
|
|
220
|
+
tracking: noopTrackingClient,
|
|
359
221
|
xapi: opts.xapi,
|
|
360
|
-
event:
|
|
361
|
-
name: "course_started",
|
|
362
|
-
courseId: opts.courseId,
|
|
363
|
-
sessionId: opts.sessionId,
|
|
364
|
-
attemptId: opts.attemptId,
|
|
365
|
-
user: opts.user
|
|
366
|
-
}),
|
|
222
|
+
event: opts.event,
|
|
367
223
|
pluginCtx,
|
|
368
|
-
lxpackBridge: opts.lxpackBridge
|
|
224
|
+
lxpackBridge: opts.lxpackBridge,
|
|
225
|
+
extraSinks: opts.extraSinks
|
|
369
226
|
});
|
|
370
227
|
markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
371
228
|
return true;
|
|
@@ -373,36 +230,160 @@ function emitCourseStarted(opts) {
|
|
|
373
230
|
return false;
|
|
374
231
|
}
|
|
375
232
|
}
|
|
376
|
-
function
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
233
|
+
function emitCourseStarted(opts) {
|
|
234
|
+
const event = buildCourseStartedEvent(opts);
|
|
235
|
+
if (event === null) return true;
|
|
236
|
+
const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
|
|
237
|
+
opts.storage,
|
|
238
|
+
opts.sessionId,
|
|
239
|
+
opts.courseId
|
|
240
|
+
);
|
|
241
|
+
if (!trackingAlreadyEmitted) {
|
|
242
|
+
try {
|
|
243
|
+
opts.tracking.track(event);
|
|
244
|
+
markCourseStartedEmittedToTracking(opts.storage, opts.sessionId, opts.courseId);
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
250
|
+
}
|
|
251
|
+
function emitCourseStartedToTrackingOnly(opts) {
|
|
252
|
+
const event = buildCourseStartedEvent(opts);
|
|
253
|
+
if (event === null) return true;
|
|
254
|
+
const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
|
|
255
|
+
opts.storage,
|
|
256
|
+
opts.sessionId,
|
|
257
|
+
opts.courseId
|
|
258
|
+
);
|
|
259
|
+
if (!trackingAlreadyEmitted) {
|
|
260
|
+
try {
|
|
261
|
+
opts.tracking.track(event);
|
|
262
|
+
markCourseStartedEmittedToTracking(opts.storage, opts.sessionId, opts.courseId);
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const pluginCtx = buildPluginContext({
|
|
268
|
+
courseId: opts.courseId,
|
|
269
|
+
sessionId: opts.sessionId,
|
|
270
|
+
attemptId: opts.attemptId,
|
|
271
|
+
user: opts.user
|
|
272
|
+
});
|
|
273
|
+
try {
|
|
274
|
+
emitTelemetryWithPlugins({
|
|
275
|
+
pluginHost: null,
|
|
276
|
+
tracking: noopTrackingClient,
|
|
277
|
+
xapi: null,
|
|
278
|
+
event,
|
|
279
|
+
pluginCtx,
|
|
280
|
+
lxpackBridge: opts.lxpackBridge,
|
|
281
|
+
extraSinks: opts.extraSinks
|
|
282
|
+
});
|
|
283
|
+
return true;
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function emitPendingCourseStarted(opts) {
|
|
289
|
+
const trackingEmitted = hasCourseStartedEmittedToTracking(
|
|
290
|
+
opts.storage,
|
|
291
|
+
opts.sessionId,
|
|
292
|
+
opts.courseId
|
|
293
|
+
);
|
|
294
|
+
const sessionStarted = hasCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
295
|
+
if (sessionStarted && !trackingEmitted) {
|
|
296
|
+
return emitCourseStartedToTrackingOnly(opts);
|
|
297
|
+
}
|
|
298
|
+
if (trackingEmitted && !sessionStarted) {
|
|
299
|
+
const event = buildCourseStartedEvent(opts);
|
|
300
|
+
if (event === null) return true;
|
|
301
|
+
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
302
|
+
}
|
|
303
|
+
if (!trackingEmitted && !sessionStarted) {
|
|
304
|
+
return emitCourseStarted(opts);
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
function assertTrackingSinkConfig(tracking) {
|
|
309
|
+
if (!tracking?.sink || !tracking?.batchSink) return;
|
|
310
|
+
throw new Error(
|
|
311
|
+
"[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
function useLessonkitProviderRuntime(config) {
|
|
315
|
+
const normalizedCourseId = useMemo(
|
|
316
|
+
() => assertValidId(config.courseId, "courseId"),
|
|
317
|
+
[config.courseId]
|
|
318
|
+
);
|
|
319
|
+
const normalizedConfig = useMemo(
|
|
320
|
+
() => ({ ...config, courseId: normalizedCourseId }),
|
|
321
|
+
[config, normalizedCourseId]
|
|
322
|
+
);
|
|
323
|
+
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
324
|
+
const extraSinksRef = useRef(normalizedConfig.sinks);
|
|
325
|
+
extraSinksRef.current = normalizedConfig.sinks;
|
|
326
|
+
const headlessRef = useRef(null);
|
|
327
|
+
const sessionIdRef = useRef(resolveSessionId(defaultStorage, normalizedConfig.session?.sessionId));
|
|
328
|
+
const prevConfiguredSessionIdRef = useRef(normalizedConfig.session?.sessionId);
|
|
329
|
+
if (normalizedConfig.session?.sessionId) {
|
|
330
|
+
sessionIdRef.current = normalizedConfig.session.sessionId;
|
|
382
331
|
} else if (prevConfiguredSessionIdRef.current) {
|
|
383
332
|
sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
|
|
384
333
|
}
|
|
385
|
-
const attemptIdRef = useRef(
|
|
386
|
-
const userRef = useRef(
|
|
387
|
-
attemptIdRef.current =
|
|
388
|
-
userRef.current =
|
|
389
|
-
const courseIdRef = useRef(
|
|
390
|
-
courseIdRef.current =
|
|
391
|
-
const lxpackBridgeModeRef = useRef(
|
|
392
|
-
lxpackBridgeModeRef.current =
|
|
393
|
-
const pluginHost = useMemo(() => createReactPluginHost(
|
|
334
|
+
const attemptIdRef = useRef(normalizedConfig.session?.attemptId);
|
|
335
|
+
const userRef = useRef(normalizedConfig.session?.user);
|
|
336
|
+
attemptIdRef.current = normalizedConfig.session?.attemptId;
|
|
337
|
+
userRef.current = normalizedConfig.session?.user;
|
|
338
|
+
const courseIdRef = useRef(normalizedCourseId);
|
|
339
|
+
courseIdRef.current = normalizedCourseId;
|
|
340
|
+
const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
|
|
341
|
+
lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
|
|
342
|
+
const pluginHost = useMemo(() => createReactPluginHost(normalizedConfig.plugins), [normalizedConfig.plugins]);
|
|
394
343
|
const pluginHostRef = useRef(pluginHost);
|
|
395
344
|
pluginHostRef.current = pluginHost;
|
|
396
345
|
const progressRef = useRef(createProgressController());
|
|
397
346
|
const courseStartedEmittedToSinkRef = useRef(false);
|
|
398
|
-
const prevCourseIdForProgressRef = useRef(
|
|
347
|
+
const prevCourseIdForProgressRef = useRef(normalizedCourseId);
|
|
399
348
|
const pendingCourseIdResetRef = useRef(false);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
349
|
+
const prevUseV2RuntimeRef = useRef(useV2Runtime);
|
|
350
|
+
const xapiCourseStartedSentOnClientRef = useRef(false);
|
|
351
|
+
if (prevUseV2RuntimeRef.current !== useV2Runtime) {
|
|
352
|
+
prevUseV2RuntimeRef.current = useV2Runtime;
|
|
353
|
+
if (useV2Runtime) {
|
|
354
|
+
headlessRef.current = createLessonkitRuntime({
|
|
355
|
+
courseId: normalizedCourseId,
|
|
356
|
+
runtimeVersion: "v2",
|
|
357
|
+
session: normalizedConfig.session
|
|
358
|
+
});
|
|
359
|
+
progressRef.current = headlessRef.current.progress;
|
|
360
|
+
} else {
|
|
361
|
+
headlessRef.current = null;
|
|
362
|
+
progressRef.current = createProgressController();
|
|
363
|
+
}
|
|
364
|
+
pendingCourseIdResetRef.current = true;
|
|
365
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
366
|
+
} else if (useV2Runtime && !headlessRef.current) {
|
|
367
|
+
headlessRef.current = createLessonkitRuntime({
|
|
368
|
+
courseId: normalizedCourseId,
|
|
369
|
+
runtimeVersion: "v2",
|
|
370
|
+
session: normalizedConfig.session
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
374
|
+
prevCourseIdForProgressRef.current = normalizedCourseId;
|
|
375
|
+
if (useV2Runtime && headlessRef.current) {
|
|
376
|
+
headlessRef.current.resetForCourseChange(normalizedCourseId);
|
|
377
|
+
progressRef.current = headlessRef.current.progress;
|
|
378
|
+
} else {
|
|
379
|
+
progressRef.current = createProgressController();
|
|
380
|
+
}
|
|
403
381
|
pendingCourseIdResetRef.current = true;
|
|
404
382
|
courseStartedEmittedToSinkRef.current = false;
|
|
405
383
|
}
|
|
384
|
+
if (useV2Runtime && headlessRef.current) {
|
|
385
|
+
progressRef.current = headlessRef.current.progress;
|
|
386
|
+
}
|
|
406
387
|
const [progress, setProgress] = useState(() => progressRef.current.getState());
|
|
407
388
|
const syncProgress = useCallback(() => {
|
|
408
389
|
setProgress(progressRef.current.getState());
|
|
@@ -412,16 +393,16 @@ function LessonkitProvider(props) {
|
|
|
412
393
|
const xapiQueueRef = useRef(createInMemoryXAPIQueue());
|
|
413
394
|
const xapiRef = useRef(null);
|
|
414
395
|
const [xapi, setXapi] = useState(null);
|
|
415
|
-
const prevXapiCourseIdRef = useRef(
|
|
416
|
-
const xapiEnabled =
|
|
417
|
-
const xapiClient =
|
|
418
|
-
const xapiTransport =
|
|
419
|
-
const courseId =
|
|
420
|
-
const trackingEnabled =
|
|
396
|
+
const prevXapiCourseIdRef = useRef(normalizedCourseId);
|
|
397
|
+
const xapiEnabled = normalizedConfig.xapi?.enabled;
|
|
398
|
+
const xapiClient = normalizedConfig.xapi?.client;
|
|
399
|
+
const xapiTransport = normalizedConfig.xapi?.transport;
|
|
400
|
+
const courseId = normalizedCourseId;
|
|
401
|
+
const trackingEnabled = normalizedConfig.tracking?.enabled;
|
|
421
402
|
useIsoLayoutEffect(() => {
|
|
422
403
|
const courseChanged = prevXapiCourseIdRef.current !== courseId;
|
|
423
404
|
if (courseChanged) {
|
|
424
|
-
if (
|
|
405
|
+
if (normalizedConfig.xapi?.client) {
|
|
425
406
|
const g = globalThis;
|
|
426
407
|
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
|
|
427
408
|
console.warn(
|
|
@@ -432,20 +413,24 @@ function LessonkitProvider(props) {
|
|
|
432
413
|
}
|
|
433
414
|
xapiQueueRef.current = createInMemoryXAPIQueue();
|
|
434
415
|
prevXapiCourseIdRef.current = courseId;
|
|
416
|
+
xapiCourseStartedSentOnClientRef.current = false;
|
|
435
417
|
}
|
|
436
418
|
const prev = xapiRef.current;
|
|
437
|
-
const next = createXapiClientFromConfig(
|
|
419
|
+
const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
|
|
438
420
|
xapiRef.current = next;
|
|
439
421
|
setXapi(next);
|
|
440
|
-
if (next
|
|
422
|
+
if (next) {
|
|
441
423
|
const sessionId = sessionIdRef.current;
|
|
442
424
|
const cid = courseIdRef.current;
|
|
443
|
-
const trackingActive = isTrackingActive(
|
|
425
|
+
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
444
426
|
const alreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
|
|
445
|
-
|
|
427
|
+
const clientChanged = !prev || prev !== next;
|
|
428
|
+
const skipBootstrap = trackingActive && !alreadyStarted;
|
|
429
|
+
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
|
|
430
|
+
if (needsBootstrap) {
|
|
446
431
|
try {
|
|
447
432
|
const statement = telemetryEventToXAPIStatement2(
|
|
448
|
-
|
|
433
|
+
buildTelemetryEvent({
|
|
449
434
|
name: "course_started",
|
|
450
435
|
courseId: cid,
|
|
451
436
|
sessionId,
|
|
@@ -455,7 +440,10 @@ function LessonkitProvider(props) {
|
|
|
455
440
|
);
|
|
456
441
|
if (statement) {
|
|
457
442
|
next.send(statement);
|
|
458
|
-
|
|
443
|
+
if (!alreadyStarted) {
|
|
444
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
445
|
+
}
|
|
446
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
459
447
|
}
|
|
460
448
|
} catch {
|
|
461
449
|
}
|
|
@@ -483,46 +471,53 @@ function LessonkitProvider(props) {
|
|
|
483
471
|
const trackingRef = useRef(createTrackingClient2());
|
|
484
472
|
const trackingClientForUnmountRef = useRef(trackingRef.current);
|
|
485
473
|
const [tracking, setTracking] = useState(() => trackingRef.current);
|
|
486
|
-
const trackingSink =
|
|
487
|
-
const trackingBatchSink =
|
|
488
|
-
const batchEnabled =
|
|
489
|
-
const batchFlushIntervalMs =
|
|
490
|
-
const batchMaxBatchSize =
|
|
474
|
+
const trackingSink = normalizedConfig.tracking?.sink;
|
|
475
|
+
const trackingBatchSink = normalizedConfig.tracking?.batchSink;
|
|
476
|
+
const batchEnabled = normalizedConfig.tracking?.batch?.enabled;
|
|
477
|
+
const batchFlushIntervalMs = normalizedConfig.tracking?.batch?.flushIntervalMs;
|
|
478
|
+
const batchMaxBatchSize = normalizedConfig.tracking?.batch?.maxBatchSize;
|
|
491
479
|
const buildCurrentPluginCtx = useCallback(
|
|
492
480
|
() => buildPluginContext({
|
|
493
481
|
courseId: courseIdRef.current,
|
|
494
482
|
sessionId: sessionIdRef.current,
|
|
495
|
-
attemptId: attemptIdRef.current
|
|
483
|
+
attemptId: attemptIdRef.current,
|
|
484
|
+
user: userRef.current
|
|
496
485
|
}),
|
|
497
486
|
[]
|
|
498
487
|
);
|
|
499
488
|
useIsoLayoutEffect(() => {
|
|
500
489
|
const prev = trackingRef.current;
|
|
501
|
-
const baseSink =
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
|
|
490
|
+
const baseSink = normalizedConfig.tracking?.sink;
|
|
491
|
+
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
492
|
+
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
493
|
+
const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
|
|
494
|
+
const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
|
|
495
|
+
const host = pluginHostRef.current;
|
|
496
|
+
const ctx = buildCurrentPluginCtx();
|
|
497
|
+
const delivered = host.deliverTelemetryBatch(events, ctx);
|
|
498
|
+
const perEventForBatch = [];
|
|
499
|
+
const collector = (event) => {
|
|
500
|
+
perEventForBatch.push(event);
|
|
501
|
+
};
|
|
502
|
+
const composedPerEvent = host.composeTrackingSink(collector, buildCurrentPluginCtx) ?? collector;
|
|
503
|
+
for (const event of delivered) {
|
|
504
|
+
await Promise.resolve(composedPerEvent(event));
|
|
505
|
+
}
|
|
506
|
+
return userBatchSink(perEventForBatch);
|
|
507
|
+
} : userBatchSink;
|
|
513
508
|
const next = createTrackingClientFromConfig({
|
|
514
|
-
tracking: { ...
|
|
509
|
+
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
515
510
|
});
|
|
516
511
|
trackingRef.current = next;
|
|
517
512
|
trackingClientForUnmountRef.current = next;
|
|
518
513
|
setTracking(next);
|
|
519
514
|
const sessionId = sessionIdRef.current;
|
|
520
515
|
const cid = courseIdRef.current;
|
|
521
|
-
const trackingActive = isTrackingActive(
|
|
516
|
+
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
522
517
|
if (!trackingActive) {
|
|
523
518
|
courseStartedEmittedToSinkRef.current = false;
|
|
524
|
-
} else if (!courseStartedEmittedToSinkRef.current
|
|
525
|
-
const emitted =
|
|
519
|
+
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
520
|
+
const emitted = emitPendingCourseStarted({
|
|
526
521
|
pluginHost: pluginHostRef.current,
|
|
527
522
|
tracking: next,
|
|
528
523
|
xapi: xapiRef.current,
|
|
@@ -531,8 +526,12 @@ function LessonkitProvider(props) {
|
|
|
531
526
|
courseId: cid,
|
|
532
527
|
attemptId: attemptIdRef.current,
|
|
533
528
|
user: userRef.current,
|
|
534
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
529
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
530
|
+
extraSinks: extraSinksRef.current
|
|
535
531
|
});
|
|
532
|
+
if (emitted) {
|
|
533
|
+
markCourseStartedEmittedToTracking(defaultStorage, sessionId, cid);
|
|
534
|
+
}
|
|
536
535
|
courseStartedEmittedToSinkRef.current = emitted;
|
|
537
536
|
} else if (trackingActive) {
|
|
538
537
|
courseStartedEmittedToSinkRef.current = true;
|
|
@@ -549,8 +548,8 @@ function LessonkitProvider(props) {
|
|
|
549
548
|
batchEnabled,
|
|
550
549
|
batchFlushIntervalMs,
|
|
551
550
|
batchMaxBatchSize,
|
|
552
|
-
|
|
553
|
-
|
|
551
|
+
normalizedConfig.plugins,
|
|
552
|
+
normalizedCourseId,
|
|
554
553
|
buildCurrentPluginCtx
|
|
555
554
|
]);
|
|
556
555
|
const emitWithBridge = useCallback((trackingClient, event) => {
|
|
@@ -562,14 +561,32 @@ function LessonkitProvider(props) {
|
|
|
562
561
|
pluginCtx: buildPluginContext({
|
|
563
562
|
courseId: courseIdRef.current,
|
|
564
563
|
sessionId: sessionIdRef.current,
|
|
565
|
-
attemptId: attemptIdRef.current
|
|
564
|
+
attemptId: attemptIdRef.current,
|
|
565
|
+
user: userRef.current
|
|
566
566
|
}),
|
|
567
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
567
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
568
|
+
extraSinks: extraSinksRef.current
|
|
568
569
|
});
|
|
569
570
|
}, []);
|
|
571
|
+
const emitLifecycleEvent = useCallback(
|
|
572
|
+
(name, data, lessonId) => {
|
|
573
|
+
const event = tryBuildTelemetryEvent({
|
|
574
|
+
name,
|
|
575
|
+
courseId: courseIdRef.current,
|
|
576
|
+
lessonId: lessonId ?? activeLessonIdRef.current,
|
|
577
|
+
sessionId: sessionIdRef.current,
|
|
578
|
+
attemptId: attemptIdRef.current,
|
|
579
|
+
user: userRef.current,
|
|
580
|
+
data
|
|
581
|
+
});
|
|
582
|
+
if (!event) return;
|
|
583
|
+
emitWithBridge(trackingRef.current, event);
|
|
584
|
+
},
|
|
585
|
+
[emitWithBridge]
|
|
586
|
+
);
|
|
570
587
|
const track = useCallback(
|
|
571
588
|
(name, data, opts) => {
|
|
572
|
-
const event =
|
|
589
|
+
const event = tryBuildTelemetryEvent({
|
|
573
590
|
name,
|
|
574
591
|
courseId: courseIdRef.current,
|
|
575
592
|
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
@@ -587,7 +604,7 @@ function LessonkitProvider(props) {
|
|
|
587
604
|
if (!pendingCourseIdResetRef.current) return;
|
|
588
605
|
pendingCourseIdResetRef.current = false;
|
|
589
606
|
syncProgress();
|
|
590
|
-
if (!isTrackingActive(
|
|
607
|
+
if (!isTrackingActive(normalizedConfig.tracking)) return;
|
|
591
608
|
const sessionId = sessionIdRef.current;
|
|
592
609
|
const cid = courseIdRef.current;
|
|
593
610
|
void (async () => {
|
|
@@ -595,8 +612,8 @@ function LessonkitProvider(props) {
|
|
|
595
612
|
await trackingRef.current?.flush?.();
|
|
596
613
|
} catch {
|
|
597
614
|
}
|
|
598
|
-
if (!courseStartedEmittedToSinkRef.current
|
|
599
|
-
const emitted =
|
|
615
|
+
if (!courseStartedEmittedToSinkRef.current) {
|
|
616
|
+
const emitted = emitPendingCourseStarted({
|
|
600
617
|
pluginHost: pluginHostRef.current,
|
|
601
618
|
tracking: trackingRef.current,
|
|
602
619
|
xapi: xapiRef.current,
|
|
@@ -605,12 +622,13 @@ function LessonkitProvider(props) {
|
|
|
605
622
|
courseId: cid,
|
|
606
623
|
attemptId: attemptIdRef.current,
|
|
607
624
|
user: userRef.current,
|
|
608
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
625
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
626
|
+
extraSinks: extraSinksRef.current
|
|
609
627
|
});
|
|
610
628
|
courseStartedEmittedToSinkRef.current = emitted;
|
|
611
629
|
}
|
|
612
630
|
})();
|
|
613
|
-
}, [
|
|
631
|
+
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
|
|
614
632
|
const emitLessonCompleted = useCallback(
|
|
615
633
|
(lessonId, durationMs) => {
|
|
616
634
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -622,20 +640,27 @@ function LessonkitProvider(props) {
|
|
|
622
640
|
);
|
|
623
641
|
const completeLesson = useCallback(
|
|
624
642
|
(lessonId) => {
|
|
643
|
+
if (useV2Runtime && headlessRef.current) {
|
|
644
|
+
headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
|
|
645
|
+
syncProgress();
|
|
646
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
625
649
|
const result = progressRef.current.completeLesson(lessonId, Date.now());
|
|
626
650
|
if (!result.didComplete) return;
|
|
627
651
|
syncProgress();
|
|
628
652
|
emitLessonCompleted(lessonId, result.durationMs);
|
|
629
653
|
void Promise.resolve(trackingRef.current?.flush?.());
|
|
630
654
|
},
|
|
631
|
-
[syncProgress, emitLessonCompleted]
|
|
655
|
+
[syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
632
656
|
);
|
|
633
657
|
useEffect(() => {
|
|
634
658
|
return () => {
|
|
635
659
|
const client = trackingClientForUnmountRef.current;
|
|
660
|
+
const xapi2 = xapiRef.current;
|
|
636
661
|
void (async () => {
|
|
637
662
|
try {
|
|
638
|
-
await
|
|
663
|
+
await xapi2?.flush();
|
|
639
664
|
} catch {
|
|
640
665
|
}
|
|
641
666
|
try {
|
|
@@ -651,8 +676,19 @@ function LessonkitProvider(props) {
|
|
|
651
676
|
}, []);
|
|
652
677
|
const setActiveLesson = useCallback(
|
|
653
678
|
(lessonId) => {
|
|
679
|
+
if (useV2Runtime && headlessRef.current) {
|
|
680
|
+
headlessRef.current.setActiveLesson(lessonId, emitLifecycleEvent);
|
|
681
|
+
syncProgress();
|
|
682
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
654
685
|
const current = progressRef.current.getState();
|
|
655
686
|
if (current.activeLessonId === lessonId) return;
|
|
687
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
688
|
+
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
689
|
+
syncProgress();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
656
692
|
const previous = current.activeLessonId;
|
|
657
693
|
if (previous && previous !== lessonId) {
|
|
658
694
|
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
@@ -665,32 +701,58 @@ function LessonkitProvider(props) {
|
|
|
665
701
|
syncProgress();
|
|
666
702
|
track("lesson_started", { lessonId }, { lessonId });
|
|
667
703
|
},
|
|
668
|
-
[track, syncProgress, emitLessonCompleted]
|
|
704
|
+
[track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
669
705
|
);
|
|
670
706
|
const completeCourse = useCallback(() => {
|
|
707
|
+
if (useV2Runtime && headlessRef.current) {
|
|
708
|
+
headlessRef.current.completeCourse(emitLifecycleEvent);
|
|
709
|
+
syncProgress();
|
|
710
|
+
void trackingRef.current?.flush?.();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const current = progressRef.current.getState();
|
|
714
|
+
if (current.activeLessonId) {
|
|
715
|
+
const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
|
|
716
|
+
if (lessonResult.didComplete) {
|
|
717
|
+
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
671
720
|
const result = progressRef.current.completeCourse();
|
|
672
721
|
if (!result.didComplete) return;
|
|
673
722
|
syncProgress();
|
|
674
723
|
track("course_completed");
|
|
675
724
|
void trackingRef.current?.flush?.();
|
|
676
|
-
}, [track, syncProgress]);
|
|
677
|
-
const sessionUser =
|
|
678
|
-
const
|
|
679
|
-
|
|
725
|
+
}, [track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]);
|
|
726
|
+
const sessionUser = normalizedConfig.session?.user;
|
|
727
|
+
const sessionUserKey = useMemo(
|
|
728
|
+
() => sessionUser ? JSON.stringify(sessionUser) : "",
|
|
729
|
+
[sessionUser]
|
|
730
|
+
);
|
|
731
|
+
const sessionAttemptId = normalizedConfig.session?.attemptId;
|
|
732
|
+
const sessionConfiguredId = normalizedConfig.session?.sessionId;
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
if (useV2Runtime && headlessRef.current) {
|
|
735
|
+
headlessRef.current.updateConfig({
|
|
736
|
+
courseId: normalizedCourseId,
|
|
737
|
+
session: normalizedConfig.session
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}, [useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey, normalizedConfig.session]);
|
|
680
741
|
useEffect(() => {
|
|
681
742
|
if (!pluginHost) return;
|
|
682
743
|
const ctx = buildPluginContext({
|
|
683
744
|
courseId: courseIdRef.current,
|
|
684
745
|
sessionId: sessionIdRef.current,
|
|
685
|
-
attemptId: attemptIdRef.current
|
|
746
|
+
attemptId: attemptIdRef.current,
|
|
747
|
+
user: userRef.current
|
|
686
748
|
});
|
|
687
749
|
pluginHost.setupAll(ctx);
|
|
688
750
|
return () => {
|
|
689
751
|
pluginHost.disposeAll();
|
|
690
752
|
};
|
|
691
|
-
}, [pluginHost,
|
|
753
|
+
}, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
692
754
|
useEffect(() => {
|
|
693
|
-
const nextConfigured =
|
|
755
|
+
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
694
756
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
695
757
|
if (nextConfigured === prevConfigured) return;
|
|
696
758
|
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
@@ -711,10 +773,10 @@ function LessonkitProvider(props) {
|
|
|
711
773
|
migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
|
|
712
774
|
sessionIdRef.current = nextAuto;
|
|
713
775
|
}
|
|
714
|
-
}, [sessionConfiguredId,
|
|
776
|
+
}, [sessionConfiguredId, normalizedCourseId]);
|
|
715
777
|
const runtime = useMemo(
|
|
716
778
|
() => ({
|
|
717
|
-
config,
|
|
779
|
+
config: normalizedConfig,
|
|
718
780
|
tracking,
|
|
719
781
|
xapi,
|
|
720
782
|
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
@@ -726,7 +788,7 @@ function LessonkitProvider(props) {
|
|
|
726
788
|
plugins: pluginHost
|
|
727
789
|
}),
|
|
728
790
|
[
|
|
729
|
-
|
|
791
|
+
normalizedConfig,
|
|
730
792
|
tracking,
|
|
731
793
|
xapi,
|
|
732
794
|
progress,
|
|
@@ -740,6 +802,14 @@ function LessonkitProvider(props) {
|
|
|
740
802
|
sessionConfiguredId
|
|
741
803
|
]
|
|
742
804
|
);
|
|
805
|
+
return runtime;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/context.tsx
|
|
809
|
+
import { jsx } from "react/jsx-runtime";
|
|
810
|
+
var LessonkitContext = createContext(null);
|
|
811
|
+
function LessonkitProvider(props) {
|
|
812
|
+
const runtime = useLessonkitProviderRuntime(props.config);
|
|
743
813
|
return /* @__PURE__ */ jsx(LessonkitContext.Provider, { value: runtime, children: props.children });
|
|
744
814
|
}
|
|
745
815
|
|
|
@@ -762,46 +832,74 @@ function useCompletion() {
|
|
|
762
832
|
const { completeLesson, completeCourse } = useLessonkit();
|
|
763
833
|
return useMemo2(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
764
834
|
}
|
|
765
|
-
function useQuizState() {
|
|
835
|
+
function useQuizState(enclosingLessonId) {
|
|
766
836
|
const { track } = useLessonkit();
|
|
837
|
+
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
767
838
|
return useMemo2(
|
|
768
839
|
() => ({
|
|
769
840
|
answer: (opts) => {
|
|
770
|
-
track("quiz_answered", opts);
|
|
841
|
+
track("quiz_answered", opts, trackOpts);
|
|
771
842
|
},
|
|
772
843
|
complete: (opts) => {
|
|
773
|
-
track("quiz_completed", opts);
|
|
844
|
+
track("quiz_completed", opts, trackOpts);
|
|
774
845
|
}
|
|
775
846
|
}),
|
|
776
|
-
[track]
|
|
847
|
+
[track, enclosingLessonId]
|
|
777
848
|
);
|
|
778
849
|
}
|
|
779
850
|
|
|
851
|
+
// src/lessonContext.tsx
|
|
852
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
853
|
+
var LessonContext = createContext2(void 0);
|
|
854
|
+
function useEnclosingLessonId() {
|
|
855
|
+
return useContext2(LessonContext);
|
|
856
|
+
}
|
|
857
|
+
|
|
780
858
|
// src/runtime/validateComponentId.ts
|
|
781
|
-
import {
|
|
782
|
-
|
|
783
|
-
function isDevEnvironment2() {
|
|
859
|
+
import { assertValidId as assertValidId2 } from "@lessonkit/core";
|
|
860
|
+
function isDevEnvironment3() {
|
|
784
861
|
const g = globalThis;
|
|
785
862
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
786
863
|
}
|
|
787
|
-
function
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
864
|
+
function normalizeComponentId(id, path) {
|
|
865
|
+
return assertValidId2(id, path);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/runtime/lessonMountRegistry.ts
|
|
869
|
+
var mountCounts = /* @__PURE__ */ new Map();
|
|
870
|
+
var warnedConcurrentLessons = false;
|
|
871
|
+
function registerLessonMount(lessonId) {
|
|
872
|
+
if (isDevEnvironment3() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
|
|
873
|
+
warnedConcurrentLessons = true;
|
|
874
|
+
console.warn(
|
|
875
|
+
"[lessonkit] Multiple <Lesson> components are mounted; only one should be active at a time. Set autoCompleteOnUnmount={false} on routed lessons or unmount the previous lesson before showing the next."
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
mountCounts.set(lessonId, (mountCounts.get(lessonId) ?? 0) + 1);
|
|
879
|
+
return () => {
|
|
880
|
+
const next = (mountCounts.get(lessonId) ?? 1) - 1;
|
|
881
|
+
if (next <= 0) {
|
|
882
|
+
mountCounts.delete(lessonId);
|
|
883
|
+
} else {
|
|
884
|
+
mountCounts.set(lessonId, next);
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function getLessonMountCount(lessonId) {
|
|
889
|
+
return mountCounts.get(lessonId) ?? 0;
|
|
796
890
|
}
|
|
797
891
|
|
|
798
892
|
// src/components.tsx
|
|
799
893
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
894
|
+
var warnedQuizOutsideLesson = false;
|
|
895
|
+
function resetQuizWarningsForTests() {
|
|
896
|
+
warnedQuizOutsideLesson = false;
|
|
897
|
+
}
|
|
800
898
|
function Course(props) {
|
|
801
|
-
|
|
899
|
+
const courseId = useMemo3(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
802
900
|
const providerConfig = useMemo3(
|
|
803
|
-
() => ({ ...props.config, courseId
|
|
804
|
-
[props.config,
|
|
901
|
+
() => ({ ...props.config, courseId }),
|
|
902
|
+
[props.config, courseId]
|
|
805
903
|
);
|
|
806
904
|
return /* @__PURE__ */ jsx2(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
|
|
807
905
|
/* @__PURE__ */ jsx2("h1", { children: props.title }),
|
|
@@ -809,41 +907,64 @@ function Course(props) {
|
|
|
809
907
|
] }) });
|
|
810
908
|
}
|
|
811
909
|
function Lesson(props) {
|
|
812
|
-
|
|
910
|
+
const lessonId = useMemo3(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
911
|
+
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
813
912
|
const { setActiveLesson, config } = useLessonkit();
|
|
814
913
|
const { completeLesson } = useCompletion();
|
|
815
|
-
const id = props.lessonId;
|
|
816
914
|
const lessonMountGenerationRef = useRef2(0);
|
|
817
915
|
useEffect2(() => {
|
|
916
|
+
const unregister = registerLessonMount(lessonId);
|
|
818
917
|
const generation = ++lessonMountGenerationRef.current;
|
|
819
|
-
setActiveLesson(
|
|
918
|
+
setActiveLesson(lessonId);
|
|
820
919
|
return () => {
|
|
821
|
-
|
|
920
|
+
unregister();
|
|
921
|
+
if (getLessonMountCount(lessonId) > 0) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (!autoComplete) return;
|
|
822
925
|
queueMicrotask(() => {
|
|
823
926
|
if (lessonMountGenerationRef.current !== generation) return;
|
|
824
927
|
completeLesson(lessonId);
|
|
825
928
|
});
|
|
826
929
|
};
|
|
827
|
-
}, [
|
|
828
|
-
return /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
|
|
930
|
+
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
931
|
+
return /* @__PURE__ */ jsx2(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
|
|
829
932
|
/* @__PURE__ */ jsx2("h2", { children: props.title }),
|
|
830
933
|
/* @__PURE__ */ jsx2("div", { children: props.children })
|
|
831
|
-
] });
|
|
934
|
+
] }) });
|
|
832
935
|
}
|
|
833
936
|
function Scenario(props) {
|
|
834
|
-
|
|
835
|
-
|
|
937
|
+
const blockId = useMemo3(
|
|
938
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
939
|
+
[props.blockId]
|
|
940
|
+
);
|
|
941
|
+
return /* @__PURE__ */ jsx2("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
836
942
|
}
|
|
837
943
|
function Reflection(props) {
|
|
838
|
-
|
|
944
|
+
const blockId = useMemo3(
|
|
945
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
946
|
+
[props.blockId]
|
|
947
|
+
);
|
|
839
948
|
const promptId = useId();
|
|
840
|
-
|
|
949
|
+
const hintId = useId();
|
|
950
|
+
const [internalValue, setInternalValue] = useState2("");
|
|
951
|
+
const isControlled = props.value !== void 0;
|
|
952
|
+
const value = isControlled ? props.value : internalValue;
|
|
953
|
+
const handleChange = (event) => {
|
|
954
|
+
if (!isControlled) setInternalValue(event.target.value);
|
|
955
|
+
props.onChange?.(event.target.value);
|
|
956
|
+
};
|
|
957
|
+
return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
841
958
|
props.prompt ? /* @__PURE__ */ jsx2("p", { id: promptId, children: props.prompt }) : null,
|
|
959
|
+
props.hint ? /* @__PURE__ */ jsx2("p", { id: hintId, style: visuallyHiddenStyle, children: props.hint }) : null,
|
|
842
960
|
props.children,
|
|
843
961
|
/* @__PURE__ */ jsx2(
|
|
844
962
|
"textarea",
|
|
845
963
|
{
|
|
964
|
+
value,
|
|
965
|
+
onChange: handleChange,
|
|
846
966
|
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
967
|
+
"aria-describedby": props.hint ? hintId : void 0,
|
|
847
968
|
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
848
969
|
}
|
|
849
970
|
)
|
|
@@ -856,23 +977,41 @@ function KnowledgeCheck(props) {
|
|
|
856
977
|
checkId: props.checkId,
|
|
857
978
|
question: props.question,
|
|
858
979
|
choices: props.choices,
|
|
859
|
-
answer: props.answer
|
|
980
|
+
answer: props.answer,
|
|
981
|
+
passingScore: props.passingScore
|
|
860
982
|
}
|
|
861
983
|
);
|
|
862
984
|
}
|
|
863
985
|
function Quiz(props) {
|
|
864
|
-
|
|
865
|
-
const
|
|
866
|
-
const
|
|
986
|
+
const checkId = useMemo3(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
987
|
+
const enclosingLessonId = useEnclosingLessonId();
|
|
988
|
+
const missingLesson = enclosingLessonId === void 0;
|
|
989
|
+
useEffect2(() => {
|
|
990
|
+
if (!missingLesson || isDevEnvironment3()) return;
|
|
991
|
+
if (!warnedQuizOutsideLesson) {
|
|
992
|
+
warnedQuizOutsideLesson = true;
|
|
993
|
+
console.error(
|
|
994
|
+
"[lessonkit] <Quiz> must be wrapped in <Lesson>; quiz telemetry will not be emitted."
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
}, [missingLesson]);
|
|
998
|
+
if (missingLesson && isDevEnvironment3()) {
|
|
999
|
+
throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
|
|
1000
|
+
}
|
|
1001
|
+
const quiz = useQuizState(enclosingLessonId);
|
|
1002
|
+
const { plugins, config, session } = useLessonkit();
|
|
867
1003
|
const [selected, setSelected] = useState2(null);
|
|
868
1004
|
const [selectionCorrect, setSelectionCorrect] = useState2(null);
|
|
1005
|
+
const [quizPassed, setQuizPassed] = useState2(false);
|
|
869
1006
|
const completedRef = useRef2(false);
|
|
870
1007
|
const questionId = useId();
|
|
1008
|
+
const choicesKey = props.choices.join("\0");
|
|
871
1009
|
useEffect2(() => {
|
|
872
1010
|
completedRef.current = false;
|
|
1011
|
+
setQuizPassed(false);
|
|
873
1012
|
setSelected(null);
|
|
874
1013
|
setSelectionCorrect(null);
|
|
875
|
-
}, [
|
|
1014
|
+
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
|
|
876
1015
|
const isChoiceCorrect = (choice, custom) => {
|
|
877
1016
|
if (!custom) return choice === props.answer;
|
|
878
1017
|
if (custom.passed !== void 0) return custom.passed;
|
|
@@ -881,7 +1020,11 @@ function Quiz(props) {
|
|
|
881
1020
|
}
|
|
882
1021
|
return choice === props.answer;
|
|
883
1022
|
};
|
|
884
|
-
|
|
1023
|
+
if (missingLesson) {
|
|
1024
|
+
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ jsx2("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1025
|
+
}
|
|
1026
|
+
const passed = quizPassed;
|
|
1027
|
+
return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
885
1028
|
/* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
|
|
886
1029
|
/* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
|
|
887
1030
|
/* @__PURE__ */ jsx2("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
@@ -893,17 +1036,21 @@ function Quiz(props) {
|
|
|
893
1036
|
name: questionId,
|
|
894
1037
|
value: c,
|
|
895
1038
|
checked: selected === c,
|
|
1039
|
+
disabled: passed,
|
|
1040
|
+
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
896
1041
|
onChange: () => {
|
|
1042
|
+
if (passed) return;
|
|
897
1043
|
setSelected(c);
|
|
898
1044
|
const pluginCtx = buildPluginContext({
|
|
899
1045
|
courseId: config.courseId,
|
|
900
1046
|
sessionId: session.sessionId,
|
|
901
|
-
attemptId: session.attemptId
|
|
1047
|
+
attemptId: session.attemptId,
|
|
1048
|
+
user: session.user
|
|
902
1049
|
});
|
|
903
1050
|
const custom = plugins?.scoreAssessment(
|
|
904
1051
|
{
|
|
905
|
-
checkId
|
|
906
|
-
lessonId:
|
|
1052
|
+
checkId,
|
|
1053
|
+
lessonId: enclosingLessonId,
|
|
907
1054
|
response: c
|
|
908
1055
|
},
|
|
909
1056
|
pluginCtx
|
|
@@ -911,18 +1058,20 @@ function Quiz(props) {
|
|
|
911
1058
|
const correct = isChoiceCorrect(c, custom);
|
|
912
1059
|
setSelectionCorrect(correct);
|
|
913
1060
|
quiz.answer({
|
|
914
|
-
checkId
|
|
1061
|
+
checkId,
|
|
915
1062
|
question: props.question,
|
|
916
1063
|
choice: c,
|
|
917
1064
|
correct
|
|
918
1065
|
});
|
|
919
1066
|
if (correct && !completedRef.current) {
|
|
920
1067
|
completedRef.current = true;
|
|
1068
|
+
setQuizPassed(true);
|
|
1069
|
+
const maxScore = custom?.maxScore ?? 1;
|
|
921
1070
|
quiz.complete({
|
|
922
|
-
checkId
|
|
1071
|
+
checkId,
|
|
923
1072
|
score: custom?.score ?? 1,
|
|
924
|
-
maxScore
|
|
925
|
-
passingScore:
|
|
1073
|
+
maxScore,
|
|
1074
|
+
passingScore: props.passingScore ?? maxScore
|
|
926
1075
|
});
|
|
927
1076
|
}
|
|
928
1077
|
}
|
|
@@ -934,23 +1083,51 @@ function Quiz(props) {
|
|
|
934
1083
|
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
935
1084
|
] });
|
|
936
1085
|
}
|
|
937
|
-
function ProgressTracker() {
|
|
1086
|
+
function ProgressTracker(props) {
|
|
938
1087
|
const { progress } = useLessonkit();
|
|
939
1088
|
const completed = progress.completedLessonIds.size;
|
|
940
|
-
|
|
1089
|
+
if (props.totalLessons != null) {
|
|
1090
|
+
const total = props.totalLessons;
|
|
1091
|
+
const displayed = Math.min(completed, total);
|
|
1092
|
+
return /* @__PURE__ */ jsx2("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx2(
|
|
1093
|
+
"div",
|
|
1094
|
+
{
|
|
1095
|
+
role: "progressbar",
|
|
1096
|
+
"aria-valuemin": 0,
|
|
1097
|
+
"aria-valuemax": total,
|
|
1098
|
+
"aria-valuenow": displayed,
|
|
1099
|
+
"aria-label": "Lessons completed",
|
|
1100
|
+
children: /* @__PURE__ */ jsxs("p", { children: [
|
|
1101
|
+
"Lessons completed: ",
|
|
1102
|
+
displayed,
|
|
1103
|
+
" of ",
|
|
1104
|
+
total
|
|
1105
|
+
] })
|
|
1106
|
+
}
|
|
1107
|
+
) });
|
|
1108
|
+
}
|
|
1109
|
+
return /* @__PURE__ */ jsx2("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs("p", { children: [
|
|
941
1110
|
"Lessons completed: ",
|
|
942
1111
|
completed
|
|
943
1112
|
] }) });
|
|
944
1113
|
}
|
|
945
1114
|
|
|
946
1115
|
// src/index.tsx
|
|
947
|
-
import {
|
|
1116
|
+
import {
|
|
1117
|
+
buildTelemetryEvent as buildTelemetryEvent2,
|
|
1118
|
+
createLessonkitRuntime as createLessonkitRuntime2,
|
|
1119
|
+
createPluginRegistry as createPluginRegistry2,
|
|
1120
|
+
createTelemetryPipeline as createTelemetryPipeline2,
|
|
1121
|
+
defineAssessmentPlugin,
|
|
1122
|
+
defineLifecyclePlugin,
|
|
1123
|
+
defineTelemetryPlugin
|
|
1124
|
+
} from "@lessonkit/core";
|
|
948
1125
|
|
|
949
1126
|
// src/theme/ThemeProvider.tsx
|
|
950
1127
|
import React3, {
|
|
951
|
-
createContext as
|
|
1128
|
+
createContext as createContext3,
|
|
952
1129
|
useCallback as useCallback2,
|
|
953
|
-
useContext as
|
|
1130
|
+
useContext as useContext3,
|
|
954
1131
|
useLayoutEffect as useLayoutEffect2,
|
|
955
1132
|
useMemo as useMemo4,
|
|
956
1133
|
useRef as useRef3,
|
|
@@ -982,7 +1159,7 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
982
1159
|
|
|
983
1160
|
// src/theme/ThemeProvider.tsx
|
|
984
1161
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
985
|
-
var ThemeContext =
|
|
1162
|
+
var ThemeContext = createContext3(null);
|
|
986
1163
|
var useIsoLayoutEffect2 = typeof window !== "undefined" ? useLayoutEffect2 : React3.useEffect;
|
|
987
1164
|
function getSystemMode() {
|
|
988
1165
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -1062,7 +1239,7 @@ function ThemeProvider(props) {
|
|
|
1062
1239
|
return /* @__PURE__ */ jsx3(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx3("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
1063
1240
|
}
|
|
1064
1241
|
function useTheme() {
|
|
1065
|
-
const ctx =
|
|
1242
|
+
const ctx = useContext3(ThemeContext);
|
|
1066
1243
|
if (!ctx) {
|
|
1067
1244
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
1068
1245
|
}
|
|
@@ -1109,6 +1286,12 @@ var BLOCK_CATALOG = [
|
|
|
1109
1286
|
props: [
|
|
1110
1287
|
{ name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
|
|
1111
1288
|
{ name: "lessonId", type: "LessonId", required: true, description: "Stable lesson identifier for telemetry and packaging." },
|
|
1289
|
+
{
|
|
1290
|
+
name: "autoCompleteOnUnmount",
|
|
1291
|
+
type: "boolean",
|
|
1292
|
+
required: false,
|
|
1293
|
+
description: "When false, unmount does not emit lesson_completed (default true)."
|
|
1294
|
+
},
|
|
1112
1295
|
{ name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
|
|
1113
1296
|
],
|
|
1114
1297
|
requiredIds: ["lessonId"],
|
|
@@ -1161,6 +1344,9 @@ var BLOCK_CATALOG = [
|
|
|
1161
1344
|
props: [
|
|
1162
1345
|
{ name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
|
|
1163
1346
|
{ name: "prompt", type: "string", required: false, description: "Reflection question or instruction." },
|
|
1347
|
+
{ name: "hint", type: "string", required: false, description: "Optional hint linked via aria-describedby." },
|
|
1348
|
+
{ name: "value", type: "string", required: false, description: "Controlled textarea value." },
|
|
1349
|
+
{ name: "onChange", type: "(value: string) => void", required: false, description: "Called when the learner edits the textarea." },
|
|
1164
1350
|
{ name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
|
|
1165
1351
|
],
|
|
1166
1352
|
requiredIds: [],
|
|
@@ -1179,6 +1365,7 @@ var BLOCK_CATALOG = [
|
|
|
1179
1365
|
},
|
|
1180
1366
|
telemetry: {
|
|
1181
1367
|
emits: [],
|
|
1368
|
+
requiresActiveLesson: true,
|
|
1182
1369
|
manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
|
|
1183
1370
|
}
|
|
1184
1371
|
},
|
|
@@ -1216,7 +1403,14 @@ var BLOCK_CATALOG = [
|
|
|
1216
1403
|
type: "ProgressTracker",
|
|
1217
1404
|
category: "chrome",
|
|
1218
1405
|
description: "Displays count of completed lessons from runtime progress state.",
|
|
1219
|
-
props: [
|
|
1406
|
+
props: [
|
|
1407
|
+
{
|
|
1408
|
+
name: "totalLessons",
|
|
1409
|
+
type: "number",
|
|
1410
|
+
required: false,
|
|
1411
|
+
description: "When set, renders role=progressbar with aria-valuenow/max."
|
|
1412
|
+
}
|
|
1413
|
+
],
|
|
1220
1414
|
requiredIds: [],
|
|
1221
1415
|
parentConstraints: ["Course"],
|
|
1222
1416
|
a11y: {
|
|
@@ -1268,9 +1462,15 @@ export {
|
|
|
1268
1462
|
ThemeProvider,
|
|
1269
1463
|
blockCatalogVersion,
|
|
1270
1464
|
buildBlockCatalog,
|
|
1271
|
-
|
|
1272
|
-
|
|
1465
|
+
buildTelemetryEvent2 as buildTelemetryEvent,
|
|
1466
|
+
createLessonkitRuntime2 as createLessonkitRuntime,
|
|
1467
|
+
createPluginRegistry2 as createPluginRegistry,
|
|
1468
|
+
createTelemetryPipeline2 as createTelemetryPipeline,
|
|
1469
|
+
defineAssessmentPlugin,
|
|
1470
|
+
defineLifecyclePlugin,
|
|
1471
|
+
defineTelemetryPlugin,
|
|
1273
1472
|
getBlockCatalogEntry,
|
|
1473
|
+
resetQuizWarningsForTests,
|
|
1274
1474
|
useCompletion,
|
|
1275
1475
|
useLessonkit,
|
|
1276
1476
|
useProgress,
|