@lessonkit/react 0.9.3 → 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 +585 -410
- package/dist/index.d.cts +21 -14
- package/dist/index.d.ts +21 -14
- package/dist/index.js +573 -379
- 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,35 +230,159 @@ 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
|
+
}
|
|
403
364
|
pendingCourseIdResetRef.current = true;
|
|
404
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
|
+
}
|
|
381
|
+
pendingCourseIdResetRef.current = true;
|
|
382
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
383
|
+
}
|
|
384
|
+
if (useV2Runtime && headlessRef.current) {
|
|
385
|
+
progressRef.current = headlessRef.current.progress;
|
|
405
386
|
}
|
|
406
387
|
const [progress, setProgress] = useState(() => progressRef.current.getState());
|
|
407
388
|
const syncProgress = useCallback(() => {
|
|
@@ -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,43 +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 =
|
|
490
|
+
const baseSink = normalizedConfig.tracking?.sink;
|
|
491
|
+
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
492
|
+
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
502
493
|
const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
|
|
503
|
-
const batchSink = pluginHostRef.current &&
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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;
|
|
510
508
|
const next = createTrackingClientFromConfig({
|
|
511
|
-
tracking: { ...
|
|
509
|
+
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
512
510
|
});
|
|
513
511
|
trackingRef.current = next;
|
|
514
512
|
trackingClientForUnmountRef.current = next;
|
|
515
513
|
setTracking(next);
|
|
516
514
|
const sessionId = sessionIdRef.current;
|
|
517
515
|
const cid = courseIdRef.current;
|
|
518
|
-
const trackingActive = isTrackingActive(
|
|
516
|
+
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
519
517
|
if (!trackingActive) {
|
|
520
518
|
courseStartedEmittedToSinkRef.current = false;
|
|
521
|
-
} else if (!courseStartedEmittedToSinkRef.current
|
|
522
|
-
const emitted =
|
|
519
|
+
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
520
|
+
const emitted = emitPendingCourseStarted({
|
|
523
521
|
pluginHost: pluginHostRef.current,
|
|
524
522
|
tracking: next,
|
|
525
523
|
xapi: xapiRef.current,
|
|
@@ -528,8 +526,12 @@ function LessonkitProvider(props) {
|
|
|
528
526
|
courseId: cid,
|
|
529
527
|
attemptId: attemptIdRef.current,
|
|
530
528
|
user: userRef.current,
|
|
531
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
529
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
530
|
+
extraSinks: extraSinksRef.current
|
|
532
531
|
});
|
|
532
|
+
if (emitted) {
|
|
533
|
+
markCourseStartedEmittedToTracking(defaultStorage, sessionId, cid);
|
|
534
|
+
}
|
|
533
535
|
courseStartedEmittedToSinkRef.current = emitted;
|
|
534
536
|
} else if (trackingActive) {
|
|
535
537
|
courseStartedEmittedToSinkRef.current = true;
|
|
@@ -546,8 +548,8 @@ function LessonkitProvider(props) {
|
|
|
546
548
|
batchEnabled,
|
|
547
549
|
batchFlushIntervalMs,
|
|
548
550
|
batchMaxBatchSize,
|
|
549
|
-
|
|
550
|
-
|
|
551
|
+
normalizedConfig.plugins,
|
|
552
|
+
normalizedCourseId,
|
|
551
553
|
buildCurrentPluginCtx
|
|
552
554
|
]);
|
|
553
555
|
const emitWithBridge = useCallback((trackingClient, event) => {
|
|
@@ -559,14 +561,32 @@ function LessonkitProvider(props) {
|
|
|
559
561
|
pluginCtx: buildPluginContext({
|
|
560
562
|
courseId: courseIdRef.current,
|
|
561
563
|
sessionId: sessionIdRef.current,
|
|
562
|
-
attemptId: attemptIdRef.current
|
|
564
|
+
attemptId: attemptIdRef.current,
|
|
565
|
+
user: userRef.current
|
|
563
566
|
}),
|
|
564
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
567
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
568
|
+
extraSinks: extraSinksRef.current
|
|
565
569
|
});
|
|
566
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
|
+
);
|
|
567
587
|
const track = useCallback(
|
|
568
588
|
(name, data, opts) => {
|
|
569
|
-
const event =
|
|
589
|
+
const event = tryBuildTelemetryEvent({
|
|
570
590
|
name,
|
|
571
591
|
courseId: courseIdRef.current,
|
|
572
592
|
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
@@ -584,7 +604,7 @@ function LessonkitProvider(props) {
|
|
|
584
604
|
if (!pendingCourseIdResetRef.current) return;
|
|
585
605
|
pendingCourseIdResetRef.current = false;
|
|
586
606
|
syncProgress();
|
|
587
|
-
if (!isTrackingActive(
|
|
607
|
+
if (!isTrackingActive(normalizedConfig.tracking)) return;
|
|
588
608
|
const sessionId = sessionIdRef.current;
|
|
589
609
|
const cid = courseIdRef.current;
|
|
590
610
|
void (async () => {
|
|
@@ -592,8 +612,8 @@ function LessonkitProvider(props) {
|
|
|
592
612
|
await trackingRef.current?.flush?.();
|
|
593
613
|
} catch {
|
|
594
614
|
}
|
|
595
|
-
if (!courseStartedEmittedToSinkRef.current
|
|
596
|
-
const emitted =
|
|
615
|
+
if (!courseStartedEmittedToSinkRef.current) {
|
|
616
|
+
const emitted = emitPendingCourseStarted({
|
|
597
617
|
pluginHost: pluginHostRef.current,
|
|
598
618
|
tracking: trackingRef.current,
|
|
599
619
|
xapi: xapiRef.current,
|
|
@@ -602,12 +622,13 @@ function LessonkitProvider(props) {
|
|
|
602
622
|
courseId: cid,
|
|
603
623
|
attemptId: attemptIdRef.current,
|
|
604
624
|
user: userRef.current,
|
|
605
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
625
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
626
|
+
extraSinks: extraSinksRef.current
|
|
606
627
|
});
|
|
607
628
|
courseStartedEmittedToSinkRef.current = emitted;
|
|
608
629
|
}
|
|
609
630
|
})();
|
|
610
|
-
}, [
|
|
631
|
+
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
|
|
611
632
|
const emitLessonCompleted = useCallback(
|
|
612
633
|
(lessonId, durationMs) => {
|
|
613
634
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -619,13 +640,19 @@ function LessonkitProvider(props) {
|
|
|
619
640
|
);
|
|
620
641
|
const completeLesson = useCallback(
|
|
621
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
|
+
}
|
|
622
649
|
const result = progressRef.current.completeLesson(lessonId, Date.now());
|
|
623
650
|
if (!result.didComplete) return;
|
|
624
651
|
syncProgress();
|
|
625
652
|
emitLessonCompleted(lessonId, result.durationMs);
|
|
626
653
|
void Promise.resolve(trackingRef.current?.flush?.());
|
|
627
654
|
},
|
|
628
|
-
[syncProgress, emitLessonCompleted]
|
|
655
|
+
[syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
629
656
|
);
|
|
630
657
|
useEffect(() => {
|
|
631
658
|
return () => {
|
|
@@ -649,8 +676,19 @@ function LessonkitProvider(props) {
|
|
|
649
676
|
}, []);
|
|
650
677
|
const setActiveLesson = useCallback(
|
|
651
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
|
+
}
|
|
652
685
|
const current = progressRef.current.getState();
|
|
653
686
|
if (current.activeLessonId === lessonId) return;
|
|
687
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
688
|
+
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
689
|
+
syncProgress();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
654
692
|
const previous = current.activeLessonId;
|
|
655
693
|
if (previous && previous !== lessonId) {
|
|
656
694
|
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
@@ -663,9 +701,15 @@ function LessonkitProvider(props) {
|
|
|
663
701
|
syncProgress();
|
|
664
702
|
track("lesson_started", { lessonId }, { lessonId });
|
|
665
703
|
},
|
|
666
|
-
[track, syncProgress, emitLessonCompleted]
|
|
704
|
+
[track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
667
705
|
);
|
|
668
706
|
const completeCourse = useCallback(() => {
|
|
707
|
+
if (useV2Runtime && headlessRef.current) {
|
|
708
|
+
headlessRef.current.completeCourse(emitLifecycleEvent);
|
|
709
|
+
syncProgress();
|
|
710
|
+
void trackingRef.current?.flush?.();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
669
713
|
const current = progressRef.current.getState();
|
|
670
714
|
if (current.activeLessonId) {
|
|
671
715
|
const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
|
|
@@ -678,24 +722,37 @@ function LessonkitProvider(props) {
|
|
|
678
722
|
syncProgress();
|
|
679
723
|
track("course_completed");
|
|
680
724
|
void trackingRef.current?.flush?.();
|
|
681
|
-
}, [track, syncProgress, emitLessonCompleted]);
|
|
682
|
-
const sessionUser =
|
|
683
|
-
const
|
|
684
|
-
|
|
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]);
|
|
685
741
|
useEffect(() => {
|
|
686
742
|
if (!pluginHost) return;
|
|
687
743
|
const ctx = buildPluginContext({
|
|
688
744
|
courseId: courseIdRef.current,
|
|
689
745
|
sessionId: sessionIdRef.current,
|
|
690
|
-
attemptId: attemptIdRef.current
|
|
746
|
+
attemptId: attemptIdRef.current,
|
|
747
|
+
user: userRef.current
|
|
691
748
|
});
|
|
692
749
|
pluginHost.setupAll(ctx);
|
|
693
750
|
return () => {
|
|
694
751
|
pluginHost.disposeAll();
|
|
695
752
|
};
|
|
696
|
-
}, [pluginHost,
|
|
753
|
+
}, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
697
754
|
useEffect(() => {
|
|
698
|
-
const nextConfigured =
|
|
755
|
+
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
699
756
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
700
757
|
if (nextConfigured === prevConfigured) return;
|
|
701
758
|
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
@@ -716,10 +773,10 @@ function LessonkitProvider(props) {
|
|
|
716
773
|
migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
|
|
717
774
|
sessionIdRef.current = nextAuto;
|
|
718
775
|
}
|
|
719
|
-
}, [sessionConfiguredId,
|
|
776
|
+
}, [sessionConfiguredId, normalizedCourseId]);
|
|
720
777
|
const runtime = useMemo(
|
|
721
778
|
() => ({
|
|
722
|
-
config,
|
|
779
|
+
config: normalizedConfig,
|
|
723
780
|
tracking,
|
|
724
781
|
xapi,
|
|
725
782
|
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
@@ -731,7 +788,7 @@ function LessonkitProvider(props) {
|
|
|
731
788
|
plugins: pluginHost
|
|
732
789
|
}),
|
|
733
790
|
[
|
|
734
|
-
|
|
791
|
+
normalizedConfig,
|
|
735
792
|
tracking,
|
|
736
793
|
xapi,
|
|
737
794
|
progress,
|
|
@@ -745,6 +802,14 @@ function LessonkitProvider(props) {
|
|
|
745
802
|
sessionConfiguredId
|
|
746
803
|
]
|
|
747
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);
|
|
748
813
|
return /* @__PURE__ */ jsx(LessonkitContext.Provider, { value: runtime, children: props.children });
|
|
749
814
|
}
|
|
750
815
|
|
|
@@ -767,46 +832,74 @@ function useCompletion() {
|
|
|
767
832
|
const { completeLesson, completeCourse } = useLessonkit();
|
|
768
833
|
return useMemo2(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
769
834
|
}
|
|
770
|
-
function useQuizState() {
|
|
835
|
+
function useQuizState(enclosingLessonId) {
|
|
771
836
|
const { track } = useLessonkit();
|
|
837
|
+
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
772
838
|
return useMemo2(
|
|
773
839
|
() => ({
|
|
774
840
|
answer: (opts) => {
|
|
775
|
-
track("quiz_answered", opts);
|
|
841
|
+
track("quiz_answered", opts, trackOpts);
|
|
776
842
|
},
|
|
777
843
|
complete: (opts) => {
|
|
778
|
-
track("quiz_completed", opts);
|
|
844
|
+
track("quiz_completed", opts, trackOpts);
|
|
779
845
|
}
|
|
780
846
|
}),
|
|
781
|
-
[track]
|
|
847
|
+
[track, enclosingLessonId]
|
|
782
848
|
);
|
|
783
849
|
}
|
|
784
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
|
+
|
|
785
858
|
// src/runtime/validateComponentId.ts
|
|
786
|
-
import {
|
|
787
|
-
|
|
788
|
-
function isDevEnvironment2() {
|
|
859
|
+
import { assertValidId as assertValidId2 } from "@lessonkit/core";
|
|
860
|
+
function isDevEnvironment3() {
|
|
789
861
|
const g = globalThis;
|
|
790
862
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
791
863
|
}
|
|
792
|
-
function
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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;
|
|
801
890
|
}
|
|
802
891
|
|
|
803
892
|
// src/components.tsx
|
|
804
893
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
894
|
+
var warnedQuizOutsideLesson = false;
|
|
895
|
+
function resetQuizWarningsForTests() {
|
|
896
|
+
warnedQuizOutsideLesson = false;
|
|
897
|
+
}
|
|
805
898
|
function Course(props) {
|
|
806
|
-
|
|
899
|
+
const courseId = useMemo3(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
807
900
|
const providerConfig = useMemo3(
|
|
808
|
-
() => ({ ...props.config, courseId
|
|
809
|
-
[props.config,
|
|
901
|
+
() => ({ ...props.config, courseId }),
|
|
902
|
+
[props.config, courseId]
|
|
810
903
|
);
|
|
811
904
|
return /* @__PURE__ */ jsx2(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
|
|
812
905
|
/* @__PURE__ */ jsx2("h1", { children: props.title }),
|
|
@@ -814,41 +907,64 @@ function Course(props) {
|
|
|
814
907
|
] }) });
|
|
815
908
|
}
|
|
816
909
|
function Lesson(props) {
|
|
817
|
-
|
|
910
|
+
const lessonId = useMemo3(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
911
|
+
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
818
912
|
const { setActiveLesson, config } = useLessonkit();
|
|
819
913
|
const { completeLesson } = useCompletion();
|
|
820
|
-
const id = props.lessonId;
|
|
821
914
|
const lessonMountGenerationRef = useRef2(0);
|
|
822
915
|
useEffect2(() => {
|
|
916
|
+
const unregister = registerLessonMount(lessonId);
|
|
823
917
|
const generation = ++lessonMountGenerationRef.current;
|
|
824
|
-
setActiveLesson(
|
|
918
|
+
setActiveLesson(lessonId);
|
|
825
919
|
return () => {
|
|
826
|
-
|
|
920
|
+
unregister();
|
|
921
|
+
if (getLessonMountCount(lessonId) > 0) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (!autoComplete) return;
|
|
827
925
|
queueMicrotask(() => {
|
|
828
926
|
if (lessonMountGenerationRef.current !== generation) return;
|
|
829
927
|
completeLesson(lessonId);
|
|
830
928
|
});
|
|
831
929
|
};
|
|
832
|
-
}, [
|
|
833
|
-
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: [
|
|
834
932
|
/* @__PURE__ */ jsx2("h2", { children: props.title }),
|
|
835
933
|
/* @__PURE__ */ jsx2("div", { children: props.children })
|
|
836
|
-
] });
|
|
934
|
+
] }) });
|
|
837
935
|
}
|
|
838
936
|
function Scenario(props) {
|
|
839
|
-
|
|
840
|
-
|
|
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 });
|
|
841
942
|
}
|
|
842
943
|
function Reflection(props) {
|
|
843
|
-
|
|
944
|
+
const blockId = useMemo3(
|
|
945
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
946
|
+
[props.blockId]
|
|
947
|
+
);
|
|
844
948
|
const promptId = useId();
|
|
845
|
-
|
|
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: [
|
|
846
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,
|
|
847
960
|
props.children,
|
|
848
961
|
/* @__PURE__ */ jsx2(
|
|
849
962
|
"textarea",
|
|
850
963
|
{
|
|
964
|
+
value,
|
|
965
|
+
onChange: handleChange,
|
|
851
966
|
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
967
|
+
"aria-describedby": props.hint ? hintId : void 0,
|
|
852
968
|
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
853
969
|
}
|
|
854
970
|
)
|
|
@@ -867,18 +983,35 @@ function KnowledgeCheck(props) {
|
|
|
867
983
|
);
|
|
868
984
|
}
|
|
869
985
|
function Quiz(props) {
|
|
870
|
-
|
|
871
|
-
const
|
|
872
|
-
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();
|
|
873
1003
|
const [selected, setSelected] = useState2(null);
|
|
874
1004
|
const [selectionCorrect, setSelectionCorrect] = useState2(null);
|
|
1005
|
+
const [quizPassed, setQuizPassed] = useState2(false);
|
|
875
1006
|
const completedRef = useRef2(false);
|
|
876
1007
|
const questionId = useId();
|
|
1008
|
+
const choicesKey = props.choices.join("\0");
|
|
877
1009
|
useEffect2(() => {
|
|
878
1010
|
completedRef.current = false;
|
|
1011
|
+
setQuizPassed(false);
|
|
879
1012
|
setSelected(null);
|
|
880
1013
|
setSelectionCorrect(null);
|
|
881
|
-
}, [
|
|
1014
|
+
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
|
|
882
1015
|
const isChoiceCorrect = (choice, custom) => {
|
|
883
1016
|
if (!custom) return choice === props.answer;
|
|
884
1017
|
if (custom.passed !== void 0) return custom.passed;
|
|
@@ -887,7 +1020,11 @@ function Quiz(props) {
|
|
|
887
1020
|
}
|
|
888
1021
|
return choice === props.answer;
|
|
889
1022
|
};
|
|
890
|
-
|
|
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: [
|
|
891
1028
|
/* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
|
|
892
1029
|
/* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
|
|
893
1030
|
/* @__PURE__ */ jsx2("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
@@ -899,17 +1036,21 @@ function Quiz(props) {
|
|
|
899
1036
|
name: questionId,
|
|
900
1037
|
value: c,
|
|
901
1038
|
checked: selected === c,
|
|
1039
|
+
disabled: passed,
|
|
1040
|
+
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
902
1041
|
onChange: () => {
|
|
1042
|
+
if (passed) return;
|
|
903
1043
|
setSelected(c);
|
|
904
1044
|
const pluginCtx = buildPluginContext({
|
|
905
1045
|
courseId: config.courseId,
|
|
906
1046
|
sessionId: session.sessionId,
|
|
907
|
-
attemptId: session.attemptId
|
|
1047
|
+
attemptId: session.attemptId,
|
|
1048
|
+
user: session.user
|
|
908
1049
|
});
|
|
909
1050
|
const custom = plugins?.scoreAssessment(
|
|
910
1051
|
{
|
|
911
|
-
checkId
|
|
912
|
-
lessonId:
|
|
1052
|
+
checkId,
|
|
1053
|
+
lessonId: enclosingLessonId,
|
|
913
1054
|
response: c
|
|
914
1055
|
},
|
|
915
1056
|
pluginCtx
|
|
@@ -917,18 +1058,20 @@ function Quiz(props) {
|
|
|
917
1058
|
const correct = isChoiceCorrect(c, custom);
|
|
918
1059
|
setSelectionCorrect(correct);
|
|
919
1060
|
quiz.answer({
|
|
920
|
-
checkId
|
|
1061
|
+
checkId,
|
|
921
1062
|
question: props.question,
|
|
922
1063
|
choice: c,
|
|
923
1064
|
correct
|
|
924
1065
|
});
|
|
925
1066
|
if (correct && !completedRef.current) {
|
|
926
1067
|
completedRef.current = true;
|
|
1068
|
+
setQuizPassed(true);
|
|
1069
|
+
const maxScore = custom?.maxScore ?? 1;
|
|
927
1070
|
quiz.complete({
|
|
928
|
-
checkId
|
|
1071
|
+
checkId,
|
|
929
1072
|
score: custom?.score ?? 1,
|
|
930
|
-
maxScore
|
|
931
|
-
passingScore: props.passingScore ??
|
|
1073
|
+
maxScore,
|
|
1074
|
+
passingScore: props.passingScore ?? maxScore
|
|
932
1075
|
});
|
|
933
1076
|
}
|
|
934
1077
|
}
|
|
@@ -940,23 +1083,51 @@ function Quiz(props) {
|
|
|
940
1083
|
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
941
1084
|
] });
|
|
942
1085
|
}
|
|
943
|
-
function ProgressTracker() {
|
|
1086
|
+
function ProgressTracker(props) {
|
|
944
1087
|
const { progress } = useLessonkit();
|
|
945
1088
|
const completed = progress.completedLessonIds.size;
|
|
946
|
-
|
|
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: [
|
|
947
1110
|
"Lessons completed: ",
|
|
948
1111
|
completed
|
|
949
1112
|
] }) });
|
|
950
1113
|
}
|
|
951
1114
|
|
|
952
1115
|
// src/index.tsx
|
|
953
|
-
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";
|
|
954
1125
|
|
|
955
1126
|
// src/theme/ThemeProvider.tsx
|
|
956
1127
|
import React3, {
|
|
957
|
-
createContext as
|
|
1128
|
+
createContext as createContext3,
|
|
958
1129
|
useCallback as useCallback2,
|
|
959
|
-
useContext as
|
|
1130
|
+
useContext as useContext3,
|
|
960
1131
|
useLayoutEffect as useLayoutEffect2,
|
|
961
1132
|
useMemo as useMemo4,
|
|
962
1133
|
useRef as useRef3,
|
|
@@ -988,7 +1159,7 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
988
1159
|
|
|
989
1160
|
// src/theme/ThemeProvider.tsx
|
|
990
1161
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
991
|
-
var ThemeContext =
|
|
1162
|
+
var ThemeContext = createContext3(null);
|
|
992
1163
|
var useIsoLayoutEffect2 = typeof window !== "undefined" ? useLayoutEffect2 : React3.useEffect;
|
|
993
1164
|
function getSystemMode() {
|
|
994
1165
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -1068,7 +1239,7 @@ function ThemeProvider(props) {
|
|
|
1068
1239
|
return /* @__PURE__ */ jsx3(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx3("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
1069
1240
|
}
|
|
1070
1241
|
function useTheme() {
|
|
1071
|
-
const ctx =
|
|
1242
|
+
const ctx = useContext3(ThemeContext);
|
|
1072
1243
|
if (!ctx) {
|
|
1073
1244
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
1074
1245
|
}
|
|
@@ -1115,6 +1286,12 @@ var BLOCK_CATALOG = [
|
|
|
1115
1286
|
props: [
|
|
1116
1287
|
{ name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
|
|
1117
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
|
+
},
|
|
1118
1295
|
{ name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
|
|
1119
1296
|
],
|
|
1120
1297
|
requiredIds: ["lessonId"],
|
|
@@ -1167,6 +1344,9 @@ var BLOCK_CATALOG = [
|
|
|
1167
1344
|
props: [
|
|
1168
1345
|
{ name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
|
|
1169
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." },
|
|
1170
1350
|
{ name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
|
|
1171
1351
|
],
|
|
1172
1352
|
requiredIds: [],
|
|
@@ -1185,6 +1365,7 @@ var BLOCK_CATALOG = [
|
|
|
1185
1365
|
},
|
|
1186
1366
|
telemetry: {
|
|
1187
1367
|
emits: [],
|
|
1368
|
+
requiresActiveLesson: true,
|
|
1188
1369
|
manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
|
|
1189
1370
|
}
|
|
1190
1371
|
},
|
|
@@ -1222,7 +1403,14 @@ var BLOCK_CATALOG = [
|
|
|
1222
1403
|
type: "ProgressTracker",
|
|
1223
1404
|
category: "chrome",
|
|
1224
1405
|
description: "Displays count of completed lessons from runtime progress state.",
|
|
1225
|
-
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
|
+
],
|
|
1226
1414
|
requiredIds: [],
|
|
1227
1415
|
parentConstraints: ["Course"],
|
|
1228
1416
|
a11y: {
|
|
@@ -1274,9 +1462,15 @@ export {
|
|
|
1274
1462
|
ThemeProvider,
|
|
1275
1463
|
blockCatalogVersion,
|
|
1276
1464
|
buildBlockCatalog,
|
|
1277
|
-
|
|
1278
|
-
|
|
1465
|
+
buildTelemetryEvent2 as buildTelemetryEvent,
|
|
1466
|
+
createLessonkitRuntime2 as createLessonkitRuntime,
|
|
1467
|
+
createPluginRegistry2 as createPluginRegistry,
|
|
1468
|
+
createTelemetryPipeline2 as createTelemetryPipeline,
|
|
1469
|
+
defineAssessmentPlugin,
|
|
1470
|
+
defineLifecyclePlugin,
|
|
1471
|
+
defineTelemetryPlugin,
|
|
1279
1472
|
getBlockCatalogEntry,
|
|
1473
|
+
resetQuizWarningsForTests,
|
|
1280
1474
|
useCompletion,
|
|
1281
1475
|
useLessonkit,
|
|
1282
1476
|
useProgress,
|