@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.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,71 +214,207 @@ 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
|
+
}
|
|
395
|
+
pendingCourseIdResetRef.current = true;
|
|
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
|
+
}
|
|
445
412
|
pendingCourseIdResetRef.current = true;
|
|
446
413
|
courseStartedEmittedToSinkRef.current = false;
|
|
447
414
|
}
|
|
415
|
+
if (useV2Runtime && headlessRef.current) {
|
|
416
|
+
progressRef.current = headlessRef.current.progress;
|
|
417
|
+
}
|
|
448
418
|
const [progress, setProgress] = (0, import_react.useState)(() => progressRef.current.getState());
|
|
449
419
|
const syncProgress = (0, import_react.useCallback)(() => {
|
|
450
420
|
setProgress(progressRef.current.getState());
|
|
@@ -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,46 +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 =
|
|
521
|
+
const baseSink = normalizedConfig.tracking?.sink;
|
|
522
|
+
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
523
|
+
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
544
524
|
const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
|
|
545
|
-
const batchSink = pluginHostRef.current &&
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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;
|
|
552
539
|
const next = createTrackingClientFromConfig({
|
|
553
|
-
tracking: { ...
|
|
540
|
+
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
554
541
|
});
|
|
555
542
|
trackingRef.current = next;
|
|
556
543
|
trackingClientForUnmountRef.current = next;
|
|
557
544
|
setTracking(next);
|
|
558
545
|
const sessionId = sessionIdRef.current;
|
|
559
546
|
const cid = courseIdRef.current;
|
|
560
|
-
const trackingActive = isTrackingActive(
|
|
547
|
+
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
561
548
|
if (!trackingActive) {
|
|
562
549
|
courseStartedEmittedToSinkRef.current = false;
|
|
563
|
-
} else if (!courseStartedEmittedToSinkRef.current
|
|
564
|
-
const emitted =
|
|
550
|
+
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
551
|
+
const emitted = emitPendingCourseStarted({
|
|
565
552
|
pluginHost: pluginHostRef.current,
|
|
566
553
|
tracking: next,
|
|
567
554
|
xapi: xapiRef.current,
|
|
@@ -570,8 +557,12 @@ function LessonkitProvider(props) {
|
|
|
570
557
|
courseId: cid,
|
|
571
558
|
attemptId: attemptIdRef.current,
|
|
572
559
|
user: userRef.current,
|
|
573
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
560
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
561
|
+
extraSinks: extraSinksRef.current
|
|
574
562
|
});
|
|
563
|
+
if (emitted) {
|
|
564
|
+
(0, import_core5.markCourseStartedEmittedToTracking)(defaultStorage, sessionId, cid);
|
|
565
|
+
}
|
|
575
566
|
courseStartedEmittedToSinkRef.current = emitted;
|
|
576
567
|
} else if (trackingActive) {
|
|
577
568
|
courseStartedEmittedToSinkRef.current = true;
|
|
@@ -588,8 +579,8 @@ function LessonkitProvider(props) {
|
|
|
588
579
|
batchEnabled,
|
|
589
580
|
batchFlushIntervalMs,
|
|
590
581
|
batchMaxBatchSize,
|
|
591
|
-
|
|
592
|
-
|
|
582
|
+
normalizedConfig.plugins,
|
|
583
|
+
normalizedCourseId,
|
|
593
584
|
buildCurrentPluginCtx
|
|
594
585
|
]);
|
|
595
586
|
const emitWithBridge = (0, import_react.useCallback)((trackingClient, event) => {
|
|
@@ -601,14 +592,32 @@ function LessonkitProvider(props) {
|
|
|
601
592
|
pluginCtx: buildPluginContext({
|
|
602
593
|
courseId: courseIdRef.current,
|
|
603
594
|
sessionId: sessionIdRef.current,
|
|
604
|
-
attemptId: attemptIdRef.current
|
|
595
|
+
attemptId: attemptIdRef.current,
|
|
596
|
+
user: userRef.current
|
|
605
597
|
}),
|
|
606
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
598
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
599
|
+
extraSinks: extraSinksRef.current
|
|
607
600
|
});
|
|
608
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
|
+
);
|
|
609
618
|
const track = (0, import_react.useCallback)(
|
|
610
619
|
(name, data, opts) => {
|
|
611
|
-
const event =
|
|
620
|
+
const event = (0, import_core2.tryBuildTelemetryEvent)({
|
|
612
621
|
name,
|
|
613
622
|
courseId: courseIdRef.current,
|
|
614
623
|
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
@@ -626,7 +635,7 @@ function LessonkitProvider(props) {
|
|
|
626
635
|
if (!pendingCourseIdResetRef.current) return;
|
|
627
636
|
pendingCourseIdResetRef.current = false;
|
|
628
637
|
syncProgress();
|
|
629
|
-
if (!isTrackingActive(
|
|
638
|
+
if (!isTrackingActive(normalizedConfig.tracking)) return;
|
|
630
639
|
const sessionId = sessionIdRef.current;
|
|
631
640
|
const cid = courseIdRef.current;
|
|
632
641
|
void (async () => {
|
|
@@ -634,8 +643,8 @@ function LessonkitProvider(props) {
|
|
|
634
643
|
await trackingRef.current?.flush?.();
|
|
635
644
|
} catch {
|
|
636
645
|
}
|
|
637
|
-
if (!courseStartedEmittedToSinkRef.current
|
|
638
|
-
const emitted =
|
|
646
|
+
if (!courseStartedEmittedToSinkRef.current) {
|
|
647
|
+
const emitted = emitPendingCourseStarted({
|
|
639
648
|
pluginHost: pluginHostRef.current,
|
|
640
649
|
tracking: trackingRef.current,
|
|
641
650
|
xapi: xapiRef.current,
|
|
@@ -644,12 +653,13 @@ function LessonkitProvider(props) {
|
|
|
644
653
|
courseId: cid,
|
|
645
654
|
attemptId: attemptIdRef.current,
|
|
646
655
|
user: userRef.current,
|
|
647
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
656
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
657
|
+
extraSinks: extraSinksRef.current
|
|
648
658
|
});
|
|
649
659
|
courseStartedEmittedToSinkRef.current = emitted;
|
|
650
660
|
}
|
|
651
661
|
})();
|
|
652
|
-
}, [
|
|
662
|
+
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
|
|
653
663
|
const emitLessonCompleted = (0, import_react.useCallback)(
|
|
654
664
|
(lessonId, durationMs) => {
|
|
655
665
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -661,13 +671,19 @@ function LessonkitProvider(props) {
|
|
|
661
671
|
);
|
|
662
672
|
const completeLesson = (0, import_react.useCallback)(
|
|
663
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
|
+
}
|
|
664
680
|
const result = progressRef.current.completeLesson(lessonId, Date.now());
|
|
665
681
|
if (!result.didComplete) return;
|
|
666
682
|
syncProgress();
|
|
667
683
|
emitLessonCompleted(lessonId, result.durationMs);
|
|
668
684
|
void Promise.resolve(trackingRef.current?.flush?.());
|
|
669
685
|
},
|
|
670
|
-
[syncProgress, emitLessonCompleted]
|
|
686
|
+
[syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
671
687
|
);
|
|
672
688
|
(0, import_react.useEffect)(() => {
|
|
673
689
|
return () => {
|
|
@@ -691,8 +707,19 @@ function LessonkitProvider(props) {
|
|
|
691
707
|
}, []);
|
|
692
708
|
const setActiveLesson = (0, import_react.useCallback)(
|
|
693
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
|
+
}
|
|
694
716
|
const current = progressRef.current.getState();
|
|
695
717
|
if (current.activeLessonId === lessonId) return;
|
|
718
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
719
|
+
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
720
|
+
syncProgress();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
696
723
|
const previous = current.activeLessonId;
|
|
697
724
|
if (previous && previous !== lessonId) {
|
|
698
725
|
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
@@ -705,9 +732,15 @@ function LessonkitProvider(props) {
|
|
|
705
732
|
syncProgress();
|
|
706
733
|
track("lesson_started", { lessonId }, { lessonId });
|
|
707
734
|
},
|
|
708
|
-
[track, syncProgress, emitLessonCompleted]
|
|
735
|
+
[track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
709
736
|
);
|
|
710
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
|
+
}
|
|
711
744
|
const current = progressRef.current.getState();
|
|
712
745
|
if (current.activeLessonId) {
|
|
713
746
|
const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
|
|
@@ -720,24 +753,37 @@ function LessonkitProvider(props) {
|
|
|
720
753
|
syncProgress();
|
|
721
754
|
track("course_completed");
|
|
722
755
|
void trackingRef.current?.flush?.();
|
|
723
|
-
}, [track, syncProgress, emitLessonCompleted]);
|
|
724
|
-
const sessionUser =
|
|
725
|
-
const
|
|
726
|
-
|
|
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]);
|
|
727
772
|
(0, import_react.useEffect)(() => {
|
|
728
773
|
if (!pluginHost) return;
|
|
729
774
|
const ctx = buildPluginContext({
|
|
730
775
|
courseId: courseIdRef.current,
|
|
731
776
|
sessionId: sessionIdRef.current,
|
|
732
|
-
attemptId: attemptIdRef.current
|
|
777
|
+
attemptId: attemptIdRef.current,
|
|
778
|
+
user: userRef.current
|
|
733
779
|
});
|
|
734
780
|
pluginHost.setupAll(ctx);
|
|
735
781
|
return () => {
|
|
736
782
|
pluginHost.disposeAll();
|
|
737
783
|
};
|
|
738
|
-
}, [pluginHost,
|
|
784
|
+
}, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
739
785
|
(0, import_react.useEffect)(() => {
|
|
740
|
-
const nextConfigured =
|
|
786
|
+
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
741
787
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
742
788
|
if (nextConfigured === prevConfigured) return;
|
|
743
789
|
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
@@ -745,23 +791,23 @@ function LessonkitProvider(props) {
|
|
|
745
791
|
if (nextConfigured) {
|
|
746
792
|
const fromIds = /* @__PURE__ */ new Set();
|
|
747
793
|
if (prevConfigured) fromIds.add(prevConfigured);
|
|
748
|
-
const tabId = getTabSessionId(defaultStorage);
|
|
794
|
+
const tabId = (0, import_core5.getTabSessionId)(defaultStorage);
|
|
749
795
|
if (tabId) fromIds.add(tabId);
|
|
750
796
|
for (const fromId of fromIds) {
|
|
751
797
|
if (fromId !== nextConfigured) {
|
|
752
|
-
migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
|
|
798
|
+
(0, import_core5.migrateCourseStartedMark)(defaultStorage, fromId, nextConfigured, cid);
|
|
753
799
|
}
|
|
754
800
|
}
|
|
755
801
|
sessionIdRef.current = nextConfigured;
|
|
756
802
|
} else if (prevConfigured) {
|
|
757
|
-
const nextAuto = resolveSessionId(defaultStorage, void 0);
|
|
758
|
-
migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
|
|
803
|
+
const nextAuto = (0, import_core5.resolveSessionId)(defaultStorage, void 0);
|
|
804
|
+
(0, import_core5.migrateCourseStartedMark)(defaultStorage, prevConfigured, nextAuto, cid);
|
|
759
805
|
sessionIdRef.current = nextAuto;
|
|
760
806
|
}
|
|
761
|
-
}, [sessionConfiguredId,
|
|
807
|
+
}, [sessionConfiguredId, normalizedCourseId]);
|
|
762
808
|
const runtime = (0, import_react.useMemo)(
|
|
763
809
|
() => ({
|
|
764
|
-
config,
|
|
810
|
+
config: normalizedConfig,
|
|
765
811
|
tracking,
|
|
766
812
|
xapi,
|
|
767
813
|
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
@@ -773,7 +819,7 @@ function LessonkitProvider(props) {
|
|
|
773
819
|
plugins: pluginHost
|
|
774
820
|
}),
|
|
775
821
|
[
|
|
776
|
-
|
|
822
|
+
normalizedConfig,
|
|
777
823
|
tracking,
|
|
778
824
|
xapi,
|
|
779
825
|
progress,
|
|
@@ -787,13 +833,21 @@ function LessonkitProvider(props) {
|
|
|
787
833
|
sessionConfiguredId
|
|
788
834
|
]
|
|
789
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);
|
|
790
844
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LessonkitContext.Provider, { value: runtime, children: props.children });
|
|
791
845
|
}
|
|
792
846
|
|
|
793
847
|
// src/hooks.ts
|
|
794
|
-
var
|
|
848
|
+
var import_react3 = require("react");
|
|
795
849
|
function useLessonkit() {
|
|
796
|
-
const ctx = (0,
|
|
850
|
+
const ctx = (0, import_react3.useContext)(LessonkitContext);
|
|
797
851
|
if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
|
|
798
852
|
return ctx;
|
|
799
853
|
}
|
|
@@ -803,52 +857,80 @@ function useProgress() {
|
|
|
803
857
|
}
|
|
804
858
|
function useTracking() {
|
|
805
859
|
const { track } = useLessonkit();
|
|
806
|
-
return (0,
|
|
860
|
+
return (0, import_react3.useMemo)(() => ({ track }), [track]);
|
|
807
861
|
}
|
|
808
862
|
function useCompletion() {
|
|
809
863
|
const { completeLesson, completeCourse } = useLessonkit();
|
|
810
|
-
return (0,
|
|
864
|
+
return (0, import_react3.useMemo)(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
811
865
|
}
|
|
812
|
-
function useQuizState() {
|
|
866
|
+
function useQuizState(enclosingLessonId) {
|
|
813
867
|
const { track } = useLessonkit();
|
|
814
|
-
|
|
868
|
+
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
869
|
+
return (0, import_react3.useMemo)(
|
|
815
870
|
() => ({
|
|
816
871
|
answer: (opts) => {
|
|
817
|
-
track("quiz_answered", opts);
|
|
872
|
+
track("quiz_answered", opts, trackOpts);
|
|
818
873
|
},
|
|
819
874
|
complete: (opts) => {
|
|
820
|
-
track("quiz_completed", opts);
|
|
875
|
+
track("quiz_completed", opts, trackOpts);
|
|
821
876
|
}
|
|
822
877
|
}),
|
|
823
|
-
[track]
|
|
878
|
+
[track, enclosingLessonId]
|
|
824
879
|
);
|
|
825
880
|
}
|
|
826
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
|
+
|
|
827
889
|
// src/runtime/validateComponentId.ts
|
|
828
|
-
var
|
|
829
|
-
|
|
830
|
-
function isDevEnvironment2() {
|
|
890
|
+
var import_core9 = require("@lessonkit/core");
|
|
891
|
+
function isDevEnvironment3() {
|
|
831
892
|
const g = globalThis;
|
|
832
893
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
833
894
|
}
|
|
834
|
-
function
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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;
|
|
843
921
|
}
|
|
844
922
|
|
|
845
923
|
// src/components.tsx
|
|
846
924
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
925
|
+
var warnedQuizOutsideLesson = false;
|
|
926
|
+
function resetQuizWarningsForTests() {
|
|
927
|
+
warnedQuizOutsideLesson = false;
|
|
928
|
+
}
|
|
847
929
|
function Course(props) {
|
|
848
|
-
|
|
849
|
-
const providerConfig = (0,
|
|
850
|
-
() => ({ ...props.config, courseId
|
|
851
|
-
[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]
|
|
852
934
|
);
|
|
853
935
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": props.title, children: [
|
|
854
936
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { children: props.title }),
|
|
@@ -856,41 +938,64 @@ function Course(props) {
|
|
|
856
938
|
] }) });
|
|
857
939
|
}
|
|
858
940
|
function Lesson(props) {
|
|
859
|
-
|
|
941
|
+
const lessonId = (0, import_react5.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
942
|
+
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
860
943
|
const { setActiveLesson, config } = useLessonkit();
|
|
861
944
|
const { completeLesson } = useCompletion();
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
945
|
+
const lessonMountGenerationRef = (0, import_react5.useRef)(0);
|
|
946
|
+
(0, import_react5.useEffect)(() => {
|
|
947
|
+
const unregister = registerLessonMount(lessonId);
|
|
865
948
|
const generation = ++lessonMountGenerationRef.current;
|
|
866
|
-
setActiveLesson(
|
|
949
|
+
setActiveLesson(lessonId);
|
|
867
950
|
return () => {
|
|
868
|
-
|
|
951
|
+
unregister();
|
|
952
|
+
if (getLessonMountCount(lessonId) > 0) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (!autoComplete) return;
|
|
869
956
|
queueMicrotask(() => {
|
|
870
957
|
if (lessonMountGenerationRef.current !== generation) return;
|
|
871
958
|
completeLesson(lessonId);
|
|
872
959
|
});
|
|
873
960
|
};
|
|
874
|
-
}, [
|
|
875
|
-
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: [
|
|
876
963
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: props.title }),
|
|
877
964
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: props.children })
|
|
878
|
-
] });
|
|
965
|
+
] }) });
|
|
879
966
|
}
|
|
880
967
|
function Scenario(props) {
|
|
881
|
-
|
|
882
|
-
|
|
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 });
|
|
883
973
|
}
|
|
884
974
|
function Reflection(props) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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: [
|
|
888
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,
|
|
889
991
|
props.children,
|
|
890
992
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
891
993
|
"textarea",
|
|
892
994
|
{
|
|
995
|
+
value,
|
|
996
|
+
onChange: handleChange,
|
|
893
997
|
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
998
|
+
"aria-describedby": props.hint ? hintId : void 0,
|
|
894
999
|
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
895
1000
|
}
|
|
896
1001
|
)
|
|
@@ -909,18 +1014,35 @@ function KnowledgeCheck(props) {
|
|
|
909
1014
|
);
|
|
910
1015
|
}
|
|
911
1016
|
function Quiz(props) {
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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)(() => {
|
|
920
1041
|
completedRef.current = false;
|
|
1042
|
+
setQuizPassed(false);
|
|
921
1043
|
setSelected(null);
|
|
922
1044
|
setSelectionCorrect(null);
|
|
923
|
-
}, [
|
|
1045
|
+
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
|
|
924
1046
|
const isChoiceCorrect = (choice, custom) => {
|
|
925
1047
|
if (!custom) return choice === props.answer;
|
|
926
1048
|
if (custom.passed !== void 0) return custom.passed;
|
|
@@ -929,7 +1051,11 @@ function Quiz(props) {
|
|
|
929
1051
|
}
|
|
930
1052
|
return choice === props.answer;
|
|
931
1053
|
};
|
|
932
|
-
|
|
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: [
|
|
933
1059
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
|
|
934
1060
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
|
|
935
1061
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
|
|
@@ -941,17 +1067,21 @@ function Quiz(props) {
|
|
|
941
1067
|
name: questionId,
|
|
942
1068
|
value: c,
|
|
943
1069
|
checked: selected === c,
|
|
1070
|
+
disabled: passed,
|
|
1071
|
+
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
944
1072
|
onChange: () => {
|
|
1073
|
+
if (passed) return;
|
|
945
1074
|
setSelected(c);
|
|
946
1075
|
const pluginCtx = buildPluginContext({
|
|
947
1076
|
courseId: config.courseId,
|
|
948
1077
|
sessionId: session.sessionId,
|
|
949
|
-
attemptId: session.attemptId
|
|
1078
|
+
attemptId: session.attemptId,
|
|
1079
|
+
user: session.user
|
|
950
1080
|
});
|
|
951
1081
|
const custom = plugins?.scoreAssessment(
|
|
952
1082
|
{
|
|
953
|
-
checkId
|
|
954
|
-
lessonId:
|
|
1083
|
+
checkId,
|
|
1084
|
+
lessonId: enclosingLessonId,
|
|
955
1085
|
response: c
|
|
956
1086
|
},
|
|
957
1087
|
pluginCtx
|
|
@@ -959,18 +1089,20 @@ function Quiz(props) {
|
|
|
959
1089
|
const correct = isChoiceCorrect(c, custom);
|
|
960
1090
|
setSelectionCorrect(correct);
|
|
961
1091
|
quiz.answer({
|
|
962
|
-
checkId
|
|
1092
|
+
checkId,
|
|
963
1093
|
question: props.question,
|
|
964
1094
|
choice: c,
|
|
965
1095
|
correct
|
|
966
1096
|
});
|
|
967
1097
|
if (correct && !completedRef.current) {
|
|
968
1098
|
completedRef.current = true;
|
|
1099
|
+
setQuizPassed(true);
|
|
1100
|
+
const maxScore = custom?.maxScore ?? 1;
|
|
969
1101
|
quiz.complete({
|
|
970
|
-
checkId
|
|
1102
|
+
checkId,
|
|
971
1103
|
score: custom?.score ?? 1,
|
|
972
|
-
maxScore
|
|
973
|
-
passingScore: props.passingScore ??
|
|
1104
|
+
maxScore,
|
|
1105
|
+
passingScore: props.passingScore ?? maxScore
|
|
974
1106
|
});
|
|
975
1107
|
}
|
|
976
1108
|
}
|
|
@@ -982,20 +1114,40 @@ function Quiz(props) {
|
|
|
982
1114
|
selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
983
1115
|
] });
|
|
984
1116
|
}
|
|
985
|
-
function ProgressTracker() {
|
|
1117
|
+
function ProgressTracker(props) {
|
|
986
1118
|
const { progress } = useLessonkit();
|
|
987
1119
|
const completed = progress.completedLessonIds.size;
|
|
988
|
-
|
|
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: [
|
|
989
1141
|
"Lessons completed: ",
|
|
990
1142
|
completed
|
|
991
1143
|
] }) });
|
|
992
1144
|
}
|
|
993
1145
|
|
|
994
1146
|
// src/index.tsx
|
|
995
|
-
var
|
|
1147
|
+
var import_core10 = require("@lessonkit/core");
|
|
996
1148
|
|
|
997
1149
|
// src/theme/ThemeProvider.tsx
|
|
998
|
-
var
|
|
1150
|
+
var import_react6 = __toESM(require("react"), 1);
|
|
999
1151
|
var import_themes = require("@lessonkit/themes");
|
|
1000
1152
|
|
|
1001
1153
|
// src/theme/applyCssVariables.ts
|
|
@@ -1015,8 +1167,8 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
1015
1167
|
|
|
1016
1168
|
// src/theme/ThemeProvider.tsx
|
|
1017
1169
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1018
|
-
var ThemeContext = (0,
|
|
1019
|
-
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;
|
|
1020
1172
|
function getSystemMode() {
|
|
1021
1173
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
1022
1174
|
return "light";
|
|
@@ -1034,7 +1186,7 @@ function ThemeProvider(props) {
|
|
|
1034
1186
|
const preset = props.preset ?? "default";
|
|
1035
1187
|
const mode = props.mode ?? "light";
|
|
1036
1188
|
const targetKind = props.target ?? "document";
|
|
1037
|
-
const [resolvedMode, setResolvedMode] = (0,
|
|
1189
|
+
const [resolvedMode, setResolvedMode] = (0, import_react6.useState)(
|
|
1038
1190
|
() => mode === "system" ? getSystemMode() : mode
|
|
1039
1191
|
);
|
|
1040
1192
|
useIsoLayoutEffect2(() => {
|
|
@@ -1050,20 +1202,20 @@ function ThemeProvider(props) {
|
|
|
1050
1202
|
return () => mq.removeEventListener("change", onChange);
|
|
1051
1203
|
}, [mode]);
|
|
1052
1204
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
1053
|
-
const effectiveTheme = (0,
|
|
1205
|
+
const effectiveTheme = (0, import_react6.useMemo)(() => {
|
|
1054
1206
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
1055
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));
|
|
1056
1208
|
return (0, import_themes.mergeThemes)(base, props.theme ?? {});
|
|
1057
1209
|
}, [preset, mode, dataTheme, props.theme]);
|
|
1058
|
-
const hostRef = (0,
|
|
1059
|
-
const appliedKeysRef = (0,
|
|
1210
|
+
const hostRef = (0, import_react6.useRef)(null);
|
|
1211
|
+
const appliedKeysRef = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
|
|
1060
1212
|
useIsoLayoutEffect2(() => {
|
|
1061
1213
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
1062
1214
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
1063
1215
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
1064
1216
|
}
|
|
1065
1217
|
}, [targetKind, dataTheme]);
|
|
1066
|
-
const inject = (0,
|
|
1218
|
+
const inject = (0, import_react6.useCallback)(() => {
|
|
1067
1219
|
const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
|
|
1068
1220
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
1069
1221
|
if (!el) return;
|
|
@@ -1080,7 +1232,7 @@ function ThemeProvider(props) {
|
|
|
1080
1232
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
1081
1233
|
};
|
|
1082
1234
|
}, [inject, targetKind]);
|
|
1083
|
-
const value = (0,
|
|
1235
|
+
const value = (0, import_react6.useMemo)(
|
|
1084
1236
|
() => ({
|
|
1085
1237
|
theme: effectiveTheme,
|
|
1086
1238
|
preset,
|
|
@@ -1095,7 +1247,7 @@ function ThemeProvider(props) {
|
|
|
1095
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 }) });
|
|
1096
1248
|
}
|
|
1097
1249
|
function useTheme() {
|
|
1098
|
-
const ctx = (0,
|
|
1250
|
+
const ctx = (0, import_react6.useContext)(ThemeContext);
|
|
1099
1251
|
if (!ctx) {
|
|
1100
1252
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
1101
1253
|
}
|
|
@@ -1142,6 +1294,12 @@ var BLOCK_CATALOG = [
|
|
|
1142
1294
|
props: [
|
|
1143
1295
|
{ name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
|
|
1144
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
|
+
},
|
|
1145
1303
|
{ name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
|
|
1146
1304
|
],
|
|
1147
1305
|
requiredIds: ["lessonId"],
|
|
@@ -1194,6 +1352,9 @@ var BLOCK_CATALOG = [
|
|
|
1194
1352
|
props: [
|
|
1195
1353
|
{ name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
|
|
1196
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." },
|
|
1197
1358
|
{ name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
|
|
1198
1359
|
],
|
|
1199
1360
|
requiredIds: [],
|
|
@@ -1212,6 +1373,7 @@ var BLOCK_CATALOG = [
|
|
|
1212
1373
|
},
|
|
1213
1374
|
telemetry: {
|
|
1214
1375
|
emits: [],
|
|
1376
|
+
requiresActiveLesson: true,
|
|
1215
1377
|
manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
|
|
1216
1378
|
}
|
|
1217
1379
|
},
|
|
@@ -1249,7 +1411,14 @@ var BLOCK_CATALOG = [
|
|
|
1249
1411
|
type: "ProgressTracker",
|
|
1250
1412
|
category: "chrome",
|
|
1251
1413
|
description: "Displays count of completed lessons from runtime progress state.",
|
|
1252
|
-
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
|
+
],
|
|
1253
1422
|
requiredIds: [],
|
|
1254
1423
|
parentConstraints: ["Course"],
|
|
1255
1424
|
a11y: {
|
|
@@ -1302,9 +1471,15 @@ function getBlockCatalogEntry(type) {
|
|
|
1302
1471
|
ThemeProvider,
|
|
1303
1472
|
blockCatalogVersion,
|
|
1304
1473
|
buildBlockCatalog,
|
|
1305
|
-
|
|
1306
|
-
|
|
1474
|
+
buildTelemetryEvent,
|
|
1475
|
+
createLessonkitRuntime,
|
|
1476
|
+
createPluginRegistry,
|
|
1477
|
+
createTelemetryPipeline,
|
|
1478
|
+
defineAssessmentPlugin,
|
|
1479
|
+
defineLifecyclePlugin,
|
|
1480
|
+
defineTelemetryPlugin,
|
|
1307
1481
|
getBlockCatalogEntry,
|
|
1482
|
+
resetQuizWarningsForTests,
|
|
1308
1483
|
useCompletion,
|
|
1309
1484
|
useLessonkit,
|
|
1310
1485
|
useProgress,
|