@lessonkit/react 1.3.0 → 1.4.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/block-catalog.v3.json +1005 -107
- package/dist/AssessmentLessonGuard-D2Plzybb.d.cts +21 -0
- package/dist/AssessmentLessonGuard-D2Plzybb.d.ts +21 -0
- package/dist/blocks-entry.cjs +4563 -0
- package/dist/blocks-entry.d.cts +411 -0
- package/dist/blocks-entry.d.ts +411 -0
- package/dist/blocks-entry.js +69 -0
- package/dist/chunk-4LQ4TTEE.js +4018 -0
- package/dist/chunk-TDM3ARE7.js +1775 -0
- package/dist/chunk-UUTXECVW.js +252 -0
- package/dist/index.cjs +2555 -313
- package/dist/index.d.cts +36 -282
- package/dist/index.d.ts +36 -282
- package/dist/index.js +433 -4065
- package/dist/testing.cjs +540 -0
- package/dist/testing.d.cts +16 -0
- package/dist/testing.d.ts +16 -0
- package/dist/testing.js +18 -0
- package/package.json +33 -16
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
Accordion: () => Accordion,
|
|
34
|
+
ArithmeticQuiz: () => ArithmeticQuiz,
|
|
34
35
|
AssessmentSequence: () => AssessmentSequence,
|
|
35
36
|
BLOCK_CATALOG: () => BLOCK_CATALOG,
|
|
36
37
|
BLOCK_CATALOG_V2: () => BLOCK_CATALOG_V2,
|
|
@@ -39,6 +40,7 @@ __export(index_exports, {
|
|
|
39
40
|
DialogCards: () => DialogCards,
|
|
40
41
|
DragAndDrop: () => DragAndDrop,
|
|
41
42
|
DragTheWords: () => DragTheWords,
|
|
43
|
+
Essay: () => Essay,
|
|
42
44
|
FillInTheBlanks: () => FillInTheBlanks,
|
|
43
45
|
FindHotspot: () => FindHotspot,
|
|
44
46
|
FindMultipleHotspots: () => FindMultipleHotspots,
|
|
@@ -46,36 +48,48 @@ __export(index_exports, {
|
|
|
46
48
|
Heading: () => Heading,
|
|
47
49
|
Image: () => Image,
|
|
48
50
|
ImageHotspots: () => ImageHotspots,
|
|
51
|
+
ImagePairing: () => ImagePairing,
|
|
52
|
+
ImageSequencing: () => ImageSequencing,
|
|
49
53
|
ImageSlider: () => ImageSlider,
|
|
54
|
+
InformationWall: () => InformationWall,
|
|
50
55
|
InteractiveBook: () => InteractiveBook,
|
|
56
|
+
InteractiveVideo: () => InteractiveVideo,
|
|
51
57
|
KnowledgeCheck: () => KnowledgeCheck,
|
|
52
58
|
Lesson: () => Lesson,
|
|
53
59
|
LessonkitProvider: () => LessonkitProvider,
|
|
54
60
|
MarkTheWords: () => MarkTheWords,
|
|
61
|
+
MemoryGame: () => MemoryGame,
|
|
55
62
|
Page: () => Page,
|
|
63
|
+
ParallaxSlideshow: () => ParallaxSlideshow,
|
|
56
64
|
ProgressTracker: () => ProgressTracker,
|
|
65
|
+
Questionnaire: () => Questionnaire,
|
|
57
66
|
Quiz: () => Quiz,
|
|
58
67
|
Reflection: () => Reflection,
|
|
59
68
|
Scenario: () => Scenario,
|
|
60
69
|
Slide: () => Slide,
|
|
61
70
|
SlideDeck: () => SlideDeck,
|
|
71
|
+
Summary: () => Summary,
|
|
62
72
|
Text: () => Text,
|
|
63
73
|
ThemeProvider: () => ThemeProvider,
|
|
74
|
+
TimedCue: () => TimedCue,
|
|
64
75
|
TrueFalse: () => TrueFalse,
|
|
76
|
+
Video: () => Video,
|
|
77
|
+
assertProductionCourseConfig: () => assertProductionCourseConfig,
|
|
65
78
|
blockCatalogV2Version: () => blockCatalogV2Version,
|
|
66
79
|
blockCatalogV3Version: () => blockCatalogV3Version,
|
|
67
80
|
blockCatalogVersion: () => blockCatalogVersion,
|
|
68
81
|
buildBlockCatalog: () => buildBlockCatalog,
|
|
69
|
-
buildTelemetryEvent: () =>
|
|
70
|
-
createLessonkitRuntime: () =>
|
|
71
|
-
createPluginRegistry: () =>
|
|
72
|
-
createTelemetryPipeline: () =>
|
|
73
|
-
defineAssessmentPlugin: () =>
|
|
74
|
-
defineLifecyclePlugin: () =>
|
|
75
|
-
defineTelemetryPlugin: () =>
|
|
82
|
+
buildTelemetryEvent: () => import_core21.buildTelemetryEvent,
|
|
83
|
+
createLessonkitRuntime: () => import_core21.createLessonkitRuntime,
|
|
84
|
+
createPluginRegistry: () => import_core21.createPluginRegistry,
|
|
85
|
+
createTelemetryPipeline: () => import_core21.createTelemetryPipeline,
|
|
86
|
+
defineAssessmentPlugin: () => import_core21.defineAssessmentPlugin,
|
|
87
|
+
defineLifecyclePlugin: () => import_core21.defineLifecyclePlugin,
|
|
88
|
+
defineTelemetryPlugin: () => import_core21.defineTelemetryPlugin,
|
|
76
89
|
getBlockCatalogEntry: () => getBlockCatalogEntry,
|
|
77
90
|
resetAssessmentWarningsForTests: () => resetAssessmentWarningsForTests,
|
|
78
91
|
resetQuizWarningsForTests: () => resetQuizWarningsForTests,
|
|
92
|
+
shouldEnforceProductionGuard: () => shouldEnforceProductionGuard,
|
|
79
93
|
useAssessmentState: () => useAssessmentState,
|
|
80
94
|
useCompletion: () => useCompletion,
|
|
81
95
|
useLessonkit: () => useLessonkit,
|
|
@@ -99,16 +113,25 @@ var import_core8 = require("@lessonkit/core");
|
|
|
99
113
|
|
|
100
114
|
// src/runtime/observability.ts
|
|
101
115
|
var import_xapi = require("@lessonkit/xapi");
|
|
102
|
-
function createXapiQueueFromObservability(
|
|
103
|
-
const opts = {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
if (observability?.onXapiQueueCap) {
|
|
108
|
-
opts.onCap = observability.onXapiQueueCap;
|
|
109
|
-
}
|
|
116
|
+
function createXapiQueueFromObservability(getObservability) {
|
|
117
|
+
const opts = {
|
|
118
|
+
onDepth: (size) => getObservability?.()?.onXapiQueueDepth?.(size),
|
|
119
|
+
onCap: () => getObservability?.()?.onXapiQueueCap?.()
|
|
120
|
+
};
|
|
110
121
|
return (0, import_xapi.createInMemoryXAPIQueue)(opts);
|
|
111
122
|
}
|
|
123
|
+
function wrapBatchSink(batchSink, observability) {
|
|
124
|
+
if (!batchSink || !observability?.onTelemetrySinkError) return batchSink;
|
|
125
|
+
const onError = observability.onTelemetrySinkError;
|
|
126
|
+
return async (events) => {
|
|
127
|
+
try {
|
|
128
|
+
await batchSink(events);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
onError(err, { sinkId: "tracking-batch" });
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
112
135
|
function wrapTrackingSink(sink, observability) {
|
|
113
136
|
if (!sink || !observability?.onTelemetrySinkError) return sink;
|
|
114
137
|
const onError = observability.onTelemetrySinkError;
|
|
@@ -118,16 +141,108 @@ function wrapTrackingSink(sink, observability) {
|
|
|
118
141
|
if (result != null && typeof result.catch === "function") {
|
|
119
142
|
return result.catch((err) => {
|
|
120
143
|
onError(err, { sinkId: "tracking" });
|
|
144
|
+
throw err;
|
|
121
145
|
});
|
|
122
146
|
}
|
|
123
147
|
return result;
|
|
124
148
|
} catch (err) {
|
|
125
149
|
onError(err, { sinkId: "tracking" });
|
|
126
|
-
|
|
150
|
+
throw err;
|
|
127
151
|
}
|
|
128
152
|
});
|
|
129
153
|
}
|
|
130
154
|
|
|
155
|
+
// src/runtime/productionGuard.ts
|
|
156
|
+
var import_meta = {};
|
|
157
|
+
function isProductionEnvironment() {
|
|
158
|
+
try {
|
|
159
|
+
if (import_meta.env?.PROD === true) return true;
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
const g = globalThis;
|
|
163
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
164
|
+
}
|
|
165
|
+
function shouldEnforceProductionGuard() {
|
|
166
|
+
try {
|
|
167
|
+
if (import_meta.env?.MODE === "test") return false;
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
return isProductionEnvironment();
|
|
171
|
+
}
|
|
172
|
+
function looksLikeConsoleSink(fn) {
|
|
173
|
+
if (typeof fn !== "function") return false;
|
|
174
|
+
const src = Function.prototype.toString.call(fn);
|
|
175
|
+
return /console\.(log|debug|info)\s*\(/.test(src);
|
|
176
|
+
}
|
|
177
|
+
function isTrackingDeliveryConfigured(tracking) {
|
|
178
|
+
if (!tracking || tracking.enabled === false) return false;
|
|
179
|
+
return Boolean(tracking.sink || tracking.batchSink);
|
|
180
|
+
}
|
|
181
|
+
function isXapiDeliveryConfigured(xapi) {
|
|
182
|
+
if (!xapi || xapi.enabled === false) return false;
|
|
183
|
+
if (xapi.client) return true;
|
|
184
|
+
return typeof xapi.transport === "function";
|
|
185
|
+
}
|
|
186
|
+
function trackingUsesConsole(config) {
|
|
187
|
+
const tracking = config.tracking;
|
|
188
|
+
if (!tracking || tracking.enabled === false) return false;
|
|
189
|
+
if (tracking.batchSink && looksLikeConsoleSink(tracking.batchSink)) return true;
|
|
190
|
+
if (tracking.sink && looksLikeConsoleSink(tracking.sink)) return true;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
function xapiUsesConsole(config) {
|
|
194
|
+
const xapi = config.xapi;
|
|
195
|
+
if (!xapi || xapi.enabled === false || xapi.client) return false;
|
|
196
|
+
return typeof xapi.transport === "function" && looksLikeConsoleSink(xapi.transport);
|
|
197
|
+
}
|
|
198
|
+
function observabilityIncomplete(observability, opts) {
|
|
199
|
+
if (!opts.trackingEnabled && !opts.xapiEnabled) return false;
|
|
200
|
+
const required = [observability?.onLxpackBridgeMiss];
|
|
201
|
+
if (opts.trackingEnabled) {
|
|
202
|
+
required.push(observability?.onTelemetrySinkError, observability?.onTelemetryBufferDrop);
|
|
203
|
+
}
|
|
204
|
+
if (opts.xapiEnabled) {
|
|
205
|
+
required.push(
|
|
206
|
+
observability?.onXapiQueueDepth,
|
|
207
|
+
observability?.onXapiQueueCap,
|
|
208
|
+
observability?.onXapiTransportError
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return required.some((hook) => !hook);
|
|
212
|
+
}
|
|
213
|
+
function requiredObservabilityHookCount(opts) {
|
|
214
|
+
let count = 1;
|
|
215
|
+
if (opts.trackingEnabled) count += 2;
|
|
216
|
+
if (opts.xapiEnabled) count += 3;
|
|
217
|
+
return count;
|
|
218
|
+
}
|
|
219
|
+
function assertProductionCourseConfig(config) {
|
|
220
|
+
if (!isProductionEnvironment()) return;
|
|
221
|
+
if (config.tracking && config.tracking.enabled !== false && !isTrackingDeliveryConfigured(config.tracking)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
"[lessonkit] Production build has tracking enabled but no sink or batchSink configured."
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const trackingEnabled = isTrackingDeliveryConfigured(config.tracking);
|
|
227
|
+
const xapiEnabled = isXapiDeliveryConfigured(config.xapi);
|
|
228
|
+
if (trackingUsesConsole(config)) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
"[lessonkit] Production build uses console telemetry sinks. Wire createFetchBatchSink or a real sink. See production checklist."
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (xapiUsesConsole(config)) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"[lessonkit] Production build uses console xAPI transport. Wire createFetchTransport to your LRS proxy. See production checklist."
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (observabilityIncomplete(config.observability, { trackingEnabled, xapiEnabled })) {
|
|
239
|
+
const hookCount = requiredObservabilityHookCount({ trackingEnabled, xapiEnabled });
|
|
240
|
+
throw new Error(
|
|
241
|
+
`[lessonkit] Production build missing observability hooks. Wire all ${hookCount} config.observability callbacks before go-live.`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
131
246
|
// src/provider/useLessonkitProviderRuntime.ts
|
|
132
247
|
var import_xapi5 = require("@lessonkit/xapi");
|
|
133
248
|
|
|
@@ -226,7 +341,7 @@ var import_core4 = require("@lessonkit/core");
|
|
|
226
341
|
|
|
227
342
|
// src/runtime/xapi.ts
|
|
228
343
|
var import_xapi3 = require("@lessonkit/xapi");
|
|
229
|
-
function createXapiClientFromConfig(config, queue) {
|
|
344
|
+
function createXapiClientFromConfig(config, queue, observability) {
|
|
230
345
|
if (config.xapi?.enabled === false) return null;
|
|
231
346
|
if (config.xapi?.client) return config.xapi.client;
|
|
232
347
|
if (!config.courseId) return null;
|
|
@@ -235,7 +350,10 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
235
350
|
return (0, import_xapi3.createXAPIClient)({
|
|
236
351
|
courseId: config.courseId,
|
|
237
352
|
transport: config.xapi?.transport,
|
|
238
|
-
|
|
353
|
+
exitTransport: config.xapi?.exitTransport,
|
|
354
|
+
abortInFlight: config.xapi?.abortInFlight,
|
|
355
|
+
queue,
|
|
356
|
+
onTransportError: observability?.onXapiTransportError
|
|
239
357
|
});
|
|
240
358
|
}
|
|
241
359
|
|
|
@@ -283,6 +401,7 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
|
283
401
|
const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(opts.event);
|
|
284
402
|
if (statement) {
|
|
285
403
|
opts.xapi.send(statement);
|
|
404
|
+
await opts.xapi.flush();
|
|
286
405
|
xapiStatementSent = true;
|
|
287
406
|
}
|
|
288
407
|
}
|
|
@@ -318,12 +437,26 @@ function emitTelemetryWithPlugins(opts) {
|
|
|
318
437
|
}
|
|
319
438
|
|
|
320
439
|
// src/provider/courseStarted/emit.ts
|
|
321
|
-
|
|
440
|
+
function resolveTrackingClient(source) {
|
|
441
|
+
return typeof source === "function" ? source() : source;
|
|
442
|
+
}
|
|
443
|
+
var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
|
|
444
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Map();
|
|
322
445
|
function isTrackingActive(tracking) {
|
|
323
446
|
return tracking?.enabled !== false;
|
|
324
447
|
}
|
|
325
448
|
function isCourseStartedSinkSettled(result) {
|
|
326
|
-
return result === "emitted";
|
|
449
|
+
return result === "emitted" || result === "filtered";
|
|
450
|
+
}
|
|
451
|
+
async function deliverToTracking(client, event) {
|
|
452
|
+
if (client.deliver) {
|
|
453
|
+
return client.deliver(event);
|
|
454
|
+
}
|
|
455
|
+
client.track(event);
|
|
456
|
+
const flushed = await client.flush?.();
|
|
457
|
+
if (flushed === false) return false;
|
|
458
|
+
if (flushed === true) return true;
|
|
459
|
+
return false;
|
|
327
460
|
}
|
|
328
461
|
function buildCourseStartedEvent(opts) {
|
|
329
462
|
const pluginCtx = buildPluginContext({
|
|
@@ -346,25 +479,45 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
346
479
|
if ((0, import_core5.hasCourseStartedEmittedToTracking)(storage, sessionId, courseId)) {
|
|
347
480
|
return true;
|
|
348
481
|
}
|
|
349
|
-
|
|
350
|
-
|
|
482
|
+
const existing = courseStartedTrackingFlights.get(flightKey);
|
|
483
|
+
if (existing) {
|
|
484
|
+
return existing;
|
|
351
485
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
486
|
+
let resolveFlight;
|
|
487
|
+
const flight = new Promise((resolve) => {
|
|
488
|
+
resolveFlight = resolve;
|
|
489
|
+
});
|
|
490
|
+
courseStartedTrackingFlights.set(flightKey, flight);
|
|
491
|
+
void (async () => {
|
|
492
|
+
try {
|
|
493
|
+
if (shouldCommit && !shouldCommit()) {
|
|
494
|
+
resolveFlight(false);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const client = resolveTrackingClient(tracking);
|
|
498
|
+
const delivered = await deliverToTracking(client, event);
|
|
499
|
+
if (shouldCommit && !shouldCommit()) {
|
|
500
|
+
resolveFlight(false);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (!delivered) {
|
|
504
|
+
resolveFlight(false);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if ((0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId) === false) {
|
|
508
|
+
resolveFlight(false);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
resolveFlight(true);
|
|
512
|
+
} catch {
|
|
513
|
+
resolveFlight(false);
|
|
514
|
+
} finally {
|
|
515
|
+
if (courseStartedTrackingFlights.get(flightKey) === flight) {
|
|
516
|
+
courseStartedTrackingFlights.delete(flightKey);
|
|
517
|
+
}
|
|
366
518
|
}
|
|
367
|
-
}
|
|
519
|
+
})();
|
|
520
|
+
return flight;
|
|
368
521
|
}
|
|
369
522
|
async function emitCourseStartedPipelineOnly(opts) {
|
|
370
523
|
try {
|
|
@@ -378,8 +531,10 @@ async function emitCourseStartedPipelineOnly(opts) {
|
|
|
378
531
|
skipXapi: opts.skipXapi
|
|
379
532
|
});
|
|
380
533
|
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
381
|
-
(0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
382
|
-
(0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId)
|
|
534
|
+
if ((0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId) === false) return "failed";
|
|
535
|
+
if ((0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId) === false) {
|
|
536
|
+
return "failed";
|
|
537
|
+
}
|
|
383
538
|
if (xapiStatementSent) {
|
|
384
539
|
opts.onXapiStatementSent?.();
|
|
385
540
|
}
|
|
@@ -430,19 +585,64 @@ async function emitCourseStartedToTrackingOnly(opts) {
|
|
|
430
585
|
extraSinks: opts.extraSinks,
|
|
431
586
|
skipXapi: true
|
|
432
587
|
});
|
|
433
|
-
(0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId)
|
|
588
|
+
if ((0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId) === false) {
|
|
589
|
+
return "failed";
|
|
590
|
+
}
|
|
434
591
|
return "emitted";
|
|
435
592
|
} catch {
|
|
436
593
|
return "failed";
|
|
437
594
|
}
|
|
438
595
|
}
|
|
439
596
|
async function emitPendingCourseStarted(opts) {
|
|
597
|
+
const flightKey = `${opts.sessionId}:${opts.courseId}`;
|
|
598
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
599
|
+
const existing = courseStartedEmitFlights.get(flightKey);
|
|
600
|
+
const flight = existing ?? startPendingCourseStartedFlight(opts, flightKey);
|
|
601
|
+
const result = await flight;
|
|
602
|
+
if (result !== "failed") return result;
|
|
603
|
+
const sessionStarted = (0, import_core5.hasCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
604
|
+
const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
605
|
+
opts.storage,
|
|
606
|
+
opts.sessionId,
|
|
607
|
+
opts.courseId
|
|
608
|
+
);
|
|
609
|
+
const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
|
|
610
|
+
opts.storage,
|
|
611
|
+
opts.sessionId,
|
|
612
|
+
opts.courseId
|
|
613
|
+
);
|
|
614
|
+
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
615
|
+
return "emitted";
|
|
616
|
+
}
|
|
617
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
618
|
+
}
|
|
619
|
+
return "failed";
|
|
620
|
+
}
|
|
621
|
+
function startPendingCourseStartedFlight(opts, flightKey) {
|
|
622
|
+
const flight = emitPendingCourseStartedInner(opts);
|
|
623
|
+
courseStartedEmitFlights.set(flightKey, flight);
|
|
624
|
+
void flight.finally(() => {
|
|
625
|
+
if (courseStartedEmitFlights.get(flightKey) === flight) {
|
|
626
|
+
courseStartedEmitFlights.delete(flightKey);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
return flight;
|
|
630
|
+
}
|
|
631
|
+
async function emitPendingCourseStartedInner(opts) {
|
|
440
632
|
const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
441
633
|
opts.storage,
|
|
442
634
|
opts.sessionId,
|
|
443
635
|
opts.courseId
|
|
444
636
|
);
|
|
445
637
|
const sessionStarted = (0, import_core5.hasCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
638
|
+
const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
|
|
639
|
+
opts.storage,
|
|
640
|
+
opts.sessionId,
|
|
641
|
+
opts.courseId
|
|
642
|
+
);
|
|
643
|
+
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
644
|
+
return "emitted";
|
|
645
|
+
}
|
|
446
646
|
if (sessionStarted && !trackingEmitted) {
|
|
447
647
|
return emitCourseStartedToTrackingOnly(opts);
|
|
448
648
|
}
|
|
@@ -454,14 +654,6 @@ async function emitPendingCourseStarted(opts) {
|
|
|
454
654
|
if (!trackingEmitted && !sessionStarted) {
|
|
455
655
|
return emitCourseStarted(opts);
|
|
456
656
|
}
|
|
457
|
-
const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
|
|
458
|
-
opts.storage,
|
|
459
|
-
opts.sessionId,
|
|
460
|
-
opts.courseId
|
|
461
|
-
);
|
|
462
|
-
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
463
|
-
return "emitted";
|
|
464
|
-
}
|
|
465
657
|
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
466
658
|
const event = buildCourseStartedEvent(opts);
|
|
467
659
|
if (event === null) return "filtered";
|
|
@@ -472,7 +664,7 @@ async function emitPendingCourseStarted(opts) {
|
|
|
472
664
|
onXapiStatementSent: opts.onXapiStatementSent
|
|
473
665
|
});
|
|
474
666
|
}
|
|
475
|
-
return "
|
|
667
|
+
return "failed";
|
|
476
668
|
}
|
|
477
669
|
function assertTrackingSinkConfig(tracking) {
|
|
478
670
|
if (!tracking?.sink || !tracking?.batchSink) return;
|
|
@@ -490,6 +682,7 @@ function createTrackingClientFromConfig(config, observability) {
|
|
|
490
682
|
sink: config.tracking?.sink,
|
|
491
683
|
batchSink: config.tracking?.batchSink,
|
|
492
684
|
batch: config.tracking?.batch,
|
|
685
|
+
exitBatchSink: config.tracking?.exitBatchSink,
|
|
493
686
|
onBufferDrop: observability?.onTelemetryBufferDrop
|
|
494
687
|
});
|
|
495
688
|
}
|
|
@@ -519,6 +712,9 @@ function useLessonkitProviderRuntime(config) {
|
|
|
519
712
|
() => ({ ...config, courseId: normalizedCourseId }),
|
|
520
713
|
[config, normalizedCourseId]
|
|
521
714
|
);
|
|
715
|
+
if (shouldEnforceProductionGuard()) {
|
|
716
|
+
assertProductionCourseConfig(normalizedConfig);
|
|
717
|
+
}
|
|
522
718
|
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
523
719
|
(0, import_react.useEffect)(() => {
|
|
524
720
|
if (useV2Runtime) return;
|
|
@@ -565,6 +761,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
565
761
|
const pendingCourseIdResetRef = (0, import_react.useRef)(false);
|
|
566
762
|
const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
|
|
567
763
|
const xapiCourseStartedSentOnClientRef = (0, import_react.useRef)(false);
|
|
764
|
+
const xapiBootstrapSendRef = (0, import_react.useRef)(false);
|
|
568
765
|
if (prevUseV2RuntimeRef.current !== useV2Runtime) {
|
|
569
766
|
prevUseV2RuntimeRef.current = useV2Runtime;
|
|
570
767
|
if (useV2Runtime) {
|
|
@@ -613,7 +810,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
613
810
|
}, []);
|
|
614
811
|
const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
|
|
615
812
|
activeLessonIdRef.current = progress.activeLessonId;
|
|
616
|
-
const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(
|
|
813
|
+
const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(() => observabilityRef.current));
|
|
617
814
|
const xapiRef = (0, import_react.useRef)(null);
|
|
618
815
|
const [xapi, setXapi] = (0, import_react.useState)(null);
|
|
619
816
|
const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
|
|
@@ -634,22 +831,29 @@ function useLessonkitProviderRuntime(config) {
|
|
|
634
831
|
}
|
|
635
832
|
void xapiRef.current?.flush();
|
|
636
833
|
}
|
|
637
|
-
xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
|
|
834
|
+
xapiQueueRef.current = createXapiQueueFromObservability(() => observabilityRef.current);
|
|
638
835
|
prevXapiCourseIdRef.current = courseId;
|
|
639
836
|
xapiCourseStartedSentOnClientRef.current = false;
|
|
837
|
+
xapiBootstrapSendRef.current = false;
|
|
640
838
|
}
|
|
641
839
|
const prev = xapiRef.current;
|
|
642
|
-
const next = createXapiClientFromConfig(
|
|
840
|
+
const next = createXapiClientFromConfig(
|
|
841
|
+
normalizedConfig,
|
|
842
|
+
xapiQueueRef.current,
|
|
843
|
+
observabilityRef.current
|
|
844
|
+
);
|
|
643
845
|
xapiRef.current = next;
|
|
644
846
|
setXapi(next);
|
|
847
|
+
let bootstrapSent = false;
|
|
848
|
+
let bootstrapAlreadyStarted = false;
|
|
645
849
|
if (next) {
|
|
646
850
|
const sessionId = sessionIdRef.current;
|
|
647
851
|
const cid = courseIdRef.current;
|
|
648
852
|
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
649
|
-
|
|
853
|
+
bootstrapAlreadyStarted = (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid);
|
|
650
854
|
const clientChanged = !prev || prev !== next;
|
|
651
|
-
const skipBootstrap = trackingActive && !
|
|
652
|
-
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!
|
|
855
|
+
const skipBootstrap = trackingActive && !bootstrapAlreadyStarted;
|
|
856
|
+
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && !xapiBootstrapSendRef.current && (!bootstrapAlreadyStarted || clientChanged);
|
|
653
857
|
if (needsBootstrap) {
|
|
654
858
|
try {
|
|
655
859
|
const event = buildCourseStartedEvent({
|
|
@@ -660,15 +864,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
660
864
|
user: userRef.current,
|
|
661
865
|
lxpackBridge: lxpackBridgeModeRef.current
|
|
662
866
|
});
|
|
663
|
-
if (event
|
|
664
|
-
} else {
|
|
867
|
+
if (event !== null) {
|
|
665
868
|
const statement = (0, import_xapi5.telemetryEventToXAPIStatement)(event);
|
|
666
869
|
if (statement) {
|
|
667
870
|
next.send(statement);
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
871
|
+
xapiBootstrapSendRef.current = true;
|
|
872
|
+
bootstrapSent = true;
|
|
672
873
|
}
|
|
673
874
|
}
|
|
674
875
|
} catch {
|
|
@@ -686,12 +887,23 @@ function useLessonkitProviderRuntime(config) {
|
|
|
686
887
|
if (cancelled) return;
|
|
687
888
|
try {
|
|
688
889
|
await next?.flush();
|
|
890
|
+
if (bootstrapSent && !cancelled) {
|
|
891
|
+
if (!bootstrapAlreadyStarted) {
|
|
892
|
+
(0, import_core5.markCourseStarted)(defaultStorage, sessionIdRef.current, courseIdRef.current);
|
|
893
|
+
}
|
|
894
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
895
|
+
}
|
|
689
896
|
} catch {
|
|
690
897
|
}
|
|
691
898
|
})();
|
|
692
899
|
return () => {
|
|
693
900
|
cancelled = true;
|
|
694
|
-
void
|
|
901
|
+
void (async () => {
|
|
902
|
+
try {
|
|
903
|
+
await prev?.flush();
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
})();
|
|
695
907
|
};
|
|
696
908
|
}, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
|
|
697
909
|
const trackingRef = (0, import_react.useRef)((0, import_core8.createTrackingClient)());
|
|
@@ -714,7 +926,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
714
926
|
useIsoLayoutEffect(() => {
|
|
715
927
|
const prev = trackingRef.current;
|
|
716
928
|
const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
|
|
717
|
-
const userBatchSink =
|
|
929
|
+
const userBatchSink = wrapBatchSink(
|
|
930
|
+
normalizedConfig.tracking?.batchSink,
|
|
931
|
+
observabilityRef.current
|
|
932
|
+
);
|
|
718
933
|
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
719
934
|
const sink = pluginHostRef.current && baseSink ? (
|
|
720
935
|
/* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
|
|
@@ -752,13 +967,13 @@ function useLessonkitProviderRuntime(config) {
|
|
|
752
967
|
} else if (courseStartedFullySettled) {
|
|
753
968
|
courseStartedEmittedToSinkRef.current = true;
|
|
754
969
|
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
755
|
-
const generation =
|
|
970
|
+
const generation = courseStartedEmitGenerationRef.current;
|
|
756
971
|
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
757
972
|
void (async () => {
|
|
758
973
|
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
759
974
|
const result = await emitPendingCourseStarted({
|
|
760
975
|
pluginHost: pluginHostRef.current,
|
|
761
|
-
tracking:
|
|
976
|
+
tracking: () => trackingRef.current,
|
|
762
977
|
xapi: xapiRef.current,
|
|
763
978
|
storage: defaultStorage,
|
|
764
979
|
sessionId,
|
|
@@ -768,7 +983,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
768
983
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
769
984
|
onLxpackBridgeMiss,
|
|
770
985
|
extraSinks: extraSinksRef.current,
|
|
771
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
986
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
|
|
772
987
|
onXapiStatementSent: () => {
|
|
773
988
|
xapiCourseStartedSentOnClientRef.current = true;
|
|
774
989
|
},
|
|
@@ -779,7 +994,6 @@ function useLessonkitProviderRuntime(config) {
|
|
|
779
994
|
})();
|
|
780
995
|
}
|
|
781
996
|
return () => {
|
|
782
|
-
courseStartedEmitGenerationRef.current += 1;
|
|
783
997
|
if (prev !== trackingRef.current) {
|
|
784
998
|
void disposeTrackingClient(prev);
|
|
785
999
|
}
|
|
@@ -857,9 +1071,11 @@ function useLessonkitProviderRuntime(config) {
|
|
|
857
1071
|
} catch {
|
|
858
1072
|
}
|
|
859
1073
|
if (!courseStartedEmittedToSinkRef.current) {
|
|
1074
|
+
const generation = courseStartedEmitGenerationRef.current;
|
|
1075
|
+
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
860
1076
|
const result = await emitPendingCourseStarted({
|
|
861
1077
|
pluginHost: pluginHostRef.current,
|
|
862
|
-
tracking: trackingRef.current,
|
|
1078
|
+
tracking: () => trackingRef.current,
|
|
863
1079
|
xapi: xapiRef.current,
|
|
864
1080
|
storage: defaultStorage,
|
|
865
1081
|
sessionId,
|
|
@@ -868,8 +1084,14 @@ function useLessonkitProviderRuntime(config) {
|
|
|
868
1084
|
user: userRef.current,
|
|
869
1085
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
870
1086
|
onLxpackBridgeMiss,
|
|
871
|
-
extraSinks: extraSinksRef.current
|
|
1087
|
+
extraSinks: extraSinksRef.current,
|
|
1088
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
|
|
1089
|
+
onXapiStatementSent: () => {
|
|
1090
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
1091
|
+
},
|
|
1092
|
+
shouldCommit
|
|
872
1093
|
});
|
|
1094
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
873
1095
|
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
874
1096
|
}
|
|
875
1097
|
})();
|
|
@@ -924,18 +1146,23 @@ function useLessonkitProviderRuntime(config) {
|
|
|
924
1146
|
}, []);
|
|
925
1147
|
(0, import_react.useEffect)(() => {
|
|
926
1148
|
if (typeof document === "undefined") return;
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
1149
|
+
const flushOnPageExit = () => {
|
|
1150
|
+
try {
|
|
1151
|
+
xapiRef.current?.flushOnExit?.();
|
|
1152
|
+
trackingRef.current?.flushOnExit?.();
|
|
1153
|
+
} finally {
|
|
1154
|
+
void xapiRef.current?.flush();
|
|
1155
|
+
void trackingRef.current?.flush?.();
|
|
1156
|
+
}
|
|
930
1157
|
};
|
|
931
1158
|
const onVisibilityChange = () => {
|
|
932
|
-
if (document.visibilityState === "hidden")
|
|
1159
|
+
if (document.visibilityState === "hidden") flushOnPageExit();
|
|
933
1160
|
};
|
|
934
1161
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
935
|
-
window.addEventListener("pagehide",
|
|
1162
|
+
window.addEventListener("pagehide", flushOnPageExit);
|
|
936
1163
|
return () => {
|
|
937
1164
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
938
|
-
window.removeEventListener("pagehide",
|
|
1165
|
+
window.removeEventListener("pagehide", flushOnPageExit);
|
|
939
1166
|
};
|
|
940
1167
|
}, []);
|
|
941
1168
|
const setActiveLesson = (0, import_react.useCallback)(
|
|
@@ -2210,9 +2437,13 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2210
2437
|
const [submitted, setSubmitted] = (0, import_react16.useState)(false);
|
|
2211
2438
|
const completedRef = (0, import_react16.useRef)(false);
|
|
2212
2439
|
const answeredRef = (0, import_react16.useRef)(false);
|
|
2440
|
+
const checkSnapshotRef = (0, import_react16.useRef)(null);
|
|
2441
|
+
const telemetryReplayedRef = (0, import_react16.useRef)(false);
|
|
2213
2442
|
const reset = () => {
|
|
2214
2443
|
completedRef.current = false;
|
|
2215
2444
|
answeredRef.current = false;
|
|
2445
|
+
checkSnapshotRef.current = null;
|
|
2446
|
+
telemetryReplayedRef.current = false;
|
|
2216
2447
|
setPassed(false);
|
|
2217
2448
|
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
2218
2449
|
setShowSolutions(false);
|
|
@@ -2229,6 +2460,31 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2229
2460
|
});
|
|
2230
2461
|
const maxScore = blanks.length;
|
|
2231
2462
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2463
|
+
const replayTelemetry = (nextValues, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
|
|
2464
|
+
if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
|
|
2465
|
+
telemetryReplayedRef.current = true;
|
|
2466
|
+
const nextPassedThreshold = meetsPassingThreshold(
|
|
2467
|
+
nextScore,
|
|
2468
|
+
nextMaxScore || 1,
|
|
2469
|
+
props.passingScore
|
|
2470
|
+
);
|
|
2471
|
+
assessment.answer({
|
|
2472
|
+
checkId,
|
|
2473
|
+
interactionType: INTERACTION3,
|
|
2474
|
+
question: props.template,
|
|
2475
|
+
response: nextValues,
|
|
2476
|
+
correct: nextPassedThreshold
|
|
2477
|
+
});
|
|
2478
|
+
if (nextPassed || nextPassedThreshold) {
|
|
2479
|
+
assessment.complete({
|
|
2480
|
+
checkId,
|
|
2481
|
+
interactionType: INTERACTION3,
|
|
2482
|
+
score: nextScore,
|
|
2483
|
+
maxScore: nextMaxScore,
|
|
2484
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2232
2488
|
const handle = (0, import_react16.useMemo)(
|
|
2233
2489
|
() => buildAssessmentHandle({
|
|
2234
2490
|
checkId,
|
|
@@ -2248,20 +2504,33 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2248
2504
|
getCurrentState: () => ({ values, passed, showSolutions, submitted }),
|
|
2249
2505
|
resume: (state) => {
|
|
2250
2506
|
const raw = state.values;
|
|
2251
|
-
|
|
2507
|
+
let nextValues = values;
|
|
2508
|
+
if (raw && typeof raw === "object") {
|
|
2509
|
+
nextValues = { ...raw };
|
|
2510
|
+
setValues(nextValues);
|
|
2511
|
+
}
|
|
2512
|
+
let nextPassed = passed;
|
|
2513
|
+
let nextSubmitted = submitted;
|
|
2252
2514
|
readBooleanStateField(state, "passed", (value) => {
|
|
2515
|
+
nextPassed = value;
|
|
2253
2516
|
setPassed(value);
|
|
2254
2517
|
completedRef.current = value;
|
|
2255
2518
|
answeredRef.current = value;
|
|
2256
2519
|
});
|
|
2257
2520
|
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2258
2521
|
readBooleanStateField(state, "submitted", (value) => {
|
|
2522
|
+
nextSubmitted = value;
|
|
2259
2523
|
setSubmitted(value);
|
|
2260
2524
|
if (value) answeredRef.current = true;
|
|
2261
2525
|
});
|
|
2526
|
+
let nextScore = 0;
|
|
2527
|
+
blanks.forEach((b) => {
|
|
2528
|
+
if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
|
|
2529
|
+
});
|
|
2530
|
+
replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
|
|
2262
2531
|
}
|
|
2263
2532
|
}),
|
|
2264
|
-
[allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, submitted, values]
|
|
2533
|
+
[allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
|
|
2265
2534
|
);
|
|
2266
2535
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2267
2536
|
const check = () => {
|
|
@@ -2272,7 +2541,10 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2272
2541
|
return;
|
|
2273
2542
|
}
|
|
2274
2543
|
if (!allFilled) return;
|
|
2275
|
-
if (
|
|
2544
|
+
if (passed) return;
|
|
2545
|
+
const snapshot = JSON.stringify(values);
|
|
2546
|
+
if (checkSnapshotRef.current === snapshot) return;
|
|
2547
|
+
checkSnapshotRef.current = snapshot;
|
|
2276
2548
|
answeredRef.current = true;
|
|
2277
2549
|
setSubmitted(true);
|
|
2278
2550
|
assessment.answer({
|
|
@@ -2297,12 +2569,13 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2297
2569
|
(0, import_react16.useEffect)(() => {
|
|
2298
2570
|
if (!allFilled) {
|
|
2299
2571
|
answeredRef.current = false;
|
|
2572
|
+
checkSnapshotRef.current = null;
|
|
2300
2573
|
setSubmitted(false);
|
|
2301
2574
|
}
|
|
2302
2575
|
}, [allFilled]);
|
|
2303
2576
|
(0, import_react16.useEffect)(() => {
|
|
2304
|
-
if (props.autoCheck && allFilled) check();
|
|
2305
|
-
}, [allFilled, props.autoCheck, values, passedThreshold]);
|
|
2577
|
+
if (props.autoCheck && allFilled && !passed) check();
|
|
2578
|
+
}, [allFilled, props.autoCheck, values, passedThreshold, passed]);
|
|
2306
2579
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
2307
2580
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
2308
2581
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: parsed.parts.map((part, i) => {
|
|
@@ -2361,9 +2634,13 @@ function DragTheWordsInner(props, ref) {
|
|
|
2361
2634
|
const [submitted, setSubmitted] = (0, import_react17.useState)(false);
|
|
2362
2635
|
const completedRef = (0, import_react17.useRef)(false);
|
|
2363
2636
|
const answeredRef = (0, import_react17.useRef)(false);
|
|
2637
|
+
const checkSnapshotRef = (0, import_react17.useRef)(null);
|
|
2638
|
+
const telemetryReplayedRef = (0, import_react17.useRef)(false);
|
|
2364
2639
|
const reset = () => {
|
|
2365
2640
|
completedRef.current = false;
|
|
2366
2641
|
answeredRef.current = false;
|
|
2642
|
+
checkSnapshotRef.current = null;
|
|
2643
|
+
telemetryReplayedRef.current = false;
|
|
2367
2644
|
setPassed(false);
|
|
2368
2645
|
setSubmitted(false);
|
|
2369
2646
|
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
@@ -2381,6 +2658,31 @@ function DragTheWordsInner(props, ref) {
|
|
|
2381
2658
|
});
|
|
2382
2659
|
const maxScore = answers.length;
|
|
2383
2660
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2661
|
+
const replayTelemetry = (nextZones, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
|
|
2662
|
+
if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
|
|
2663
|
+
telemetryReplayedRef.current = true;
|
|
2664
|
+
const nextPassedThreshold = meetsPassingThreshold(
|
|
2665
|
+
nextScore,
|
|
2666
|
+
nextMaxScore || 1,
|
|
2667
|
+
props.passingScore
|
|
2668
|
+
);
|
|
2669
|
+
assessment.answer({
|
|
2670
|
+
checkId,
|
|
2671
|
+
interactionType: INTERACTION4,
|
|
2672
|
+
question: props.template,
|
|
2673
|
+
response: nextZones,
|
|
2674
|
+
correct: nextPassedThreshold
|
|
2675
|
+
});
|
|
2676
|
+
if (nextPassed || nextPassedThreshold) {
|
|
2677
|
+
assessment.complete({
|
|
2678
|
+
checkId,
|
|
2679
|
+
interactionType: INTERACTION4,
|
|
2680
|
+
score: nextScore,
|
|
2681
|
+
maxScore: nextMaxScore,
|
|
2682
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
};
|
|
2384
2686
|
const handle = (0, import_react17.useMemo)(
|
|
2385
2687
|
() => buildAssessmentHandle({
|
|
2386
2688
|
checkId,
|
|
@@ -2401,22 +2703,35 @@ function DragTheWordsInner(props, ref) {
|
|
|
2401
2703
|
getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
|
|
2402
2704
|
resume: (state) => {
|
|
2403
2705
|
const rawZones = state.zones;
|
|
2404
|
-
|
|
2706
|
+
let nextZones = zones;
|
|
2707
|
+
if (rawZones && typeof rawZones === "object") {
|
|
2708
|
+
nextZones = { ...rawZones };
|
|
2709
|
+
setZones(nextZones);
|
|
2710
|
+
}
|
|
2405
2711
|
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2712
|
+
let nextPassed = passed;
|
|
2713
|
+
let nextSubmitted = submitted;
|
|
2406
2714
|
readBooleanStateField(state, "passed", (value) => {
|
|
2715
|
+
nextPassed = value;
|
|
2407
2716
|
setPassed(value);
|
|
2408
2717
|
completedRef.current = value;
|
|
2409
2718
|
answeredRef.current = value;
|
|
2410
2719
|
});
|
|
2411
2720
|
readBooleanStateField(state, "submitted", (value) => {
|
|
2721
|
+
nextSubmitted = value;
|
|
2412
2722
|
setSubmitted(value);
|
|
2413
2723
|
if (value) answeredRef.current = true;
|
|
2414
2724
|
});
|
|
2415
2725
|
const kw = state.keyboardWord;
|
|
2416
2726
|
if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
|
|
2727
|
+
let nextScore = 0;
|
|
2728
|
+
answers.forEach((ans, i) => {
|
|
2729
|
+
if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
|
|
2730
|
+
});
|
|
2731
|
+
replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
|
|
2417
2732
|
}
|
|
2418
2733
|
}),
|
|
2419
|
-
[allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, submitted, zones]
|
|
2734
|
+
[allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
|
|
2420
2735
|
);
|
|
2421
2736
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2422
2737
|
const placeInZone = (zoneId, word) => {
|
|
@@ -2446,7 +2761,10 @@ function DragTheWordsInner(props, ref) {
|
|
|
2446
2761
|
return;
|
|
2447
2762
|
}
|
|
2448
2763
|
if (!allFilled) return;
|
|
2449
|
-
if (
|
|
2764
|
+
if (passed) return;
|
|
2765
|
+
const snapshot = JSON.stringify(zones);
|
|
2766
|
+
if (checkSnapshotRef.current === snapshot) return;
|
|
2767
|
+
checkSnapshotRef.current = snapshot;
|
|
2450
2768
|
answeredRef.current = true;
|
|
2451
2769
|
setSubmitted(true);
|
|
2452
2770
|
assessment.answer({
|
|
@@ -2471,12 +2789,13 @@ function DragTheWordsInner(props, ref) {
|
|
|
2471
2789
|
(0, import_react17.useEffect)(() => {
|
|
2472
2790
|
if (!allFilled) {
|
|
2473
2791
|
answeredRef.current = false;
|
|
2792
|
+
checkSnapshotRef.current = null;
|
|
2474
2793
|
setSubmitted(false);
|
|
2475
2794
|
}
|
|
2476
2795
|
}, [allFilled]);
|
|
2477
2796
|
(0, import_react17.useEffect)(() => {
|
|
2478
|
-
if (props.autoCheck && allFilled) check();
|
|
2479
|
-
}, [allFilled, props.autoCheck, zones, passedThreshold]);
|
|
2797
|
+
if (props.autoCheck && allFilled && !passed) check();
|
|
2798
|
+
}, [allFilled, props.autoCheck, zones, passedThreshold, passed]);
|
|
2480
2799
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
2481
2800
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
2482
2801
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
@@ -2876,6 +3195,10 @@ function useCompoundPersistence(opts) {
|
|
|
2876
3195
|
}, [ctx, opts.index, opts.pageCount]);
|
|
2877
3196
|
const buildStateRef = (0, import_react21.useRef)(buildState);
|
|
2878
3197
|
buildStateRef.current = buildState;
|
|
3198
|
+
const transformStateRef = (0, import_react21.useRef)(opts.transformState);
|
|
3199
|
+
transformStateRef.current = opts.transformState;
|
|
3200
|
+
const persistNowRef = (0, import_react21.useRef)(() => {
|
|
3201
|
+
});
|
|
2879
3202
|
const finalizeHydration = (0, import_react21.useCallback)(
|
|
2880
3203
|
(childStates) => {
|
|
2881
3204
|
loadedChildStatesRef.current = {
|
|
@@ -2884,6 +3207,7 @@ function useCompoundPersistence(opts) {
|
|
|
2884
3207
|
};
|
|
2885
3208
|
skipSaveUntilHydratedRef.current = false;
|
|
2886
3209
|
pendingChildResumeRef.current = null;
|
|
3210
|
+
queueMicrotask(() => persistNowRef.current());
|
|
2887
3211
|
},
|
|
2888
3212
|
[]
|
|
2889
3213
|
);
|
|
@@ -2896,6 +3220,14 @@ function useCompoundPersistence(opts) {
|
|
|
2896
3220
|
alreadyResumed: resumedChildKeysRef.current
|
|
2897
3221
|
});
|
|
2898
3222
|
if (!applied) {
|
|
3223
|
+
if (handles.size === 0) {
|
|
3224
|
+
const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
|
|
3225
|
+
resumeChildHandles(handles, registeredOnly2, {
|
|
3226
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
3227
|
+
});
|
|
3228
|
+
finalizeHydration(registeredOnly2);
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
2899
3231
|
const handlesAtWait = handles.size;
|
|
2900
3232
|
queueMicrotask(() => {
|
|
2901
3233
|
if (pendingChildResumeRef.current !== pending) return;
|
|
@@ -2929,8 +3261,14 @@ function useCompoundPersistence(opts) {
|
|
|
2929
3261
|
});
|
|
2930
3262
|
const persistNow = (0, import_react21.useCallback)(() => {
|
|
2931
3263
|
if (!opts.enabled || !opts.courseId) return;
|
|
2932
|
-
|
|
3264
|
+
if (skipSaveUntilHydratedRef.current) return;
|
|
3265
|
+
const built = buildStateRef.current();
|
|
3266
|
+
const state = transformStateRef.current ? transformStateRef.current(built) : built;
|
|
3267
|
+
saveResume(state);
|
|
2933
3268
|
}, [opts.enabled, opts.courseId, saveResume]);
|
|
3269
|
+
(0, import_react21.useEffect)(() => {
|
|
3270
|
+
persistNowRef.current = persistNow;
|
|
3271
|
+
}, [persistNow]);
|
|
2934
3272
|
const notifyImperativeResume = (0, import_react21.useCallback)(
|
|
2935
3273
|
(state) => {
|
|
2936
3274
|
const clamped = (0, import_core14.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
|
|
@@ -2952,12 +3290,12 @@ function useCompoundPersistence(opts) {
|
|
|
2952
3290
|
}
|
|
2953
3291
|
};
|
|
2954
3292
|
}, [bridgeRef, notifyImperativeResume]);
|
|
2955
|
-
(0, import_react21.useEffect)(() => {
|
|
2956
|
-
persistNow();
|
|
2957
|
-
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
2958
3293
|
(0, import_react21.useEffect)(() => {
|
|
2959
3294
|
applyPendingChildResume();
|
|
2960
3295
|
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
3296
|
+
(0, import_react21.useEffect)(() => {
|
|
3297
|
+
persistNow();
|
|
3298
|
+
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
2961
3299
|
(0, import_react21.useEffect)(() => {
|
|
2962
3300
|
if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
|
|
2963
3301
|
const flushOnExit = () => {
|
|
@@ -2982,7 +3320,8 @@ function useCompoundShell(opts) {
|
|
|
2982
3320
|
index: opts.index,
|
|
2983
3321
|
setIndex: opts.setIndex,
|
|
2984
3322
|
enabled: opts.persistEnabled,
|
|
2985
|
-
storage: opts.storage
|
|
3323
|
+
storage: opts.storage,
|
|
3324
|
+
transformState: opts.transformState
|
|
2986
3325
|
});
|
|
2987
3326
|
const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
|
|
2988
3327
|
const visibleIndex = (0, import_core15.clampCompoundPageIndex)(opts.index, opts.pageCount);
|
|
@@ -3037,6 +3376,8 @@ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
|
3037
3376
|
"InteractiveBook",
|
|
3038
3377
|
"Slide",
|
|
3039
3378
|
"SlideDeck",
|
|
3379
|
+
"TimedCue",
|
|
3380
|
+
"InteractiveVideo",
|
|
3040
3381
|
"AssessmentSequence"
|
|
3041
3382
|
]);
|
|
3042
3383
|
function warnOrThrow(msg, strict) {
|
|
@@ -3265,14 +3606,40 @@ function Image(props) {
|
|
|
3265
3606
|
}
|
|
3266
3607
|
setLessonkitBlockType(Image, "Image");
|
|
3267
3608
|
|
|
3268
|
-
// src/blocks/
|
|
3609
|
+
// src/blocks/Video.tsx
|
|
3269
3610
|
var import_react26 = require("react");
|
|
3270
3611
|
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
3612
|
+
function Video(props) {
|
|
3613
|
+
const blockId = (0, import_react26.useMemo)(
|
|
3614
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3615
|
+
[props.blockId]
|
|
3616
|
+
);
|
|
3617
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { "aria-label": props.title ?? "Video", "data-lk-block-id": blockId, "data-testid": "video", children: [
|
|
3618
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { "data-testid": "video-title", children: props.title }) : null,
|
|
3619
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3620
|
+
"video",
|
|
3621
|
+
{
|
|
3622
|
+
controls: true,
|
|
3623
|
+
preload: "metadata",
|
|
3624
|
+
poster: props.poster,
|
|
3625
|
+
src: props.src,
|
|
3626
|
+
"data-testid": "video-player",
|
|
3627
|
+
style: { maxWidth: "100%" },
|
|
3628
|
+
children: props.captions ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("track", { kind: "captions", src: props.captions, srcLang: "en", label: "Captions", default: true }) : null
|
|
3629
|
+
}
|
|
3630
|
+
)
|
|
3631
|
+
] });
|
|
3632
|
+
}
|
|
3633
|
+
setLessonkitBlockType(Video, "Video");
|
|
3634
|
+
|
|
3635
|
+
// src/blocks/Page.tsx
|
|
3636
|
+
var import_react27 = require("react");
|
|
3637
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3271
3638
|
function Page(props) {
|
|
3272
3639
|
validateCompoundChildren("Page", props.children);
|
|
3273
3640
|
const { track } = useLessonkit();
|
|
3274
3641
|
const lessonId = useEnclosingLessonId();
|
|
3275
|
-
(0,
|
|
3642
|
+
(0, import_react27.useEffect)(() => {
|
|
3276
3643
|
if (props.hidden || !lessonId || props.parentType) return;
|
|
3277
3644
|
track(
|
|
3278
3645
|
"compound_page_viewed",
|
|
@@ -3284,7 +3651,7 @@ function Page(props) {
|
|
|
3284
3651
|
{ lessonId }
|
|
3285
3652
|
);
|
|
3286
3653
|
}, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3287
|
-
return /* @__PURE__ */ (0,
|
|
3654
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3288
3655
|
"section",
|
|
3289
3656
|
{
|
|
3290
3657
|
"aria-label": props.title ?? "Page",
|
|
@@ -3292,8 +3659,8 @@ function Page(props) {
|
|
|
3292
3659
|
"data-testid": `page-${props.blockId}`,
|
|
3293
3660
|
hidden: props.hidden ? true : void 0,
|
|
3294
3661
|
children: [
|
|
3295
|
-
props.title ? /* @__PURE__ */ (0,
|
|
3296
|
-
/* @__PURE__ */ (0,
|
|
3662
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { children: props.title }) : null,
|
|
3663
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { children: props.children }) })
|
|
3297
3664
|
]
|
|
3298
3665
|
}
|
|
3299
3666
|
);
|
|
@@ -3301,9 +3668,9 @@ function Page(props) {
|
|
|
3301
3668
|
setLessonkitBlockType(Page, "Page");
|
|
3302
3669
|
|
|
3303
3670
|
// src/blocks/InteractiveBook.tsx
|
|
3304
|
-
var
|
|
3305
|
-
var
|
|
3306
|
-
var InteractiveBookInner = (0,
|
|
3671
|
+
var import_react28 = __toESM(require("react"), 1);
|
|
3672
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3673
|
+
var InteractiveBookInner = (0, import_react28.forwardRef)(
|
|
3307
3674
|
function InteractiveBookInner2(props, ref) {
|
|
3308
3675
|
const { blockId, pages, index, setIndex, persistEnabled } = props;
|
|
3309
3676
|
validateCompoundChildren("InteractiveBook", pages);
|
|
@@ -3318,11 +3685,11 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
|
|
|
3318
3685
|
persistEnabled,
|
|
3319
3686
|
ref
|
|
3320
3687
|
});
|
|
3321
|
-
const pageTitles = (0,
|
|
3688
|
+
const pageTitles = (0, import_react28.useMemo)(
|
|
3322
3689
|
() => pages.map((page) => page.props.title),
|
|
3323
3690
|
[pages]
|
|
3324
3691
|
);
|
|
3325
|
-
(0,
|
|
3692
|
+
(0, import_react28.useEffect)(() => {
|
|
3326
3693
|
if (!lessonId || pages.length === 0) return;
|
|
3327
3694
|
track(
|
|
3328
3695
|
"book_page_viewed",
|
|
@@ -3334,31 +3701,31 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
|
|
|
3334
3701
|
{ lessonId }
|
|
3335
3702
|
);
|
|
3336
3703
|
}, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
|
|
3337
|
-
return /* @__PURE__ */ (0,
|
|
3338
|
-
/* @__PURE__ */ (0,
|
|
3339
|
-
/* @__PURE__ */ (0,
|
|
3704
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
|
|
3705
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: props.title }),
|
|
3706
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { children: [
|
|
3340
3707
|
"Page ",
|
|
3341
3708
|
progress.current,
|
|
3342
3709
|
" of ",
|
|
3343
3710
|
progress.total
|
|
3344
3711
|
] }),
|
|
3345
|
-
props.showBookScore && ctx ? /* @__PURE__ */ (0,
|
|
3712
|
+
props.showBookScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { "data-testid": "book-score", children: [
|
|
3346
3713
|
"Score: ",
|
|
3347
3714
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3348
3715
|
" /",
|
|
3349
3716
|
" ",
|
|
3350
3717
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3351
3718
|
] }) : null,
|
|
3352
|
-
/* @__PURE__ */ (0,
|
|
3353
|
-
(page, i) =>
|
|
3719
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
|
|
3720
|
+
(page, i) => import_react28.default.cloneElement(page, {
|
|
3354
3721
|
key: page.key ?? page.props.blockId,
|
|
3355
3722
|
hidden: i !== visibleIndex,
|
|
3356
3723
|
pageIndex: i,
|
|
3357
3724
|
parentType: "InteractiveBook"
|
|
3358
3725
|
})
|
|
3359
3726
|
) }),
|
|
3360
|
-
/* @__PURE__ */ (0,
|
|
3361
|
-
/* @__PURE__ */ (0,
|
|
3727
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("nav", { "aria-label": "Book navigation", children: [
|
|
3728
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3362
3729
|
"button",
|
|
3363
3730
|
{
|
|
3364
3731
|
type: "button",
|
|
@@ -3368,7 +3735,7 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
|
|
|
3368
3735
|
children: "Previous"
|
|
3369
3736
|
}
|
|
3370
3737
|
),
|
|
3371
|
-
/* @__PURE__ */ (0,
|
|
3738
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3372
3739
|
"button",
|
|
3373
3740
|
{
|
|
3374
3741
|
type: "button",
|
|
@@ -3382,13 +3749,13 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
|
|
|
3382
3749
|
] });
|
|
3383
3750
|
}
|
|
3384
3751
|
);
|
|
3385
|
-
var InteractiveBook = (0,
|
|
3386
|
-
const blockId = (0,
|
|
3752
|
+
var InteractiveBook = (0, import_react28.forwardRef)(function InteractiveBook2(props, ref) {
|
|
3753
|
+
const blockId = (0, import_react28.useMemo)(
|
|
3387
3754
|
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3388
3755
|
[props.blockId]
|
|
3389
3756
|
);
|
|
3390
|
-
const pages =
|
|
3391
|
-
|
|
3757
|
+
const pages = import_react28.default.Children.toArray(props.children).filter(
|
|
3758
|
+
import_react28.default.isValidElement
|
|
3392
3759
|
);
|
|
3393
3760
|
const { config, storage } = useLessonkit();
|
|
3394
3761
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
@@ -3399,12 +3766,12 @@ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(p
|
|
|
3399
3766
|
persistEnabled,
|
|
3400
3767
|
storage
|
|
3401
3768
|
});
|
|
3402
|
-
const [index, setIndex] = (0,
|
|
3403
|
-
const setIndexStable = (0,
|
|
3404
|
-
(0,
|
|
3769
|
+
const [index, setIndex] = (0, import_react28.useState)(initialIndex);
|
|
3770
|
+
const setIndexStable = (0, import_react28.useCallback)((i) => setIndex(i), []);
|
|
3771
|
+
(0, import_react28.useEffect)(() => {
|
|
3405
3772
|
setIndex(initialIndex);
|
|
3406
3773
|
}, [config.courseId, blockId, initialIndex]);
|
|
3407
|
-
return /* @__PURE__ */ (0,
|
|
3774
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3408
3775
|
InteractiveBookInner,
|
|
3409
3776
|
{
|
|
3410
3777
|
...props,
|
|
@@ -3420,13 +3787,13 @@ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(p
|
|
|
3420
3787
|
setLessonkitBlockType(InteractiveBook, "InteractiveBook");
|
|
3421
3788
|
|
|
3422
3789
|
// src/blocks/Slide.tsx
|
|
3423
|
-
var
|
|
3424
|
-
var
|
|
3790
|
+
var import_react29 = require("react");
|
|
3791
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
3425
3792
|
function Slide(props) {
|
|
3426
3793
|
validateCompoundChildren("Slide", props.children);
|
|
3427
3794
|
const { track } = useLessonkit();
|
|
3428
3795
|
const lessonId = useEnclosingLessonId();
|
|
3429
|
-
(0,
|
|
3796
|
+
(0, import_react29.useEffect)(() => {
|
|
3430
3797
|
if (props.hidden || !lessonId || props.parentType) return;
|
|
3431
3798
|
track(
|
|
3432
3799
|
"compound_page_viewed",
|
|
@@ -3438,7 +3805,7 @@ function Slide(props) {
|
|
|
3438
3805
|
{ lessonId }
|
|
3439
3806
|
);
|
|
3440
3807
|
}, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3441
|
-
return /* @__PURE__ */ (0,
|
|
3808
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
|
|
3442
3809
|
"section",
|
|
3443
3810
|
{
|
|
3444
3811
|
"aria-label": props.title ?? "Slide",
|
|
@@ -3446,8 +3813,8 @@ function Slide(props) {
|
|
|
3446
3813
|
"data-testid": `slide-${props.blockId}`,
|
|
3447
3814
|
hidden: props.hidden ? true : void 0,
|
|
3448
3815
|
children: [
|
|
3449
|
-
props.title ? /* @__PURE__ */ (0,
|
|
3450
|
-
/* @__PURE__ */ (0,
|
|
3816
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { children: props.title }) : null,
|
|
3817
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { children: props.children }) })
|
|
3451
3818
|
]
|
|
3452
3819
|
}
|
|
3453
3820
|
);
|
|
@@ -3455,10 +3822,10 @@ function Slide(props) {
|
|
|
3455
3822
|
setLessonkitBlockType(Slide, "Slide");
|
|
3456
3823
|
|
|
3457
3824
|
// src/blocks/SlideDeck.tsx
|
|
3458
|
-
var
|
|
3825
|
+
var import_react31 = __toESM(require("react"), 1);
|
|
3459
3826
|
|
|
3460
3827
|
// src/compound/useCompoundKeyboardNav.ts
|
|
3461
|
-
var
|
|
3828
|
+
var import_react30 = require("react");
|
|
3462
3829
|
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
|
|
3463
3830
|
function isEditableTarget(target) {
|
|
3464
3831
|
if (!(target instanceof HTMLElement)) return false;
|
|
@@ -3471,7 +3838,7 @@ function isEditableTarget(target) {
|
|
|
3471
3838
|
}
|
|
3472
3839
|
function useCompoundKeyboardNav(opts) {
|
|
3473
3840
|
const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
|
|
3474
|
-
(0,
|
|
3841
|
+
(0, import_react30.useEffect)(() => {
|
|
3475
3842
|
const el = containerRef.current;
|
|
3476
3843
|
if (!el || pageCount === 0) return;
|
|
3477
3844
|
const onKeyDown = (event) => {
|
|
@@ -3516,13 +3883,13 @@ function useCompoundKeyboardNav(opts) {
|
|
|
3516
3883
|
}
|
|
3517
3884
|
|
|
3518
3885
|
// src/blocks/SlideDeck.tsx
|
|
3519
|
-
var
|
|
3520
|
-
var SlideDeckInner = (0,
|
|
3886
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
3887
|
+
var SlideDeckInner = (0, import_react31.forwardRef)(function SlideDeckInner2(props, ref) {
|
|
3521
3888
|
const { blockId, slides, index, setIndex, persistEnabled } = props;
|
|
3522
3889
|
validateCompoundChildren("SlideDeck", slides);
|
|
3523
3890
|
const { config, track } = useLessonkit();
|
|
3524
3891
|
const lessonId = useEnclosingLessonId();
|
|
3525
|
-
const containerRef = (0,
|
|
3892
|
+
const containerRef = (0, import_react31.useRef)(null);
|
|
3526
3893
|
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3527
3894
|
courseId: config.courseId,
|
|
3528
3895
|
compoundId: blockId,
|
|
@@ -3532,7 +3899,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3532
3899
|
persistEnabled,
|
|
3533
3900
|
ref
|
|
3534
3901
|
});
|
|
3535
|
-
const setIndexStable = (0,
|
|
3902
|
+
const setIndexStable = (0, import_react31.useCallback)((i) => setIndex(i), [setIndex]);
|
|
3536
3903
|
useCompoundKeyboardNav({
|
|
3537
3904
|
containerRef,
|
|
3538
3905
|
visibleIndex,
|
|
@@ -3541,11 +3908,11 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3541
3908
|
goPrev,
|
|
3542
3909
|
setIndex: setIndexStable
|
|
3543
3910
|
});
|
|
3544
|
-
const slideTitles = (0,
|
|
3911
|
+
const slideTitles = (0, import_react31.useMemo)(
|
|
3545
3912
|
() => slides.map((slide) => slide.props.title),
|
|
3546
3913
|
[slides]
|
|
3547
3914
|
);
|
|
3548
|
-
(0,
|
|
3915
|
+
(0, import_react31.useEffect)(() => {
|
|
3549
3916
|
if (!lessonId || slides.length === 0) return;
|
|
3550
3917
|
track(
|
|
3551
3918
|
"slide_viewed",
|
|
@@ -3557,7 +3924,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3557
3924
|
{ lessonId }
|
|
3558
3925
|
);
|
|
3559
3926
|
}, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
|
|
3560
|
-
return /* @__PURE__ */ (0,
|
|
3927
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
|
|
3561
3928
|
"section",
|
|
3562
3929
|
{
|
|
3563
3930
|
ref: containerRef,
|
|
@@ -3566,30 +3933,30 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3566
3933
|
"data-testid": "slide-deck",
|
|
3567
3934
|
"data-lk-block-id": blockId,
|
|
3568
3935
|
children: [
|
|
3569
|
-
/* @__PURE__ */ (0,
|
|
3570
|
-
/* @__PURE__ */ (0,
|
|
3936
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h3", { children: props.title }),
|
|
3937
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { children: [
|
|
3571
3938
|
"Slide ",
|
|
3572
3939
|
progress.current,
|
|
3573
3940
|
" of ",
|
|
3574
3941
|
progress.total
|
|
3575
3942
|
] }),
|
|
3576
|
-
props.showDeckScore && ctx ? /* @__PURE__ */ (0,
|
|
3943
|
+
props.showDeckScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { "data-testid": "deck-score", children: [
|
|
3577
3944
|
"Score: ",
|
|
3578
3945
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3579
3946
|
" /",
|
|
3580
3947
|
" ",
|
|
3581
3948
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3582
3949
|
] }) : null,
|
|
3583
|
-
/* @__PURE__ */ (0,
|
|
3584
|
-
(slide, i) =>
|
|
3950
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "data-testid": "slide-deck-slide", children: slides.map(
|
|
3951
|
+
(slide, i) => import_react31.default.cloneElement(slide, {
|
|
3585
3952
|
key: slide.key ?? slide.props.blockId,
|
|
3586
3953
|
hidden: i !== visibleIndex,
|
|
3587
3954
|
slideIndex: i,
|
|
3588
3955
|
parentType: "SlideDeck"
|
|
3589
3956
|
})
|
|
3590
3957
|
) }),
|
|
3591
|
-
/* @__PURE__ */ (0,
|
|
3592
|
-
/* @__PURE__ */ (0,
|
|
3958
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("nav", { "aria-label": "Slide navigation", children: [
|
|
3959
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3593
3960
|
"button",
|
|
3594
3961
|
{
|
|
3595
3962
|
type: "button",
|
|
@@ -3599,7 +3966,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3599
3966
|
children: "Previous slide"
|
|
3600
3967
|
}
|
|
3601
3968
|
),
|
|
3602
|
-
/* @__PURE__ */ (0,
|
|
3969
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3603
3970
|
"button",
|
|
3604
3971
|
{
|
|
3605
3972
|
type: "button",
|
|
@@ -3614,13 +3981,13 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3614
3981
|
}
|
|
3615
3982
|
);
|
|
3616
3983
|
});
|
|
3617
|
-
var SlideDeck = (0,
|
|
3618
|
-
const blockId = (0,
|
|
3984
|
+
var SlideDeck = (0, import_react31.forwardRef)(function SlideDeck2(props, ref) {
|
|
3985
|
+
const blockId = (0, import_react31.useMemo)(
|
|
3619
3986
|
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3620
3987
|
[props.blockId]
|
|
3621
3988
|
);
|
|
3622
|
-
const slides =
|
|
3623
|
-
|
|
3989
|
+
const slides = import_react31.default.Children.toArray(props.children).filter(
|
|
3990
|
+
import_react31.default.isValidElement
|
|
3624
3991
|
);
|
|
3625
3992
|
const { config, storage } = useLessonkit();
|
|
3626
3993
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
@@ -3631,12 +3998,12 @@ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
|
|
|
3631
3998
|
persistEnabled,
|
|
3632
3999
|
storage
|
|
3633
4000
|
});
|
|
3634
|
-
const [index, setIndex] = (0,
|
|
3635
|
-
const setIndexStable = (0,
|
|
3636
|
-
(0,
|
|
4001
|
+
const [index, setIndex] = (0, import_react31.useState)(initialIndex);
|
|
4002
|
+
const setIndexStable = (0, import_react31.useCallback)((i) => setIndex(i), []);
|
|
4003
|
+
(0, import_react31.useEffect)(() => {
|
|
3637
4004
|
setIndex(initialIndex);
|
|
3638
4005
|
}, [config.courseId, blockId, initialIndex]);
|
|
3639
|
-
return /* @__PURE__ */ (0,
|
|
4006
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3640
4007
|
SlideDeckInner,
|
|
3641
4008
|
{
|
|
3642
4009
|
...props,
|
|
@@ -3651,72 +4018,1675 @@ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
|
|
|
3651
4018
|
});
|
|
3652
4019
|
setLessonkitBlockType(SlideDeck, "SlideDeck");
|
|
3653
4020
|
|
|
3654
|
-
// src/blocks/
|
|
3655
|
-
var
|
|
3656
|
-
var
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
const
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
4021
|
+
// src/blocks/TimedCue.tsx
|
|
4022
|
+
var import_react32 = __toESM(require("react"), 1);
|
|
4023
|
+
var import_accessibility3 = require("@lessonkit/accessibility");
|
|
4024
|
+
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
4025
|
+
function TimedCue(props) {
|
|
4026
|
+
validateCompoundChildren("TimedCue", props.children, true);
|
|
4027
|
+
const child = import_react32.default.Children.only(props.children);
|
|
4028
|
+
const overlayRef = (0, import_react32.useRef)(null);
|
|
4029
|
+
(0, import_react32.useEffect)(() => {
|
|
4030
|
+
if (props.hidden || !overlayRef.current) return;
|
|
4031
|
+
const trap = (0, import_accessibility3.trapFocus)(overlayRef.current, { restoreFocus: false });
|
|
4032
|
+
trap.activate();
|
|
4033
|
+
const firstFocusable = overlayRef.current.querySelector(
|
|
4034
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
4035
|
+
);
|
|
4036
|
+
firstFocusable?.focus();
|
|
4037
|
+
return () => trap.deactivate();
|
|
4038
|
+
}, [props.hidden, props.cueIndex]);
|
|
4039
|
+
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
|
|
4040
|
+
"div",
|
|
4041
|
+
{
|
|
4042
|
+
ref: overlayRef,
|
|
4043
|
+
role: "dialog",
|
|
4044
|
+
"aria-modal": props.hidden ? void 0 : true,
|
|
4045
|
+
"aria-hidden": props.hidden ? true : void 0,
|
|
4046
|
+
hidden: props.hidden ? true : void 0,
|
|
4047
|
+
"aria-label": props.label ?? `Interaction at ${props.atSeconds} seconds`,
|
|
4048
|
+
"data-testid": `timed-cue-${props.cueIndex ?? 0}`,
|
|
4049
|
+
"data-lk-cue-at": props.atSeconds,
|
|
4050
|
+
className: "lk-timed-cue-overlay",
|
|
4051
|
+
style: {
|
|
4052
|
+
position: "relative",
|
|
4053
|
+
zIndex: 2,
|
|
4054
|
+
background: "var(--lk-surface, #fff)",
|
|
4055
|
+
padding: "1rem",
|
|
4056
|
+
border: "1px solid var(--lk-border, #ccc)",
|
|
4057
|
+
marginTop: "0.5rem"
|
|
4058
|
+
},
|
|
4059
|
+
children: [
|
|
4060
|
+
props.hidden ? null : props.label ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { "data-testid": "timed-cue-label", children: props.label }) : null,
|
|
4061
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CompoundPageIndexProvider, { pageIndex: props.cueIndex ?? 0, children: child })
|
|
4062
|
+
]
|
|
4063
|
+
}
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
setLessonkitBlockType(TimedCue, "TimedCue");
|
|
4067
|
+
|
|
4068
|
+
// src/blocks/InteractiveVideo.tsx
|
|
4069
|
+
var import_react33 = __toESM(require("react"), 1);
|
|
4070
|
+
var import_core19 = require("@lessonkit/core");
|
|
4071
|
+
|
|
4072
|
+
// src/compound/useCompoundVideoShell.ts
|
|
4073
|
+
var import_core18 = require("@lessonkit/core");
|
|
4074
|
+
var IV_META_KEY = "__lk_iv__";
|
|
4075
|
+
function readInteractiveVideoMeta(childStates) {
|
|
4076
|
+
const raw = childStates[IV_META_KEY];
|
|
4077
|
+
if (!raw || typeof raw !== "object") return null;
|
|
4078
|
+
const currentTime = typeof raw.currentTime === "number" ? raw.currentTime : 0;
|
|
4079
|
+
const completedCueIndices = Array.isArray(raw.completedCueIndices) ? raw.completedCueIndices.filter((n) => typeof n === "number") : [];
|
|
4080
|
+
return { currentTime, completedCueIndices };
|
|
4081
|
+
}
|
|
4082
|
+
function mergeVideoMetaIntoState(state, meta) {
|
|
4083
|
+
return {
|
|
4084
|
+
...state,
|
|
4085
|
+
childStates: {
|
|
4086
|
+
...state.childStates,
|
|
4087
|
+
[IV_META_KEY]: meta
|
|
4088
|
+
}
|
|
3678
4089
|
};
|
|
3679
|
-
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
3680
|
-
const expanded = open.has(section.id);
|
|
3681
|
-
const panelId = `${baseId}-${section.id}`;
|
|
3682
|
-
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
3683
|
-
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
3684
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3685
|
-
"button",
|
|
3686
|
-
{
|
|
3687
|
-
id: triggerId,
|
|
3688
|
-
type: "button",
|
|
3689
|
-
"aria-expanded": expanded,
|
|
3690
|
-
"aria-controls": panelId,
|
|
3691
|
-
"data-testid": `accordion-trigger-${section.id}`,
|
|
3692
|
-
onClick: () => toggle(section.id),
|
|
3693
|
-
children: section.title
|
|
3694
|
-
}
|
|
3695
|
-
) }),
|
|
3696
|
-
expanded ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
3697
|
-
] }, section.id);
|
|
3698
|
-
}) });
|
|
3699
4090
|
}
|
|
3700
|
-
setLessonkitBlockType(Accordion, "Accordion");
|
|
3701
4091
|
|
|
3702
|
-
// src/blocks/
|
|
3703
|
-
var
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
const
|
|
3707
|
-
|
|
3708
|
-
const
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
4092
|
+
// src/blocks/InteractiveVideo.tsx
|
|
4093
|
+
var import_jsx_runtime23 = require("react/jsx-runtime");
|
|
4094
|
+
function loadVideoMeta(storage, courseId, blockId, enabled) {
|
|
4095
|
+
if (!enabled || !courseId) return { currentTime: 0, completedCueIndices: [] };
|
|
4096
|
+
const saved = (0, import_core19.loadCompoundState)(storage, courseId, blockId);
|
|
4097
|
+
if (!saved) return { currentTime: 0, completedCueIndices: [] };
|
|
4098
|
+
const meta = readInteractiveVideoMeta(saved.childStates);
|
|
4099
|
+
return meta ?? { currentTime: 0, completedCueIndices: [] };
|
|
4100
|
+
}
|
|
4101
|
+
function getCueChildCheckId(cue) {
|
|
4102
|
+
const child = import_react33.default.Children.only(cue.props.children);
|
|
4103
|
+
if (!import_react33.default.isValidElement(child)) return null;
|
|
4104
|
+
const props = child.props;
|
|
4105
|
+
if (typeof props.checkId !== "string") return null;
|
|
4106
|
+
return normalizeComponentId(props.checkId, "checkId");
|
|
4107
|
+
}
|
|
4108
|
+
function cueRequiresAnswer(cue) {
|
|
4109
|
+
return Boolean(cue.props.mustComplete && getCueChildCheckId(cue));
|
|
4110
|
+
}
|
|
4111
|
+
var InteractiveVideoInner = (0, import_react33.forwardRef)(function InteractiveVideoInner2(props, ref) {
|
|
4112
|
+
const { blockId, cues, index, setIndex, persistEnabled, initialMeta } = props;
|
|
4113
|
+
validateCompoundChildren("InteractiveVideo", cues);
|
|
4114
|
+
const { config, track, storage } = useLessonkit();
|
|
4115
|
+
const lessonId = useEnclosingLessonId();
|
|
4116
|
+
const videoRef = (0, import_react33.useRef)(null);
|
|
4117
|
+
const completedCuesRef = (0, import_react33.useRef)(new Set(initialMeta.completedCueIndices));
|
|
4118
|
+
const [completedCues, setCompletedCues] = (0, import_react33.useState)(
|
|
4119
|
+
() => new Set(initialMeta.completedCueIndices)
|
|
4120
|
+
);
|
|
4121
|
+
const [overlayActive, setOverlayActive] = (0, import_react33.useState)(false);
|
|
4122
|
+
const firedCuesRef = (0, import_react33.useRef)(new Set(initialMeta.completedCueIndices));
|
|
4123
|
+
const resumeOverlayCheckedRef = (0, import_react33.useRef)(false);
|
|
4124
|
+
const sortedCues = (0, import_react33.useMemo)(
|
|
4125
|
+
() => [...cues].sort((a, b) => (a.props.atSeconds ?? 0) - (b.props.atSeconds ?? 0)),
|
|
4126
|
+
[cues]
|
|
4127
|
+
);
|
|
4128
|
+
(0, import_react33.useEffect)(() => {
|
|
4129
|
+
completedCuesRef.current = completedCues;
|
|
4130
|
+
}, [completedCues]);
|
|
4131
|
+
const transformState = (0, import_react33.useCallback)(
|
|
4132
|
+
(state) => mergeVideoMetaIntoState(state, {
|
|
4133
|
+
currentTime: videoRef.current?.currentTime ?? initialMeta.currentTime,
|
|
4134
|
+
completedCueIndices: [...completedCuesRef.current]
|
|
4135
|
+
}),
|
|
4136
|
+
[initialMeta.currentTime]
|
|
4137
|
+
);
|
|
4138
|
+
const { ctx } = useCompoundShell({
|
|
4139
|
+
courseId: config.courseId,
|
|
4140
|
+
compoundId: blockId,
|
|
4141
|
+
pageCount: sortedCues.length,
|
|
4142
|
+
index,
|
|
4143
|
+
setIndex,
|
|
4144
|
+
persistEnabled,
|
|
4145
|
+
ref,
|
|
4146
|
+
storage,
|
|
4147
|
+
transformState
|
|
4148
|
+
});
|
|
4149
|
+
const activeCue = sortedCues[index];
|
|
4150
|
+
const cueCanContinue = (0, import_react33.useCallback)(
|
|
4151
|
+
(cue) => {
|
|
4152
|
+
if (!cue || !cueRequiresAnswer(cue)) return true;
|
|
4153
|
+
const checkId = getCueChildCheckId(cue);
|
|
4154
|
+
if (!checkId) return true;
|
|
4155
|
+
const entry = ctx?.getRegisteredHandles().get(checkId);
|
|
4156
|
+
if (!entry) return false;
|
|
4157
|
+
return entry.handle.getAnswerGiven();
|
|
4158
|
+
},
|
|
4159
|
+
[ctx]
|
|
4160
|
+
);
|
|
4161
|
+
const canContinueActiveCue = cueCanContinue(activeCue);
|
|
4162
|
+
(0, import_react33.useEffect)(() => {
|
|
4163
|
+
const video = videoRef.current;
|
|
4164
|
+
if (!video || initialMeta.currentTime <= 0) return;
|
|
4165
|
+
video.currentTime = initialMeta.currentTime;
|
|
4166
|
+
}, [initialMeta.currentTime]);
|
|
4167
|
+
(0, import_react33.useEffect)(() => {
|
|
4168
|
+
if (resumeOverlayCheckedRef.current || sortedCues.length === 0) return;
|
|
4169
|
+
resumeOverlayCheckedRef.current = true;
|
|
4170
|
+
const hasSavedProgress = initialMeta.currentTime > 0 || initialMeta.completedCueIndices.length > 0 || persistEnabled && config.courseId && (0, import_core19.loadCompoundState)(storage, config.courseId, blockId) !== null;
|
|
4171
|
+
if (!hasSavedProgress) return;
|
|
4172
|
+
const video = videoRef.current;
|
|
4173
|
+
if (!video) return;
|
|
4174
|
+
const cue = sortedCues[index];
|
|
4175
|
+
if (!cue || completedCues.has(index)) return;
|
|
4176
|
+
setOverlayActive(true);
|
|
4177
|
+
video.pause();
|
|
4178
|
+
const at = cue.props.atSeconds ?? 0;
|
|
4179
|
+
if (video.currentTime < at) {
|
|
4180
|
+
video.currentTime = at;
|
|
4181
|
+
}
|
|
4182
|
+
}, [
|
|
4183
|
+
blockId,
|
|
4184
|
+
completedCues,
|
|
4185
|
+
config.courseId,
|
|
4186
|
+
index,
|
|
4187
|
+
initialMeta.completedCueIndices.length,
|
|
4188
|
+
initialMeta.currentTime,
|
|
4189
|
+
persistEnabled,
|
|
4190
|
+
sortedCues,
|
|
4191
|
+
storage
|
|
4192
|
+
]);
|
|
4193
|
+
const mandatoryIncompleteBefore = (0, import_react33.useCallback)(
|
|
4194
|
+
(time) => {
|
|
4195
|
+
for (let i = 0; i < sortedCues.length; i++) {
|
|
4196
|
+
const cue = sortedCues[i];
|
|
4197
|
+
if ((cue.props.atSeconds ?? 0) >= time) break;
|
|
4198
|
+
if (cue.props.mustComplete && !completedCues.has(i)) return cue.props.atSeconds ?? 0;
|
|
4199
|
+
}
|
|
4200
|
+
return null;
|
|
4201
|
+
},
|
|
4202
|
+
[sortedCues, completedCues]
|
|
4203
|
+
);
|
|
4204
|
+
const activateCue = (0, import_react33.useCallback)(
|
|
4205
|
+
(i) => {
|
|
4206
|
+
const cue = sortedCues[i];
|
|
4207
|
+
if (!cue || firedCuesRef.current.has(i)) return;
|
|
4208
|
+
firedCuesRef.current.add(i);
|
|
4209
|
+
videoRef.current?.pause();
|
|
4210
|
+
setIndex(i);
|
|
4211
|
+
setOverlayActive(true);
|
|
4212
|
+
if (lessonId) {
|
|
4213
|
+
track(
|
|
4214
|
+
"video_cue_reached",
|
|
4215
|
+
{ blockId, cueIndex: i, atSeconds: cue.props.atSeconds ?? 0, cueLabel: cue.props.label },
|
|
4216
|
+
{ lessonId }
|
|
4217
|
+
);
|
|
4218
|
+
}
|
|
4219
|
+
},
|
|
4220
|
+
[blockId, lessonId, setIndex, sortedCues, track]
|
|
4221
|
+
);
|
|
4222
|
+
const onTimeUpdate = () => {
|
|
4223
|
+
const video = videoRef.current;
|
|
4224
|
+
if (!video || overlayActive) return;
|
|
4225
|
+
const t = video.currentTime;
|
|
4226
|
+
const blockSeek = mandatoryIncompleteBefore(t);
|
|
4227
|
+
if (blockSeek !== null && t > blockSeek + 0.5) {
|
|
4228
|
+
video.currentTime = blockSeek;
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
for (let i = 0; i < sortedCues.length; i++) {
|
|
4232
|
+
if (firedCuesRef.current.has(i)) continue;
|
|
4233
|
+
const at = sortedCues[i]?.props.atSeconds ?? 0;
|
|
4234
|
+
if (t >= at) {
|
|
4235
|
+
activateCue(i);
|
|
4236
|
+
break;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
};
|
|
4240
|
+
const completeCue = () => {
|
|
4241
|
+
const cue = sortedCues[index];
|
|
4242
|
+
if (!cue || !cueCanContinue(cue)) return;
|
|
4243
|
+
setCompletedCues((prev) => {
|
|
4244
|
+
const next = /* @__PURE__ */ new Set([...prev, index]);
|
|
4245
|
+
completedCuesRef.current = next;
|
|
4246
|
+
return next;
|
|
4247
|
+
});
|
|
4248
|
+
setOverlayActive(false);
|
|
4249
|
+
if (lessonId) {
|
|
4250
|
+
track(
|
|
4251
|
+
"video_segment_completed",
|
|
4252
|
+
{
|
|
4253
|
+
blockId,
|
|
4254
|
+
segmentIndex: index,
|
|
4255
|
+
atSeconds: cue.props.atSeconds ?? 0,
|
|
4256
|
+
segmentLabel: cue.props.label
|
|
4257
|
+
},
|
|
4258
|
+
{ lessonId }
|
|
4259
|
+
);
|
|
4260
|
+
}
|
|
4261
|
+
videoRef.current?.play().catch(() => {
|
|
4262
|
+
});
|
|
4263
|
+
};
|
|
4264
|
+
return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-video", "data-lk-block-id": blockId, children: [
|
|
4265
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("h3", { children: props.title }),
|
|
4266
|
+
props.showVideoScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("p", { "data-testid": "video-score", children: [
|
|
4267
|
+
"Score: ",
|
|
4268
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
4269
|
+
" /",
|
|
4270
|
+
" ",
|
|
4271
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
4272
|
+
] }) : null,
|
|
4273
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { position: "relative" }, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
4274
|
+
"video",
|
|
4275
|
+
{
|
|
4276
|
+
ref: videoRef,
|
|
4277
|
+
src: props.src,
|
|
4278
|
+
poster: props.poster,
|
|
4279
|
+
controls: true,
|
|
4280
|
+
"data-testid": "interactive-video-player",
|
|
4281
|
+
onTimeUpdate,
|
|
4282
|
+
onSeeking: () => {
|
|
4283
|
+
const video = videoRef.current;
|
|
4284
|
+
if (!video) return;
|
|
4285
|
+
const blockSeek = mandatoryIncompleteBefore(video.currentTime);
|
|
4286
|
+
if (blockSeek !== null && video.currentTime > blockSeek + 0.5) {
|
|
4287
|
+
video.currentTime = blockSeek;
|
|
4288
|
+
}
|
|
4289
|
+
},
|
|
4290
|
+
children: props.captions ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("track", { kind: "captions", src: props.captions, srcLang: "en", label: "Captions", default: true }) : null
|
|
4291
|
+
}
|
|
4292
|
+
) }),
|
|
4293
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { "data-testid": "interactive-video-cues", children: sortedCues.map(
|
|
4294
|
+
(cue, i) => import_react33.default.cloneElement(cue, {
|
|
4295
|
+
key: cue.key ?? i,
|
|
4296
|
+
hidden: !overlayActive || i !== index,
|
|
4297
|
+
cueIndex: i,
|
|
4298
|
+
parentType: "InteractiveVideo"
|
|
4299
|
+
})
|
|
4300
|
+
) }),
|
|
4301
|
+
overlayActive ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
|
|
4302
|
+
activeCue?.props.mustComplete && !canContinueActiveCue ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "status", "data-testid": "cue-must-complete-hint", children: "Complete the interaction to continue." }) : null,
|
|
4303
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
4304
|
+
"button",
|
|
4305
|
+
{
|
|
4306
|
+
type: "button",
|
|
4307
|
+
"data-testid": "cue-continue",
|
|
4308
|
+
disabled: !canContinueActiveCue,
|
|
4309
|
+
"aria-disabled": !canContinueActiveCue,
|
|
4310
|
+
onClick: completeCue,
|
|
4311
|
+
children: "Continue video"
|
|
4312
|
+
}
|
|
4313
|
+
)
|
|
4314
|
+
] }) : null
|
|
4315
|
+
] });
|
|
4316
|
+
});
|
|
4317
|
+
var InteractiveVideo = (0, import_react33.forwardRef)(
|
|
4318
|
+
function InteractiveVideo2(props, ref) {
|
|
4319
|
+
const blockId = (0, import_react33.useMemo)(
|
|
4320
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
4321
|
+
[props.blockId]
|
|
4322
|
+
);
|
|
4323
|
+
const cues = import_react33.default.Children.toArray(props.children).filter(
|
|
4324
|
+
import_react33.default.isValidElement
|
|
4325
|
+
);
|
|
4326
|
+
const { config, storage } = useLessonkit();
|
|
4327
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
4328
|
+
const initialMeta = (0, import_react33.useMemo)(
|
|
4329
|
+
() => loadVideoMeta(storage, config.courseId, blockId, persistEnabled),
|
|
4330
|
+
[storage, config.courseId, blockId, persistEnabled]
|
|
4331
|
+
);
|
|
4332
|
+
const initialIndex = useCompoundInitialIndex({
|
|
4333
|
+
courseId: config.courseId,
|
|
4334
|
+
compoundId: blockId,
|
|
4335
|
+
pageCount: cues.length,
|
|
4336
|
+
persistEnabled,
|
|
4337
|
+
storage
|
|
4338
|
+
});
|
|
4339
|
+
const [index, setIndex] = (0, import_react33.useState)(initialIndex);
|
|
4340
|
+
const setIndexStable = (0, import_react33.useCallback)((i) => setIndex(i), []);
|
|
4341
|
+
(0, import_react33.useEffect)(() => {
|
|
4342
|
+
setIndex(initialIndex);
|
|
4343
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
4344
|
+
return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
4345
|
+
InteractiveVideoInner,
|
|
4346
|
+
{
|
|
4347
|
+
...props,
|
|
4348
|
+
ref,
|
|
4349
|
+
blockId,
|
|
4350
|
+
cues,
|
|
4351
|
+
index,
|
|
4352
|
+
setIndex,
|
|
4353
|
+
persistEnabled,
|
|
4354
|
+
initialMeta
|
|
4355
|
+
}
|
|
4356
|
+
) });
|
|
4357
|
+
}
|
|
4358
|
+
);
|
|
4359
|
+
setLessonkitBlockType(InteractiveVideo, "InteractiveVideo");
|
|
4360
|
+
|
|
4361
|
+
// src/blocks/Summary.tsx
|
|
4362
|
+
var import_react34 = require("react");
|
|
4363
|
+
var import_jsx_runtime24 = require("react/jsx-runtime");
|
|
4364
|
+
var INTERACTION6 = "summary";
|
|
4365
|
+
function SummaryInner(props, ref) {
|
|
4366
|
+
const checkId = (0, import_react34.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4367
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4368
|
+
const [selectedIndices, setSelectedIndices] = (0, import_react34.useState)([]);
|
|
4369
|
+
const [passed, setPassed] = (0, import_react34.useState)(false);
|
|
4370
|
+
const [checked, setChecked] = (0, import_react34.useState)(false);
|
|
4371
|
+
const completedRef = (0, import_react34.useRef)(false);
|
|
4372
|
+
const telemetryReplayedRef = (0, import_react34.useRef)(false);
|
|
4373
|
+
const correctKey = props.correct.join("\0");
|
|
4374
|
+
const statementsKey = props.statements.join("\0");
|
|
4375
|
+
const selected = selectedIndices.map((i) => props.statements[i] ?? "");
|
|
4376
|
+
const reset = () => {
|
|
4377
|
+
completedRef.current = false;
|
|
4378
|
+
telemetryReplayedRef.current = false;
|
|
4379
|
+
setSelectedIndices([]);
|
|
4380
|
+
setPassed(false);
|
|
4381
|
+
setChecked(false);
|
|
4382
|
+
};
|
|
4383
|
+
(0, import_react34.useEffect)(() => {
|
|
4384
|
+
reset();
|
|
4385
|
+
}, [checkId, correctKey, statementsKey]);
|
|
4386
|
+
const isCorrect = selected.length === props.correct.length && selected.every((s, i) => s === props.correct[i]);
|
|
4387
|
+
const maxScore = props.correct.length || 1;
|
|
4388
|
+
const score = isCorrect ? maxScore : 0;
|
|
4389
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
4390
|
+
const availableIndices = props.statements.map((_, i) => i).filter((i) => !selectedIndices.includes(i));
|
|
4391
|
+
const handle = (0, import_react34.useMemo)(
|
|
4392
|
+
() => buildAssessmentHandle({
|
|
4393
|
+
checkId,
|
|
4394
|
+
getScore: () => passed ? score : 0,
|
|
4395
|
+
getMaxScore: () => maxScore,
|
|
4396
|
+
getAnswerGiven: () => selectedIndices.length > 0,
|
|
4397
|
+
resetTask: reset,
|
|
4398
|
+
showSolutions: () => {
|
|
4399
|
+
},
|
|
4400
|
+
getXAPIData: () => ({
|
|
4401
|
+
checkId,
|
|
4402
|
+
interactionType: INTERACTION6,
|
|
4403
|
+
response: selected,
|
|
4404
|
+
correct: passedThreshold,
|
|
4405
|
+
score: passed ? score : 0,
|
|
4406
|
+
maxScore
|
|
4407
|
+
}),
|
|
4408
|
+
getCurrentState: () => ({ selectedIndices, passed, checked }),
|
|
4409
|
+
resume: (state) => {
|
|
4410
|
+
let nextIndices = [];
|
|
4411
|
+
if (Array.isArray(state.selectedIndices)) {
|
|
4412
|
+
nextIndices = [...state.selectedIndices];
|
|
4413
|
+
} else if (Array.isArray(state.selected)) {
|
|
4414
|
+
const legacy = state.selected;
|
|
4415
|
+
nextIndices = legacy.map((text) => props.statements.indexOf(text)).filter((i) => i >= 0);
|
|
4416
|
+
}
|
|
4417
|
+
setSelectedIndices(nextIndices);
|
|
4418
|
+
const nextSelected = nextIndices.map((i) => props.statements[i] ?? "");
|
|
4419
|
+
const nextIsCorrect = nextSelected.length === props.correct.length && nextSelected.every((s, i) => s === props.correct[i]);
|
|
4420
|
+
const nextScore = nextIsCorrect ? maxScore : 0;
|
|
4421
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
4422
|
+
setPassed(value);
|
|
4423
|
+
completedRef.current = value;
|
|
4424
|
+
if (value) {
|
|
4425
|
+
if (!telemetryReplayedRef.current) {
|
|
4426
|
+
telemetryReplayedRef.current = true;
|
|
4427
|
+
assessment.answer({
|
|
4428
|
+
checkId,
|
|
4429
|
+
interactionType: INTERACTION6,
|
|
4430
|
+
response: nextSelected,
|
|
4431
|
+
correct: true
|
|
4432
|
+
});
|
|
4433
|
+
assessment.complete({
|
|
4434
|
+
checkId,
|
|
4435
|
+
interactionType: INTERACTION6,
|
|
4436
|
+
score: nextScore,
|
|
4437
|
+
maxScore,
|
|
4438
|
+
passingScore: props.passingScore ?? maxScore
|
|
4439
|
+
});
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
});
|
|
4443
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
4444
|
+
}
|
|
4445
|
+
}),
|
|
4446
|
+
[
|
|
4447
|
+
assessment,
|
|
4448
|
+
checkId,
|
|
4449
|
+
checked,
|
|
4450
|
+
maxScore,
|
|
4451
|
+
passed,
|
|
4452
|
+
passedThreshold,
|
|
4453
|
+
props.passingScore,
|
|
4454
|
+
props.statements,
|
|
4455
|
+
score,
|
|
4456
|
+
selected,
|
|
4457
|
+
selectedIndices.length
|
|
4458
|
+
]
|
|
4459
|
+
);
|
|
4460
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4461
|
+
const addStatement = (statementIndex) => {
|
|
4462
|
+
if (passed && !props.enableRetry) return;
|
|
4463
|
+
setChecked(false);
|
|
4464
|
+
setSelectedIndices((prev) => [...prev, statementIndex]);
|
|
4465
|
+
};
|
|
4466
|
+
const removeLast = () => {
|
|
4467
|
+
if (passed && !props.enableRetry) return;
|
|
4468
|
+
setChecked(false);
|
|
4469
|
+
setSelectedIndices((prev) => prev.slice(0, -1));
|
|
4470
|
+
};
|
|
4471
|
+
const check = () => {
|
|
4472
|
+
if (selectedIndices.length === 0) return;
|
|
4473
|
+
setChecked(true);
|
|
4474
|
+
assessment.answer({
|
|
4475
|
+
checkId,
|
|
4476
|
+
interactionType: INTERACTION6,
|
|
4477
|
+
response: selected,
|
|
4478
|
+
correct: passedThreshold
|
|
4479
|
+
});
|
|
4480
|
+
if (passedThreshold && !completedRef.current) {
|
|
4481
|
+
completedRef.current = true;
|
|
4482
|
+
setPassed(true);
|
|
4483
|
+
assessment.complete({
|
|
4484
|
+
checkId,
|
|
4485
|
+
interactionType: INTERACTION6,
|
|
4486
|
+
score,
|
|
4487
|
+
maxScore,
|
|
4488
|
+
passingScore: props.passingScore ?? maxScore
|
|
4489
|
+
});
|
|
4490
|
+
}
|
|
4491
|
+
};
|
|
4492
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("section", { "aria-label": "Summary", "data-lk-check-id": checkId, "data-testid": "summary", children: [
|
|
4493
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { children: "Select statements in order to build the summary." }),
|
|
4494
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("ol", { "data-testid": "summary-selected", children: selected.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { children: s }, `${i}-${selectedIndices[i]}`)) }),
|
|
4495
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { role: "group", "aria-label": "Available statements", children: availableIndices.map((statementIndex) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4496
|
+
"button",
|
|
4497
|
+
{
|
|
4498
|
+
type: "button",
|
|
4499
|
+
"data-testid": `summary-statement-${statementIndex}`,
|
|
4500
|
+
disabled: passed && !props.enableRetry,
|
|
4501
|
+
onClick: () => addStatement(statementIndex),
|
|
4502
|
+
style: { display: "block", margin: "0.25rem 0" },
|
|
4503
|
+
children: props.statements[statementIndex]
|
|
4504
|
+
},
|
|
4505
|
+
statementIndex
|
|
4506
|
+
)) }),
|
|
4507
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4508
|
+
"button",
|
|
4509
|
+
{
|
|
4510
|
+
type: "button",
|
|
4511
|
+
"data-testid": "summary-undo",
|
|
4512
|
+
disabled: passed && !props.enableRetry || selectedIndices.length === 0,
|
|
4513
|
+
onClick: removeLast,
|
|
4514
|
+
children: "Remove last"
|
|
4515
|
+
}
|
|
4516
|
+
),
|
|
4517
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4518
|
+
"button",
|
|
4519
|
+
{
|
|
4520
|
+
type: "button",
|
|
4521
|
+
"data-testid": "summary-check",
|
|
4522
|
+
disabled: selectedIndices.length === 0 || passed && !props.enableRetry,
|
|
4523
|
+
onClick: check,
|
|
4524
|
+
children: "Check"
|
|
4525
|
+
}
|
|
4526
|
+
),
|
|
4527
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "summary-feedback", children: passedThreshold ? "Correct" : "Try again" }) : null,
|
|
4528
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("button", { type: "button", "data-testid": "summary-retry", onClick: reset, children: "Try again" }) : null
|
|
4529
|
+
] });
|
|
4530
|
+
}
|
|
4531
|
+
var SummaryInnerForwarded = (0, import_react34.forwardRef)(SummaryInner);
|
|
4532
|
+
var Summary = (0, import_react34.forwardRef)(function Summary2(props, ref) {
|
|
4533
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(AssessmentLessonGuard, { blockLabel: "Summary", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SummaryInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
4534
|
+
});
|
|
4535
|
+
setLessonkitBlockType(Summary, "Summary");
|
|
4536
|
+
|
|
4537
|
+
// src/blocks/ImagePairing.tsx
|
|
4538
|
+
var import_react35 = require("react");
|
|
4539
|
+
var import_jsx_runtime25 = require("react/jsx-runtime");
|
|
4540
|
+
var INTERACTION7 = "imagePairing";
|
|
4541
|
+
function shuffleCards(cards) {
|
|
4542
|
+
const next = [...cards];
|
|
4543
|
+
for (let i = next.length - 1; i > 0; i -= 1) {
|
|
4544
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
4545
|
+
[next[i], next[j]] = [next[j], next[i]];
|
|
4546
|
+
}
|
|
4547
|
+
return next;
|
|
4548
|
+
}
|
|
4549
|
+
function buildDeck(pairs) {
|
|
4550
|
+
const cards = pairs.flatMap(
|
|
4551
|
+
(pair) => [0, 1].map((copy) => ({
|
|
4552
|
+
cardKey: `${pair.id}-${copy}`,
|
|
4553
|
+
pairId: pair.id,
|
|
4554
|
+
label: pair.label,
|
|
4555
|
+
imageSrc: pair.imageSrc
|
|
4556
|
+
}))
|
|
4557
|
+
);
|
|
4558
|
+
return shuffleCards(cards);
|
|
4559
|
+
}
|
|
4560
|
+
function ImagePairingInner(props, ref) {
|
|
4561
|
+
const checkId = (0, import_react35.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4562
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4563
|
+
const pairsKey = props.pairs.map((p) => p.id).join("\0");
|
|
4564
|
+
const [cards, setCards] = (0, import_react35.useState)(() => buildDeck(props.pairs));
|
|
4565
|
+
const [matched, setMatched] = (0, import_react35.useState)(() => /* @__PURE__ */ new Set());
|
|
4566
|
+
const [revealed, setRevealed] = (0, import_react35.useState)(() => /* @__PURE__ */ new Set());
|
|
4567
|
+
const [keyboardSelection, setKeyboardSelection] = (0, import_react35.useState)(null);
|
|
4568
|
+
const [passed, setPassed] = (0, import_react35.useState)(false);
|
|
4569
|
+
const completedRef = (0, import_react35.useRef)(false);
|
|
4570
|
+
const telemetryReplayedRef = (0, import_react35.useRef)(false);
|
|
4571
|
+
const reset = () => {
|
|
4572
|
+
completedRef.current = false;
|
|
4573
|
+
telemetryReplayedRef.current = false;
|
|
4574
|
+
setCards(buildDeck(props.pairs));
|
|
4575
|
+
setMatched(/* @__PURE__ */ new Set());
|
|
4576
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
4577
|
+
setKeyboardSelection(null);
|
|
4578
|
+
setPassed(false);
|
|
4579
|
+
};
|
|
4580
|
+
(0, import_react35.useEffect)(() => {
|
|
4581
|
+
reset();
|
|
4582
|
+
}, [checkId, pairsKey]);
|
|
4583
|
+
const totalPairs = props.pairs.length;
|
|
4584
|
+
const matchedCount = matched.size;
|
|
4585
|
+
const maxScore = totalPairs || 1;
|
|
4586
|
+
const score = matchedCount;
|
|
4587
|
+
const allMatched = totalPairs > 0 && matchedCount === totalPairs;
|
|
4588
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
4589
|
+
const completeIfReady = (nextMatched) => {
|
|
4590
|
+
if (nextMatched.size === totalPairs && totalPairs > 0 && !completedRef.current) {
|
|
4591
|
+
const finalScore = nextMatched.size;
|
|
4592
|
+
const finalPassed = meetsPassingThreshold(finalScore, maxScore, props.passingScore);
|
|
4593
|
+
completedRef.current = true;
|
|
4594
|
+
setPassed(true);
|
|
4595
|
+
assessment.answer({
|
|
4596
|
+
checkId,
|
|
4597
|
+
interactionType: INTERACTION7,
|
|
4598
|
+
response: { matchedPairIds: [...nextMatched] },
|
|
4599
|
+
correct: finalPassed
|
|
4600
|
+
});
|
|
4601
|
+
assessment.complete({
|
|
4602
|
+
checkId,
|
|
4603
|
+
interactionType: INTERACTION7,
|
|
4604
|
+
score: finalScore,
|
|
4605
|
+
maxScore,
|
|
4606
|
+
passingScore: props.passingScore ?? maxScore
|
|
4607
|
+
});
|
|
4608
|
+
}
|
|
4609
|
+
};
|
|
4610
|
+
const tryMatch = (firstKey, secondKey) => {
|
|
4611
|
+
if (firstKey === secondKey) return;
|
|
4612
|
+
const first = cards.find((c) => c.cardKey === firstKey);
|
|
4613
|
+
const second = cards.find((c) => c.cardKey === secondKey);
|
|
4614
|
+
if (!first || !second) return;
|
|
4615
|
+
setRevealed((prev) => /* @__PURE__ */ new Set([...prev, firstKey, secondKey]));
|
|
4616
|
+
if (first.pairId === second.pairId) {
|
|
4617
|
+
setMatched((prev) => {
|
|
4618
|
+
const next = /* @__PURE__ */ new Set([...prev, first.pairId]);
|
|
4619
|
+
completeIfReady(next);
|
|
4620
|
+
return next;
|
|
4621
|
+
});
|
|
4622
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
4623
|
+
setKeyboardSelection(null);
|
|
4624
|
+
} else {
|
|
4625
|
+
window.setTimeout(() => {
|
|
4626
|
+
setRevealed((prev) => {
|
|
4627
|
+
const next = new Set(prev);
|
|
4628
|
+
next.delete(firstKey);
|
|
4629
|
+
next.delete(secondKey);
|
|
4630
|
+
return next;
|
|
4631
|
+
});
|
|
4632
|
+
setKeyboardSelection(null);
|
|
4633
|
+
}, 800);
|
|
4634
|
+
}
|
|
4635
|
+
};
|
|
4636
|
+
const selectCard = (cardKey) => {
|
|
4637
|
+
if (passed && !props.enableRetry) return;
|
|
4638
|
+
if (matched.has(cards.find((c) => c.cardKey === cardKey)?.pairId ?? "")) return;
|
|
4639
|
+
if (keyboardSelection === null) {
|
|
4640
|
+
setKeyboardSelection(cardKey);
|
|
4641
|
+
setRevealed((prev) => /* @__PURE__ */ new Set([...prev, cardKey]));
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
if (keyboardSelection === cardKey) {
|
|
4645
|
+
setKeyboardSelection(null);
|
|
4646
|
+
setRevealed((prev) => {
|
|
4647
|
+
const next = new Set(prev);
|
|
4648
|
+
next.delete(cardKey);
|
|
4649
|
+
return next;
|
|
4650
|
+
});
|
|
4651
|
+
return;
|
|
4652
|
+
}
|
|
4653
|
+
tryMatch(keyboardSelection, cardKey);
|
|
4654
|
+
};
|
|
4655
|
+
const handle = (0, import_react35.useMemo)(
|
|
4656
|
+
() => buildAssessmentHandle({
|
|
4657
|
+
checkId,
|
|
4658
|
+
getScore: () => score,
|
|
4659
|
+
getMaxScore: () => maxScore,
|
|
4660
|
+
getAnswerGiven: () => matchedCount > 0,
|
|
4661
|
+
resetTask: reset,
|
|
4662
|
+
showSolutions: () => {
|
|
4663
|
+
},
|
|
4664
|
+
getXAPIData: () => ({
|
|
4665
|
+
checkId,
|
|
4666
|
+
interactionType: INTERACTION7,
|
|
4667
|
+
response: { matchedPairIds: [...matched] },
|
|
4668
|
+
correct: allMatched && passedThreshold,
|
|
4669
|
+
score,
|
|
4670
|
+
maxScore
|
|
4671
|
+
}),
|
|
4672
|
+
getCurrentState: () => ({
|
|
4673
|
+
matched: [...matched],
|
|
4674
|
+
revealed: [...revealed],
|
|
4675
|
+
keyboardSelection,
|
|
4676
|
+
passed
|
|
4677
|
+
}),
|
|
4678
|
+
resume: (state) => {
|
|
4679
|
+
if (Array.isArray(state.matched)) setMatched(new Set(state.matched));
|
|
4680
|
+
if (Array.isArray(state.revealed)) setRevealed(new Set(state.revealed));
|
|
4681
|
+
const sel = state.keyboardSelection;
|
|
4682
|
+
if (sel === null || typeof sel === "string") setKeyboardSelection(sel ?? null);
|
|
4683
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
4684
|
+
setPassed(value);
|
|
4685
|
+
completedRef.current = value;
|
|
4686
|
+
if (value && !telemetryReplayedRef.current) {
|
|
4687
|
+
telemetryReplayedRef.current = true;
|
|
4688
|
+
const matchedIds = Array.isArray(state.matched) ? state.matched : [...matched];
|
|
4689
|
+
const finalScore = matchedIds.length;
|
|
4690
|
+
assessment.answer({
|
|
4691
|
+
checkId,
|
|
4692
|
+
interactionType: INTERACTION7,
|
|
4693
|
+
response: { matchedPairIds: matchedIds },
|
|
4694
|
+
correct: true
|
|
4695
|
+
});
|
|
4696
|
+
assessment.complete({
|
|
4697
|
+
checkId,
|
|
4698
|
+
interactionType: INTERACTION7,
|
|
4699
|
+
score: finalScore,
|
|
4700
|
+
maxScore,
|
|
4701
|
+
passingScore: props.passingScore ?? maxScore
|
|
4702
|
+
});
|
|
4703
|
+
}
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
4706
|
+
}),
|
|
4707
|
+
[allMatched, checkId, keyboardSelection, matched, matchedCount, maxScore, passed, passedThreshold, revealed, score]
|
|
4708
|
+
);
|
|
4709
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4710
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { "aria-label": "Image Pairing", "data-lk-check-id": checkId, "data-testid": "image-pairing", children: [
|
|
4711
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { children: "Match the image pairs (select two cards with keyboard or click)." }),
|
|
4712
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { role: "list", "aria-label": "Image cards", "data-testid": "image-pairing-grid", children: cards.map((card) => {
|
|
4713
|
+
const isMatched = matched.has(card.pairId);
|
|
4714
|
+
const isRevealed = isMatched || revealed.has(card.cardKey);
|
|
4715
|
+
const isSelected = keyboardSelection === card.cardKey;
|
|
4716
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4717
|
+
"button",
|
|
4718
|
+
{
|
|
4719
|
+
type: "button",
|
|
4720
|
+
role: "listitem",
|
|
4721
|
+
"data-testid": `pairing-card-${card.cardKey}`,
|
|
4722
|
+
"aria-pressed": isSelected,
|
|
4723
|
+
disabled: isMatched || passed && !props.enableRetry,
|
|
4724
|
+
onClick: () => selectCard(card.cardKey),
|
|
4725
|
+
style: {
|
|
4726
|
+
margin: "0.25rem",
|
|
4727
|
+
minWidth: "6rem",
|
|
4728
|
+
minHeight: "6rem",
|
|
4729
|
+
border: isSelected ? "2px solid currentColor" : "1px solid currentColor"
|
|
4730
|
+
},
|
|
4731
|
+
children: isRevealed ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
|
|
4732
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: card.imageSrc, alt: card.label, style: { maxWidth: "5rem", maxHeight: "5rem" } }),
|
|
4733
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "lk-visually-hidden", children: card.label })
|
|
4734
|
+
] }) : "?"
|
|
4735
|
+
},
|
|
4736
|
+
card.cardKey
|
|
4737
|
+
);
|
|
4738
|
+
}) }),
|
|
4739
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { role: "status", "aria-live": "polite", "data-testid": "image-pairing-progress", children: [
|
|
4740
|
+
matchedCount,
|
|
4741
|
+
" / ",
|
|
4742
|
+
totalPairs,
|
|
4743
|
+
" pairs matched"
|
|
4744
|
+
] }),
|
|
4745
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("button", { type: "button", "data-testid": "image-pairing-retry", onClick: reset, children: "Try again" }) : null
|
|
4746
|
+
] });
|
|
4747
|
+
}
|
|
4748
|
+
var ImagePairingInnerForwarded = (0, import_react35.forwardRef)(ImagePairingInner);
|
|
4749
|
+
var ImagePairing = (0, import_react35.forwardRef)(function ImagePairing2(props, ref) {
|
|
4750
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(AssessmentLessonGuard, { blockLabel: "ImagePairing", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ImagePairingInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
4751
|
+
});
|
|
4752
|
+
setLessonkitBlockType(ImagePairing, "ImagePairing");
|
|
4753
|
+
|
|
4754
|
+
// src/blocks/ImageSequencing.tsx
|
|
4755
|
+
var import_react36 = require("react");
|
|
4756
|
+
var import_jsx_runtime26 = require("react/jsx-runtime");
|
|
4757
|
+
var INTERACTION8 = "imageSequencing";
|
|
4758
|
+
function ImageSequencingInner(props, ref) {
|
|
4759
|
+
const checkId = (0, import_react36.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4760
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4761
|
+
const imagesKey = props.images.map((i) => i.id).join("\0");
|
|
4762
|
+
const orderKey = props.correctOrder.join("\0");
|
|
4763
|
+
const [order, setOrder] = (0, import_react36.useState)(() => props.images.map((i) => i.id));
|
|
4764
|
+
const [passed, setPassed] = (0, import_react36.useState)(false);
|
|
4765
|
+
const [checked, setChecked] = (0, import_react36.useState)(false);
|
|
4766
|
+
const completedRef = (0, import_react36.useRef)(false);
|
|
4767
|
+
const telemetryReplayedRef = (0, import_react36.useRef)(false);
|
|
4768
|
+
const reset = () => {
|
|
4769
|
+
completedRef.current = false;
|
|
4770
|
+
telemetryReplayedRef.current = false;
|
|
4771
|
+
setOrder(props.images.map((i) => i.id));
|
|
4772
|
+
setPassed(false);
|
|
4773
|
+
setChecked(false);
|
|
4774
|
+
};
|
|
4775
|
+
(0, import_react36.useEffect)(() => {
|
|
4776
|
+
reset();
|
|
4777
|
+
}, [checkId, imagesKey, orderKey]);
|
|
4778
|
+
const isCorrect = order.every((id, i) => id === props.correctOrder[i]);
|
|
4779
|
+
const maxScore = props.correctOrder.length || 1;
|
|
4780
|
+
const score = isCorrect ? maxScore : 0;
|
|
4781
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
4782
|
+
const move = (index, direction) => {
|
|
4783
|
+
if (passed && !props.enableRetry) return;
|
|
4784
|
+
setChecked(false);
|
|
4785
|
+
const nextIndex = index + direction;
|
|
4786
|
+
if (nextIndex < 0 || nextIndex >= order.length) return;
|
|
4787
|
+
setOrder((prev) => {
|
|
4788
|
+
const next = [...prev];
|
|
4789
|
+
[next[index], next[nextIndex]] = [next[nextIndex], next[index]];
|
|
4790
|
+
return next;
|
|
4791
|
+
});
|
|
4792
|
+
};
|
|
4793
|
+
const handle = (0, import_react36.useMemo)(
|
|
4794
|
+
() => buildAssessmentHandle({
|
|
4795
|
+
checkId,
|
|
4796
|
+
getScore: () => passed ? score : 0,
|
|
4797
|
+
getMaxScore: () => maxScore,
|
|
4798
|
+
getAnswerGiven: () => order.length > 0,
|
|
4799
|
+
resetTask: reset,
|
|
4800
|
+
showSolutions: () => {
|
|
4801
|
+
},
|
|
4802
|
+
getXAPIData: () => ({
|
|
4803
|
+
checkId,
|
|
4804
|
+
interactionType: INTERACTION8,
|
|
4805
|
+
response: order,
|
|
4806
|
+
correct: passedThreshold,
|
|
4807
|
+
score: passed ? score : 0,
|
|
4808
|
+
maxScore
|
|
4809
|
+
}),
|
|
4810
|
+
getCurrentState: () => ({ order, passed, checked }),
|
|
4811
|
+
resume: (state) => {
|
|
4812
|
+
let nextOrder = order;
|
|
4813
|
+
if (Array.isArray(state.order)) {
|
|
4814
|
+
nextOrder = [...state.order];
|
|
4815
|
+
setOrder(nextOrder);
|
|
4816
|
+
}
|
|
4817
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
4818
|
+
setPassed(value);
|
|
4819
|
+
completedRef.current = value;
|
|
4820
|
+
if (value && !telemetryReplayedRef.current) {
|
|
4821
|
+
telemetryReplayedRef.current = true;
|
|
4822
|
+
const nextIsCorrect = nextOrder.every((id, i) => id === props.correctOrder[i]);
|
|
4823
|
+
const nextScore = nextIsCorrect ? maxScore : 0;
|
|
4824
|
+
assessment.answer({
|
|
4825
|
+
checkId,
|
|
4826
|
+
interactionType: INTERACTION8,
|
|
4827
|
+
response: nextOrder,
|
|
4828
|
+
correct: nextIsCorrect
|
|
4829
|
+
});
|
|
4830
|
+
assessment.complete({
|
|
4831
|
+
checkId,
|
|
4832
|
+
interactionType: INTERACTION8,
|
|
4833
|
+
score: nextScore,
|
|
4834
|
+
maxScore,
|
|
4835
|
+
passingScore: props.passingScore ?? maxScore
|
|
4836
|
+
});
|
|
4837
|
+
}
|
|
4838
|
+
});
|
|
4839
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
4840
|
+
}
|
|
4841
|
+
}),
|
|
4842
|
+
[checkId, checked, maxScore, order, passed, passedThreshold, score]
|
|
4843
|
+
);
|
|
4844
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4845
|
+
const check = () => {
|
|
4846
|
+
setChecked(true);
|
|
4847
|
+
assessment.answer({
|
|
4848
|
+
checkId,
|
|
4849
|
+
interactionType: INTERACTION8,
|
|
4850
|
+
response: order,
|
|
4851
|
+
correct: passedThreshold
|
|
4852
|
+
});
|
|
4853
|
+
if (passedThreshold && !completedRef.current) {
|
|
4854
|
+
completedRef.current = true;
|
|
4855
|
+
setPassed(true);
|
|
4856
|
+
assessment.complete({
|
|
4857
|
+
checkId,
|
|
4858
|
+
interactionType: INTERACTION8,
|
|
4859
|
+
score,
|
|
4860
|
+
maxScore,
|
|
4861
|
+
passingScore: props.passingScore ?? maxScore
|
|
4862
|
+
});
|
|
4863
|
+
}
|
|
4864
|
+
};
|
|
4865
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { "aria-label": "Image Sequencing", "data-lk-check-id": checkId, "data-testid": "image-sequencing", children: [
|
|
4866
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { children: "Reorder the images into the correct sequence." }),
|
|
4867
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ol", { "data-testid": "image-sequencing-list", children: order.map((id, index) => {
|
|
4868
|
+
const image = props.images.find((i) => i.id === id);
|
|
4869
|
+
if (!image) return null;
|
|
4870
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("li", { "data-testid": `sequencing-item-${id}`, children: [
|
|
4871
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: image.src, alt: image.alt, style: { maxWidth: "8rem", verticalAlign: "middle" } }),
|
|
4872
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
|
|
4873
|
+
"button",
|
|
4874
|
+
{
|
|
4875
|
+
type: "button",
|
|
4876
|
+
"data-testid": `sequencing-up-${id}`,
|
|
4877
|
+
"aria-label": `Move ${image.alt} up`,
|
|
4878
|
+
disabled: index === 0 || passed && !props.enableRetry,
|
|
4879
|
+
onClick: () => move(index, -1),
|
|
4880
|
+
children: "Up"
|
|
4881
|
+
}
|
|
4882
|
+
),
|
|
4883
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
|
|
4884
|
+
"button",
|
|
4885
|
+
{
|
|
4886
|
+
type: "button",
|
|
4887
|
+
"data-testid": `sequencing-down-${id}`,
|
|
4888
|
+
"aria-label": `Move ${image.alt} down`,
|
|
4889
|
+
disabled: index >= order.length - 1 || passed && !props.enableRetry,
|
|
4890
|
+
onClick: () => move(index, 1),
|
|
4891
|
+
children: "Down"
|
|
4892
|
+
}
|
|
4893
|
+
)
|
|
4894
|
+
] }, id);
|
|
4895
|
+
}) }),
|
|
4896
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
|
|
4897
|
+
"button",
|
|
4898
|
+
{
|
|
4899
|
+
type: "button",
|
|
4900
|
+
"data-testid": "image-sequencing-check",
|
|
4901
|
+
disabled: passed && !props.enableRetry,
|
|
4902
|
+
onClick: check,
|
|
4903
|
+
children: "Check"
|
|
4904
|
+
}
|
|
4905
|
+
),
|
|
4906
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "image-sequencing-feedback", children: passedThreshold ? "Correct" : "Try again" }) : null,
|
|
4907
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("button", { type: "button", "data-testid": "image-sequencing-retry", onClick: reset, children: "Try again" }) : null
|
|
4908
|
+
] });
|
|
4909
|
+
}
|
|
4910
|
+
var ImageSequencingInnerForwarded = (0, import_react36.forwardRef)(ImageSequencingInner);
|
|
4911
|
+
var ImageSequencing = (0, import_react36.forwardRef)(
|
|
4912
|
+
function ImageSequencing2(props, ref) {
|
|
4913
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AssessmentLessonGuard, { blockLabel: "ImageSequencing", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ImageSequencingInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
4914
|
+
}
|
|
4915
|
+
);
|
|
4916
|
+
setLessonkitBlockType(ImageSequencing, "ImageSequencing");
|
|
4917
|
+
|
|
4918
|
+
// src/blocks/ArithmeticQuiz.tsx
|
|
4919
|
+
var import_react37 = require("react");
|
|
4920
|
+
var import_jsx_runtime27 = require("react/jsx-runtime");
|
|
4921
|
+
var INTERACTION9 = "arithmeticQuiz";
|
|
4922
|
+
function ArithmeticQuizInner(props, ref) {
|
|
4923
|
+
const checkId = (0, import_react37.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4924
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4925
|
+
const problemsKey = props.problems.map((p) => `${p.question}\0${p.answer}`).join("|");
|
|
4926
|
+
const [answers, setAnswers] = (0, import_react37.useState)(
|
|
4927
|
+
() => Object.fromEntries(props.problems.map((_, i) => [i, ""]))
|
|
4928
|
+
);
|
|
4929
|
+
const [passed, setPassed] = (0, import_react37.useState)(false);
|
|
4930
|
+
const [checked, setChecked] = (0, import_react37.useState)(false);
|
|
4931
|
+
const [timeLeft, setTimeLeft] = (0, import_react37.useState)(
|
|
4932
|
+
props.timeLimitSeconds ?? null
|
|
4933
|
+
);
|
|
4934
|
+
const completedRef = (0, import_react37.useRef)(false);
|
|
4935
|
+
const telemetryReplayedRef = (0, import_react37.useRef)(false);
|
|
4936
|
+
const reset = () => {
|
|
4937
|
+
completedRef.current = false;
|
|
4938
|
+
telemetryReplayedRef.current = false;
|
|
4939
|
+
setAnswers(Object.fromEntries(props.problems.map((_, i) => [i, ""])));
|
|
4940
|
+
setPassed(false);
|
|
4941
|
+
setChecked(false);
|
|
4942
|
+
setTimeLeft(props.timeLimitSeconds ?? null);
|
|
4943
|
+
};
|
|
4944
|
+
(0, import_react37.useEffect)(() => {
|
|
4945
|
+
reset();
|
|
4946
|
+
}, [checkId, problemsKey, props.timeLimitSeconds]);
|
|
4947
|
+
let score = 0;
|
|
4948
|
+
props.problems.forEach((p, i) => {
|
|
4949
|
+
if ((answers[i] ?? "").trim() === p.answer.trim()) score += 1;
|
|
4950
|
+
});
|
|
4951
|
+
const maxScore = props.problems.length || 1;
|
|
4952
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
4953
|
+
const allFilled = props.problems.every((_, i) => (answers[i] ?? "").trim().length > 0);
|
|
4954
|
+
const runCheck = (0, import_react37.useCallback)(
|
|
4955
|
+
(force = false) => {
|
|
4956
|
+
if (!force && !allFilled) return;
|
|
4957
|
+
setChecked(true);
|
|
4958
|
+
assessment.answer({
|
|
4959
|
+
checkId,
|
|
4960
|
+
interactionType: INTERACTION9,
|
|
4961
|
+
response: answers,
|
|
4962
|
+
correct: passedThreshold
|
|
4963
|
+
});
|
|
4964
|
+
if (passedThreshold && !completedRef.current) {
|
|
4965
|
+
completedRef.current = true;
|
|
4966
|
+
setPassed(true);
|
|
4967
|
+
assessment.complete({
|
|
4968
|
+
checkId,
|
|
4969
|
+
interactionType: INTERACTION9,
|
|
4970
|
+
score,
|
|
4971
|
+
maxScore,
|
|
4972
|
+
passingScore: props.passingScore ?? maxScore
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4975
|
+
},
|
|
4976
|
+
[allFilled, answers, assessment, checkId, maxScore, passedThreshold, props.passingScore, score]
|
|
4977
|
+
);
|
|
4978
|
+
(0, import_react37.useEffect)(() => {
|
|
4979
|
+
if (timeLeft === null || passed || checked) return;
|
|
4980
|
+
if (timeLeft <= 0) {
|
|
4981
|
+
runCheck(true);
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
const id = window.setTimeout(() => setTimeLeft((t) => t !== null ? t - 1 : t), 1e3);
|
|
4985
|
+
return () => window.clearTimeout(id);
|
|
4986
|
+
}, [checked, passed, runCheck, timeLeft]);
|
|
4987
|
+
const handle = (0, import_react37.useMemo)(
|
|
4988
|
+
() => buildAssessmentHandle({
|
|
4989
|
+
checkId,
|
|
4990
|
+
getScore: () => passed ? score : 0,
|
|
4991
|
+
getMaxScore: () => maxScore,
|
|
4992
|
+
getAnswerGiven: () => allFilled,
|
|
4993
|
+
resetTask: reset,
|
|
4994
|
+
showSolutions: () => {
|
|
4995
|
+
},
|
|
4996
|
+
getXAPIData: () => ({
|
|
4997
|
+
checkId,
|
|
4998
|
+
interactionType: INTERACTION9,
|
|
4999
|
+
response: answers,
|
|
5000
|
+
correct: passedThreshold,
|
|
5001
|
+
score: passed ? score : 0,
|
|
5002
|
+
maxScore
|
|
5003
|
+
}),
|
|
5004
|
+
getCurrentState: () => ({ answers, passed, checked, timeLeft }),
|
|
5005
|
+
resume: (state) => {
|
|
5006
|
+
const raw = state.answers;
|
|
5007
|
+
let nextAnswers = answers;
|
|
5008
|
+
if (raw && typeof raw === "object") {
|
|
5009
|
+
nextAnswers = { ...raw };
|
|
5010
|
+
setAnswers(nextAnswers);
|
|
5011
|
+
}
|
|
5012
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
5013
|
+
setPassed(value);
|
|
5014
|
+
completedRef.current = value;
|
|
5015
|
+
if (value && !telemetryReplayedRef.current) {
|
|
5016
|
+
telemetryReplayedRef.current = true;
|
|
5017
|
+
let nextScore = 0;
|
|
5018
|
+
props.problems.forEach((p, i) => {
|
|
5019
|
+
if ((nextAnswers[i] ?? "").trim() === p.answer.trim()) nextScore += 1;
|
|
5020
|
+
});
|
|
5021
|
+
assessment.answer({
|
|
5022
|
+
checkId,
|
|
5023
|
+
interactionType: INTERACTION9,
|
|
5024
|
+
response: nextAnswers,
|
|
5025
|
+
correct: true
|
|
5026
|
+
});
|
|
5027
|
+
assessment.complete({
|
|
5028
|
+
checkId,
|
|
5029
|
+
interactionType: INTERACTION9,
|
|
5030
|
+
score: nextScore,
|
|
5031
|
+
maxScore,
|
|
5032
|
+
passingScore: props.passingScore ?? maxScore
|
|
5033
|
+
});
|
|
5034
|
+
}
|
|
5035
|
+
});
|
|
5036
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
5037
|
+
if (typeof state.timeLeft === "number") setTimeLeft(state.timeLeft);
|
|
5038
|
+
}
|
|
5039
|
+
}),
|
|
5040
|
+
[allFilled, answers, checkId, checked, maxScore, passed, passedThreshold, score, timeLeft]
|
|
5041
|
+
);
|
|
5042
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
5043
|
+
const onInput = (index, value) => {
|
|
5044
|
+
if (passed && !props.enableRetry) return;
|
|
5045
|
+
setChecked(false);
|
|
5046
|
+
setAnswers((prev) => ({ ...prev, [index]: value }));
|
|
5047
|
+
};
|
|
5048
|
+
return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": "Arithmetic Quiz", "data-lk-check-id": checkId, "data-testid": "arithmetic-quiz", children: [
|
|
5049
|
+
props.timeLimitSeconds ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { "data-testid": "arithmetic-timer", role: "timer", "aria-live": "polite", children: [
|
|
5050
|
+
"Time left: ",
|
|
5051
|
+
timeLeft ?? 0,
|
|
5052
|
+
"s"
|
|
5053
|
+
] }) : null,
|
|
5054
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ol", { "data-testid": "arithmetic-problems", children: props.problems.map((problem, index) => /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("li", { children: [
|
|
5055
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)("label", { htmlFor: `${checkId}-problem-${index}`, children: problem.question }),
|
|
5056
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
5057
|
+
"input",
|
|
5058
|
+
{
|
|
5059
|
+
id: `${checkId}-problem-${index}`,
|
|
5060
|
+
type: "text",
|
|
5061
|
+
inputMode: "numeric",
|
|
5062
|
+
"data-testid": `arithmetic-answer-${index}`,
|
|
5063
|
+
value: answers[index] ?? "",
|
|
5064
|
+
disabled: passed && !props.enableRetry,
|
|
5065
|
+
onChange: (e) => onInput(index, e.target.value)
|
|
5066
|
+
}
|
|
5067
|
+
)
|
|
5068
|
+
] }, index)) }),
|
|
5069
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
5070
|
+
"button",
|
|
5071
|
+
{
|
|
5072
|
+
type: "button",
|
|
5073
|
+
"data-testid": "arithmetic-check",
|
|
5074
|
+
disabled: !allFilled && timeLeft !== 0 || passed && !props.enableRetry,
|
|
5075
|
+
onClick: () => runCheck(),
|
|
5076
|
+
children: "Check"
|
|
5077
|
+
}
|
|
5078
|
+
),
|
|
5079
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { role: "status", "aria-live": "polite", "data-testid": "arithmetic-feedback", children: [
|
|
5080
|
+
passedThreshold ? "Correct" : "Try again",
|
|
5081
|
+
" (",
|
|
5082
|
+
score,
|
|
5083
|
+
"/",
|
|
5084
|
+
maxScore,
|
|
5085
|
+
")"
|
|
5086
|
+
] }) : null,
|
|
5087
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", "data-testid": "arithmetic-retry", onClick: reset, children: "Try again" }) : null
|
|
5088
|
+
] });
|
|
5089
|
+
}
|
|
5090
|
+
var ArithmeticQuizInnerForwarded = (0, import_react37.forwardRef)(ArithmeticQuizInner);
|
|
5091
|
+
var ArithmeticQuiz = (0, import_react37.forwardRef)(
|
|
5092
|
+
function ArithmeticQuiz2(props, ref) {
|
|
5093
|
+
return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(AssessmentLessonGuard, { blockLabel: "ArithmeticQuiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ArithmeticQuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
5094
|
+
}
|
|
5095
|
+
);
|
|
5096
|
+
setLessonkitBlockType(ArithmeticQuiz, "ArithmeticQuiz");
|
|
5097
|
+
|
|
5098
|
+
// src/blocks/Essay.tsx
|
|
5099
|
+
var import_react38 = __toESM(require("react"), 1);
|
|
5100
|
+
var import_jsx_runtime28 = require("react/jsx-runtime");
|
|
5101
|
+
var INTERACTION10 = "essay";
|
|
5102
|
+
function EssayInner(props, ref) {
|
|
5103
|
+
const checkId = (0, import_react38.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
5104
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
5105
|
+
const [text, setText] = (0, import_react38.useState)("");
|
|
5106
|
+
const [submitted, setSubmitted] = (0, import_react38.useState)(false);
|
|
5107
|
+
const completedRef = (0, import_react38.useRef)(false);
|
|
5108
|
+
const telemetryReplayedRef = (0, import_react38.useRef)(false);
|
|
5109
|
+
const questionId = import_react38.default.useId();
|
|
5110
|
+
const minLength = props.minLength ?? 0;
|
|
5111
|
+
const meetsMinLength = text.trim().length >= minLength;
|
|
5112
|
+
const reset = () => {
|
|
5113
|
+
completedRef.current = false;
|
|
5114
|
+
telemetryReplayedRef.current = false;
|
|
5115
|
+
setText("");
|
|
5116
|
+
setSubmitted(false);
|
|
5117
|
+
};
|
|
5118
|
+
(0, import_react38.useEffect)(() => {
|
|
5119
|
+
reset();
|
|
5120
|
+
}, [checkId, props.question, props.minLength]);
|
|
5121
|
+
const handle = (0, import_react38.useMemo)(
|
|
5122
|
+
() => buildAssessmentHandle({
|
|
5123
|
+
checkId,
|
|
5124
|
+
getScore: () => 0,
|
|
5125
|
+
getMaxScore: () => 1,
|
|
5126
|
+
getAnswerGiven: () => submitted && meetsMinLength,
|
|
5127
|
+
resetTask: reset,
|
|
5128
|
+
showSolutions: () => {
|
|
5129
|
+
},
|
|
5130
|
+
getXAPIData: () => ({
|
|
5131
|
+
checkId,
|
|
5132
|
+
interactionType: INTERACTION10,
|
|
5133
|
+
question: props.question,
|
|
5134
|
+
response: text,
|
|
5135
|
+
score: 0,
|
|
5136
|
+
maxScore: 1
|
|
5137
|
+
}),
|
|
5138
|
+
getCurrentState: () => ({ text, submitted }),
|
|
5139
|
+
resume: (state) => {
|
|
5140
|
+
const nextText = readStringField(state, "text");
|
|
5141
|
+
if (typeof nextText === "string") setText(nextText);
|
|
5142
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
5143
|
+
setSubmitted(value);
|
|
5144
|
+
completedRef.current = value;
|
|
5145
|
+
if (value && !telemetryReplayedRef.current) {
|
|
5146
|
+
telemetryReplayedRef.current = true;
|
|
5147
|
+
const response = typeof nextText === "string" ? nextText : text;
|
|
5148
|
+
assessment.answer({
|
|
5149
|
+
checkId,
|
|
5150
|
+
interactionType: INTERACTION10,
|
|
5151
|
+
question: props.question,
|
|
5152
|
+
response,
|
|
5153
|
+
correct: false
|
|
5154
|
+
});
|
|
5155
|
+
assessment.complete({
|
|
5156
|
+
checkId,
|
|
5157
|
+
interactionType: INTERACTION10,
|
|
5158
|
+
score: 0,
|
|
5159
|
+
maxScore: 1,
|
|
5160
|
+
passingScore: props.passingScore ?? 1
|
|
5161
|
+
});
|
|
5162
|
+
}
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
}),
|
|
5166
|
+
[checkId, meetsMinLength, props.question, submitted, text]
|
|
5167
|
+
);
|
|
5168
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
5169
|
+
const submit = () => {
|
|
5170
|
+
if (!meetsMinLength || submitted && !props.enableRetry) return;
|
|
5171
|
+
setSubmitted(true);
|
|
5172
|
+
if (!completedRef.current) {
|
|
5173
|
+
completedRef.current = true;
|
|
5174
|
+
assessment.answer({
|
|
5175
|
+
checkId,
|
|
5176
|
+
interactionType: INTERACTION10,
|
|
5177
|
+
question: props.question,
|
|
5178
|
+
response: text,
|
|
5179
|
+
correct: false
|
|
5180
|
+
});
|
|
5181
|
+
assessment.complete({
|
|
5182
|
+
checkId,
|
|
5183
|
+
interactionType: INTERACTION10,
|
|
5184
|
+
score: 0,
|
|
5185
|
+
maxScore: 1,
|
|
5186
|
+
passingScore: props.passingScore ?? 1
|
|
5187
|
+
});
|
|
5188
|
+
}
|
|
5189
|
+
};
|
|
5190
|
+
return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("section", { "aria-label": "Essay", "data-lk-check-id": checkId, "data-testid": "essay", children: [
|
|
5191
|
+
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { id: questionId, children: props.question }),
|
|
5192
|
+
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
|
|
5193
|
+
"textarea",
|
|
5194
|
+
{
|
|
5195
|
+
"aria-labelledby": questionId,
|
|
5196
|
+
"data-testid": "essay-textarea",
|
|
5197
|
+
value: text,
|
|
5198
|
+
disabled: submitted && !props.enableRetry,
|
|
5199
|
+
onChange: (e) => {
|
|
5200
|
+
if (submitted && !props.enableRetry) return;
|
|
5201
|
+
setSubmitted(false);
|
|
5202
|
+
completedRef.current = false;
|
|
5203
|
+
setText(e.target.value);
|
|
5204
|
+
},
|
|
5205
|
+
rows: 6,
|
|
5206
|
+
style: { width: "100%" }
|
|
5207
|
+
}
|
|
5208
|
+
),
|
|
5209
|
+
minLength > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { "data-testid": "essay-min-length", children: [
|
|
5210
|
+
"Minimum length: ",
|
|
5211
|
+
minLength,
|
|
5212
|
+
" characters (",
|
|
5213
|
+
text.trim().length,
|
|
5214
|
+
"/",
|
|
5215
|
+
minLength,
|
|
5216
|
+
")"
|
|
5217
|
+
] }) : null,
|
|
5218
|
+
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
|
|
5219
|
+
"button",
|
|
5220
|
+
{
|
|
5221
|
+
type: "button",
|
|
5222
|
+
"data-testid": "essay-submit",
|
|
5223
|
+
disabled: !meetsMinLength || submitted && !props.enableRetry,
|
|
5224
|
+
onClick: submit,
|
|
5225
|
+
children: "Submit"
|
|
5226
|
+
}
|
|
5227
|
+
),
|
|
5228
|
+
submitted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "essay-submitted", children: "Response submitted for review." }) : null,
|
|
5229
|
+
props.enableRetry && submitted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("button", { type: "button", "data-testid": "essay-retry", onClick: reset, children: "Try again" }) : null
|
|
5230
|
+
] });
|
|
5231
|
+
}
|
|
5232
|
+
var EssayInnerForwarded = (0, import_react38.forwardRef)(EssayInner);
|
|
5233
|
+
var Essay = (0, import_react38.forwardRef)(function Essay2(props, ref) {
|
|
5234
|
+
return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(AssessmentLessonGuard, { blockLabel: "Essay", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(EssayInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
5235
|
+
});
|
|
5236
|
+
setLessonkitBlockType(Essay, "Essay");
|
|
5237
|
+
|
|
5238
|
+
// src/blocks/Questionnaire.tsx
|
|
5239
|
+
var import_react39 = require("react");
|
|
5240
|
+
var import_jsx_runtime29 = require("react/jsx-runtime");
|
|
5241
|
+
function Questionnaire(props) {
|
|
5242
|
+
const blockId = (0, import_react39.useMemo)(
|
|
5243
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
5244
|
+
[props.blockId]
|
|
5245
|
+
);
|
|
5246
|
+
const fieldsKey = props.fields.map((f) => `${f.id}:${f.type}:${f.label}`).join("|");
|
|
5247
|
+
const [values, setValues] = (0, import_react39.useState)(
|
|
5248
|
+
() => Object.fromEntries(props.fields.map((f) => [f.id, ""]))
|
|
5249
|
+
);
|
|
5250
|
+
const [submitted, setSubmitted] = (0, import_react39.useState)(false);
|
|
5251
|
+
const { track } = useLessonkit();
|
|
5252
|
+
const lessonId = useEnclosingLessonId();
|
|
5253
|
+
const baseId = (0, import_react39.useId)();
|
|
5254
|
+
(0, import_react39.useEffect)(() => {
|
|
5255
|
+
setValues(Object.fromEntries(props.fields.map((f) => [f.id, ""])));
|
|
5256
|
+
setSubmitted(false);
|
|
5257
|
+
}, [blockId, fieldsKey, props.fields]);
|
|
5258
|
+
const submit = () => {
|
|
5259
|
+
if (submitted) return;
|
|
5260
|
+
setSubmitted(true);
|
|
5261
|
+
if (lessonId) {
|
|
5262
|
+
track(
|
|
5263
|
+
"questionnaire_submitted",
|
|
5264
|
+
{ blockId, fieldCount: props.fields.length },
|
|
5265
|
+
{ lessonId }
|
|
5266
|
+
);
|
|
5267
|
+
}
|
|
5268
|
+
};
|
|
5269
|
+
return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("section", { "aria-label": "Questionnaire", "data-lk-block-id": blockId, "data-testid": "questionnaire", children: [
|
|
5270
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
|
|
5271
|
+
"form",
|
|
5272
|
+
{
|
|
5273
|
+
onSubmit: (e) => {
|
|
5274
|
+
e.preventDefault();
|
|
5275
|
+
submit();
|
|
5276
|
+
},
|
|
5277
|
+
children: [
|
|
5278
|
+
props.fields.map((field) => {
|
|
5279
|
+
const fieldId = `${baseId}-${field.id}`;
|
|
5280
|
+
return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { "data-testid": `questionnaire-field-${field.id}`, children: [
|
|
5281
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("label", { htmlFor: fieldId, children: field.label }),
|
|
5282
|
+
field.type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
|
|
5283
|
+
"textarea",
|
|
5284
|
+
{
|
|
5285
|
+
id: fieldId,
|
|
5286
|
+
"data-testid": `questionnaire-input-${field.id}`,
|
|
5287
|
+
value: values[field.id] ?? "",
|
|
5288
|
+
disabled: submitted,
|
|
5289
|
+
rows: 4,
|
|
5290
|
+
style: { display: "block", width: "100%" },
|
|
5291
|
+
onChange: (e) => setValues((prev) => ({ ...prev, [field.id]: e.target.value }))
|
|
5292
|
+
}
|
|
5293
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
|
|
5294
|
+
"input",
|
|
5295
|
+
{
|
|
5296
|
+
id: fieldId,
|
|
5297
|
+
type: "text",
|
|
5298
|
+
"data-testid": `questionnaire-input-${field.id}`,
|
|
5299
|
+
value: values[field.id] ?? "",
|
|
5300
|
+
disabled: submitted,
|
|
5301
|
+
style: { display: "block", width: "100%" },
|
|
5302
|
+
onChange: (e) => setValues((prev) => ({ ...prev, [field.id]: e.target.value }))
|
|
5303
|
+
}
|
|
5304
|
+
)
|
|
5305
|
+
] }, field.id);
|
|
5306
|
+
}),
|
|
5307
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("button", { type: "submit", "data-testid": "questionnaire-submit", disabled: submitted, children: "Submit" })
|
|
5308
|
+
]
|
|
5309
|
+
}
|
|
5310
|
+
),
|
|
5311
|
+
submitted ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "questionnaire-submitted", children: "Thank you for your responses." }) : null
|
|
5312
|
+
] });
|
|
5313
|
+
}
|
|
5314
|
+
setLessonkitBlockType(Questionnaire, "Questionnaire");
|
|
5315
|
+
|
|
5316
|
+
// src/blocks/MemoryGame.tsx
|
|
5317
|
+
var import_react40 = require("react");
|
|
5318
|
+
var import_jsx_runtime30 = require("react/jsx-runtime");
|
|
5319
|
+
function shuffleCards2(cards) {
|
|
5320
|
+
const next = [...cards];
|
|
5321
|
+
for (let i = next.length - 1; i > 0; i -= 1) {
|
|
5322
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
5323
|
+
[next[i], next[j]] = [next[j], next[i]];
|
|
5324
|
+
}
|
|
5325
|
+
return next;
|
|
5326
|
+
}
|
|
5327
|
+
function buildDeck2(pairs) {
|
|
5328
|
+
const cards = pairs.flatMap(
|
|
5329
|
+
(pair) => [0, 1].map((copy) => ({
|
|
5330
|
+
cardKey: `${pair.id}-${copy}`,
|
|
5331
|
+
pairId: pair.id,
|
|
5332
|
+
label: pair.label
|
|
5333
|
+
}))
|
|
5334
|
+
);
|
|
5335
|
+
return shuffleCards2(cards);
|
|
5336
|
+
}
|
|
5337
|
+
function MemoryGame(props) {
|
|
5338
|
+
const pairsKey = props.pairs.map((p) => p.id).join("\0");
|
|
5339
|
+
const [cards, setCards] = (0, import_react40.useState)(() => buildDeck2(props.pairs));
|
|
5340
|
+
const [matched, setMatched] = (0, import_react40.useState)(() => /* @__PURE__ */ new Set());
|
|
5341
|
+
const [revealed, setRevealed] = (0, import_react40.useState)(() => /* @__PURE__ */ new Set());
|
|
5342
|
+
const [selection, setSelection] = (0, import_react40.useState)(null);
|
|
5343
|
+
const [complete, setComplete] = (0, import_react40.useState)(false);
|
|
5344
|
+
const { track } = useLessonkit();
|
|
5345
|
+
const lessonId = useEnclosingLessonId();
|
|
5346
|
+
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
5347
|
+
(0, import_react40.useEffect)(() => {
|
|
5348
|
+
setCards(buildDeck2(props.pairs));
|
|
5349
|
+
setMatched(/* @__PURE__ */ new Set());
|
|
5350
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
5351
|
+
setSelection(null);
|
|
5352
|
+
setComplete(false);
|
|
5353
|
+
}, [props.blockId, pairsKey]);
|
|
5354
|
+
const cardIndexByKey = (0, import_react40.useMemo)(
|
|
5355
|
+
() => Object.fromEntries(cards.map((c, i) => [c.cardKey, i])),
|
|
5356
|
+
[cards]
|
|
5357
|
+
);
|
|
5358
|
+
const flipCard = (cardKey, face) => {
|
|
5359
|
+
const cardIndex = cardIndexByKey[cardKey];
|
|
5360
|
+
if (typeof cardIndex === "number") {
|
|
5361
|
+
track(
|
|
5362
|
+
"memory_card_flipped",
|
|
5363
|
+
{ blockId: props.blockId, cardIndex, face },
|
|
5364
|
+
trackOpts
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
5367
|
+
};
|
|
5368
|
+
const tryMatch = (firstKey, secondKey) => {
|
|
5369
|
+
const first = cards.find((c) => c.cardKey === firstKey);
|
|
5370
|
+
const second = cards.find((c) => c.cardKey === secondKey);
|
|
5371
|
+
if (!first || !second) return;
|
|
5372
|
+
setRevealed((prev) => /* @__PURE__ */ new Set([...prev, firstKey, secondKey]));
|
|
5373
|
+
flipCard(secondKey, "back");
|
|
5374
|
+
if (first.pairId === second.pairId) {
|
|
5375
|
+
setMatched((prev) => {
|
|
5376
|
+
const next = /* @__PURE__ */ new Set([...prev, first.pairId]);
|
|
5377
|
+
if (next.size === props.pairs.length) setComplete(true);
|
|
5378
|
+
return next;
|
|
5379
|
+
});
|
|
5380
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
5381
|
+
setSelection(null);
|
|
5382
|
+
} else {
|
|
5383
|
+
window.setTimeout(() => {
|
|
5384
|
+
setRevealed((prev) => {
|
|
5385
|
+
const next = new Set(prev);
|
|
5386
|
+
next.delete(firstKey);
|
|
5387
|
+
next.delete(secondKey);
|
|
5388
|
+
return next;
|
|
5389
|
+
});
|
|
5390
|
+
flipCard(firstKey, "front");
|
|
5391
|
+
flipCard(secondKey, "front");
|
|
5392
|
+
setSelection(null);
|
|
5393
|
+
}, 800);
|
|
5394
|
+
}
|
|
5395
|
+
};
|
|
5396
|
+
const selectCard = (cardKey) => {
|
|
5397
|
+
if (complete) return;
|
|
5398
|
+
if (matched.has(cards.find((c) => c.cardKey === cardKey)?.pairId ?? "")) return;
|
|
5399
|
+
if (selection === null) {
|
|
5400
|
+
setSelection(cardKey);
|
|
5401
|
+
setRevealed((prev) => /* @__PURE__ */ new Set([...prev, cardKey]));
|
|
5402
|
+
flipCard(cardKey, "back");
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5405
|
+
if (selection === cardKey) {
|
|
5406
|
+
setSelection(null);
|
|
5407
|
+
setRevealed((prev) => {
|
|
5408
|
+
const next = new Set(prev);
|
|
5409
|
+
next.delete(cardKey);
|
|
5410
|
+
return next;
|
|
5411
|
+
});
|
|
5412
|
+
flipCard(cardKey, "front");
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
5415
|
+
tryMatch(selection, cardKey);
|
|
5416
|
+
};
|
|
5417
|
+
const restart = () => {
|
|
5418
|
+
setCards(buildDeck2(props.pairs));
|
|
5419
|
+
setMatched(/* @__PURE__ */ new Set());
|
|
5420
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
5421
|
+
setSelection(null);
|
|
5422
|
+
setComplete(false);
|
|
5423
|
+
};
|
|
5424
|
+
return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("section", { "aria-label": "Memory Game", "data-lk-block-id": props.blockId, "data-testid": "memory-game", children: [
|
|
5425
|
+
/* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { role: "list", "aria-label": "Memory cards", "data-testid": "memory-game-grid", children: cards.map((card) => {
|
|
5426
|
+
const isMatched = matched.has(card.pairId);
|
|
5427
|
+
const isRevealed = isMatched || revealed.has(card.cardKey);
|
|
5428
|
+
const isSelected = selection === card.cardKey;
|
|
5429
|
+
return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
|
|
5430
|
+
"button",
|
|
5431
|
+
{
|
|
5432
|
+
type: "button",
|
|
5433
|
+
role: "listitem",
|
|
5434
|
+
"data-testid": `memory-card-${card.cardKey}`,
|
|
5435
|
+
"aria-pressed": isSelected,
|
|
5436
|
+
disabled: isMatched || complete,
|
|
5437
|
+
onClick: () => selectCard(card.cardKey),
|
|
5438
|
+
style: {
|
|
5439
|
+
margin: "0.25rem",
|
|
5440
|
+
minWidth: "5rem",
|
|
5441
|
+
minHeight: "5rem",
|
|
5442
|
+
border: isSelected ? "2px solid currentColor" : "1px solid currentColor"
|
|
5443
|
+
},
|
|
5444
|
+
children: isRevealed ? card.label : "?"
|
|
5445
|
+
},
|
|
5446
|
+
card.cardKey
|
|
5447
|
+
);
|
|
5448
|
+
}) }),
|
|
5449
|
+
complete ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "memory-game-complete", children: "All pairs matched!" }) : null,
|
|
5450
|
+
props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { "data-testid": "memory-game-self-score", children: "Self-score mode enabled" }) : null,
|
|
5451
|
+
/* @__PURE__ */ (0, import_jsx_runtime30.jsx)("button", { type: "button", "data-testid": "memory-game-restart", onClick: restart, children: "Restart" })
|
|
5452
|
+
] });
|
|
5453
|
+
}
|
|
5454
|
+
setLessonkitBlockType(MemoryGame, "MemoryGame");
|
|
5455
|
+
|
|
5456
|
+
// src/blocks/InformationWall.tsx
|
|
5457
|
+
var import_react41 = require("react");
|
|
5458
|
+
var import_jsx_runtime31 = require("react/jsx-runtime");
|
|
5459
|
+
function InformationWall(props) {
|
|
5460
|
+
const blockId = (0, import_react41.useMemo)(
|
|
5461
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
5462
|
+
[props.blockId]
|
|
5463
|
+
);
|
|
5464
|
+
const [query, setQuery] = (0, import_react41.useState)("");
|
|
5465
|
+
const { track } = useLessonkit();
|
|
5466
|
+
const lessonId = useEnclosingLessonId();
|
|
5467
|
+
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
5468
|
+
const debounceRef = (0, import_react41.useRef)(null);
|
|
5469
|
+
const filtered = (0, import_react41.useMemo)(() => {
|
|
5470
|
+
const q = query.trim().toLowerCase();
|
|
5471
|
+
if (!q) return props.panels;
|
|
5472
|
+
return props.panels.filter(
|
|
5473
|
+
(panel) => panel.title.toLowerCase().includes(q) || panel.body.toLowerCase().includes(q)
|
|
5474
|
+
);
|
|
5475
|
+
}, [props.panels, query]);
|
|
5476
|
+
(0, import_react41.useEffect)(
|
|
5477
|
+
() => () => {
|
|
5478
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
5479
|
+
},
|
|
5480
|
+
[]
|
|
5481
|
+
);
|
|
5482
|
+
const onSearch = (value) => {
|
|
5483
|
+
setQuery(value);
|
|
5484
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
5485
|
+
debounceRef.current = setTimeout(() => {
|
|
5486
|
+
const q = value.trim().toLowerCase();
|
|
5487
|
+
const resultCount = q ? props.panels.filter(
|
|
5488
|
+
(panel) => panel.title.toLowerCase().includes(q) || panel.body.toLowerCase().includes(q)
|
|
5489
|
+
).length : props.panels.length;
|
|
5490
|
+
track(
|
|
5491
|
+
"information_wall_search",
|
|
5492
|
+
{ blockId, query: value, resultCount },
|
|
5493
|
+
trackOpts
|
|
5494
|
+
);
|
|
5495
|
+
}, 300);
|
|
5496
|
+
};
|
|
5497
|
+
return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("section", { "aria-label": "Information Wall", "data-lk-block-id": blockId, "data-testid": "information-wall", children: [
|
|
5498
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)("label", { htmlFor: `${blockId}-search`, children: "Search panels" }),
|
|
5499
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
|
|
5500
|
+
"input",
|
|
5501
|
+
{
|
|
5502
|
+
id: `${blockId}-search`,
|
|
5503
|
+
type: "search",
|
|
5504
|
+
"data-testid": "information-wall-search",
|
|
5505
|
+
value: query,
|
|
5506
|
+
placeholder: "Search\u2026",
|
|
5507
|
+
onChange: (e) => onSearch(e.target.value)
|
|
5508
|
+
}
|
|
5509
|
+
),
|
|
5510
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { "data-testid": "information-wall-result-count", children: [
|
|
5511
|
+
filtered.length,
|
|
5512
|
+
" panel",
|
|
5513
|
+
filtered.length === 1 ? "" : "s"
|
|
5514
|
+
] }),
|
|
5515
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { "data-testid": "information-wall-panels", children: filtered.map((panel) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { "data-testid": `information-panel-${panel.id}`, children: [
|
|
5516
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)("h4", { children: panel.title }),
|
|
5517
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { children: panel.body })
|
|
5518
|
+
] }, panel.id)) })
|
|
5519
|
+
] });
|
|
5520
|
+
}
|
|
5521
|
+
setLessonkitBlockType(InformationWall, "InformationWall");
|
|
5522
|
+
|
|
5523
|
+
// src/blocks/ParallaxSlideshow.tsx
|
|
5524
|
+
var import_react42 = require("react");
|
|
5525
|
+
var import_jsx_runtime32 = require("react/jsx-runtime");
|
|
5526
|
+
function usePrefersReducedMotion() {
|
|
5527
|
+
const [reduced, setReduced] = (0, import_react42.useState)(false);
|
|
5528
|
+
(0, import_react42.useEffect)(() => {
|
|
5529
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
5530
|
+
setReduced(mq.matches);
|
|
5531
|
+
const onChange = (e) => setReduced(e.matches);
|
|
5532
|
+
mq.addEventListener("change", onChange);
|
|
5533
|
+
return () => mq.removeEventListener("change", onChange);
|
|
5534
|
+
}, []);
|
|
5535
|
+
return reduced;
|
|
5536
|
+
}
|
|
5537
|
+
function ParallaxSlideshow(props) {
|
|
5538
|
+
const [index, setIndex] = (0, import_react42.useState)(0);
|
|
5539
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
5540
|
+
const { track } = useLessonkit();
|
|
5541
|
+
const lessonId = useEnclosingLessonId();
|
|
5542
|
+
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
5543
|
+
const slide = props.slides[index];
|
|
5544
|
+
(0, import_react42.useEffect)(() => {
|
|
5545
|
+
track(
|
|
5546
|
+
"parallax_slide_viewed",
|
|
5547
|
+
{ blockId: props.blockId, slideIndex: index },
|
|
5548
|
+
trackOpts
|
|
5549
|
+
);
|
|
5550
|
+
}, [index, props.blockId, track, trackOpts]);
|
|
5551
|
+
if (!slide) return null;
|
|
5552
|
+
const goTo = (next) => {
|
|
5553
|
+
if (next < 0 || next >= props.slides.length) return;
|
|
5554
|
+
setIndex(next);
|
|
5555
|
+
};
|
|
5556
|
+
return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
|
|
5557
|
+
"section",
|
|
5558
|
+
{
|
|
5559
|
+
"aria-label": "Parallax slideshow",
|
|
5560
|
+
"data-lk-block-id": props.blockId,
|
|
5561
|
+
"data-testid": "parallax-slideshow",
|
|
5562
|
+
"data-reduced-motion": reducedMotion ? "true" : "false",
|
|
5563
|
+
children: [
|
|
5564
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
|
|
5565
|
+
"article",
|
|
5566
|
+
{
|
|
5567
|
+
"data-testid": `parallax-slide-${index}`,
|
|
5568
|
+
style: reducedMotion ? void 0 : {
|
|
5569
|
+
backgroundAttachment: "fixed",
|
|
5570
|
+
backgroundImage: slide.imageSrc ? `url(${slide.imageSrc})` : void 0,
|
|
5571
|
+
backgroundPosition: "center",
|
|
5572
|
+
backgroundSize: "cover",
|
|
5573
|
+
minHeight: "12rem",
|
|
5574
|
+
padding: "1rem"
|
|
5575
|
+
},
|
|
5576
|
+
children: [
|
|
5577
|
+
reducedMotion && slide.imageSrc ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
|
|
5578
|
+
"img",
|
|
5579
|
+
{
|
|
5580
|
+
src: slide.imageSrc,
|
|
5581
|
+
alt: "",
|
|
5582
|
+
"data-testid": "parallax-slide-image",
|
|
5583
|
+
style: { maxWidth: "100%" }
|
|
5584
|
+
}
|
|
5585
|
+
) : null,
|
|
5586
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsx)("h3", { "data-testid": "parallax-slide-title", children: slide.title }),
|
|
5587
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { "data-testid": "parallax-slide-body", children: slide.body })
|
|
5588
|
+
]
|
|
5589
|
+
}
|
|
5590
|
+
),
|
|
5591
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("nav", { "aria-label": "Slide navigation", children: [
|
|
5592
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
|
|
5593
|
+
"button",
|
|
5594
|
+
{
|
|
5595
|
+
type: "button",
|
|
5596
|
+
"data-testid": "parallax-prev",
|
|
5597
|
+
disabled: index === 0,
|
|
5598
|
+
onClick: () => goTo(index - 1),
|
|
5599
|
+
children: "Previous"
|
|
5600
|
+
}
|
|
5601
|
+
),
|
|
5602
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { "data-testid": "parallax-progress", children: [
|
|
5603
|
+
index + 1,
|
|
5604
|
+
" / ",
|
|
5605
|
+
props.slides.length
|
|
5606
|
+
] }),
|
|
5607
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
|
|
5608
|
+
"button",
|
|
5609
|
+
{
|
|
5610
|
+
type: "button",
|
|
5611
|
+
"data-testid": "parallax-next",
|
|
5612
|
+
disabled: index >= props.slides.length - 1,
|
|
5613
|
+
onClick: () => goTo(index + 1),
|
|
5614
|
+
children: "Next"
|
|
5615
|
+
}
|
|
5616
|
+
)
|
|
5617
|
+
] })
|
|
5618
|
+
]
|
|
5619
|
+
}
|
|
5620
|
+
);
|
|
5621
|
+
}
|
|
5622
|
+
setLessonkitBlockType(ParallaxSlideshow, "ParallaxSlideshow");
|
|
5623
|
+
|
|
5624
|
+
// src/blocks/Accordion.tsx
|
|
5625
|
+
var import_react43 = require("react");
|
|
5626
|
+
var import_jsx_runtime33 = require("react/jsx-runtime");
|
|
5627
|
+
function Accordion(props) {
|
|
5628
|
+
if (isDevEnvironment4()) {
|
|
5629
|
+
validateAccordionSections(props.sections);
|
|
5630
|
+
}
|
|
5631
|
+
const [open, setOpen] = (0, import_react43.useState)(/* @__PURE__ */ new Set());
|
|
5632
|
+
const { track } = useLessonkit();
|
|
5633
|
+
const lessonId = useEnclosingLessonId();
|
|
5634
|
+
const baseId = (0, import_react43.useId)();
|
|
5635
|
+
const toggle = (sectionId) => {
|
|
5636
|
+
setOpen((prev) => {
|
|
5637
|
+
const next = new Set(prev);
|
|
5638
|
+
const expanded = !next.has(sectionId);
|
|
5639
|
+
if (expanded) next.add(sectionId);
|
|
5640
|
+
else next.delete(sectionId);
|
|
5641
|
+
track(
|
|
5642
|
+
"accordion_section_toggled",
|
|
5643
|
+
{ blockId: props.blockId, sectionId, expanded },
|
|
5644
|
+
lessonId ? { lessonId } : void 0
|
|
5645
|
+
);
|
|
5646
|
+
return next;
|
|
5647
|
+
});
|
|
5648
|
+
};
|
|
5649
|
+
return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
5650
|
+
const expanded = open.has(section.id);
|
|
5651
|
+
const panelId = `${baseId}-${section.id}`;
|
|
5652
|
+
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
5653
|
+
return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
5654
|
+
/* @__PURE__ */ (0, import_jsx_runtime33.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
|
|
5655
|
+
"button",
|
|
5656
|
+
{
|
|
5657
|
+
id: triggerId,
|
|
5658
|
+
type: "button",
|
|
5659
|
+
"aria-expanded": expanded,
|
|
5660
|
+
"aria-controls": panelId,
|
|
5661
|
+
"data-testid": `accordion-trigger-${section.id}`,
|
|
5662
|
+
onClick: () => toggle(section.id),
|
|
5663
|
+
children: section.title
|
|
5664
|
+
}
|
|
5665
|
+
) }),
|
|
5666
|
+
expanded ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
5667
|
+
] }, section.id);
|
|
5668
|
+
}) });
|
|
5669
|
+
}
|
|
5670
|
+
setLessonkitBlockType(Accordion, "Accordion");
|
|
5671
|
+
|
|
5672
|
+
// src/blocks/DialogCards.tsx
|
|
5673
|
+
var import_react44 = require("react");
|
|
5674
|
+
var import_jsx_runtime34 = require("react/jsx-runtime");
|
|
5675
|
+
function DialogCards(props) {
|
|
5676
|
+
const [index, setIndex] = (0, import_react44.useState)(0);
|
|
5677
|
+
const [flipped, setFlipped] = (0, import_react44.useState)(false);
|
|
5678
|
+
const card = props.cards[index];
|
|
5679
|
+
if (!card) return null;
|
|
5680
|
+
return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
|
|
5681
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("p", { children: [
|
|
5682
|
+
"Card ",
|
|
5683
|
+
index + 1,
|
|
5684
|
+
" of ",
|
|
5685
|
+
props.cards.length
|
|
5686
|
+
] }),
|
|
5687
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
|
|
5688
|
+
"button",
|
|
5689
|
+
{
|
|
3720
5690
|
type: "button",
|
|
3721
5691
|
"data-testid": "dialog-card-flip",
|
|
3722
5692
|
"aria-pressed": flipped,
|
|
@@ -3725,8 +5695,8 @@ function DialogCards(props) {
|
|
|
3725
5695
|
children: flipped ? card.back : card.front
|
|
3726
5696
|
}
|
|
3727
5697
|
),
|
|
3728
|
-
/* @__PURE__ */ (0,
|
|
3729
|
-
/* @__PURE__ */ (0,
|
|
5698
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("nav", { "aria-label": "Card navigation", children: [
|
|
5699
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
|
|
3730
5700
|
"button",
|
|
3731
5701
|
{
|
|
3732
5702
|
type: "button",
|
|
@@ -3739,7 +5709,7 @@ function DialogCards(props) {
|
|
|
3739
5709
|
children: "Previous"
|
|
3740
5710
|
}
|
|
3741
5711
|
),
|
|
3742
|
-
/* @__PURE__ */ (0,
|
|
5712
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
|
|
3743
5713
|
"button",
|
|
3744
5714
|
{
|
|
3745
5715
|
type: "button",
|
|
@@ -3758,11 +5728,11 @@ function DialogCards(props) {
|
|
|
3758
5728
|
setLessonkitBlockType(DialogCards, "DialogCards");
|
|
3759
5729
|
|
|
3760
5730
|
// src/blocks/Flashcards.tsx
|
|
3761
|
-
var
|
|
3762
|
-
var
|
|
5731
|
+
var import_react45 = require("react");
|
|
5732
|
+
var import_jsx_runtime35 = require("react/jsx-runtime");
|
|
3763
5733
|
function Flashcards(props) {
|
|
3764
|
-
const [index, setIndex] = (0,
|
|
3765
|
-
const [face, setFace] = (0,
|
|
5734
|
+
const [index, setIndex] = (0, import_react45.useState)(0);
|
|
5735
|
+
const [face, setFace] = (0, import_react45.useState)("front");
|
|
3766
5736
|
const { track } = useLessonkit();
|
|
3767
5737
|
const lessonId = useEnclosingLessonId();
|
|
3768
5738
|
const card = props.cards[index];
|
|
@@ -3776,10 +5746,10 @@ function Flashcards(props) {
|
|
|
3776
5746
|
lessonId ? { lessonId } : void 0
|
|
3777
5747
|
);
|
|
3778
5748
|
};
|
|
3779
|
-
return /* @__PURE__ */ (0,
|
|
3780
|
-
/* @__PURE__ */ (0,
|
|
3781
|
-
props.selfScore ? /* @__PURE__ */ (0,
|
|
3782
|
-
/* @__PURE__ */ (0,
|
|
5749
|
+
return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
|
|
5750
|
+
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
|
|
5751
|
+
props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
|
|
5752
|
+
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
|
|
3783
5753
|
"button",
|
|
3784
5754
|
{
|
|
3785
5755
|
type: "button",
|
|
@@ -3797,10 +5767,10 @@ function Flashcards(props) {
|
|
|
3797
5767
|
setLessonkitBlockType(Flashcards, "Flashcards");
|
|
3798
5768
|
|
|
3799
5769
|
// src/blocks/ImageHotspots.tsx
|
|
3800
|
-
var
|
|
3801
|
-
var
|
|
5770
|
+
var import_react46 = require("react");
|
|
5771
|
+
var import_jsx_runtime36 = require("react/jsx-runtime");
|
|
3802
5772
|
function ImageHotspots(props) {
|
|
3803
|
-
const [active, setActive] = (0,
|
|
5773
|
+
const [active, setActive] = (0, import_react46.useState)(null);
|
|
3804
5774
|
const { track } = useLessonkit();
|
|
3805
5775
|
const lessonId = useEnclosingLessonId();
|
|
3806
5776
|
const open = (hotspotId) => {
|
|
@@ -3811,10 +5781,10 @@ function ImageHotspots(props) {
|
|
|
3811
5781
|
lessonId ? { lessonId } : void 0
|
|
3812
5782
|
);
|
|
3813
5783
|
};
|
|
3814
|
-
return /* @__PURE__ */ (0,
|
|
3815
|
-
/* @__PURE__ */ (0,
|
|
3816
|
-
/* @__PURE__ */ (0,
|
|
3817
|
-
props.hotspots.map((h) => /* @__PURE__ */ (0,
|
|
5784
|
+
return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
|
|
5785
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
5786
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
5787
|
+
props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
3818
5788
|
"button",
|
|
3819
5789
|
{
|
|
3820
5790
|
type: "button",
|
|
@@ -3833,19 +5803,19 @@ function ImageHotspots(props) {
|
|
|
3833
5803
|
h.id
|
|
3834
5804
|
))
|
|
3835
5805
|
] }),
|
|
3836
|
-
active ? /* @__PURE__ */ (0,
|
|
5806
|
+
active ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
|
|
3837
5807
|
props.hotspots.find((h) => h.id === active)?.content,
|
|
3838
|
-
/* @__PURE__ */ (0,
|
|
5808
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("button", { type: "button", onClick: () => setActive(null), children: "Close" })
|
|
3839
5809
|
] }) : null
|
|
3840
5810
|
] });
|
|
3841
5811
|
}
|
|
3842
5812
|
setLessonkitBlockType(ImageHotspots, "ImageHotspots");
|
|
3843
5813
|
|
|
3844
5814
|
// src/blocks/ImageSlider.tsx
|
|
3845
|
-
var
|
|
3846
|
-
var
|
|
5815
|
+
var import_react47 = require("react");
|
|
5816
|
+
var import_jsx_runtime37 = require("react/jsx-runtime");
|
|
3847
5817
|
function ImageSlider(props) {
|
|
3848
|
-
const [index, setIndex] = (0,
|
|
5818
|
+
const [index, setIndex] = (0, import_react47.useState)(0);
|
|
3849
5819
|
const { track } = useLessonkit();
|
|
3850
5820
|
const lessonId = useEnclosingLessonId();
|
|
3851
5821
|
const slide = props.slides[index];
|
|
@@ -3858,11 +5828,11 @@ function ImageSlider(props) {
|
|
|
3858
5828
|
lessonId ? { lessonId } : void 0
|
|
3859
5829
|
);
|
|
3860
5830
|
};
|
|
3861
|
-
return /* @__PURE__ */ (0,
|
|
3862
|
-
/* @__PURE__ */ (0,
|
|
3863
|
-
slide.caption ? /* @__PURE__ */ (0,
|
|
3864
|
-
/* @__PURE__ */ (0,
|
|
3865
|
-
/* @__PURE__ */ (0,
|
|
5831
|
+
return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
5832
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
|
|
5833
|
+
slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { children: slide.caption }) : null,
|
|
5834
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("nav", { "aria-label": "Slide navigation", children: [
|
|
5835
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
3866
5836
|
"button",
|
|
3867
5837
|
{
|
|
3868
5838
|
type: "button",
|
|
@@ -3872,12 +5842,12 @@ function ImageSlider(props) {
|
|
|
3872
5842
|
children: "Previous"
|
|
3873
5843
|
}
|
|
3874
5844
|
),
|
|
3875
|
-
/* @__PURE__ */ (0,
|
|
5845
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { children: [
|
|
3876
5846
|
index + 1,
|
|
3877
5847
|
" / ",
|
|
3878
5848
|
props.slides.length
|
|
3879
5849
|
] }),
|
|
3880
|
-
/* @__PURE__ */ (0,
|
|
5850
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
3881
5851
|
"button",
|
|
3882
5852
|
{
|
|
3883
5853
|
type: "button",
|
|
@@ -3893,21 +5863,42 @@ function ImageSlider(props) {
|
|
|
3893
5863
|
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
3894
5864
|
|
|
3895
5865
|
// src/blocks/FindHotspot.tsx
|
|
3896
|
-
var
|
|
3897
|
-
var
|
|
3898
|
-
var
|
|
5866
|
+
var import_react48 = require("react");
|
|
5867
|
+
var import_jsx_runtime38 = require("react/jsx-runtime");
|
|
5868
|
+
var INTERACTION11 = "findHotspot";
|
|
3899
5869
|
function FindHotspotInner(props, ref) {
|
|
3900
|
-
const checkId = (0,
|
|
3901
|
-
const [selected, setSelected] = (0,
|
|
3902
|
-
const [checked, setChecked] = (0,
|
|
5870
|
+
const checkId = (0, import_react48.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
5871
|
+
const [selected, setSelected] = (0, import_react48.useState)(null);
|
|
5872
|
+
const [checked, setChecked] = (0, import_react48.useState)(false);
|
|
5873
|
+
const telemetryReplayedRef = (0, import_react48.useRef)(false);
|
|
3903
5874
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3904
5875
|
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
3905
|
-
(0,
|
|
5876
|
+
(0, import_react48.useEffect)(() => {
|
|
3906
5877
|
setSelected(null);
|
|
3907
5878
|
setChecked(false);
|
|
5879
|
+
telemetryReplayedRef.current = false;
|
|
3908
5880
|
}, [checkId, props.correctTargetId, targetIdsKey]);
|
|
3909
5881
|
const correct = selected === props.correctTargetId;
|
|
3910
|
-
const
|
|
5882
|
+
const replayTelemetry = (nextSelected, nextChecked, nextCorrect) => {
|
|
5883
|
+
if (telemetryReplayedRef.current || !nextChecked || nextSelected === null) return;
|
|
5884
|
+
telemetryReplayedRef.current = true;
|
|
5885
|
+
assessment.answer({
|
|
5886
|
+
checkId,
|
|
5887
|
+
interactionType: INTERACTION11,
|
|
5888
|
+
response: nextSelected,
|
|
5889
|
+
correct: nextCorrect
|
|
5890
|
+
});
|
|
5891
|
+
if (nextCorrect) {
|
|
5892
|
+
assessment.complete({
|
|
5893
|
+
checkId,
|
|
5894
|
+
interactionType: INTERACTION11,
|
|
5895
|
+
score: 1,
|
|
5896
|
+
maxScore: 1,
|
|
5897
|
+
passingScore: props.passingScore ?? 1
|
|
5898
|
+
});
|
|
5899
|
+
}
|
|
5900
|
+
};
|
|
5901
|
+
const handle = (0, import_react48.useMemo)(
|
|
3911
5902
|
() => buildAssessmentHandle({
|
|
3912
5903
|
checkId,
|
|
3913
5904
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -3916,11 +5907,12 @@ function FindHotspotInner(props, ref) {
|
|
|
3916
5907
|
resetTask: () => {
|
|
3917
5908
|
setSelected(null);
|
|
3918
5909
|
setChecked(false);
|
|
5910
|
+
telemetryReplayedRef.current = false;
|
|
3919
5911
|
},
|
|
3920
5912
|
showSolutions: () => setSelected(props.correctTargetId),
|
|
3921
5913
|
getXAPIData: () => ({
|
|
3922
5914
|
checkId,
|
|
3923
|
-
interactionType:
|
|
5915
|
+
interactionType: INTERACTION11,
|
|
3924
5916
|
response: selected ?? void 0,
|
|
3925
5917
|
correct: checked ? correct : void 0,
|
|
3926
5918
|
score: checked && correct ? 1 : 0,
|
|
@@ -3928,15 +5920,23 @@ function FindHotspotInner(props, ref) {
|
|
|
3928
5920
|
}),
|
|
3929
5921
|
getCurrentState: () => ({ selected, checked }),
|
|
3930
5922
|
resume: (state) => {
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
5923
|
+
let nextSelected = selected;
|
|
5924
|
+
const rawSelected = readStringField(state, "selected");
|
|
5925
|
+
if (typeof rawSelected === "string" || rawSelected === null) {
|
|
5926
|
+
const valid = rawSelected === null || props.targets.some((t) => t.id === rawSelected);
|
|
5927
|
+
nextSelected = valid ? rawSelected : null;
|
|
5928
|
+
setSelected(nextSelected);
|
|
3935
5929
|
}
|
|
3936
|
-
|
|
5930
|
+
let nextChecked = checked;
|
|
5931
|
+
readBooleanStateField(state, "checked", (value) => {
|
|
5932
|
+
nextChecked = value;
|
|
5933
|
+
setChecked(value);
|
|
5934
|
+
});
|
|
5935
|
+
const nextCorrect = nextSelected === props.correctTargetId;
|
|
5936
|
+
replayTelemetry(nextSelected, nextChecked, nextCorrect);
|
|
3937
5937
|
}
|
|
3938
5938
|
}),
|
|
3939
|
-
[
|
|
5939
|
+
[assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
|
|
3940
5940
|
);
|
|
3941
5941
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3942
5942
|
const selectTarget = (id) => {
|
|
@@ -3948,24 +5948,24 @@ function FindHotspotInner(props, ref) {
|
|
|
3948
5948
|
setChecked(true);
|
|
3949
5949
|
assessment.answer({
|
|
3950
5950
|
checkId,
|
|
3951
|
-
interactionType:
|
|
5951
|
+
interactionType: INTERACTION11,
|
|
3952
5952
|
response: selected,
|
|
3953
5953
|
correct
|
|
3954
5954
|
});
|
|
3955
5955
|
if (correct) {
|
|
3956
5956
|
assessment.complete({
|
|
3957
5957
|
checkId,
|
|
3958
|
-
interactionType:
|
|
5958
|
+
interactionType: INTERACTION11,
|
|
3959
5959
|
score: 1,
|
|
3960
5960
|
maxScore: 1,
|
|
3961
5961
|
passingScore: props.passingScore ?? 1
|
|
3962
5962
|
});
|
|
3963
5963
|
}
|
|
3964
5964
|
};
|
|
3965
|
-
return /* @__PURE__ */ (0,
|
|
3966
|
-
/* @__PURE__ */ (0,
|
|
3967
|
-
/* @__PURE__ */ (0,
|
|
3968
|
-
props.targets.map((t) => /* @__PURE__ */ (0,
|
|
5965
|
+
return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
5966
|
+
/* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
5967
|
+
/* @__PURE__ */ (0, import_jsx_runtime38.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
5968
|
+
props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
|
|
3969
5969
|
"button",
|
|
3970
5970
|
{
|
|
3971
5971
|
type: "button",
|
|
@@ -3984,24 +5984,24 @@ function FindHotspotInner(props, ref) {
|
|
|
3984
5984
|
t.id
|
|
3985
5985
|
))
|
|
3986
5986
|
] }),
|
|
3987
|
-
/* @__PURE__ */ (0,
|
|
3988
|
-
checked ? /* @__PURE__ */ (0,
|
|
5987
|
+
/* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
|
|
5988
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3989
5989
|
] });
|
|
3990
5990
|
}
|
|
3991
|
-
var FindHotspotInnerForwarded = (0,
|
|
3992
|
-
var FindHotspot = (0,
|
|
3993
|
-
return /* @__PURE__ */ (0,
|
|
5991
|
+
var FindHotspotInnerForwarded = (0, import_react48.forwardRef)(FindHotspotInner);
|
|
5992
|
+
var FindHotspot = (0, import_react48.forwardRef)(function FindHotspot2(props, ref) {
|
|
5993
|
+
return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3994
5994
|
});
|
|
3995
5995
|
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
3996
5996
|
|
|
3997
5997
|
// src/blocks/FindMultipleHotspots.tsx
|
|
3998
|
-
var
|
|
3999
|
-
var
|
|
4000
|
-
var
|
|
5998
|
+
var import_react49 = require("react");
|
|
5999
|
+
var import_jsx_runtime39 = require("react/jsx-runtime");
|
|
6000
|
+
var INTERACTION12 = "findMultipleHotspots";
|
|
4001
6001
|
function FindMultipleHotspotsInner(props, ref) {
|
|
4002
|
-
const checkId = (0,
|
|
4003
|
-
const [selected, setSelected] = (0,
|
|
4004
|
-
const [checked, setChecked] = (0,
|
|
6002
|
+
const checkId = (0, import_react49.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
6003
|
+
const [selected, setSelected] = (0, import_react49.useState)(/* @__PURE__ */ new Set());
|
|
6004
|
+
const [checked, setChecked] = (0, import_react49.useState)(false);
|
|
4005
6005
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4006
6006
|
const toggle = (id) => {
|
|
4007
6007
|
setSelected((prev) => {
|
|
@@ -4013,7 +6013,7 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4013
6013
|
setChecked(false);
|
|
4014
6014
|
};
|
|
4015
6015
|
const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
|
|
4016
|
-
const handle = (0,
|
|
6016
|
+
const handle = (0, import_react49.useMemo)(
|
|
4017
6017
|
() => buildAssessmentHandle({
|
|
4018
6018
|
checkId,
|
|
4019
6019
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -4026,7 +6026,7 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4026
6026
|
showSolutions: () => setSelected(new Set(props.correctTargetIds)),
|
|
4027
6027
|
getXAPIData: () => ({
|
|
4028
6028
|
checkId,
|
|
4029
|
-
interactionType:
|
|
6029
|
+
interactionType: INTERACTION12,
|
|
4030
6030
|
response: [...selected],
|
|
4031
6031
|
correct: checked ? correct : void 0,
|
|
4032
6032
|
score: checked && correct ? 1 : 0,
|
|
@@ -4047,24 +6047,24 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4047
6047
|
setChecked(true);
|
|
4048
6048
|
assessment.answer({
|
|
4049
6049
|
checkId,
|
|
4050
|
-
interactionType:
|
|
6050
|
+
interactionType: INTERACTION12,
|
|
4051
6051
|
response: [...selected],
|
|
4052
6052
|
correct
|
|
4053
6053
|
});
|
|
4054
6054
|
if (correct) {
|
|
4055
6055
|
assessment.complete({
|
|
4056
6056
|
checkId,
|
|
4057
|
-
interactionType:
|
|
6057
|
+
interactionType: INTERACTION12,
|
|
4058
6058
|
score: 1,
|
|
4059
6059
|
maxScore: 1,
|
|
4060
6060
|
passingScore: props.passingScore ?? 1
|
|
4061
6061
|
});
|
|
4062
6062
|
}
|
|
4063
6063
|
};
|
|
4064
|
-
return /* @__PURE__ */ (0,
|
|
4065
|
-
/* @__PURE__ */ (0,
|
|
4066
|
-
/* @__PURE__ */ (0,
|
|
4067
|
-
props.targets.map((t) => /* @__PURE__ */ (0,
|
|
6064
|
+
return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
6065
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
6066
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
6067
|
+
props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
|
|
4068
6068
|
"button",
|
|
4069
6069
|
{
|
|
4070
6070
|
type: "button",
|
|
@@ -4083,23 +6083,23 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4083
6083
|
t.id
|
|
4084
6084
|
))
|
|
4085
6085
|
] }),
|
|
4086
|
-
/* @__PURE__ */ (0,
|
|
4087
|
-
checked ? /* @__PURE__ */ (0,
|
|
6086
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
6087
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
4088
6088
|
] });
|
|
4089
6089
|
}
|
|
4090
|
-
var FindMultipleHotspotsInnerForwarded = (0,
|
|
4091
|
-
var FindMultipleHotspots = (0,
|
|
6090
|
+
var FindMultipleHotspotsInnerForwarded = (0, import_react49.forwardRef)(FindMultipleHotspotsInner);
|
|
6091
|
+
var FindMultipleHotspots = (0, import_react49.forwardRef)(
|
|
4092
6092
|
function FindMultipleHotspots2(props, ref) {
|
|
4093
|
-
return /* @__PURE__ */ (0,
|
|
6093
|
+
return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
4094
6094
|
}
|
|
4095
6095
|
);
|
|
4096
6096
|
setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
|
|
4097
6097
|
|
|
4098
6098
|
// src/index.tsx
|
|
4099
|
-
var
|
|
6099
|
+
var import_core21 = require("@lessonkit/core");
|
|
4100
6100
|
|
|
4101
6101
|
// src/theme/ThemeProvider.tsx
|
|
4102
|
-
var
|
|
6102
|
+
var import_react50 = __toESM(require("react"), 1);
|
|
4103
6103
|
var import_themes = require("@lessonkit/themes");
|
|
4104
6104
|
|
|
4105
6105
|
// src/theme/applyCssVariables.ts
|
|
@@ -4118,11 +6118,11 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
4118
6118
|
}
|
|
4119
6119
|
|
|
4120
6120
|
// src/theme/ThemeProvider.tsx
|
|
4121
|
-
var
|
|
4122
|
-
var ThemeContext = (0,
|
|
6121
|
+
var import_jsx_runtime40 = require("react/jsx-runtime");
|
|
6122
|
+
var ThemeContext = (0, import_react50.createContext)(null);
|
|
4123
6123
|
var useIsoLayoutEffect2 = (
|
|
4124
6124
|
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
4125
|
-
typeof window !== "undefined" ?
|
|
6125
|
+
typeof window !== "undefined" ? import_react50.useLayoutEffect : import_react50.default.useEffect
|
|
4126
6126
|
);
|
|
4127
6127
|
function getSystemMode() {
|
|
4128
6128
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -4141,7 +6141,7 @@ function ThemeProvider(props) {
|
|
|
4141
6141
|
const preset = props.preset ?? "default";
|
|
4142
6142
|
const mode = props.mode ?? "light";
|
|
4143
6143
|
const targetKind = props.target ?? "document";
|
|
4144
|
-
const [resolvedMode, setResolvedMode] = (0,
|
|
6144
|
+
const [resolvedMode, setResolvedMode] = (0, import_react50.useState)(
|
|
4145
6145
|
() => mode === "system" ? getSystemMode() : mode
|
|
4146
6146
|
);
|
|
4147
6147
|
useIsoLayoutEffect2(() => {
|
|
@@ -4157,20 +6157,20 @@ function ThemeProvider(props) {
|
|
|
4157
6157
|
return () => mq.removeEventListener("change", onChange);
|
|
4158
6158
|
}, [mode]);
|
|
4159
6159
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
4160
|
-
const effectiveTheme = (0,
|
|
6160
|
+
const effectiveTheme = (0, import_react50.useMemo)(() => {
|
|
4161
6161
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
4162
6162
|
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));
|
|
4163
6163
|
return (0, import_themes.mergeThemes)(base, props.theme ?? {});
|
|
4164
6164
|
}, [preset, mode, dataTheme, props.theme]);
|
|
4165
|
-
const hostRef = (0,
|
|
4166
|
-
const appliedKeysRef = (0,
|
|
6165
|
+
const hostRef = (0, import_react50.useRef)(null);
|
|
6166
|
+
const appliedKeysRef = (0, import_react50.useRef)(/* @__PURE__ */ new Set());
|
|
4167
6167
|
useIsoLayoutEffect2(() => {
|
|
4168
6168
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
4169
6169
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
4170
6170
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
4171
6171
|
}
|
|
4172
6172
|
}, [targetKind, dataTheme]);
|
|
4173
|
-
const inject = (0,
|
|
6173
|
+
const inject = (0, import_react50.useCallback)(() => {
|
|
4174
6174
|
const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
|
|
4175
6175
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
4176
6176
|
if (!el) return;
|
|
@@ -4187,7 +6187,7 @@ function ThemeProvider(props) {
|
|
|
4187
6187
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
4188
6188
|
};
|
|
4189
6189
|
}, [inject, targetKind]);
|
|
4190
|
-
const value = (0,
|
|
6190
|
+
const value = (0, import_react50.useMemo)(
|
|
4191
6191
|
() => ({
|
|
4192
6192
|
theme: effectiveTheme,
|
|
4193
6193
|
preset,
|
|
@@ -4197,12 +6197,12 @@ function ThemeProvider(props) {
|
|
|
4197
6197
|
[effectiveTheme, preset, mode, dataTheme]
|
|
4198
6198
|
);
|
|
4199
6199
|
if (targetKind === "document") {
|
|
4200
|
-
return /* @__PURE__ */ (0,
|
|
6200
|
+
return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
4201
6201
|
}
|
|
4202
|
-
return /* @__PURE__ */ (0,
|
|
6202
|
+
return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
4203
6203
|
}
|
|
4204
6204
|
function useTheme() {
|
|
4205
|
-
const ctx = (0,
|
|
6205
|
+
const ctx = (0, import_react50.useContext)(ThemeContext);
|
|
4206
6206
|
if (!ctx) {
|
|
4207
6207
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
4208
6208
|
}
|
|
@@ -4210,13 +6210,15 @@ function useTheme() {
|
|
|
4210
6210
|
}
|
|
4211
6211
|
|
|
4212
6212
|
// src/catalogV3Entries.ts
|
|
4213
|
-
var
|
|
6213
|
+
var import_core20 = require("@lessonkit/core");
|
|
4214
6214
|
var COMPOUND_PARENTS = [
|
|
4215
6215
|
"Lesson",
|
|
4216
6216
|
"Page",
|
|
4217
6217
|
"InteractiveBook",
|
|
4218
6218
|
"Slide",
|
|
4219
6219
|
"SlideDeck",
|
|
6220
|
+
"TimedCue",
|
|
6221
|
+
"InteractiveVideo",
|
|
4220
6222
|
"AssessmentSequence"
|
|
4221
6223
|
];
|
|
4222
6224
|
function extendParents(entry) {
|
|
@@ -4275,6 +6277,23 @@ var v3CompoundAndContentEntries = [
|
|
|
4275
6277
|
theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
|
|
4276
6278
|
telemetry: { emits: [] }
|
|
4277
6279
|
},
|
|
6280
|
+
{
|
|
6281
|
+
type: "Video",
|
|
6282
|
+
category: "content",
|
|
6283
|
+
description: "Self-hosted video with native controls and optional captions.",
|
|
6284
|
+
props: [
|
|
6285
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6286
|
+
{ name: "src", type: "string", required: true, description: "Video URL." },
|
|
6287
|
+
{ name: "poster", type: "string", required: false, description: "Poster image URL." },
|
|
6288
|
+
{ name: "captions", type: "string", required: false, description: "WebVTT captions URL." },
|
|
6289
|
+
{ name: "title", type: "string", required: false, description: "Accessible title." }
|
|
6290
|
+
],
|
|
6291
|
+
requiredIds: ["blockId"],
|
|
6292
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6293
|
+
a11y: { element: "video", ariaLabel: "Video", keyboard: "Native video controls.", notes: "No autoplay with sound." },
|
|
6294
|
+
theming: { surface: "global-inherit", stylingNotes: "Responsive video." },
|
|
6295
|
+
telemetry: { emits: [] }
|
|
6296
|
+
},
|
|
4278
6297
|
{
|
|
4279
6298
|
type: "Page",
|
|
4280
6299
|
category: "container",
|
|
@@ -4282,8 +6301,8 @@ var v3CompoundAndContentEntries = [
|
|
|
4282
6301
|
h5pMachineName: "H5P.Column",
|
|
4283
6302
|
h5pAlias: "Column",
|
|
4284
6303
|
description: "Column layout container (H5P Column / Page).",
|
|
4285
|
-
allowedChildTypes: [...
|
|
4286
|
-
maxNestingDepth:
|
|
6304
|
+
allowedChildTypes: [...import_core20.PAGE_ALLOWED_CHILD_TYPES],
|
|
6305
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.Page,
|
|
4287
6306
|
props: [
|
|
4288
6307
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4289
6308
|
{ name: "title", type: "string", required: false, description: "Page title." },
|
|
@@ -4303,8 +6322,8 @@ var v3CompoundAndContentEntries = [
|
|
|
4303
6322
|
h5pMachineName: "H5P.InteractiveBook",
|
|
4304
6323
|
h5pAlias: "Interactive Book",
|
|
4305
6324
|
description: "Multi-page book with chapter navigation.",
|
|
4306
|
-
allowedChildTypes: [...
|
|
4307
|
-
maxNestingDepth:
|
|
6325
|
+
allowedChildTypes: [...import_core20.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
|
|
6326
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
|
|
4308
6327
|
props: [
|
|
4309
6328
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4310
6329
|
{ name: "title", type: "string", required: true, description: "Book title." },
|
|
@@ -4328,9 +6347,9 @@ var v3CompoundAndContentEntries = [
|
|
|
4328
6347
|
compoundContract: true,
|
|
4329
6348
|
h5pMachineName: "H5P.CoursePresentation",
|
|
4330
6349
|
h5pAlias: "Course Presentation slide",
|
|
4331
|
-
description: "Single slide row in a SlideDeck.
|
|
4332
|
-
allowedChildTypes: [...
|
|
4333
|
-
maxNestingDepth:
|
|
6350
|
+
description: "Single slide row in a SlideDeck. Supports Video, Summary, and 1.4 blocks.",
|
|
6351
|
+
allowedChildTypes: [...import_core20.SLIDE_ALLOWED_CHILD_TYPES],
|
|
6352
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.Slide,
|
|
4334
6353
|
props: [
|
|
4335
6354
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4336
6355
|
{ name: "title", type: "string", required: false, description: "Slide title." },
|
|
@@ -4350,8 +6369,8 @@ var v3CompoundAndContentEntries = [
|
|
|
4350
6369
|
h5pMachineName: "H5P.CoursePresentation",
|
|
4351
6370
|
h5pAlias: "Course Presentation",
|
|
4352
6371
|
description: "Multi-slide presentation with keyboard navigation.",
|
|
4353
|
-
allowedChildTypes: [...
|
|
4354
|
-
maxNestingDepth:
|
|
6372
|
+
allowedChildTypes: [...import_core20.SLIDE_DECK_ALLOWED_CHILD_TYPES],
|
|
6373
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
|
|
4355
6374
|
props: [
|
|
4356
6375
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4357
6376
|
{ name: "title", type: "string", required: true, description: "Deck title." },
|
|
@@ -4369,6 +6388,121 @@ var v3CompoundAndContentEntries = [
|
|
|
4369
6388
|
theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
|
|
4370
6389
|
telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
|
|
4371
6390
|
},
|
|
6391
|
+
{
|
|
6392
|
+
type: "TimedCue",
|
|
6393
|
+
category: "container",
|
|
6394
|
+
compoundContract: true,
|
|
6395
|
+
h5pMachineName: "H5P.InteractiveVideo",
|
|
6396
|
+
h5pAlias: "Interactive Video timed cue",
|
|
6397
|
+
description: "Timed overlay cue within InteractiveVideo.",
|
|
6398
|
+
allowedChildTypes: [...import_core20.TIMED_CUE_ALLOWED_CHILD_TYPES],
|
|
6399
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.TimedCue,
|
|
6400
|
+
props: [
|
|
6401
|
+
{ name: "atSeconds", type: "number", required: true, description: "Cue time in seconds." },
|
|
6402
|
+
{ name: "label", type: "string", required: false, description: "Cue label." },
|
|
6403
|
+
{ name: "mustComplete", type: "boolean", required: false, description: "Block seek until completed." },
|
|
6404
|
+
{ name: "children", type: "ReactNode", required: true, description: "Single allowed child block." }
|
|
6405
|
+
],
|
|
6406
|
+
requiredIds: [],
|
|
6407
|
+
parentConstraints: ["InteractiveVideo"],
|
|
6408
|
+
a11y: { element: "dialog", ariaLabel: "Timed cue", keyboard: "Focus moves to overlay content.", notes: "Pauses parent video." },
|
|
6409
|
+
theming: { surface: "global-inherit", stylingNotes: "Overlay panel." },
|
|
6410
|
+
telemetry: { emits: [] }
|
|
6411
|
+
},
|
|
6412
|
+
{
|
|
6413
|
+
type: "InteractiveVideo",
|
|
6414
|
+
category: "container",
|
|
6415
|
+
compoundContract: true,
|
|
6416
|
+
h5pMachineName: "H5P.InteractiveVideo",
|
|
6417
|
+
h5pAlias: "Interactive Video",
|
|
6418
|
+
description: "Video with timed interaction overlays.",
|
|
6419
|
+
allowedChildTypes: [...import_core20.INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES],
|
|
6420
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.InteractiveVideo,
|
|
6421
|
+
props: [
|
|
6422
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6423
|
+
{ name: "title", type: "string", required: true, description: "Video title." },
|
|
6424
|
+
{ name: "src", type: "string", required: true, description: "Video URL." },
|
|
6425
|
+
{ name: "poster", type: "string", required: false, description: "Poster image." },
|
|
6426
|
+
{ name: "captions", type: "string", required: false, description: "WebVTT captions." },
|
|
6427
|
+
{ name: "showVideoScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
6428
|
+
{ name: "children", type: "TimedCue[]", required: true, description: "Timed cues." }
|
|
6429
|
+
],
|
|
6430
|
+
requiredIds: ["blockId"],
|
|
6431
|
+
parentConstraints: ["Lesson"],
|
|
6432
|
+
a11y: {
|
|
6433
|
+
element: "section",
|
|
6434
|
+
ariaLabel: "Interactive video",
|
|
6435
|
+
keyboard: "Native video controls; overlay interactions when paused.",
|
|
6436
|
+
notes: "H5P Interactive Video equivalent."
|
|
6437
|
+
},
|
|
6438
|
+
theming: { surface: "global-inherit", stylingNotes: "Video + overlay." },
|
|
6439
|
+
telemetry: { emits: ["video_cue_reached", "video_segment_completed"], requiresActiveLesson: true }
|
|
6440
|
+
},
|
|
6441
|
+
{
|
|
6442
|
+
type: "Questionnaire",
|
|
6443
|
+
category: "content",
|
|
6444
|
+
h5pMachineName: "H5P.Questionnaire",
|
|
6445
|
+
h5pAlias: "Questionnaire",
|
|
6446
|
+
description: "Unscored multi-field survey.",
|
|
6447
|
+
props: [
|
|
6448
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6449
|
+
{ name: "fields", type: "QuestionnaireField[]", required: true, description: "Form fields." }
|
|
6450
|
+
],
|
|
6451
|
+
requiredIds: ["blockId"],
|
|
6452
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6453
|
+
a11y: { element: "form", ariaLabel: "Questionnaire", keyboard: "Tab through fields.", notes: "Unscored survey." },
|
|
6454
|
+
theming: { surface: "global-inherit", stylingNotes: "Form layout." },
|
|
6455
|
+
telemetry: { emits: ["questionnaire_submitted"], requiresActiveLesson: true }
|
|
6456
|
+
},
|
|
6457
|
+
{
|
|
6458
|
+
type: "MemoryGame",
|
|
6459
|
+
category: "content",
|
|
6460
|
+
h5pMachineName: "H5P.MemoryGame",
|
|
6461
|
+
h5pAlias: "Memory Game",
|
|
6462
|
+
description: "Card flip memory matching game.",
|
|
6463
|
+
props: [
|
|
6464
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6465
|
+
{ name: "pairs", type: "MemoryPair[]", required: true, description: "Card pairs." },
|
|
6466
|
+
{ name: "selfScore", type: "boolean", required: false, description: "Optional self-score mode." }
|
|
6467
|
+
],
|
|
6468
|
+
requiredIds: ["blockId"],
|
|
6469
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6470
|
+
a11y: { element: "section", ariaLabel: "Memory game", keyboard: "Flip cards with buttons.", notes: "Reduced motion safe." },
|
|
6471
|
+
theming: { surface: "global-inherit", stylingNotes: "Card grid." },
|
|
6472
|
+
telemetry: { emits: ["memory_card_flipped"] }
|
|
6473
|
+
},
|
|
6474
|
+
{
|
|
6475
|
+
type: "InformationWall",
|
|
6476
|
+
category: "content",
|
|
6477
|
+
h5pMachineName: "H5P.InformationWall",
|
|
6478
|
+
h5pAlias: "Information Wall",
|
|
6479
|
+
description: "Searchable information panels.",
|
|
6480
|
+
props: [
|
|
6481
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6482
|
+
{ name: "panels", type: "InformationPanel[]", required: true, description: "Content panels." }
|
|
6483
|
+
],
|
|
6484
|
+
requiredIds: ["blockId"],
|
|
6485
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6486
|
+
a11y: { element: "section", ariaLabel: "Information wall", keyboard: "Search and browse panels.", notes: "Filterable grid." },
|
|
6487
|
+
theming: { surface: "global-inherit", stylingNotes: "Panel grid." },
|
|
6488
|
+
telemetry: { emits: ["information_wall_search"] }
|
|
6489
|
+
},
|
|
6490
|
+
{
|
|
6491
|
+
type: "ParallaxSlideshow",
|
|
6492
|
+
category: "content",
|
|
6493
|
+
h5pMachineName: "H5P.ImpressivePresentation",
|
|
6494
|
+
h5pAlias: "Slideshow (parallax)",
|
|
6495
|
+
description: "Slideshow with parallax; static fallback when reduced motion.",
|
|
6496
|
+
props: [
|
|
6497
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6498
|
+
{ name: "slides", type: "ParallaxSlide[]", required: true, description: "Slides." }
|
|
6499
|
+
],
|
|
6500
|
+
requiredIds: ["blockId"],
|
|
6501
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6502
|
+
a11y: { element: "section", ariaLabel: "Slideshow", keyboard: "Previous/next slide.", notes: "prefers-reduced-motion fallback." },
|
|
6503
|
+
theming: { surface: "global-inherit", stylingNotes: "Slide deck." },
|
|
6504
|
+
telemetry: { emits: ["parallax_slide_viewed"] }
|
|
6505
|
+
},
|
|
4372
6506
|
{
|
|
4373
6507
|
type: "Accordion",
|
|
4374
6508
|
category: "content",
|
|
@@ -4502,8 +6636,8 @@ function buildV3CatalogFromV2(v2) {
|
|
|
4502
6636
|
return {
|
|
4503
6637
|
...base,
|
|
4504
6638
|
compoundContract: true,
|
|
4505
|
-
allowedChildTypes: [...
|
|
4506
|
-
maxNestingDepth:
|
|
6639
|
+
allowedChildTypes: [...import_core20.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
|
|
6640
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
|
|
4507
6641
|
};
|
|
4508
6642
|
}
|
|
4509
6643
|
return base;
|
|
@@ -4850,6 +6984,100 @@ var v2AssessmentEntries = [
|
|
|
4850
6984
|
},
|
|
4851
6985
|
theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
|
|
4852
6986
|
telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
|
|
6987
|
+
},
|
|
6988
|
+
{
|
|
6989
|
+
type: "Summary",
|
|
6990
|
+
category: "assessment",
|
|
6991
|
+
assessmentContract: true,
|
|
6992
|
+
h5pMachineName: "H5P.Summary",
|
|
6993
|
+
h5pAlias: "Summary",
|
|
6994
|
+
description: "Construct a summary from a statement bank in correct order.",
|
|
6995
|
+
props: [
|
|
6996
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
6997
|
+
{ name: "statements", type: "string[]", required: true, description: "Available statements." },
|
|
6998
|
+
{ name: "correct", type: "string[]", required: true, description: "Correct ordered summary." },
|
|
6999
|
+
...assessmentBehaviourProps2
|
|
7000
|
+
],
|
|
7001
|
+
requiredIds: ["checkId"],
|
|
7002
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7003
|
+
a11y: { element: "section", ariaLabel: "Summary", keyboard: "Select statements in order.", notes: "H5P Summary equivalent." },
|
|
7004
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7005
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
7006
|
+
},
|
|
7007
|
+
{
|
|
7008
|
+
type: "ImagePairing",
|
|
7009
|
+
category: "assessment",
|
|
7010
|
+
assessmentContract: true,
|
|
7011
|
+
h5pMachineName: "H5P.ImagePair",
|
|
7012
|
+
h5pAlias: "Image Pairing",
|
|
7013
|
+
description: "Match image pairs in a memory-style task.",
|
|
7014
|
+
props: [
|
|
7015
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
7016
|
+
{ name: "pairs", type: "ImagePair[]", required: true, description: "Image pairs to match." },
|
|
7017
|
+
...assessmentBehaviourProps2
|
|
7018
|
+
],
|
|
7019
|
+
requiredIds: ["checkId"],
|
|
7020
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7021
|
+
a11y: { element: "section", ariaLabel: "Image Pairing", keyboard: "Select two cards to match.", notes: "H5P Image Pairing equivalent." },
|
|
7022
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7023
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
7024
|
+
},
|
|
7025
|
+
{
|
|
7026
|
+
type: "ImageSequencing",
|
|
7027
|
+
category: "assessment",
|
|
7028
|
+
assessmentContract: true,
|
|
7029
|
+
h5pMachineName: "H5P.ImageSequencing",
|
|
7030
|
+
h5pAlias: "Image Sequencing",
|
|
7031
|
+
description: "Order images in the correct sequence.",
|
|
7032
|
+
props: [
|
|
7033
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
7034
|
+
{ name: "images", type: "SequencingImage[]", required: true, description: "Images to order." },
|
|
7035
|
+
{ name: "correctOrder", type: "string[]", required: true, description: "Correct id order." },
|
|
7036
|
+
...assessmentBehaviourProps2
|
|
7037
|
+
],
|
|
7038
|
+
requiredIds: ["checkId"],
|
|
7039
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7040
|
+
a11y: { element: "section", ariaLabel: "Image Sequencing", keyboard: "Reorder with up/down.", notes: "H5P Image Sequencing equivalent." },
|
|
7041
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7042
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
7043
|
+
},
|
|
7044
|
+
{
|
|
7045
|
+
type: "ArithmeticQuiz",
|
|
7046
|
+
category: "assessment",
|
|
7047
|
+
assessmentContract: true,
|
|
7048
|
+
h5pMachineName: "H5P.ArithmeticQuiz",
|
|
7049
|
+
h5pAlias: "Arithmetic Quiz",
|
|
7050
|
+
description: "Timed arithmetic problems with optional timer.",
|
|
7051
|
+
props: [
|
|
7052
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
7053
|
+
{ name: "problems", type: "ArithmeticProblem[]", required: true, description: "Math problems." },
|
|
7054
|
+
{ name: "timeLimitSeconds", type: "number", required: false, description: "Optional time limit." },
|
|
7055
|
+
...assessmentBehaviourProps2
|
|
7056
|
+
],
|
|
7057
|
+
requiredIds: ["checkId"],
|
|
7058
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7059
|
+
a11y: { element: "section", ariaLabel: "Arithmetic Quiz", keyboard: "Text input per problem.", notes: "H5P Arithmetic Quiz equivalent." },
|
|
7060
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7061
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
7062
|
+
},
|
|
7063
|
+
{
|
|
7064
|
+
type: "Essay",
|
|
7065
|
+
category: "assessment",
|
|
7066
|
+
assessmentContract: true,
|
|
7067
|
+
h5pMachineName: "H5P.Essay",
|
|
7068
|
+
h5pAlias: "Essay",
|
|
7069
|
+
description: "Open text response; manual or plugin grading.",
|
|
7070
|
+
props: [
|
|
7071
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
7072
|
+
{ name: "question", type: "string", required: true, description: "Essay prompt." },
|
|
7073
|
+
{ name: "minLength", type: "number", required: false, description: "Minimum character length." },
|
|
7074
|
+
...assessmentBehaviourProps2
|
|
7075
|
+
],
|
|
7076
|
+
requiredIds: ["checkId"],
|
|
7077
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7078
|
+
a11y: { element: "section", ariaLabel: "Essay", keyboard: "Textarea input.", notes: "H5P Essay equivalent." },
|
|
7079
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7080
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
4853
7081
|
}
|
|
4854
7082
|
];
|
|
4855
7083
|
var BLOCK_CATALOG_V2 = [
|
|
@@ -4889,6 +7117,7 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
4889
7117
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4890
7118
|
0 && (module.exports = {
|
|
4891
7119
|
Accordion,
|
|
7120
|
+
ArithmeticQuiz,
|
|
4892
7121
|
AssessmentSequence,
|
|
4893
7122
|
BLOCK_CATALOG,
|
|
4894
7123
|
BLOCK_CATALOG_V2,
|
|
@@ -4897,6 +7126,7 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
4897
7126
|
DialogCards,
|
|
4898
7127
|
DragAndDrop,
|
|
4899
7128
|
DragTheWords,
|
|
7129
|
+
Essay,
|
|
4900
7130
|
FillInTheBlanks,
|
|
4901
7131
|
FindHotspot,
|
|
4902
7132
|
FindMultipleHotspots,
|
|
@@ -4904,22 +7134,33 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
4904
7134
|
Heading,
|
|
4905
7135
|
Image,
|
|
4906
7136
|
ImageHotspots,
|
|
7137
|
+
ImagePairing,
|
|
7138
|
+
ImageSequencing,
|
|
4907
7139
|
ImageSlider,
|
|
7140
|
+
InformationWall,
|
|
4908
7141
|
InteractiveBook,
|
|
7142
|
+
InteractiveVideo,
|
|
4909
7143
|
KnowledgeCheck,
|
|
4910
7144
|
Lesson,
|
|
4911
7145
|
LessonkitProvider,
|
|
4912
7146
|
MarkTheWords,
|
|
7147
|
+
MemoryGame,
|
|
4913
7148
|
Page,
|
|
7149
|
+
ParallaxSlideshow,
|
|
4914
7150
|
ProgressTracker,
|
|
7151
|
+
Questionnaire,
|
|
4915
7152
|
Quiz,
|
|
4916
7153
|
Reflection,
|
|
4917
7154
|
Scenario,
|
|
4918
7155
|
Slide,
|
|
4919
7156
|
SlideDeck,
|
|
7157
|
+
Summary,
|
|
4920
7158
|
Text,
|
|
4921
7159
|
ThemeProvider,
|
|
7160
|
+
TimedCue,
|
|
4922
7161
|
TrueFalse,
|
|
7162
|
+
Video,
|
|
7163
|
+
assertProductionCourseConfig,
|
|
4923
7164
|
blockCatalogV2Version,
|
|
4924
7165
|
blockCatalogV3Version,
|
|
4925
7166
|
blockCatalogVersion,
|
|
@@ -4934,6 +7175,7 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
4934
7175
|
getBlockCatalogEntry,
|
|
4935
7176
|
resetAssessmentWarningsForTests,
|
|
4936
7177
|
resetQuizWarningsForTests,
|
|
7178
|
+
shouldEnforceProductionGuard,
|
|
4937
7179
|
useAssessmentState,
|
|
4938
7180
|
useCompletion,
|
|
4939
7181
|
useLessonkit,
|