@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.cjs
CHANGED
|
@@ -42,9 +42,15 @@ __export(index_exports, {
|
|
|
42
42
|
ThemeProvider: () => ThemeProvider,
|
|
43
43
|
blockCatalogVersion: () => blockCatalogVersion,
|
|
44
44
|
buildBlockCatalog: () => buildBlockCatalog,
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
buildTelemetryEvent: () => import_core10.buildTelemetryEvent,
|
|
46
|
+
createLessonkitRuntime: () => import_core10.createLessonkitRuntime,
|
|
47
|
+
createPluginRegistry: () => import_core10.createPluginRegistry,
|
|
48
|
+
createTelemetryPipeline: () => import_core10.createTelemetryPipeline,
|
|
49
|
+
defineAssessmentPlugin: () => import_core10.defineAssessmentPlugin,
|
|
50
|
+
defineLifecyclePlugin: () => import_core10.defineLifecyclePlugin,
|
|
51
|
+
defineTelemetryPlugin: () => import_core10.defineTelemetryPlugin,
|
|
47
52
|
getBlockCatalogEntry: () => getBlockCatalogEntry,
|
|
53
|
+
resetQuizWarningsForTests: () => resetQuizWarningsForTests,
|
|
48
54
|
useCompletion: () => useCompletion,
|
|
49
55
|
useLessonkit: () => useLessonkit,
|
|
50
56
|
useProgress: () => useProgress,
|
|
@@ -55,240 +61,95 @@ __export(index_exports, {
|
|
|
55
61
|
module.exports = __toCommonJS(index_exports);
|
|
56
62
|
|
|
57
63
|
// src/components.tsx
|
|
58
|
-
var
|
|
64
|
+
var import_react5 = require("react");
|
|
59
65
|
var import_accessibility = require("@lessonkit/accessibility");
|
|
60
66
|
|
|
61
67
|
// src/context.tsx
|
|
68
|
+
var import_react2 = require("react");
|
|
69
|
+
|
|
70
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
62
71
|
var import_react = require("react");
|
|
63
|
-
var
|
|
72
|
+
var import_core8 = require("@lessonkit/core");
|
|
64
73
|
var import_xapi3 = require("@lessonkit/xapi");
|
|
65
74
|
var import_xapi4 = require("@lessonkit/xapi");
|
|
66
75
|
|
|
67
76
|
// src/runtime/emitTelemetry.ts
|
|
77
|
+
var import_core2 = require("@lessonkit/core");
|
|
78
|
+
|
|
79
|
+
// src/runtime/telemetryPipeline.ts
|
|
68
80
|
var import_core = require("@lessonkit/core");
|
|
69
81
|
var import_xapi = require("@lessonkit/xapi");
|
|
70
82
|
|
|
71
83
|
// src/runtime/lxpackBridge.ts
|
|
72
84
|
var import_bridge = require("@lessonkit/lxpack/bridge");
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
if (fromSdk) return fromSdk;
|
|
76
|
-
if (typeof window === "undefined") return null;
|
|
77
|
-
const parent = window.parent;
|
|
78
|
-
if (!parent || parent === window) return null;
|
|
79
|
-
return parent.lxpack ?? null;
|
|
85
|
+
function forwardTelemetryToLxpack(event, mode = "auto") {
|
|
86
|
+
(0, import_bridge.forwardTelemetryToBridge)(event, mode);
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return;
|
|
87
|
-
case "completeCourse":
|
|
88
|
-
bridge.completeCourse?.();
|
|
89
|
-
return;
|
|
90
|
-
case "submitAssessment": {
|
|
91
|
-
const scaled = (0, import_bridge.normalizeScore)({
|
|
92
|
-
score: action.score,
|
|
93
|
-
maxScore: action.maxScore
|
|
94
|
-
});
|
|
95
|
-
if (scaled === null) return;
|
|
96
|
-
bridge.submitAssessment?.({
|
|
97
|
-
id: action.id,
|
|
98
|
-
score: scaled,
|
|
99
|
-
passingScore: (0, import_bridge.normalizePassingThreshold)({
|
|
100
|
-
passingScore: action.passingScore,
|
|
101
|
-
maxScore: action.maxScore
|
|
102
|
-
}),
|
|
103
|
-
maxScore: action.maxScore
|
|
104
|
-
});
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
case "track":
|
|
108
|
-
bridge.track?.(action.event);
|
|
109
|
-
return;
|
|
110
|
-
default:
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
88
|
+
|
|
89
|
+
// src/runtime/telemetryPipeline.ts
|
|
90
|
+
function isDevEnvironment() {
|
|
91
|
+
const g = globalThis;
|
|
92
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
113
93
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
94
|
+
function createLegacyPipeline(opts, extraSinks = []) {
|
|
95
|
+
return (0, import_core.createTelemetryPipeline)([
|
|
96
|
+
(0, import_core.createTrackingPipelineSink)("tracking", (event) => opts.tracking.track(event)),
|
|
97
|
+
{
|
|
98
|
+
id: "xapi",
|
|
99
|
+
emit(event) {
|
|
100
|
+
try {
|
|
101
|
+
const statement = (0, import_xapi.telemetryEventToXAPIStatement)(event);
|
|
102
|
+
if (statement) opts.xapi?.send(statement);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (isDevEnvironment()) {
|
|
105
|
+
console.warn(
|
|
106
|
+
"[lessonkit] xAPI mapping skipped:",
|
|
107
|
+
err instanceof Error ? err.message : err
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "lxpack-bridge",
|
|
115
|
+
emit(event) {
|
|
116
|
+
forwardTelemetryToLxpack(event, opts.lxpackBridge);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
...extraSinks
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
function emitThroughPipeline(event, opts, extraSinks) {
|
|
123
|
+
createLegacyPipeline(opts, extraSinks).emit(event);
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
// src/runtime/emitTelemetry.ts
|
|
125
127
|
var warnedMissingCourseId = false;
|
|
126
|
-
|
|
127
|
-
function isDevEnvironment() {
|
|
128
|
+
function isDevEnvironment2() {
|
|
128
129
|
const g = globalThis;
|
|
129
130
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
130
131
|
}
|
|
131
132
|
function emitTelemetry(tracking, xapi, event, opts) {
|
|
132
133
|
if (!event.courseId) {
|
|
133
|
-
if (
|
|
134
|
+
if (isDevEnvironment2() && !warnedMissingCourseId) {
|
|
134
135
|
warnedMissingCourseId = true;
|
|
135
136
|
console.warn("[lessonkit] telemetry event missing courseId");
|
|
136
137
|
}
|
|
137
138
|
return;
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
} catch (err) {
|
|
144
|
-
if (isDevEnvironment()) {
|
|
145
|
-
console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
forwardTelemetryToLxpack(event, opts?.lxpackBridge ?? "auto");
|
|
149
|
-
}
|
|
150
|
-
function buildTrackEvent(opts) {
|
|
151
|
-
const base = {
|
|
152
|
-
timestamp: (0, import_core.nowIso)(),
|
|
153
|
-
courseId: opts.courseId,
|
|
154
|
-
sessionId: opts.sessionId,
|
|
155
|
-
attemptId: opts.attemptId,
|
|
156
|
-
user: opts.user
|
|
140
|
+
const legacy = {
|
|
141
|
+
tracking,
|
|
142
|
+
xapi,
|
|
143
|
+
lxpackBridge: opts?.lxpackBridge ?? "auto"
|
|
157
144
|
};
|
|
158
|
-
|
|
159
|
-
case "course_started":
|
|
160
|
-
return { name: "course_started", ...base };
|
|
161
|
-
case "course_completed":
|
|
162
|
-
return { name: "course_completed", ...base };
|
|
163
|
-
case "lesson_started": {
|
|
164
|
-
const data = opts.data;
|
|
165
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
166
|
-
if (!lessonId) throw new Error("lesson_started requires lessonId");
|
|
167
|
-
return {
|
|
168
|
-
name: "lesson_started",
|
|
169
|
-
...base,
|
|
170
|
-
lessonId,
|
|
171
|
-
data: { ...data, lessonId }
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
case "lesson_completed":
|
|
175
|
-
case "lesson_time_on_task": {
|
|
176
|
-
const data = opts.data;
|
|
177
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
178
|
-
if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
|
|
179
|
-
return {
|
|
180
|
-
name: opts.name,
|
|
181
|
-
...base,
|
|
182
|
-
lessonId,
|
|
183
|
-
data: { ...data, lessonId }
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
case "quiz_answered": {
|
|
187
|
-
const data = opts.data;
|
|
188
|
-
const lessonId = opts.lessonId;
|
|
189
|
-
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
190
|
-
return { name: "quiz_answered", ...base, lessonId, data };
|
|
191
|
-
}
|
|
192
|
-
case "quiz_completed": {
|
|
193
|
-
const data = opts.data;
|
|
194
|
-
const lessonId = opts.lessonId;
|
|
195
|
-
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
196
|
-
return { name: "quiz_completed", ...base, lessonId, data };
|
|
197
|
-
}
|
|
198
|
-
case "interaction":
|
|
199
|
-
return {
|
|
200
|
-
name: "interaction",
|
|
201
|
-
...base,
|
|
202
|
-
lessonId: opts.lessonId,
|
|
203
|
-
data: opts.data
|
|
204
|
-
};
|
|
205
|
-
default:
|
|
206
|
-
return { name: opts.name, ...base };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
function tryBuildTrackEvent(opts) {
|
|
210
|
-
const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
|
|
211
|
-
if (isQuiz && !opts.lessonId) {
|
|
212
|
-
if (isDevEnvironment() && !warnedMissingQuizLesson) {
|
|
213
|
-
warnedMissingQuizLesson = true;
|
|
214
|
-
console.warn(
|
|
215
|
-
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
return buildTrackEvent(opts);
|
|
145
|
+
emitThroughPipeline(event, legacy, opts?.extraSinks);
|
|
221
146
|
}
|
|
222
147
|
|
|
223
148
|
// src/runtime/ports.ts
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
getItem: () => null,
|
|
227
|
-
setItem: () => {
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
function createSessionStoragePort() {
|
|
232
|
-
if (typeof sessionStorage === "undefined") return createNoopStorage();
|
|
233
|
-
return {
|
|
234
|
-
getItem: (key) => {
|
|
235
|
-
try {
|
|
236
|
-
return sessionStorage.getItem(key);
|
|
237
|
-
} catch {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
|
-
setItem: (key, value) => {
|
|
242
|
-
try {
|
|
243
|
-
sessionStorage.setItem(key, value);
|
|
244
|
-
} catch {
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
removeItem: (key) => {
|
|
248
|
-
try {
|
|
249
|
-
sessionStorage.removeItem(key);
|
|
250
|
-
} catch {
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
}
|
|
149
|
+
var import_core3 = require("@lessonkit/core");
|
|
255
150
|
|
|
256
151
|
// src/runtime/progress.ts
|
|
257
|
-
|
|
258
|
-
let activeLessonId;
|
|
259
|
-
let completedLessonIds = /* @__PURE__ */ new Set();
|
|
260
|
-
let courseCompleted = false;
|
|
261
|
-
const lessonStartTimes = /* @__PURE__ */ new Map();
|
|
262
|
-
return {
|
|
263
|
-
getState: () => ({
|
|
264
|
-
activeLessonId,
|
|
265
|
-
completedLessonIds: new Set(completedLessonIds),
|
|
266
|
-
courseCompleted
|
|
267
|
-
}),
|
|
268
|
-
setActiveLesson: (lessonId, startedAtMs) => {
|
|
269
|
-
const previousLessonId = activeLessonId;
|
|
270
|
-
activeLessonId = lessonId;
|
|
271
|
-
lessonStartTimes.set(lessonId, startedAtMs);
|
|
272
|
-
return { previousLessonId };
|
|
273
|
-
},
|
|
274
|
-
completeLesson: (lessonId, completedAtMs) => {
|
|
275
|
-
if (completedLessonIds.has(lessonId)) return { didComplete: false };
|
|
276
|
-
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
277
|
-
if (activeLessonId === lessonId) {
|
|
278
|
-
activeLessonId = void 0;
|
|
279
|
-
}
|
|
280
|
-
const startedAt = lessonStartTimes.get(lessonId);
|
|
281
|
-
lessonStartTimes.delete(lessonId);
|
|
282
|
-
const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
|
|
283
|
-
return { durationMs, didComplete: true };
|
|
284
|
-
},
|
|
285
|
-
completeCourse: () => {
|
|
286
|
-
if (courseCompleted) return { didComplete: false };
|
|
287
|
-
courseCompleted = true;
|
|
288
|
-
return { didComplete: true };
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
}
|
|
152
|
+
var import_core4 = require("@lessonkit/core");
|
|
292
153
|
|
|
293
154
|
// src/runtime/xapi.ts
|
|
294
155
|
var import_xapi2 = require("@lessonkit/xapi");
|
|
@@ -306,64 +167,37 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
306
167
|
}
|
|
307
168
|
|
|
308
169
|
// src/runtime/session.ts
|
|
309
|
-
var
|
|
310
|
-
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
311
|
-
function getTabSessionId(storage) {
|
|
312
|
-
return storage.getItem(SESSION_STORAGE_KEY);
|
|
313
|
-
}
|
|
314
|
-
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
315
|
-
function resolveSessionId(storage, provided) {
|
|
316
|
-
if (provided) return provided;
|
|
317
|
-
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
318
|
-
if (existing) return existing;
|
|
319
|
-
const id = (0, import_core2.createSessionId)();
|
|
320
|
-
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
321
|
-
return id;
|
|
322
|
-
}
|
|
323
|
-
function courseStartedStorageKey(sessionId, courseId) {
|
|
324
|
-
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
325
|
-
}
|
|
326
|
-
function hasCourseStarted(storage, sessionId, courseId) {
|
|
327
|
-
if (!courseId) return false;
|
|
328
|
-
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
329
|
-
}
|
|
330
|
-
function markCourseStarted(storage, sessionId, courseId) {
|
|
331
|
-
if (!courseId) return;
|
|
332
|
-
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
333
|
-
}
|
|
334
|
-
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
335
|
-
if (!courseId || fromSessionId === toSessionId) return;
|
|
336
|
-
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
337
|
-
markCourseStarted(storage, toSessionId, courseId);
|
|
338
|
-
storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
|
|
339
|
-
}
|
|
340
|
-
}
|
|
170
|
+
var import_core5 = require("@lessonkit/core");
|
|
341
171
|
|
|
342
172
|
// src/runtime/plugins.ts
|
|
343
|
-
var
|
|
173
|
+
var import_core6 = require("@lessonkit/core");
|
|
344
174
|
function createReactPluginHost(plugins) {
|
|
345
175
|
if (!plugins?.length) return null;
|
|
346
|
-
return (0,
|
|
176
|
+
return (0, import_core6.createPluginRegistry)(plugins);
|
|
347
177
|
}
|
|
348
178
|
function buildPluginContext(opts) {
|
|
349
179
|
return {
|
|
350
180
|
courseId: opts.courseId,
|
|
351
181
|
sessionId: opts.sessionId,
|
|
352
|
-
attemptId: opts.attemptId
|
|
182
|
+
attemptId: opts.attemptId,
|
|
183
|
+
user: opts.user
|
|
353
184
|
};
|
|
354
185
|
}
|
|
355
186
|
function emitTelemetryWithPlugins(opts) {
|
|
356
187
|
const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
|
|
357
188
|
if (next === null) return;
|
|
358
|
-
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
189
|
+
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
190
|
+
lxpackBridge: opts.lxpackBridge ?? "auto",
|
|
191
|
+
extraSinks: opts.extraSinks
|
|
192
|
+
});
|
|
359
193
|
}
|
|
360
194
|
|
|
361
195
|
// src/runtime/telemetry.ts
|
|
362
|
-
var
|
|
196
|
+
var import_core7 = require("@lessonkit/core");
|
|
363
197
|
function createTrackingClientFromConfig(config) {
|
|
364
|
-
if (config.tracking?.enabled === false) return (0,
|
|
198
|
+
if (config.tracking?.enabled === false) return (0, import_core7.createTrackingClient)();
|
|
365
199
|
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
366
|
-
return (0,
|
|
200
|
+
return (0, import_core7.createTrackingClient)({
|
|
367
201
|
sink: config.tracking?.sink,
|
|
368
202
|
batchSink: config.tracking?.batchSink,
|
|
369
203
|
batch: config.tracking?.batch
|
|
@@ -380,70 +214,206 @@ async function disposeTrackingClient(client) {
|
|
|
380
214
|
}
|
|
381
215
|
}
|
|
382
216
|
|
|
383
|
-
// src/
|
|
384
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
385
|
-
var LessonkitContext = (0, import_react.createContext)(null);
|
|
217
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
386
218
|
var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
|
|
387
|
-
var defaultStorage = createSessionStoragePort();
|
|
219
|
+
var defaultStorage = (0, import_core3.createSessionStoragePort)();
|
|
388
220
|
function isTrackingActive(tracking) {
|
|
389
221
|
return tracking?.enabled !== false;
|
|
390
222
|
}
|
|
391
|
-
|
|
223
|
+
var noopTrackingClient = { track: () => {
|
|
224
|
+
} };
|
|
225
|
+
function buildCourseStartedEvent(opts) {
|
|
226
|
+
const pluginCtx = buildPluginContext({
|
|
227
|
+
courseId: opts.courseId,
|
|
228
|
+
sessionId: opts.sessionId,
|
|
229
|
+
attemptId: opts.attemptId,
|
|
230
|
+
user: opts.user
|
|
231
|
+
});
|
|
232
|
+
const built = (0, import_core2.buildTelemetryEvent)({
|
|
233
|
+
name: "course_started",
|
|
234
|
+
courseId: opts.courseId,
|
|
235
|
+
sessionId: opts.sessionId,
|
|
236
|
+
attemptId: opts.attemptId,
|
|
237
|
+
user: opts.user
|
|
238
|
+
});
|
|
239
|
+
return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
|
|
240
|
+
}
|
|
241
|
+
function emitCourseStartedPipelineOnly(opts) {
|
|
392
242
|
const pluginCtx = buildPluginContext({
|
|
393
243
|
courseId: opts.courseId,
|
|
394
244
|
sessionId: opts.sessionId,
|
|
395
|
-
attemptId: opts.attemptId
|
|
245
|
+
attemptId: opts.attemptId,
|
|
246
|
+
user: opts.user
|
|
396
247
|
});
|
|
397
248
|
try {
|
|
398
249
|
emitTelemetryWithPlugins({
|
|
399
|
-
pluginHost:
|
|
400
|
-
tracking:
|
|
250
|
+
pluginHost: null,
|
|
251
|
+
tracking: noopTrackingClient,
|
|
401
252
|
xapi: opts.xapi,
|
|
402
|
-
event:
|
|
403
|
-
name: "course_started",
|
|
404
|
-
courseId: opts.courseId,
|
|
405
|
-
sessionId: opts.sessionId,
|
|
406
|
-
attemptId: opts.attemptId,
|
|
407
|
-
user: opts.user
|
|
408
|
-
}),
|
|
253
|
+
event: opts.event,
|
|
409
254
|
pluginCtx,
|
|
410
|
-
lxpackBridge: opts.lxpackBridge
|
|
255
|
+
lxpackBridge: opts.lxpackBridge,
|
|
256
|
+
extraSinks: opts.extraSinks
|
|
411
257
|
});
|
|
412
|
-
markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
258
|
+
(0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
413
259
|
return true;
|
|
414
260
|
} catch {
|
|
415
261
|
return false;
|
|
416
262
|
}
|
|
417
263
|
}
|
|
418
|
-
function
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
264
|
+
function emitCourseStarted(opts) {
|
|
265
|
+
const event = buildCourseStartedEvent(opts);
|
|
266
|
+
if (event === null) return true;
|
|
267
|
+
const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
268
|
+
opts.storage,
|
|
269
|
+
opts.sessionId,
|
|
270
|
+
opts.courseId
|
|
271
|
+
);
|
|
272
|
+
if (!trackingAlreadyEmitted) {
|
|
273
|
+
try {
|
|
274
|
+
opts.tracking.track(event);
|
|
275
|
+
(0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
281
|
+
}
|
|
282
|
+
function emitCourseStartedToTrackingOnly(opts) {
|
|
283
|
+
const event = buildCourseStartedEvent(opts);
|
|
284
|
+
if (event === null) return true;
|
|
285
|
+
const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
286
|
+
opts.storage,
|
|
287
|
+
opts.sessionId,
|
|
288
|
+
opts.courseId
|
|
289
|
+
);
|
|
290
|
+
if (!trackingAlreadyEmitted) {
|
|
291
|
+
try {
|
|
292
|
+
opts.tracking.track(event);
|
|
293
|
+
(0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const pluginCtx = buildPluginContext({
|
|
299
|
+
courseId: opts.courseId,
|
|
300
|
+
sessionId: opts.sessionId,
|
|
301
|
+
attemptId: opts.attemptId,
|
|
302
|
+
user: opts.user
|
|
303
|
+
});
|
|
304
|
+
try {
|
|
305
|
+
emitTelemetryWithPlugins({
|
|
306
|
+
pluginHost: null,
|
|
307
|
+
tracking: noopTrackingClient,
|
|
308
|
+
xapi: null,
|
|
309
|
+
event,
|
|
310
|
+
pluginCtx,
|
|
311
|
+
lxpackBridge: opts.lxpackBridge,
|
|
312
|
+
extraSinks: opts.extraSinks
|
|
313
|
+
});
|
|
314
|
+
return true;
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function emitPendingCourseStarted(opts) {
|
|
320
|
+
const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
321
|
+
opts.storage,
|
|
322
|
+
opts.sessionId,
|
|
323
|
+
opts.courseId
|
|
324
|
+
);
|
|
325
|
+
const sessionStarted = (0, import_core5.hasCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
326
|
+
if (sessionStarted && !trackingEmitted) {
|
|
327
|
+
return emitCourseStartedToTrackingOnly(opts);
|
|
328
|
+
}
|
|
329
|
+
if (trackingEmitted && !sessionStarted) {
|
|
330
|
+
const event = buildCourseStartedEvent(opts);
|
|
331
|
+
if (event === null) return true;
|
|
332
|
+
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
333
|
+
}
|
|
334
|
+
if (!trackingEmitted && !sessionStarted) {
|
|
335
|
+
return emitCourseStarted(opts);
|
|
336
|
+
}
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
function assertTrackingSinkConfig(tracking) {
|
|
340
|
+
if (!tracking?.sink || !tracking?.batchSink) return;
|
|
341
|
+
throw new Error(
|
|
342
|
+
"[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
function useLessonkitProviderRuntime(config) {
|
|
346
|
+
const normalizedCourseId = (0, import_react.useMemo)(
|
|
347
|
+
() => (0, import_core8.assertValidId)(config.courseId, "courseId"),
|
|
348
|
+
[config.courseId]
|
|
349
|
+
);
|
|
350
|
+
const normalizedConfig = (0, import_react.useMemo)(
|
|
351
|
+
() => ({ ...config, courseId: normalizedCourseId }),
|
|
352
|
+
[config, normalizedCourseId]
|
|
353
|
+
);
|
|
354
|
+
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
355
|
+
const extraSinksRef = (0, import_react.useRef)(normalizedConfig.sinks);
|
|
356
|
+
extraSinksRef.current = normalizedConfig.sinks;
|
|
357
|
+
const headlessRef = (0, import_react.useRef)(null);
|
|
358
|
+
const sessionIdRef = (0, import_react.useRef)((0, import_core5.resolveSessionId)(defaultStorage, normalizedConfig.session?.sessionId));
|
|
359
|
+
const prevConfiguredSessionIdRef = (0, import_react.useRef)(normalizedConfig.session?.sessionId);
|
|
360
|
+
if (normalizedConfig.session?.sessionId) {
|
|
361
|
+
sessionIdRef.current = normalizedConfig.session.sessionId;
|
|
424
362
|
} else if (prevConfiguredSessionIdRef.current) {
|
|
425
|
-
sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
|
|
363
|
+
sessionIdRef.current = (0, import_core5.resolveSessionId)(defaultStorage, void 0);
|
|
426
364
|
}
|
|
427
|
-
const attemptIdRef = (0, import_react.useRef)(
|
|
428
|
-
const userRef = (0, import_react.useRef)(
|
|
429
|
-
attemptIdRef.current =
|
|
430
|
-
userRef.current =
|
|
431
|
-
const courseIdRef = (0, import_react.useRef)(
|
|
432
|
-
courseIdRef.current =
|
|
433
|
-
const lxpackBridgeModeRef = (0, import_react.useRef)(
|
|
434
|
-
lxpackBridgeModeRef.current =
|
|
435
|
-
const pluginHost = (0, import_react.useMemo)(() => createReactPluginHost(
|
|
365
|
+
const attemptIdRef = (0, import_react.useRef)(normalizedConfig.session?.attemptId);
|
|
366
|
+
const userRef = (0, import_react.useRef)(normalizedConfig.session?.user);
|
|
367
|
+
attemptIdRef.current = normalizedConfig.session?.attemptId;
|
|
368
|
+
userRef.current = normalizedConfig.session?.user;
|
|
369
|
+
const courseIdRef = (0, import_react.useRef)(normalizedCourseId);
|
|
370
|
+
courseIdRef.current = normalizedCourseId;
|
|
371
|
+
const lxpackBridgeModeRef = (0, import_react.useRef)(normalizedConfig.lxpack?.bridge ?? "auto");
|
|
372
|
+
lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
|
|
373
|
+
const pluginHost = (0, import_react.useMemo)(() => createReactPluginHost(normalizedConfig.plugins), [normalizedConfig.plugins]);
|
|
436
374
|
const pluginHostRef = (0, import_react.useRef)(pluginHost);
|
|
437
375
|
pluginHostRef.current = pluginHost;
|
|
438
|
-
const progressRef = (0, import_react.useRef)(createProgressController());
|
|
376
|
+
const progressRef = (0, import_react.useRef)((0, import_core4.createProgressController)());
|
|
439
377
|
const courseStartedEmittedToSinkRef = (0, import_react.useRef)(false);
|
|
440
|
-
const prevCourseIdForProgressRef = (0, import_react.useRef)(
|
|
378
|
+
const prevCourseIdForProgressRef = (0, import_react.useRef)(normalizedCourseId);
|
|
441
379
|
const pendingCourseIdResetRef = (0, import_react.useRef)(false);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
380
|
+
const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
|
|
381
|
+
const xapiCourseStartedSentOnClientRef = (0, import_react.useRef)(false);
|
|
382
|
+
if (prevUseV2RuntimeRef.current !== useV2Runtime) {
|
|
383
|
+
prevUseV2RuntimeRef.current = useV2Runtime;
|
|
384
|
+
if (useV2Runtime) {
|
|
385
|
+
headlessRef.current = (0, import_core8.createLessonkitRuntime)({
|
|
386
|
+
courseId: normalizedCourseId,
|
|
387
|
+
runtimeVersion: "v2",
|
|
388
|
+
session: normalizedConfig.session
|
|
389
|
+
});
|
|
390
|
+
progressRef.current = headlessRef.current.progress;
|
|
391
|
+
} else {
|
|
392
|
+
headlessRef.current = null;
|
|
393
|
+
progressRef.current = (0, import_core4.createProgressController)();
|
|
394
|
+
}
|
|
445
395
|
pendingCourseIdResetRef.current = true;
|
|
446
396
|
courseStartedEmittedToSinkRef.current = false;
|
|
397
|
+
} else if (useV2Runtime && !headlessRef.current) {
|
|
398
|
+
headlessRef.current = (0, import_core8.createLessonkitRuntime)({
|
|
399
|
+
courseId: normalizedCourseId,
|
|
400
|
+
runtimeVersion: "v2",
|
|
401
|
+
session: normalizedConfig.session
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
405
|
+
prevCourseIdForProgressRef.current = normalizedCourseId;
|
|
406
|
+
if (useV2Runtime && headlessRef.current) {
|
|
407
|
+
headlessRef.current.resetForCourseChange(normalizedCourseId);
|
|
408
|
+
progressRef.current = headlessRef.current.progress;
|
|
409
|
+
} else {
|
|
410
|
+
progressRef.current = (0, import_core4.createProgressController)();
|
|
411
|
+
}
|
|
412
|
+
pendingCourseIdResetRef.current = true;
|
|
413
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
414
|
+
}
|
|
415
|
+
if (useV2Runtime && headlessRef.current) {
|
|
416
|
+
progressRef.current = headlessRef.current.progress;
|
|
447
417
|
}
|
|
448
418
|
const [progress, setProgress] = (0, import_react.useState)(() => progressRef.current.getState());
|
|
449
419
|
const syncProgress = (0, import_react.useCallback)(() => {
|
|
@@ -454,16 +424,16 @@ function LessonkitProvider(props) {
|
|
|
454
424
|
const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
|
|
455
425
|
const xapiRef = (0, import_react.useRef)(null);
|
|
456
426
|
const [xapi, setXapi] = (0, import_react.useState)(null);
|
|
457
|
-
const prevXapiCourseIdRef = (0, import_react.useRef)(
|
|
458
|
-
const xapiEnabled =
|
|
459
|
-
const xapiClient =
|
|
460
|
-
const xapiTransport =
|
|
461
|
-
const courseId =
|
|
462
|
-
const trackingEnabled =
|
|
427
|
+
const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
|
|
428
|
+
const xapiEnabled = normalizedConfig.xapi?.enabled;
|
|
429
|
+
const xapiClient = normalizedConfig.xapi?.client;
|
|
430
|
+
const xapiTransport = normalizedConfig.xapi?.transport;
|
|
431
|
+
const courseId = normalizedCourseId;
|
|
432
|
+
const trackingEnabled = normalizedConfig.tracking?.enabled;
|
|
463
433
|
useIsoLayoutEffect(() => {
|
|
464
434
|
const courseChanged = prevXapiCourseIdRef.current !== courseId;
|
|
465
435
|
if (courseChanged) {
|
|
466
|
-
if (
|
|
436
|
+
if (normalizedConfig.xapi?.client) {
|
|
467
437
|
const g = globalThis;
|
|
468
438
|
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
|
|
469
439
|
console.warn(
|
|
@@ -474,20 +444,24 @@ function LessonkitProvider(props) {
|
|
|
474
444
|
}
|
|
475
445
|
xapiQueueRef.current = (0, import_xapi3.createInMemoryXAPIQueue)();
|
|
476
446
|
prevXapiCourseIdRef.current = courseId;
|
|
447
|
+
xapiCourseStartedSentOnClientRef.current = false;
|
|
477
448
|
}
|
|
478
449
|
const prev = xapiRef.current;
|
|
479
|
-
const next = createXapiClientFromConfig(
|
|
450
|
+
const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
|
|
480
451
|
xapiRef.current = next;
|
|
481
452
|
setXapi(next);
|
|
482
|
-
if (next
|
|
453
|
+
if (next) {
|
|
483
454
|
const sessionId = sessionIdRef.current;
|
|
484
455
|
const cid = courseIdRef.current;
|
|
485
|
-
const trackingActive = isTrackingActive(
|
|
486
|
-
const alreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
|
|
487
|
-
|
|
456
|
+
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
457
|
+
const alreadyStarted = (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid);
|
|
458
|
+
const clientChanged = !prev || prev !== next;
|
|
459
|
+
const skipBootstrap = trackingActive && !alreadyStarted;
|
|
460
|
+
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
|
|
461
|
+
if (needsBootstrap) {
|
|
488
462
|
try {
|
|
489
463
|
const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
|
|
490
|
-
|
|
464
|
+
(0, import_core2.buildTelemetryEvent)({
|
|
491
465
|
name: "course_started",
|
|
492
466
|
courseId: cid,
|
|
493
467
|
sessionId,
|
|
@@ -497,7 +471,10 @@ function LessonkitProvider(props) {
|
|
|
497
471
|
);
|
|
498
472
|
if (statement) {
|
|
499
473
|
next.send(statement);
|
|
500
|
-
|
|
474
|
+
if (!alreadyStarted) {
|
|
475
|
+
(0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
|
|
476
|
+
}
|
|
477
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
501
478
|
}
|
|
502
479
|
} catch {
|
|
503
480
|
}
|
|
@@ -522,49 +499,56 @@ function LessonkitProvider(props) {
|
|
|
522
499
|
void prev?.flush();
|
|
523
500
|
};
|
|
524
501
|
}, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
|
|
525
|
-
const trackingRef = (0, import_react.useRef)((0,
|
|
502
|
+
const trackingRef = (0, import_react.useRef)((0, import_core8.createTrackingClient)());
|
|
526
503
|
const trackingClientForUnmountRef = (0, import_react.useRef)(trackingRef.current);
|
|
527
504
|
const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
|
|
528
|
-
const trackingSink =
|
|
529
|
-
const trackingBatchSink =
|
|
530
|
-
const batchEnabled =
|
|
531
|
-
const batchFlushIntervalMs =
|
|
532
|
-
const batchMaxBatchSize =
|
|
505
|
+
const trackingSink = normalizedConfig.tracking?.sink;
|
|
506
|
+
const trackingBatchSink = normalizedConfig.tracking?.batchSink;
|
|
507
|
+
const batchEnabled = normalizedConfig.tracking?.batch?.enabled;
|
|
508
|
+
const batchFlushIntervalMs = normalizedConfig.tracking?.batch?.flushIntervalMs;
|
|
509
|
+
const batchMaxBatchSize = normalizedConfig.tracking?.batch?.maxBatchSize;
|
|
533
510
|
const buildCurrentPluginCtx = (0, import_react.useCallback)(
|
|
534
511
|
() => buildPluginContext({
|
|
535
512
|
courseId: courseIdRef.current,
|
|
536
513
|
sessionId: sessionIdRef.current,
|
|
537
|
-
attemptId: attemptIdRef.current
|
|
514
|
+
attemptId: attemptIdRef.current,
|
|
515
|
+
user: userRef.current
|
|
538
516
|
}),
|
|
539
517
|
[]
|
|
540
518
|
);
|
|
541
519
|
useIsoLayoutEffect(() => {
|
|
542
520
|
const prev = trackingRef.current;
|
|
543
|
-
const baseSink =
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
)
|
|
553
|
-
|
|
554
|
-
|
|
521
|
+
const baseSink = normalizedConfig.tracking?.sink;
|
|
522
|
+
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
523
|
+
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
524
|
+
const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
|
|
525
|
+
const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
|
|
526
|
+
const host = pluginHostRef.current;
|
|
527
|
+
const ctx = buildCurrentPluginCtx();
|
|
528
|
+
const delivered = host.deliverTelemetryBatch(events, ctx);
|
|
529
|
+
const perEventForBatch = [];
|
|
530
|
+
const collector = (event) => {
|
|
531
|
+
perEventForBatch.push(event);
|
|
532
|
+
};
|
|
533
|
+
const composedPerEvent = host.composeTrackingSink(collector, buildCurrentPluginCtx) ?? collector;
|
|
534
|
+
for (const event of delivered) {
|
|
535
|
+
await Promise.resolve(composedPerEvent(event));
|
|
536
|
+
}
|
|
537
|
+
return userBatchSink(perEventForBatch);
|
|
538
|
+
} : userBatchSink;
|
|
555
539
|
const next = createTrackingClientFromConfig({
|
|
556
|
-
tracking: { ...
|
|
540
|
+
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
557
541
|
});
|
|
558
542
|
trackingRef.current = next;
|
|
559
543
|
trackingClientForUnmountRef.current = next;
|
|
560
544
|
setTracking(next);
|
|
561
545
|
const sessionId = sessionIdRef.current;
|
|
562
546
|
const cid = courseIdRef.current;
|
|
563
|
-
const trackingActive = isTrackingActive(
|
|
547
|
+
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
564
548
|
if (!trackingActive) {
|
|
565
549
|
courseStartedEmittedToSinkRef.current = false;
|
|
566
|
-
} else if (!courseStartedEmittedToSinkRef.current
|
|
567
|
-
const emitted =
|
|
550
|
+
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
551
|
+
const emitted = emitPendingCourseStarted({
|
|
568
552
|
pluginHost: pluginHostRef.current,
|
|
569
553
|
tracking: next,
|
|
570
554
|
xapi: xapiRef.current,
|
|
@@ -573,8 +557,12 @@ function LessonkitProvider(props) {
|
|
|
573
557
|
courseId: cid,
|
|
574
558
|
attemptId: attemptIdRef.current,
|
|
575
559
|
user: userRef.current,
|
|
576
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
560
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
561
|
+
extraSinks: extraSinksRef.current
|
|
577
562
|
});
|
|
563
|
+
if (emitted) {
|
|
564
|
+
(0, import_core5.markCourseStartedEmittedToTracking)(defaultStorage, sessionId, cid);
|
|
565
|
+
}
|
|
578
566
|
courseStartedEmittedToSinkRef.current = emitted;
|
|
579
567
|
} else if (trackingActive) {
|
|
580
568
|
courseStartedEmittedToSinkRef.current = true;
|
|
@@ -591,8 +579,8 @@ function LessonkitProvider(props) {
|
|
|
591
579
|
batchEnabled,
|
|
592
580
|
batchFlushIntervalMs,
|
|
593
581
|
batchMaxBatchSize,
|
|
594
|
-
|
|
595
|
-
|
|
582
|
+
normalizedConfig.plugins,
|
|
583
|
+
normalizedCourseId,
|
|
596
584
|
buildCurrentPluginCtx
|
|
597
585
|
]);
|
|
598
586
|
const emitWithBridge = (0, import_react.useCallback)((trackingClient, event) => {
|
|
@@ -604,14 +592,32 @@ function LessonkitProvider(props) {
|
|
|
604
592
|
pluginCtx: buildPluginContext({
|
|
605
593
|
courseId: courseIdRef.current,
|
|
606
594
|
sessionId: sessionIdRef.current,
|
|
607
|
-
attemptId: attemptIdRef.current
|
|
595
|
+
attemptId: attemptIdRef.current,
|
|
596
|
+
user: userRef.current
|
|
608
597
|
}),
|
|
609
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
598
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
599
|
+
extraSinks: extraSinksRef.current
|
|
610
600
|
});
|
|
611
601
|
}, []);
|
|
602
|
+
const emitLifecycleEvent = (0, import_react.useCallback)(
|
|
603
|
+
(name, data, lessonId) => {
|
|
604
|
+
const event = (0, import_core2.tryBuildTelemetryEvent)({
|
|
605
|
+
name,
|
|
606
|
+
courseId: courseIdRef.current,
|
|
607
|
+
lessonId: lessonId ?? activeLessonIdRef.current,
|
|
608
|
+
sessionId: sessionIdRef.current,
|
|
609
|
+
attemptId: attemptIdRef.current,
|
|
610
|
+
user: userRef.current,
|
|
611
|
+
data
|
|
612
|
+
});
|
|
613
|
+
if (!event) return;
|
|
614
|
+
emitWithBridge(trackingRef.current, event);
|
|
615
|
+
},
|
|
616
|
+
[emitWithBridge]
|
|
617
|
+
);
|
|
612
618
|
const track = (0, import_react.useCallback)(
|
|
613
619
|
(name, data, opts) => {
|
|
614
|
-
const event =
|
|
620
|
+
const event = (0, import_core2.tryBuildTelemetryEvent)({
|
|
615
621
|
name,
|
|
616
622
|
courseId: courseIdRef.current,
|
|
617
623
|
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
@@ -629,7 +635,7 @@ function LessonkitProvider(props) {
|
|
|
629
635
|
if (!pendingCourseIdResetRef.current) return;
|
|
630
636
|
pendingCourseIdResetRef.current = false;
|
|
631
637
|
syncProgress();
|
|
632
|
-
if (!isTrackingActive(
|
|
638
|
+
if (!isTrackingActive(normalizedConfig.tracking)) return;
|
|
633
639
|
const sessionId = sessionIdRef.current;
|
|
634
640
|
const cid = courseIdRef.current;
|
|
635
641
|
void (async () => {
|
|
@@ -637,8 +643,8 @@ function LessonkitProvider(props) {
|
|
|
637
643
|
await trackingRef.current?.flush?.();
|
|
638
644
|
} catch {
|
|
639
645
|
}
|
|
640
|
-
if (!courseStartedEmittedToSinkRef.current
|
|
641
|
-
const emitted =
|
|
646
|
+
if (!courseStartedEmittedToSinkRef.current) {
|
|
647
|
+
const emitted = emitPendingCourseStarted({
|
|
642
648
|
pluginHost: pluginHostRef.current,
|
|
643
649
|
tracking: trackingRef.current,
|
|
644
650
|
xapi: xapiRef.current,
|
|
@@ -647,12 +653,13 @@ function LessonkitProvider(props) {
|
|
|
647
653
|
courseId: cid,
|
|
648
654
|
attemptId: attemptIdRef.current,
|
|
649
655
|
user: userRef.current,
|
|
650
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
656
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
657
|
+
extraSinks: extraSinksRef.current
|
|
651
658
|
});
|
|
652
659
|
courseStartedEmittedToSinkRef.current = emitted;
|
|
653
660
|
}
|
|
654
661
|
})();
|
|
655
|
-
}, [
|
|
662
|
+
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
|
|
656
663
|
const emitLessonCompleted = (0, import_react.useCallback)(
|
|
657
664
|
(lessonId, durationMs) => {
|
|
658
665
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -664,20 +671,27 @@ function LessonkitProvider(props) {
|
|
|
664
671
|
);
|
|
665
672
|
const completeLesson = (0, import_react.useCallback)(
|
|
666
673
|
(lessonId) => {
|
|
674
|
+
if (useV2Runtime && headlessRef.current) {
|
|
675
|
+
headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
|
|
676
|
+
syncProgress();
|
|
677
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
667
680
|
const result = progressRef.current.completeLesson(lessonId, Date.now());
|
|
668
681
|
if (!result.didComplete) return;
|
|
669
682
|
syncProgress();
|
|
670
683
|
emitLessonCompleted(lessonId, result.durationMs);
|
|
671
684
|
void Promise.resolve(trackingRef.current?.flush?.());
|
|
672
685
|
},
|
|
673
|
-
[syncProgress, emitLessonCompleted]
|
|
686
|
+
[syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
674
687
|
);
|
|
675
688
|
(0, import_react.useEffect)(() => {
|
|
676
689
|
return () => {
|
|
677
690
|
const client = trackingClientForUnmountRef.current;
|
|
691
|
+
const xapi2 = xapiRef.current;
|
|
678
692
|
void (async () => {
|
|
679
693
|
try {
|
|
680
|
-
await
|
|
694
|
+
await xapi2?.flush();
|
|
681
695
|
} catch {
|
|
682
696
|
}
|
|
683
697
|
try {
|
|
@@ -693,8 +707,19 @@ function LessonkitProvider(props) {
|
|
|
693
707
|
}, []);
|
|
694
708
|
const setActiveLesson = (0, import_react.useCallback)(
|
|
695
709
|
(lessonId) => {
|
|
710
|
+
if (useV2Runtime && headlessRef.current) {
|
|
711
|
+
headlessRef.current.setActiveLesson(lessonId, emitLifecycleEvent);
|
|
712
|
+
syncProgress();
|
|
713
|
+
void Promise.resolve(trackingRef.current?.flush?.());
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
696
716
|
const current = progressRef.current.getState();
|
|
697
717
|
if (current.activeLessonId === lessonId) return;
|
|
718
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
719
|
+
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
720
|
+
syncProgress();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
698
723
|
const previous = current.activeLessonId;
|
|
699
724
|
if (previous && previous !== lessonId) {
|
|
700
725
|
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
@@ -707,32 +732,58 @@ function LessonkitProvider(props) {
|
|
|
707
732
|
syncProgress();
|
|
708
733
|
track("lesson_started", { lessonId }, { lessonId });
|
|
709
734
|
},
|
|
710
|
-
[track, syncProgress, emitLessonCompleted]
|
|
735
|
+
[track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
711
736
|
);
|
|
712
737
|
const completeCourse = (0, import_react.useCallback)(() => {
|
|
738
|
+
if (useV2Runtime && headlessRef.current) {
|
|
739
|
+
headlessRef.current.completeCourse(emitLifecycleEvent);
|
|
740
|
+
syncProgress();
|
|
741
|
+
void trackingRef.current?.flush?.();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const current = progressRef.current.getState();
|
|
745
|
+
if (current.activeLessonId) {
|
|
746
|
+
const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
|
|
747
|
+
if (lessonResult.didComplete) {
|
|
748
|
+
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
713
751
|
const result = progressRef.current.completeCourse();
|
|
714
752
|
if (!result.didComplete) return;
|
|
715
753
|
syncProgress();
|
|
716
754
|
track("course_completed");
|
|
717
755
|
void trackingRef.current?.flush?.();
|
|
718
|
-
}, [track, syncProgress]);
|
|
719
|
-
const sessionUser =
|
|
720
|
-
const
|
|
721
|
-
|
|
756
|
+
}, [track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]);
|
|
757
|
+
const sessionUser = normalizedConfig.session?.user;
|
|
758
|
+
const sessionUserKey = (0, import_react.useMemo)(
|
|
759
|
+
() => sessionUser ? JSON.stringify(sessionUser) : "",
|
|
760
|
+
[sessionUser]
|
|
761
|
+
);
|
|
762
|
+
const sessionAttemptId = normalizedConfig.session?.attemptId;
|
|
763
|
+
const sessionConfiguredId = normalizedConfig.session?.sessionId;
|
|
764
|
+
(0, import_react.useEffect)(() => {
|
|
765
|
+
if (useV2Runtime && headlessRef.current) {
|
|
766
|
+
headlessRef.current.updateConfig({
|
|
767
|
+
courseId: normalizedCourseId,
|
|
768
|
+
session: normalizedConfig.session
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}, [useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey, normalizedConfig.session]);
|
|
722
772
|
(0, import_react.useEffect)(() => {
|
|
723
773
|
if (!pluginHost) return;
|
|
724
774
|
const ctx = buildPluginContext({
|
|
725
775
|
courseId: courseIdRef.current,
|
|
726
776
|
sessionId: sessionIdRef.current,
|
|
727
|
-
attemptId: attemptIdRef.current
|
|
777
|
+
attemptId: attemptIdRef.current,
|
|
778
|
+
user: userRef.current
|
|
728
779
|
});
|
|
729
780
|
pluginHost.setupAll(ctx);
|
|
730
781
|
return () => {
|
|
731
782
|
pluginHost.disposeAll();
|
|
732
783
|
};
|
|
733
|
-
}, [pluginHost,
|
|
784
|
+
}, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
734
785
|
(0, import_react.useEffect)(() => {
|
|
735
|
-
const nextConfigured =
|
|
786
|
+
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
736
787
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
737
788
|
if (nextConfigured === prevConfigured) return;
|
|
738
789
|
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
@@ -740,23 +791,23 @@ function LessonkitProvider(props) {
|
|
|
740
791
|
if (nextConfigured) {
|
|
741
792
|
const fromIds = /* @__PURE__ */ new Set();
|
|
742
793
|
if (prevConfigured) fromIds.add(prevConfigured);
|
|
743
|
-
const tabId = getTabSessionId(defaultStorage);
|
|
794
|
+
const tabId = (0, import_core5.getTabSessionId)(defaultStorage);
|
|
744
795
|
if (tabId) fromIds.add(tabId);
|
|
745
796
|
for (const fromId of fromIds) {
|
|
746
797
|
if (fromId !== nextConfigured) {
|
|
747
|
-
migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
|
|
798
|
+
(0, import_core5.migrateCourseStartedMark)(defaultStorage, fromId, nextConfigured, cid);
|
|
748
799
|
}
|
|
749
800
|
}
|
|
750
801
|
sessionIdRef.current = nextConfigured;
|
|
751
802
|
} else if (prevConfigured) {
|
|
752
|
-
const nextAuto = resolveSessionId(defaultStorage, void 0);
|
|
753
|
-
migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
|
|
803
|
+
const nextAuto = (0, import_core5.resolveSessionId)(defaultStorage, void 0);
|
|
804
|
+
(0, import_core5.migrateCourseStartedMark)(defaultStorage, prevConfigured, nextAuto, cid);
|
|
754
805
|
sessionIdRef.current = nextAuto;
|
|
755
806
|
}
|
|
756
|
-
}, [sessionConfiguredId,
|
|
807
|
+
}, [sessionConfiguredId, normalizedCourseId]);
|
|
757
808
|
const runtime = (0, import_react.useMemo)(
|
|
758
809
|
() => ({
|
|
759
|
-
config,
|
|
810
|
+
config: normalizedConfig,
|
|
760
811
|
tracking,
|
|
761
812
|
xapi,
|
|
762
813
|
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
@@ -768,7 +819,7 @@ function LessonkitProvider(props) {
|
|
|
768
819
|
plugins: pluginHost
|
|
769
820
|
}),
|
|
770
821
|
[
|
|
771
|
-
|
|
822
|
+
normalizedConfig,
|
|
772
823
|
tracking,
|
|
773
824
|
xapi,
|
|
774
825
|
progress,
|
|
@@ -782,13 +833,21 @@ function LessonkitProvider(props) {
|
|
|
782
833
|
sessionConfiguredId
|
|
783
834
|
]
|
|
784
835
|
);
|
|
836
|
+
return runtime;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/context.tsx
|
|
840
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
841
|
+
var LessonkitContext = (0, import_react2.createContext)(null);
|
|
842
|
+
function LessonkitProvider(props) {
|
|
843
|
+
const runtime = useLessonkitProviderRuntime(props.config);
|
|
785
844
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LessonkitContext.Provider, { value: runtime, children: props.children });
|
|
786
845
|
}
|
|
787
846
|
|
|
788
847
|
// src/hooks.ts
|
|
789
|
-
var
|
|
848
|
+
var import_react3 = require("react");
|
|
790
849
|
function useLessonkit() {
|
|
791
|
-
const ctx = (0,
|
|
850
|
+
const ctx = (0, import_react3.useContext)(LessonkitContext);
|
|
792
851
|
if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
|
|
793
852
|
return ctx;
|
|
794
853
|
}
|
|
@@ -798,52 +857,80 @@ function useProgress() {
|
|
|
798
857
|
}
|
|
799
858
|
function useTracking() {
|
|
800
859
|
const { track } = useLessonkit();
|
|
801
|
-
return (0,
|
|
860
|
+
return (0, import_react3.useMemo)(() => ({ track }), [track]);
|
|
802
861
|
}
|
|
803
862
|
function useCompletion() {
|
|
804
863
|
const { completeLesson, completeCourse } = useLessonkit();
|
|
805
|
-
return (0,
|
|
864
|
+
return (0, import_react3.useMemo)(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
806
865
|
}
|
|
807
|
-
function useQuizState() {
|
|
866
|
+
function useQuizState(enclosingLessonId) {
|
|
808
867
|
const { track } = useLessonkit();
|
|
809
|
-
|
|
868
|
+
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
869
|
+
return (0, import_react3.useMemo)(
|
|
810
870
|
() => ({
|
|
811
871
|
answer: (opts) => {
|
|
812
|
-
track("quiz_answered", opts);
|
|
872
|
+
track("quiz_answered", opts, trackOpts);
|
|
813
873
|
},
|
|
814
874
|
complete: (opts) => {
|
|
815
|
-
track("quiz_completed", opts);
|
|
875
|
+
track("quiz_completed", opts, trackOpts);
|
|
816
876
|
}
|
|
817
877
|
}),
|
|
818
|
-
[track]
|
|
878
|
+
[track, enclosingLessonId]
|
|
819
879
|
);
|
|
820
880
|
}
|
|
821
881
|
|
|
882
|
+
// src/lessonContext.tsx
|
|
883
|
+
var import_react4 = require("react");
|
|
884
|
+
var LessonContext = (0, import_react4.createContext)(void 0);
|
|
885
|
+
function useEnclosingLessonId() {
|
|
886
|
+
return (0, import_react4.useContext)(LessonContext);
|
|
887
|
+
}
|
|
888
|
+
|
|
822
889
|
// src/runtime/validateComponentId.ts
|
|
823
|
-
var
|
|
824
|
-
|
|
825
|
-
function isDevEnvironment2() {
|
|
890
|
+
var import_core9 = require("@lessonkit/core");
|
|
891
|
+
function isDevEnvironment3() {
|
|
826
892
|
const g = globalThis;
|
|
827
893
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
828
894
|
}
|
|
829
|
-
function
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
895
|
+
function normalizeComponentId(id, path) {
|
|
896
|
+
return (0, import_core9.assertValidId)(id, path);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/runtime/lessonMountRegistry.ts
|
|
900
|
+
var mountCounts = /* @__PURE__ */ new Map();
|
|
901
|
+
var warnedConcurrentLessons = false;
|
|
902
|
+
function registerLessonMount(lessonId) {
|
|
903
|
+
if (isDevEnvironment3() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
|
|
904
|
+
warnedConcurrentLessons = true;
|
|
905
|
+
console.warn(
|
|
906
|
+
"[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."
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
mountCounts.set(lessonId, (mountCounts.get(lessonId) ?? 0) + 1);
|
|
910
|
+
return () => {
|
|
911
|
+
const next = (mountCounts.get(lessonId) ?? 1) - 1;
|
|
912
|
+
if (next <= 0) {
|
|
913
|
+
mountCounts.delete(lessonId);
|
|
914
|
+
} else {
|
|
915
|
+
mountCounts.set(lessonId, next);
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function getLessonMountCount(lessonId) {
|
|
920
|
+
return mountCounts.get(lessonId) ?? 0;
|
|
838
921
|
}
|
|
839
922
|
|
|
840
923
|
// src/components.tsx
|
|
841
924
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
925
|
+
var warnedQuizOutsideLesson = false;
|
|
926
|
+
function resetQuizWarningsForTests() {
|
|
927
|
+
warnedQuizOutsideLesson = false;
|
|
928
|
+
}
|
|
842
929
|
function Course(props) {
|
|
843
|
-
|
|
844
|
-
const providerConfig = (0,
|
|
845
|
-
() => ({ ...props.config, courseId
|
|
846
|
-
[props.config,
|
|
930
|
+
const courseId = (0, import_react5.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
931
|
+
const providerConfig = (0, import_react5.useMemo)(
|
|
932
|
+
() => ({ ...props.config, courseId }),
|
|
933
|
+
[props.config, courseId]
|
|
847
934
|
);
|
|
848
935
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": props.title, children: [
|
|
849
936
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { children: props.title }),
|
|
@@ -851,41 +938,64 @@ function Course(props) {
|
|
|
851
938
|
] }) });
|
|
852
939
|
}
|
|
853
940
|
function Lesson(props) {
|
|
854
|
-
|
|
941
|
+
const lessonId = (0, import_react5.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
942
|
+
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
855
943
|
const { setActiveLesson, config } = useLessonkit();
|
|
856
944
|
const { completeLesson } = useCompletion();
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
945
|
+
const lessonMountGenerationRef = (0, import_react5.useRef)(0);
|
|
946
|
+
(0, import_react5.useEffect)(() => {
|
|
947
|
+
const unregister = registerLessonMount(lessonId);
|
|
860
948
|
const generation = ++lessonMountGenerationRef.current;
|
|
861
|
-
setActiveLesson(
|
|
949
|
+
setActiveLesson(lessonId);
|
|
862
950
|
return () => {
|
|
863
|
-
|
|
951
|
+
unregister();
|
|
952
|
+
if (getLessonMountCount(lessonId) > 0) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (!autoComplete) return;
|
|
864
956
|
queueMicrotask(() => {
|
|
865
957
|
if (lessonMountGenerationRef.current !== generation) return;
|
|
866
958
|
completeLesson(lessonId);
|
|
867
959
|
});
|
|
868
960
|
};
|
|
869
|
-
}, [
|
|
870
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { "aria-label": props.title, children: [
|
|
961
|
+
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
962
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { "aria-label": props.title, children: [
|
|
871
963
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: props.title }),
|
|
872
964
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: props.children })
|
|
873
|
-
] });
|
|
965
|
+
] }) });
|
|
874
966
|
}
|
|
875
967
|
function Scenario(props) {
|
|
876
|
-
|
|
877
|
-
|
|
968
|
+
const blockId = (0, import_react5.useMemo)(
|
|
969
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
970
|
+
[props.blockId]
|
|
971
|
+
);
|
|
972
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
878
973
|
}
|
|
879
974
|
function Reflection(props) {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
975
|
+
const blockId = (0, import_react5.useMemo)(
|
|
976
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
977
|
+
[props.blockId]
|
|
978
|
+
);
|
|
979
|
+
const promptId = (0, import_react5.useId)();
|
|
980
|
+
const hintId = (0, import_react5.useId)();
|
|
981
|
+
const [internalValue, setInternalValue] = (0, import_react5.useState)("");
|
|
982
|
+
const isControlled = props.value !== void 0;
|
|
983
|
+
const value = isControlled ? props.value : internalValue;
|
|
984
|
+
const handleChange = (event) => {
|
|
985
|
+
if (!isControlled) setInternalValue(event.target.value);
|
|
986
|
+
props.onChange?.(event.target.value);
|
|
987
|
+
};
|
|
988
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
883
989
|
props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: promptId, children: props.prompt }) : null,
|
|
990
|
+
props.hint ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: hintId, style: import_accessibility.visuallyHiddenStyle, children: props.hint }) : null,
|
|
884
991
|
props.children,
|
|
885
992
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
886
993
|
"textarea",
|
|
887
994
|
{
|
|
995
|
+
value,
|
|
996
|
+
onChange: handleChange,
|
|
888
997
|
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
998
|
+
"aria-describedby": props.hint ? hintId : void 0,
|
|
889
999
|
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
890
1000
|
}
|
|
891
1001
|
)
|
|
@@ -898,23 +1008,41 @@ function KnowledgeCheck(props) {
|
|
|
898
1008
|
checkId: props.checkId,
|
|
899
1009
|
question: props.question,
|
|
900
1010
|
choices: props.choices,
|
|
901
|
-
answer: props.answer
|
|
1011
|
+
answer: props.answer,
|
|
1012
|
+
passingScore: props.passingScore
|
|
902
1013
|
}
|
|
903
1014
|
);
|
|
904
1015
|
}
|
|
905
1016
|
function Quiz(props) {
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1017
|
+
const checkId = (0, import_react5.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1018
|
+
const enclosingLessonId = useEnclosingLessonId();
|
|
1019
|
+
const missingLesson = enclosingLessonId === void 0;
|
|
1020
|
+
(0, import_react5.useEffect)(() => {
|
|
1021
|
+
if (!missingLesson || isDevEnvironment3()) return;
|
|
1022
|
+
if (!warnedQuizOutsideLesson) {
|
|
1023
|
+
warnedQuizOutsideLesson = true;
|
|
1024
|
+
console.error(
|
|
1025
|
+
"[lessonkit] <Quiz> must be wrapped in <Lesson>; quiz telemetry will not be emitted."
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
}, [missingLesson]);
|
|
1029
|
+
if (missingLesson && isDevEnvironment3()) {
|
|
1030
|
+
throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
|
|
1031
|
+
}
|
|
1032
|
+
const quiz = useQuizState(enclosingLessonId);
|
|
1033
|
+
const { plugins, config, session } = useLessonkit();
|
|
1034
|
+
const [selected, setSelected] = (0, import_react5.useState)(null);
|
|
1035
|
+
const [selectionCorrect, setSelectionCorrect] = (0, import_react5.useState)(null);
|
|
1036
|
+
const [quizPassed, setQuizPassed] = (0, import_react5.useState)(false);
|
|
1037
|
+
const completedRef = (0, import_react5.useRef)(false);
|
|
1038
|
+
const questionId = (0, import_react5.useId)();
|
|
1039
|
+
const choicesKey = props.choices.join("\0");
|
|
1040
|
+
(0, import_react5.useEffect)(() => {
|
|
914
1041
|
completedRef.current = false;
|
|
1042
|
+
setQuizPassed(false);
|
|
915
1043
|
setSelected(null);
|
|
916
1044
|
setSelectionCorrect(null);
|
|
917
|
-
}, [
|
|
1045
|
+
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
|
|
918
1046
|
const isChoiceCorrect = (choice, custom) => {
|
|
919
1047
|
if (!custom) return choice === props.answer;
|
|
920
1048
|
if (custom.passed !== void 0) return custom.passed;
|
|
@@ -923,7 +1051,11 @@ function Quiz(props) {
|
|
|
923
1051
|
}
|
|
924
1052
|
return choice === props.answer;
|
|
925
1053
|
};
|
|
926
|
-
|
|
1054
|
+
if (missingLesson) {
|
|
1055
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1056
|
+
}
|
|
1057
|
+
const passed = quizPassed;
|
|
1058
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
927
1059
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
|
|
928
1060
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
|
|
929
1061
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
|
|
@@ -935,17 +1067,21 @@ function Quiz(props) {
|
|
|
935
1067
|
name: questionId,
|
|
936
1068
|
value: c,
|
|
937
1069
|
checked: selected === c,
|
|
1070
|
+
disabled: passed,
|
|
1071
|
+
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
938
1072
|
onChange: () => {
|
|
1073
|
+
if (passed) return;
|
|
939
1074
|
setSelected(c);
|
|
940
1075
|
const pluginCtx = buildPluginContext({
|
|
941
1076
|
courseId: config.courseId,
|
|
942
1077
|
sessionId: session.sessionId,
|
|
943
|
-
attemptId: session.attemptId
|
|
1078
|
+
attemptId: session.attemptId,
|
|
1079
|
+
user: session.user
|
|
944
1080
|
});
|
|
945
1081
|
const custom = plugins?.scoreAssessment(
|
|
946
1082
|
{
|
|
947
|
-
checkId
|
|
948
|
-
lessonId:
|
|
1083
|
+
checkId,
|
|
1084
|
+
lessonId: enclosingLessonId,
|
|
949
1085
|
response: c
|
|
950
1086
|
},
|
|
951
1087
|
pluginCtx
|
|
@@ -953,18 +1089,20 @@ function Quiz(props) {
|
|
|
953
1089
|
const correct = isChoiceCorrect(c, custom);
|
|
954
1090
|
setSelectionCorrect(correct);
|
|
955
1091
|
quiz.answer({
|
|
956
|
-
checkId
|
|
1092
|
+
checkId,
|
|
957
1093
|
question: props.question,
|
|
958
1094
|
choice: c,
|
|
959
1095
|
correct
|
|
960
1096
|
});
|
|
961
1097
|
if (correct && !completedRef.current) {
|
|
962
1098
|
completedRef.current = true;
|
|
1099
|
+
setQuizPassed(true);
|
|
1100
|
+
const maxScore = custom?.maxScore ?? 1;
|
|
963
1101
|
quiz.complete({
|
|
964
|
-
checkId
|
|
1102
|
+
checkId,
|
|
965
1103
|
score: custom?.score ?? 1,
|
|
966
|
-
maxScore
|
|
967
|
-
passingScore:
|
|
1104
|
+
maxScore,
|
|
1105
|
+
passingScore: props.passingScore ?? maxScore
|
|
968
1106
|
});
|
|
969
1107
|
}
|
|
970
1108
|
}
|
|
@@ -976,20 +1114,40 @@ function Quiz(props) {
|
|
|
976
1114
|
selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
977
1115
|
] });
|
|
978
1116
|
}
|
|
979
|
-
function ProgressTracker() {
|
|
1117
|
+
function ProgressTracker(props) {
|
|
980
1118
|
const { progress } = useLessonkit();
|
|
981
1119
|
const completed = progress.completedLessonIds.size;
|
|
982
|
-
|
|
1120
|
+
if (props.totalLessons != null) {
|
|
1121
|
+
const total = props.totalLessons;
|
|
1122
|
+
const displayed = Math.min(completed, total);
|
|
1123
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1124
|
+
"div",
|
|
1125
|
+
{
|
|
1126
|
+
role: "progressbar",
|
|
1127
|
+
"aria-valuemin": 0,
|
|
1128
|
+
"aria-valuemax": total,
|
|
1129
|
+
"aria-valuenow": displayed,
|
|
1130
|
+
"aria-label": "Lessons completed",
|
|
1131
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
|
|
1132
|
+
"Lessons completed: ",
|
|
1133
|
+
displayed,
|
|
1134
|
+
" of ",
|
|
1135
|
+
total
|
|
1136
|
+
] })
|
|
1137
|
+
}
|
|
1138
|
+
) });
|
|
1139
|
+
}
|
|
1140
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
|
|
983
1141
|
"Lessons completed: ",
|
|
984
1142
|
completed
|
|
985
1143
|
] }) });
|
|
986
1144
|
}
|
|
987
1145
|
|
|
988
1146
|
// src/index.tsx
|
|
989
|
-
var
|
|
1147
|
+
var import_core10 = require("@lessonkit/core");
|
|
990
1148
|
|
|
991
1149
|
// src/theme/ThemeProvider.tsx
|
|
992
|
-
var
|
|
1150
|
+
var import_react6 = __toESM(require("react"), 1);
|
|
993
1151
|
var import_themes = require("@lessonkit/themes");
|
|
994
1152
|
|
|
995
1153
|
// src/theme/applyCssVariables.ts
|
|
@@ -1009,8 +1167,8 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
1009
1167
|
|
|
1010
1168
|
// src/theme/ThemeProvider.tsx
|
|
1011
1169
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1012
|
-
var ThemeContext = (0,
|
|
1013
|
-
var useIsoLayoutEffect2 = typeof window !== "undefined" ?
|
|
1170
|
+
var ThemeContext = (0, import_react6.createContext)(null);
|
|
1171
|
+
var useIsoLayoutEffect2 = typeof window !== "undefined" ? import_react6.useLayoutEffect : import_react6.default.useEffect;
|
|
1014
1172
|
function getSystemMode() {
|
|
1015
1173
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
1016
1174
|
return "light";
|
|
@@ -1028,7 +1186,7 @@ function ThemeProvider(props) {
|
|
|
1028
1186
|
const preset = props.preset ?? "default";
|
|
1029
1187
|
const mode = props.mode ?? "light";
|
|
1030
1188
|
const targetKind = props.target ?? "document";
|
|
1031
|
-
const [resolvedMode, setResolvedMode] = (0,
|
|
1189
|
+
const [resolvedMode, setResolvedMode] = (0, import_react6.useState)(
|
|
1032
1190
|
() => mode === "system" ? getSystemMode() : mode
|
|
1033
1191
|
);
|
|
1034
1192
|
useIsoLayoutEffect2(() => {
|
|
@@ -1044,20 +1202,20 @@ function ThemeProvider(props) {
|
|
|
1044
1202
|
return () => mq.removeEventListener("change", onChange);
|
|
1045
1203
|
}, [mode]);
|
|
1046
1204
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
1047
|
-
const effectiveTheme = (0,
|
|
1205
|
+
const effectiveTheme = (0, import_react6.useMemo)(() => {
|
|
1048
1206
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
1049
1207
|
const base = preset === "default" ? modeBase : preset === "brand" ? (0, import_themes.mergeThemes)(modeBase, import_themes.brandThemeOverrides) : (0, import_themes.mergeThemes)(modeBase, (0, import_themes.getPresetTheme)(preset));
|
|
1050
1208
|
return (0, import_themes.mergeThemes)(base, props.theme ?? {});
|
|
1051
1209
|
}, [preset, mode, dataTheme, props.theme]);
|
|
1052
|
-
const hostRef = (0,
|
|
1053
|
-
const appliedKeysRef = (0,
|
|
1210
|
+
const hostRef = (0, import_react6.useRef)(null);
|
|
1211
|
+
const appliedKeysRef = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
|
|
1054
1212
|
useIsoLayoutEffect2(() => {
|
|
1055
1213
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
1056
1214
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
1057
1215
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
1058
1216
|
}
|
|
1059
1217
|
}, [targetKind, dataTheme]);
|
|
1060
|
-
const inject = (0,
|
|
1218
|
+
const inject = (0, import_react6.useCallback)(() => {
|
|
1061
1219
|
const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
|
|
1062
1220
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
1063
1221
|
if (!el) return;
|
|
@@ -1074,7 +1232,7 @@ function ThemeProvider(props) {
|
|
|
1074
1232
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
1075
1233
|
};
|
|
1076
1234
|
}, [inject, targetKind]);
|
|
1077
|
-
const value = (0,
|
|
1235
|
+
const value = (0, import_react6.useMemo)(
|
|
1078
1236
|
() => ({
|
|
1079
1237
|
theme: effectiveTheme,
|
|
1080
1238
|
preset,
|
|
@@ -1089,7 +1247,7 @@ function ThemeProvider(props) {
|
|
|
1089
1247
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
1090
1248
|
}
|
|
1091
1249
|
function useTheme() {
|
|
1092
|
-
const ctx = (0,
|
|
1250
|
+
const ctx = (0, import_react6.useContext)(ThemeContext);
|
|
1093
1251
|
if (!ctx) {
|
|
1094
1252
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
1095
1253
|
}
|
|
@@ -1136,6 +1294,12 @@ var BLOCK_CATALOG = [
|
|
|
1136
1294
|
props: [
|
|
1137
1295
|
{ name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
|
|
1138
1296
|
{ name: "lessonId", type: "LessonId", required: true, description: "Stable lesson identifier for telemetry and packaging." },
|
|
1297
|
+
{
|
|
1298
|
+
name: "autoCompleteOnUnmount",
|
|
1299
|
+
type: "boolean",
|
|
1300
|
+
required: false,
|
|
1301
|
+
description: "When false, unmount does not emit lesson_completed (default true)."
|
|
1302
|
+
},
|
|
1139
1303
|
{ name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
|
|
1140
1304
|
],
|
|
1141
1305
|
requiredIds: ["lessonId"],
|
|
@@ -1188,6 +1352,9 @@ var BLOCK_CATALOG = [
|
|
|
1188
1352
|
props: [
|
|
1189
1353
|
{ name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
|
|
1190
1354
|
{ name: "prompt", type: "string", required: false, description: "Reflection question or instruction." },
|
|
1355
|
+
{ name: "hint", type: "string", required: false, description: "Optional hint linked via aria-describedby." },
|
|
1356
|
+
{ name: "value", type: "string", required: false, description: "Controlled textarea value." },
|
|
1357
|
+
{ name: "onChange", type: "(value: string) => void", required: false, description: "Called when the learner edits the textarea." },
|
|
1191
1358
|
{ name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
|
|
1192
1359
|
],
|
|
1193
1360
|
requiredIds: [],
|
|
@@ -1206,6 +1373,7 @@ var BLOCK_CATALOG = [
|
|
|
1206
1373
|
},
|
|
1207
1374
|
telemetry: {
|
|
1208
1375
|
emits: [],
|
|
1376
|
+
requiresActiveLesson: true,
|
|
1209
1377
|
manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
|
|
1210
1378
|
}
|
|
1211
1379
|
},
|
|
@@ -1243,7 +1411,14 @@ var BLOCK_CATALOG = [
|
|
|
1243
1411
|
type: "ProgressTracker",
|
|
1244
1412
|
category: "chrome",
|
|
1245
1413
|
description: "Displays count of completed lessons from runtime progress state.",
|
|
1246
|
-
props: [
|
|
1414
|
+
props: [
|
|
1415
|
+
{
|
|
1416
|
+
name: "totalLessons",
|
|
1417
|
+
type: "number",
|
|
1418
|
+
required: false,
|
|
1419
|
+
description: "When set, renders role=progressbar with aria-valuenow/max."
|
|
1420
|
+
}
|
|
1421
|
+
],
|
|
1247
1422
|
requiredIds: [],
|
|
1248
1423
|
parentConstraints: ["Course"],
|
|
1249
1424
|
a11y: {
|
|
@@ -1296,9 +1471,15 @@ function getBlockCatalogEntry(type) {
|
|
|
1296
1471
|
ThemeProvider,
|
|
1297
1472
|
blockCatalogVersion,
|
|
1298
1473
|
buildBlockCatalog,
|
|
1299
|
-
|
|
1300
|
-
|
|
1474
|
+
buildTelemetryEvent,
|
|
1475
|
+
createLessonkitRuntime,
|
|
1476
|
+
createPluginRegistry,
|
|
1477
|
+
createTelemetryPipeline,
|
|
1478
|
+
defineAssessmentPlugin,
|
|
1479
|
+
defineLifecyclePlugin,
|
|
1480
|
+
defineTelemetryPlugin,
|
|
1301
1481
|
getBlockCatalogEntry,
|
|
1482
|
+
resetQuizWarningsForTests,
|
|
1302
1483
|
useCompletion,
|
|
1303
1484
|
useLessonkit,
|
|
1304
1485
|
useProgress,
|