@lessonkit/react 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -9
- package/block-catalog.v3.json +445 -32
- package/dist/{AssessmentLessonGuard-D2Plzybb.d.cts → AssessmentLessonGuard-BzNPbjaV.d.cts} +1 -1
- package/dist/{AssessmentLessonGuard-D2Plzybb.d.ts → AssessmentLessonGuard-BzNPbjaV.d.ts} +1 -1
- package/dist/blocks-entry.cjs +1850 -328
- package/dist/blocks-entry.d.cts +61 -1
- package/dist/blocks-entry.d.ts +61 -1
- package/dist/blocks-entry.js +12 -2
- package/dist/{chunk-4LQ4TTEE.js → chunk-5P23C2W3.js} +1830 -313
- package/dist/{chunk-TDM3ARE7.js → chunk-7TJQJFYR.js} +483 -276
- package/dist/{chunk-UUTXECVW.js → chunk-ELGQ4XI3.js} +49 -30
- package/dist/index.cjs +2620 -774
- package/dist/index.d.cts +92 -5
- package/dist/index.d.ts +92 -5
- package/dist/index.js +125 -5
- package/dist/testing.cjs +86 -50
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/package.json +15 -18
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
CompoundPageIndexProvider,
|
|
4
4
|
CompoundProvider,
|
|
5
5
|
LessonkitContext,
|
|
6
|
+
aggregateAssessmentScores,
|
|
6
7
|
buildAssessmentHandle,
|
|
8
|
+
getLessonkitBlockType,
|
|
7
9
|
isDevEnvironment,
|
|
8
10
|
meetsPassingThreshold,
|
|
9
11
|
normalizeComponentId,
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
23
25
|
usePluginScoring,
|
|
24
26
|
validateAccordionSections,
|
|
25
27
|
validateCompoundChildren
|
|
26
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-7TJQJFYR.js";
|
|
27
29
|
|
|
28
30
|
// src/blocks/TrueFalse.tsx
|
|
29
31
|
import React, { forwardRef, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -62,7 +64,7 @@ function TrueFalseInner(props, ref) {
|
|
|
62
64
|
if (passed) {
|
|
63
65
|
return { score: completedScore ?? maxScore, maxScore };
|
|
64
66
|
}
|
|
65
|
-
if (selectionCorrect) {
|
|
67
|
+
if (selected !== null && selectionCorrect) {
|
|
66
68
|
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
67
69
|
}
|
|
68
70
|
return { score: 0, maxScore };
|
|
@@ -134,7 +136,9 @@ function TrueFalseInner(props, ref) {
|
|
|
134
136
|
if (nextPassed) {
|
|
135
137
|
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
136
138
|
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
137
|
-
|
|
139
|
+
if (config.tracking?.replayResumeEvents === true) {
|
|
140
|
+
replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
|
|
141
|
+
}
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
@@ -150,7 +154,8 @@ function TrueFalseInner(props, ref) {
|
|
|
150
154
|
props.question,
|
|
151
155
|
selected,
|
|
152
156
|
selectionCorrect,
|
|
153
|
-
showSolutions
|
|
157
|
+
showSolutions,
|
|
158
|
+
config.tracking?.replayResumeEvents
|
|
154
159
|
]
|
|
155
160
|
);
|
|
156
161
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
@@ -179,6 +184,17 @@ function TrueFalseInner(props, ref) {
|
|
|
179
184
|
maxScore: scored.maxScore,
|
|
180
185
|
passingScore: props.passingScore ?? scored.maxScore
|
|
181
186
|
});
|
|
187
|
+
} else if (!scored.passed && props.enableRetry === false && !completedRef.current) {
|
|
188
|
+
completedRef.current = true;
|
|
189
|
+
setCompletedScore(scored.score);
|
|
190
|
+
setCompletedMaxScore(scored.maxScore);
|
|
191
|
+
assessment.complete({
|
|
192
|
+
checkId,
|
|
193
|
+
interactionType: INTERACTION,
|
|
194
|
+
score: scored.score,
|
|
195
|
+
maxScore: scored.maxScore,
|
|
196
|
+
passingScore: props.passingScore ?? scored.maxScore
|
|
197
|
+
});
|
|
182
198
|
}
|
|
183
199
|
};
|
|
184
200
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
@@ -244,11 +260,13 @@ function MarkTheWordsInner(props, ref) {
|
|
|
244
260
|
);
|
|
245
261
|
const [marked, setMarked] = useState2(() => /* @__PURE__ */ new Set());
|
|
246
262
|
const [passed, setPassed] = useState2(false);
|
|
263
|
+
const [submitted, setSubmitted] = useState2(false);
|
|
247
264
|
const [showSolutions, setShowSolutions] = useState2(false);
|
|
248
265
|
const completedRef = useRef2(false);
|
|
249
266
|
const reset = () => {
|
|
250
267
|
completedRef.current = false;
|
|
251
268
|
setPassed(false);
|
|
269
|
+
setSubmitted(false);
|
|
252
270
|
setMarked(/* @__PURE__ */ new Set());
|
|
253
271
|
setShowSolutions(false);
|
|
254
272
|
};
|
|
@@ -306,6 +324,27 @@ function MarkTheWordsInner(props, ref) {
|
|
|
306
324
|
return next;
|
|
307
325
|
});
|
|
308
326
|
};
|
|
327
|
+
const submitMarks = () => {
|
|
328
|
+
if (!hasTargets || completedRef.current || marked.size === 0) return;
|
|
329
|
+
completedRef.current = true;
|
|
330
|
+
setSubmitted(true);
|
|
331
|
+
const didPass = passedThreshold;
|
|
332
|
+
if (didPass) setPassed(true);
|
|
333
|
+
assessment.answer({
|
|
334
|
+
checkId,
|
|
335
|
+
interactionType: INTERACTION2,
|
|
336
|
+
question: props.text,
|
|
337
|
+
response: [...marked].map((i) => tokens[i]),
|
|
338
|
+
correct: didPass
|
|
339
|
+
});
|
|
340
|
+
assessment.complete({
|
|
341
|
+
checkId,
|
|
342
|
+
interactionType: INTERACTION2,
|
|
343
|
+
score,
|
|
344
|
+
maxScore,
|
|
345
|
+
passingScore: props.passingScore ?? maxScore
|
|
346
|
+
});
|
|
347
|
+
};
|
|
309
348
|
useEffect2(() => {
|
|
310
349
|
if (!hasTargets) {
|
|
311
350
|
if (isDevEnvironment()) {
|
|
@@ -316,8 +355,10 @@ function MarkTheWordsInner(props, ref) {
|
|
|
316
355
|
}
|
|
317
356
|
return;
|
|
318
357
|
}
|
|
358
|
+
if (props.enableRetry === false) return;
|
|
319
359
|
if (!passedThreshold || completedRef.current) return;
|
|
320
360
|
completedRef.current = true;
|
|
361
|
+
setSubmitted(true);
|
|
321
362
|
setPassed(true);
|
|
322
363
|
assessment.answer({
|
|
323
364
|
checkId,
|
|
@@ -340,6 +381,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
340
381
|
marked,
|
|
341
382
|
maxScore,
|
|
342
383
|
passedThreshold,
|
|
384
|
+
props.enableRetry,
|
|
343
385
|
props.passingScore,
|
|
344
386
|
props.correctWords,
|
|
345
387
|
props.text,
|
|
@@ -377,7 +419,8 @@ function MarkTheWordsInner(props, ref) {
|
|
|
377
419
|
i
|
|
378
420
|
);
|
|
379
421
|
}) }),
|
|
380
|
-
|
|
422
|
+
passedThreshold ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
423
|
+
props.enableRetry === false && hasTargets && marked.size > 0 && !submitted ? /* @__PURE__ */ jsx2("button", { type: "button", "data-testid": "mark-the-words-submit", onClick: submitMarks, children: "Submit" }) : null,
|
|
381
424
|
props.enableRetry && passed ? /* @__PURE__ */ jsx2("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
382
425
|
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
383
426
|
] });
|
|
@@ -388,7 +431,7 @@ var MarkTheWords = forwardRef2(function MarkTheWords2(props, ref) {
|
|
|
388
431
|
});
|
|
389
432
|
|
|
390
433
|
// src/blocks/FillInTheBlanks.tsx
|
|
391
|
-
import React3, { forwardRef as forwardRef3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
|
|
434
|
+
import React3, { forwardRef as forwardRef3, useCallback, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
|
|
392
435
|
|
|
393
436
|
// src/assessment/internal/parseStarDelimitedTemplate.ts
|
|
394
437
|
function parseStarDelimitedTemplate(template, idPrefix) {
|
|
@@ -418,11 +461,43 @@ function parseTemplate(template) {
|
|
|
418
461
|
blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
|
|
419
462
|
};
|
|
420
463
|
}
|
|
464
|
+
function normalizeBlanks(blanks) {
|
|
465
|
+
return blanks.map((b) => ({ id: b.id.trim(), answer: b.answer.trim() })).filter((b) => b.id.length > 0 && b.answer.length > 0);
|
|
466
|
+
}
|
|
467
|
+
function resolveBlanks(template, explicitBlanks) {
|
|
468
|
+
const parsed = parseTemplate(template);
|
|
469
|
+
if (!explicitBlanks) {
|
|
470
|
+
return { parts: parsed.parts, blanks: parsed.blanks };
|
|
471
|
+
}
|
|
472
|
+
const normalized = normalizeBlanks(explicitBlanks);
|
|
473
|
+
if (normalized.length !== parsed.blanks.length) {
|
|
474
|
+
if (isDevEnvironment()) {
|
|
475
|
+
console.warn(
|
|
476
|
+
"[lessonkit] FillInTheBlanks: blanks length does not match template; using parsed blanks",
|
|
477
|
+
{ templateBlanks: parsed.blanks.length, explicitBlanks: normalized.length }
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return { parts: parsed.parts, blanks: parsed.blanks };
|
|
481
|
+
}
|
|
482
|
+
let blankIdx = 0;
|
|
483
|
+
const interleavedParts = parsed.parts.map((part) => {
|
|
484
|
+
if (part.startsWith("blank-")) {
|
|
485
|
+
const id = normalized[blankIdx]?.id;
|
|
486
|
+
blankIdx += 1;
|
|
487
|
+
return id ?? part;
|
|
488
|
+
}
|
|
489
|
+
return part;
|
|
490
|
+
});
|
|
491
|
+
return { parts: interleavedParts, blanks: normalized };
|
|
492
|
+
}
|
|
421
493
|
function FillInTheBlanksInner(props, ref) {
|
|
422
494
|
const checkId = useMemo3(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
423
495
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
424
|
-
const
|
|
425
|
-
const
|
|
496
|
+
const { config } = useLessonkit();
|
|
497
|
+
const { parts, blanks } = useMemo3(
|
|
498
|
+
() => resolveBlanks(props.template, props.blanks),
|
|
499
|
+
[props.template, props.blanks]
|
|
500
|
+
);
|
|
426
501
|
const [values, setValues] = useState3(
|
|
427
502
|
() => Object.fromEntries(blanks.map((b) => [b.id, ""]))
|
|
428
503
|
);
|
|
@@ -521,13 +596,14 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
521
596
|
blanks.forEach((b) => {
|
|
522
597
|
if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
|
|
523
598
|
});
|
|
524
|
-
|
|
599
|
+
if (config.tracking?.replayResumeEvents === true) {
|
|
600
|
+
replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
|
|
601
|
+
}
|
|
525
602
|
}
|
|
526
603
|
}),
|
|
527
|
-
[allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
|
|
604
|
+
[allFilled, assessment, blanks, checkId, config.tracking?.replayResumeEvents, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
|
|
528
605
|
);
|
|
529
|
-
|
|
530
|
-
const check = () => {
|
|
606
|
+
const check = useCallback(() => {
|
|
531
607
|
if (!hasBlanks) {
|
|
532
608
|
if (isDevEnvironment()) {
|
|
533
609
|
console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
|
|
@@ -535,7 +611,7 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
535
611
|
return;
|
|
536
612
|
}
|
|
537
613
|
if (!allFilled) return;
|
|
538
|
-
if (passed) return;
|
|
614
|
+
if (passed && !props.enableRetry) return;
|
|
539
615
|
const snapshot = JSON.stringify(values);
|
|
540
616
|
if (checkSnapshotRef.current === snapshot) return;
|
|
541
617
|
checkSnapshotRef.current = snapshot;
|
|
@@ -548,9 +624,9 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
548
624
|
response: values,
|
|
549
625
|
correct: passedThreshold
|
|
550
626
|
});
|
|
551
|
-
if (passedThreshold && !completedRef.current) {
|
|
627
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
552
628
|
completedRef.current = true;
|
|
553
|
-
setPassed(true);
|
|
629
|
+
if (passedThreshold) setPassed(true);
|
|
554
630
|
assessment.complete({
|
|
555
631
|
checkId,
|
|
556
632
|
interactionType: INTERACTION3,
|
|
@@ -559,7 +635,22 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
559
635
|
passingScore: props.passingScore ?? maxScore
|
|
560
636
|
});
|
|
561
637
|
}
|
|
562
|
-
}
|
|
638
|
+
}, [
|
|
639
|
+
allFilled,
|
|
640
|
+
assessment,
|
|
641
|
+
blanks.length,
|
|
642
|
+
checkId,
|
|
643
|
+
hasBlanks,
|
|
644
|
+
maxScore,
|
|
645
|
+
passed,
|
|
646
|
+
passedThreshold,
|
|
647
|
+
props.enableRetry,
|
|
648
|
+
props.passingScore,
|
|
649
|
+
props.template,
|
|
650
|
+
score,
|
|
651
|
+
values
|
|
652
|
+
]);
|
|
653
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
563
654
|
useEffect3(() => {
|
|
564
655
|
if (!allFilled) {
|
|
565
656
|
answeredRef.current = false;
|
|
@@ -569,14 +660,17 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
569
660
|
}, [allFilled]);
|
|
570
661
|
useEffect3(() => {
|
|
571
662
|
if (props.autoCheck && allFilled && !passed) check();
|
|
572
|
-
}, [allFilled,
|
|
663
|
+
}, [allFilled, check, passed, props.autoCheck]);
|
|
573
664
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
574
665
|
return /* @__PURE__ */ jsxs3("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
575
|
-
/* @__PURE__ */ jsx3("p", { children:
|
|
666
|
+
/* @__PURE__ */ jsx3("p", { children: parts.map((part, i) => {
|
|
576
667
|
const blank = blanks.find((b) => b.id === part);
|
|
577
668
|
if (!blank) return /* @__PURE__ */ jsx3(React3.Fragment, { children: part }, i);
|
|
578
669
|
return /* @__PURE__ */ jsxs3("label", { style: { margin: "0 0.25em" }, children: [
|
|
579
|
-
/* @__PURE__ */
|
|
670
|
+
/* @__PURE__ */ jsxs3("span", { className: "lk-visually-hidden", children: [
|
|
671
|
+
"Blank ",
|
|
672
|
+
blank.id
|
|
673
|
+
] }),
|
|
580
674
|
/* @__PURE__ */ jsx3(
|
|
581
675
|
"input",
|
|
582
676
|
{
|
|
@@ -593,7 +687,16 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
593
687
|
)
|
|
594
688
|
] }, blank.id);
|
|
595
689
|
}) }),
|
|
596
|
-
!props.autoCheck ? /* @__PURE__ */ jsx3(
|
|
690
|
+
!props.autoCheck ? /* @__PURE__ */ jsx3(
|
|
691
|
+
"button",
|
|
692
|
+
{
|
|
693
|
+
type: "button",
|
|
694
|
+
"data-testid": "check-blanks",
|
|
695
|
+
disabled: !allFilled || passed && !props.enableRetry,
|
|
696
|
+
onClick: check,
|
|
697
|
+
children: "Check"
|
|
698
|
+
}
|
|
699
|
+
) : null,
|
|
597
700
|
!hasBlanks ? /* @__PURE__ */ jsx3("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
598
701
|
submitted ? /* @__PURE__ */ jsx3("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
599
702
|
props.enableRetry && passed ? /* @__PURE__ */ jsx3("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
@@ -618,6 +721,7 @@ function parseZones(template) {
|
|
|
618
721
|
function DragTheWordsInner(props, ref) {
|
|
619
722
|
const checkId = useMemo4(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
620
723
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
724
|
+
const { config } = useLessonkit();
|
|
621
725
|
const { parts, answers } = useMemo4(() => parseZones(props.template), [props.template]);
|
|
622
726
|
const [zones, setZones] = useState4(
|
|
623
727
|
() => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
|
|
@@ -722,10 +826,12 @@ function DragTheWordsInner(props, ref) {
|
|
|
722
826
|
answers.forEach((ans, i) => {
|
|
723
827
|
if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
|
|
724
828
|
});
|
|
725
|
-
|
|
829
|
+
if (config.tracking?.replayResumeEvents === true) {
|
|
830
|
+
replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
|
|
831
|
+
}
|
|
726
832
|
}
|
|
727
833
|
}),
|
|
728
|
-
[allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
|
|
834
|
+
[allFilled, answers, assessment, checkId, config.tracking?.replayResumeEvents, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
|
|
729
835
|
);
|
|
730
836
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
731
837
|
const placeInZone = (zoneId, word) => {
|
|
@@ -768,9 +874,9 @@ function DragTheWordsInner(props, ref) {
|
|
|
768
874
|
response: zones,
|
|
769
875
|
correct: passedThreshold
|
|
770
876
|
});
|
|
771
|
-
if (passedThreshold && !completedRef.current) {
|
|
877
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
772
878
|
completedRef.current = true;
|
|
773
|
-
setPassed(true);
|
|
879
|
+
if (passedThreshold) setPassed(true);
|
|
774
880
|
assessment.complete({
|
|
775
881
|
checkId,
|
|
776
882
|
interactionType: INTERACTION4,
|
|
@@ -832,7 +938,16 @@ function DragTheWordsInner(props, ref) {
|
|
|
832
938
|
part
|
|
833
939
|
);
|
|
834
940
|
}) }),
|
|
835
|
-
/* @__PURE__ */ jsx4(
|
|
941
|
+
/* @__PURE__ */ jsx4(
|
|
942
|
+
"button",
|
|
943
|
+
{
|
|
944
|
+
type: "button",
|
|
945
|
+
"data-testid": "check-drag-words",
|
|
946
|
+
disabled: !allFilled || passed && !props.enableRetry,
|
|
947
|
+
onClick: check,
|
|
948
|
+
children: "Check"
|
|
949
|
+
}
|
|
950
|
+
),
|
|
836
951
|
!hasZones ? /* @__PURE__ */ jsx4("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
837
952
|
submitted ? /* @__PURE__ */ jsx4("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
838
953
|
] });
|
|
@@ -846,6 +961,37 @@ var DragTheWords = forwardRef4(function DragTheWords2(props, ref) {
|
|
|
846
961
|
import { forwardRef as forwardRef5, useEffect as useEffect5, useMemo as useMemo5, useRef as useRef5, useState as useState5 } from "react";
|
|
847
962
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
848
963
|
var INTERACTION5 = "dragAndDrop";
|
|
964
|
+
function normalizeDragAndDropState(rawAssignments, rawPool, items, targets) {
|
|
965
|
+
const itemIds = new Set(items.map((i) => i.id));
|
|
966
|
+
const targetIds = targets.map((t) => t.id);
|
|
967
|
+
const assignments = Object.fromEntries(targetIds.map((id) => [id, ""]));
|
|
968
|
+
if (rawAssignments && typeof rawAssignments === "object") {
|
|
969
|
+
for (const targetId of targetIds) {
|
|
970
|
+
const value = rawAssignments[targetId];
|
|
971
|
+
if (typeof value === "string" && (value === "" || itemIds.has(value))) {
|
|
972
|
+
assignments[targetId] = value;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const assigned = new Set(Object.values(assignments).filter(Boolean));
|
|
977
|
+
const pool = [];
|
|
978
|
+
const seen = /* @__PURE__ */ new Set();
|
|
979
|
+
if (Array.isArray(rawPool)) {
|
|
980
|
+
for (const id of rawPool) {
|
|
981
|
+
if (typeof id === "string" && itemIds.has(id) && !assigned.has(id) && !seen.has(id)) {
|
|
982
|
+
pool.push(id);
|
|
983
|
+
seen.add(id);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
for (const item of items) {
|
|
988
|
+
if (!assigned.has(item.id) && !seen.has(item.id)) {
|
|
989
|
+
pool.push(item.id);
|
|
990
|
+
seen.add(item.id);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return { assignments, pool };
|
|
994
|
+
}
|
|
849
995
|
function DragAndDropInner(props, ref) {
|
|
850
996
|
const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
851
997
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
@@ -895,11 +1041,14 @@ function DragAndDropInner(props, ref) {
|
|
|
895
1041
|
}),
|
|
896
1042
|
getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
|
|
897
1043
|
resume: (state) => {
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1044
|
+
const normalized = normalizeDragAndDropState(
|
|
1045
|
+
state.assignments,
|
|
1046
|
+
state.pool,
|
|
1047
|
+
props.items,
|
|
1048
|
+
props.targets
|
|
1049
|
+
);
|
|
1050
|
+
setAssignments(normalized.assignments);
|
|
1051
|
+
setPool(normalized.pool);
|
|
903
1052
|
readBooleanStateField(state, "passed", (value) => {
|
|
904
1053
|
setPassed(value);
|
|
905
1054
|
completedRef.current = value;
|
|
@@ -932,9 +1081,9 @@ function DragAndDropInner(props, ref) {
|
|
|
932
1081
|
response: assignments,
|
|
933
1082
|
correct: passedThreshold
|
|
934
1083
|
});
|
|
935
|
-
if (passedThreshold && !completedRef.current) {
|
|
1084
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
936
1085
|
completedRef.current = true;
|
|
937
|
-
setPassed(true);
|
|
1086
|
+
if (passedThreshold) setPassed(true);
|
|
938
1087
|
assessment.complete({
|
|
939
1088
|
checkId,
|
|
940
1089
|
interactionType: INTERACTION5,
|
|
@@ -997,7 +1146,16 @@ function DragAndDropInner(props, ref) {
|
|
|
997
1146
|
)
|
|
998
1147
|
] }, target.id);
|
|
999
1148
|
}) }),
|
|
1000
|
-
/* @__PURE__ */ jsx5(
|
|
1149
|
+
/* @__PURE__ */ jsx5(
|
|
1150
|
+
"button",
|
|
1151
|
+
{
|
|
1152
|
+
type: "button",
|
|
1153
|
+
"data-testid": "check-drag-drop",
|
|
1154
|
+
disabled: !hasTargets || !allFilled || passed && !props.enableRetry,
|
|
1155
|
+
onClick: check,
|
|
1156
|
+
children: "Check"
|
|
1157
|
+
}
|
|
1158
|
+
),
|
|
1001
1159
|
checked ? /* @__PURE__ */ jsx5("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
1002
1160
|
] });
|
|
1003
1161
|
}
|
|
@@ -1007,7 +1165,7 @@ var DragAndDrop = forwardRef5(function DragAndDrop2(props, ref) {
|
|
|
1007
1165
|
});
|
|
1008
1166
|
|
|
1009
1167
|
// src/blocks/AssessmentSequence.tsx
|
|
1010
|
-
import React7, { forwardRef as forwardRef6, useCallback as
|
|
1168
|
+
import React7, { forwardRef as forwardRef6, useCallback as useCallback5, useEffect as useEffect8, useId, useMemo as useMemo7, useRef as useRef8, useState as useState6 } from "react";
|
|
1011
1169
|
import { deriveId } from "@lessonkit/core";
|
|
1012
1170
|
|
|
1013
1171
|
// src/compound/useCompoundShell.ts
|
|
@@ -1015,13 +1173,13 @@ import { useMemo as useMemo6 } from "react";
|
|
|
1015
1173
|
import { clampCompoundPageIndex as clampCompoundPageIndex2 } from "@lessonkit/core";
|
|
1016
1174
|
|
|
1017
1175
|
// src/compound/useCompoundNavigation.ts
|
|
1018
|
-
import { useCallback } from "react";
|
|
1176
|
+
import { useCallback as useCallback2 } from "react";
|
|
1019
1177
|
function useCompoundNavigation(pageCount, index, setIndex) {
|
|
1020
|
-
const goNext =
|
|
1178
|
+
const goNext = useCallback2(() => {
|
|
1021
1179
|
if (pageCount < 1) return;
|
|
1022
1180
|
setIndex((i) => Math.min(i + 1, pageCount - 1));
|
|
1023
1181
|
}, [pageCount, setIndex]);
|
|
1024
|
-
const goPrev =
|
|
1182
|
+
const goPrev = useCallback2(() => {
|
|
1025
1183
|
setIndex((i) => Math.max(i - 1, 0));
|
|
1026
1184
|
}, [setIndex]);
|
|
1027
1185
|
const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
|
|
@@ -1035,34 +1193,138 @@ function useCompoundNavigation(pageCount, index, setIndex) {
|
|
|
1035
1193
|
}
|
|
1036
1194
|
|
|
1037
1195
|
// src/compound/useCompoundPersistence.ts
|
|
1038
|
-
import { useCallback as
|
|
1196
|
+
import { useCallback as useCallback4, useContext as useContext2, useEffect as useEffect7, useLayoutEffect, useRef as useRef7 } from "react";
|
|
1039
1197
|
import {
|
|
1040
1198
|
clampCompoundPageIndex,
|
|
1041
|
-
createCompoundResumeState,
|
|
1199
|
+
createCompoundResumeState as createCompoundResumeState3,
|
|
1042
1200
|
createSessionStoragePort as createSessionStoragePort2,
|
|
1043
1201
|
loadCompoundState as loadCompoundState2
|
|
1044
1202
|
} from "@lessonkit/core";
|
|
1045
1203
|
|
|
1204
|
+
// src/compound/compoundHydration.ts
|
|
1205
|
+
var hydratedKeys = /* @__PURE__ */ new Set();
|
|
1206
|
+
function compoundHydrationKey(courseId, compoundId) {
|
|
1207
|
+
return `${courseId}:${compoundId}`;
|
|
1208
|
+
}
|
|
1209
|
+
function markCompoundHydrated(key) {
|
|
1210
|
+
hydratedKeys.add(key);
|
|
1211
|
+
}
|
|
1212
|
+
function isCompoundHydrated(key) {
|
|
1213
|
+
return hydratedKeys.has(key);
|
|
1214
|
+
}
|
|
1215
|
+
function clearCompoundHydrated(key) {
|
|
1216
|
+
hydratedKeys.delete(key);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/compound/useCompoundBranchShell.ts
|
|
1220
|
+
import "@lessonkit/core";
|
|
1221
|
+
var BS_META_KEY = "__lk_bs__";
|
|
1222
|
+
function createInitialBranchMeta(startNodeId) {
|
|
1223
|
+
return { activeNodeId: startNodeId, visitedNodeIds: [startNodeId] };
|
|
1224
|
+
}
|
|
1225
|
+
function readBranchingScenarioMeta(childStates) {
|
|
1226
|
+
const raw = childStates[BS_META_KEY];
|
|
1227
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1228
|
+
const activeNodeId = typeof raw.activeNodeId === "string" ? raw.activeNodeId : "";
|
|
1229
|
+
const visitedNodeIds = Array.isArray(raw.visitedNodeIds) ? raw.visitedNodeIds.filter((id) => typeof id === "string") : [];
|
|
1230
|
+
const choiceScores = raw.choiceScores && typeof raw.choiceScores === "object" && !Array.isArray(raw.choiceScores) ? raw.choiceScores : void 0;
|
|
1231
|
+
if (!activeNodeId) return null;
|
|
1232
|
+
return { activeNodeId, visitedNodeIds, choiceScores };
|
|
1233
|
+
}
|
|
1234
|
+
function sanitizeBranchMeta(meta, nodeIndexMap, startNodeId, validChoiceKeys) {
|
|
1235
|
+
const knownIds = new Set(nodeIndexMap.keys());
|
|
1236
|
+
const activeNodeId = knownIds.has(meta.activeNodeId) ? meta.activeNodeId : startNodeId;
|
|
1237
|
+
const visitedNodeIds = meta.visitedNodeIds.filter((id) => knownIds.has(id));
|
|
1238
|
+
if (!visitedNodeIds.includes(activeNodeId)) {
|
|
1239
|
+
visitedNodeIds.push(activeNodeId);
|
|
1240
|
+
}
|
|
1241
|
+
if (visitedNodeIds.length === 0) {
|
|
1242
|
+
visitedNodeIds.push(startNodeId);
|
|
1243
|
+
}
|
|
1244
|
+
const choiceScores = meta.choiceScores ? Object.fromEntries(
|
|
1245
|
+
Object.entries(meta.choiceScores).filter(
|
|
1246
|
+
([key]) => validChoiceKeys ? validChoiceKeys.has(key) : knownIds.has(key.split(":")[0] ?? "")
|
|
1247
|
+
)
|
|
1248
|
+
) : void 0;
|
|
1249
|
+
return {
|
|
1250
|
+
activeNodeId,
|
|
1251
|
+
visitedNodeIds,
|
|
1252
|
+
...Object.keys(choiceScores ?? {}).length > 0 ? { choiceScores } : {}
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function mergeBranchMetaIntoState(state, meta) {
|
|
1256
|
+
return {
|
|
1257
|
+
...state,
|
|
1258
|
+
childStates: {
|
|
1259
|
+
...state.childStates,
|
|
1260
|
+
[BS_META_KEY]: meta
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function choiceScoreKey(fromNodeId, toNodeId) {
|
|
1265
|
+
return `${fromNodeId}:${toNodeId}`;
|
|
1266
|
+
}
|
|
1267
|
+
function applyChoiceScoreUpdate(prev, fromNodeId, toNodeId, scoreWeight) {
|
|
1268
|
+
if (scoreWeight === void 0) return prev;
|
|
1269
|
+
const next = { ...prev ?? {} };
|
|
1270
|
+
for (const key of Object.keys(next)) {
|
|
1271
|
+
if (key.startsWith(`${fromNodeId}:`)) {
|
|
1272
|
+
delete next[key];
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
next[choiceScoreKey(fromNodeId, toNodeId)] = scoreWeight;
|
|
1276
|
+
return next;
|
|
1277
|
+
}
|
|
1278
|
+
function sumChoiceScores(choiceScores) {
|
|
1279
|
+
if (!choiceScores) return 0;
|
|
1280
|
+
return Object.values(choiceScores).reduce((sum, value) => sum + (Number.isFinite(value) ? value : 0), 0);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/compound/useCompoundVideoShell.ts
|
|
1284
|
+
import "@lessonkit/core";
|
|
1285
|
+
var IV_META_KEY = "__lk_iv__";
|
|
1286
|
+
function readInteractiveVideoMeta(childStates) {
|
|
1287
|
+
const raw = childStates[IV_META_KEY];
|
|
1288
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1289
|
+
const currentTime = typeof raw.currentTime === "number" ? raw.currentTime : 0;
|
|
1290
|
+
const completedCueIndices = Array.isArray(raw.completedCueIndices) ? raw.completedCueIndices.filter((n) => typeof n === "number") : [];
|
|
1291
|
+
const firedCueIndices = Array.isArray(raw.firedCueIndices) ? raw.firedCueIndices.filter((n) => typeof n === "number") : completedCueIndices;
|
|
1292
|
+
return { currentTime, completedCueIndices, firedCueIndices };
|
|
1293
|
+
}
|
|
1294
|
+
function mergeVideoMetaIntoState(state, meta) {
|
|
1295
|
+
return {
|
|
1296
|
+
...state,
|
|
1297
|
+
childStates: {
|
|
1298
|
+
...state.childStates,
|
|
1299
|
+
[IV_META_KEY]: meta
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1046
1304
|
// src/compound/resumeChildHandles.ts
|
|
1047
|
-
|
|
1305
|
+
var DEFAULT_PRESERVED_CHILD_STATE_KEYS = /* @__PURE__ */ new Set([BS_META_KEY, IV_META_KEY]);
|
|
1306
|
+
function registerablePendingKeys(childStates, preserveKeys = DEFAULT_PRESERVED_CHILD_STATE_KEYS) {
|
|
1307
|
+
return Object.keys(childStates).filter((key) => !preserveKeys.has(key));
|
|
1308
|
+
}
|
|
1309
|
+
function filterRegisteredChildStates(handles, childStates, preserveKeys = DEFAULT_PRESERVED_CHILD_STATE_KEYS) {
|
|
1048
1310
|
const filtered = {};
|
|
1049
1311
|
for (const [key, value] of Object.entries(childStates)) {
|
|
1050
|
-
if (handles.has(key)) {
|
|
1312
|
+
if (preserveKeys.has(key) || handles.has(key)) {
|
|
1051
1313
|
filtered[key] = value;
|
|
1052
1314
|
}
|
|
1053
1315
|
}
|
|
1054
1316
|
return filtered;
|
|
1055
1317
|
}
|
|
1056
1318
|
function resumeChildHandles(handles, childStates, opts) {
|
|
1057
|
-
const pendingKeys = Object.keys(childStates);
|
|
1058
1319
|
const alreadyResumed = opts?.alreadyResumed;
|
|
1059
|
-
|
|
1320
|
+
const registerableKeys = registerablePendingKeys(childStates);
|
|
1321
|
+
if (opts?.waitForHandles && registerableKeys.length > 0) {
|
|
1060
1322
|
if (handles.size === 0) return false;
|
|
1061
|
-
const registeredPending =
|
|
1323
|
+
const registeredPending = registerableKeys.filter((k) => handles.has(k));
|
|
1062
1324
|
if (registeredPending.length === 0) {
|
|
1063
1325
|
return false;
|
|
1064
1326
|
}
|
|
1065
|
-
if (registeredPending.length <
|
|
1327
|
+
if (registeredPending.length < registerableKeys.length) {
|
|
1066
1328
|
for (const key of registeredPending) {
|
|
1067
1329
|
if (alreadyResumed?.has(key)) continue;
|
|
1068
1330
|
const handle = handles.get(key);
|
|
@@ -1087,15 +1349,27 @@ function resumeChildHandles(handles, childStates, opts) {
|
|
|
1087
1349
|
}
|
|
1088
1350
|
|
|
1089
1351
|
// src/compound/useCompoundResume.ts
|
|
1090
|
-
import { useCallback as
|
|
1352
|
+
import { useCallback as useCallback3, useContext, useEffect as useEffect6, useRef as useRef6 } from "react";
|
|
1091
1353
|
import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
|
|
1092
1354
|
import { createSessionStoragePort } from "@lessonkit/core";
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1355
|
+
|
|
1356
|
+
// src/compound/compoundLoadOpts.ts
|
|
1357
|
+
function compoundLoadOpts(ctx, compoundId) {
|
|
1358
|
+
const onCorruptHook = ctx?.config?.observability?.onCompoundResumeCorrupt;
|
|
1359
|
+
if (!onCorruptHook) return void 0;
|
|
1360
|
+
return {
|
|
1361
|
+
onCorrupt: () => onCorruptHook({ compoundId, corrupt: true }),
|
|
1362
|
+
onDroppedChildKeys: (droppedChildKeys) => onCorruptHook({ compoundId, droppedChildKeys })
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/compound/useCompoundResume.ts
|
|
1367
|
+
var warnedCompoundPersistFailures = /* @__PURE__ */ new Set();
|
|
1368
|
+
function warnCompoundPersistFailure(compoundId) {
|
|
1369
|
+
if (warnedCompoundPersistFailures.has(compoundId) || !isDevEnvironment()) return;
|
|
1370
|
+
warnedCompoundPersistFailures.add(compoundId);
|
|
1097
1371
|
console.warn(
|
|
1098
|
-
|
|
1372
|
+
`[lessonkit] compound resume state for "${compoundId}" could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload.`
|
|
1099
1373
|
);
|
|
1100
1374
|
}
|
|
1101
1375
|
function useCompoundResume(opts) {
|
|
@@ -1118,23 +1392,37 @@ function useCompoundResume(opts) {
|
|
|
1118
1392
|
resumedRef.current = false;
|
|
1119
1393
|
}
|
|
1120
1394
|
if (!opts.enabled || !opts.courseId || resumedRef.current) return;
|
|
1121
|
-
|
|
1395
|
+
if (isCompoundHydrated(key)) {
|
|
1396
|
+
resumedRef.current = true;
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const saved = loadCompoundState(
|
|
1400
|
+
storageRef.current,
|
|
1401
|
+
opts.courseId,
|
|
1402
|
+
opts.compoundId,
|
|
1403
|
+
compoundLoadOpts(lessonkitCtx, opts.compoundId)
|
|
1404
|
+
);
|
|
1122
1405
|
if (saved) {
|
|
1123
1406
|
resumedRef.current = true;
|
|
1124
1407
|
opts.onResume?.(saved);
|
|
1125
1408
|
}
|
|
1126
1409
|
}, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
|
|
1127
|
-
return
|
|
1410
|
+
return useCallback3(
|
|
1128
1411
|
(state) => {
|
|
1129
1412
|
if (!opts.enabled || !opts.courseId) return;
|
|
1130
1413
|
const persisted = saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
1131
|
-
if (!persisted) warnCompoundPersistFailure();
|
|
1414
|
+
if (!persisted) warnCompoundPersistFailure(opts.compoundId);
|
|
1132
1415
|
},
|
|
1133
1416
|
[opts.enabled, opts.courseId, opts.compoundId]
|
|
1134
1417
|
);
|
|
1135
1418
|
}
|
|
1136
1419
|
|
|
1137
1420
|
// src/compound/useCompoundPersistence.ts
|
|
1421
|
+
var MAX_HYDRATION_RETRIES = 10;
|
|
1422
|
+
function isEmptyResumeState(state) {
|
|
1423
|
+
if (!state || typeof state !== "object") return true;
|
|
1424
|
+
return Object.keys(state).length === 0;
|
|
1425
|
+
}
|
|
1138
1426
|
function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = createSessionStoragePort2()) {
|
|
1139
1427
|
if (!enabled || !courseId || pageCount < 1) return 0;
|
|
1140
1428
|
const saved = loadCompoundState2(storage, courseId, compoundId);
|
|
@@ -1154,51 +1442,60 @@ function useCompoundPersistence(opts) {
|
|
|
1154
1442
|
const resumedChildKeysRef = useRef7(/* @__PURE__ */ new Set());
|
|
1155
1443
|
const loadedChildStatesRef = useRef7({});
|
|
1156
1444
|
const skipSaveUntilHydratedRef = useRef7(false);
|
|
1445
|
+
const postResumePersistPendingRef = useRef7(false);
|
|
1157
1446
|
const hydrationKeyRef = useRef7("");
|
|
1158
|
-
const
|
|
1447
|
+
const hydrationRetryRef = useRef7(0);
|
|
1159
1448
|
const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
1160
1449
|
if (hydrationKeyRef.current !== hydrationKey) {
|
|
1450
|
+
if (hydrationKeyRef.current) {
|
|
1451
|
+
clearCompoundHydrated(hydrationKeyRef.current);
|
|
1452
|
+
}
|
|
1161
1453
|
hydrationKeyRef.current = hydrationKey;
|
|
1162
|
-
hydrationInitRef.current = false;
|
|
1163
1454
|
loadedChildStatesRef.current = {};
|
|
1164
1455
|
skipSaveUntilHydratedRef.current = false;
|
|
1456
|
+
postResumePersistPendingRef.current = false;
|
|
1165
1457
|
pendingChildResumeRef.current = null;
|
|
1166
1458
|
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
1459
|
+
hydrationRetryRef.current = 0;
|
|
1167
1460
|
}
|
|
1168
|
-
|
|
1169
|
-
hydrationInitRef.current = true;
|
|
1170
|
-
const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
|
|
1171
|
-
if (saved && Object.keys(saved.childStates).length > 0) {
|
|
1172
|
-
loadedChildStatesRef.current = { ...saved.childStates };
|
|
1173
|
-
skipSaveUntilHydratedRef.current = true;
|
|
1174
|
-
pendingChildResumeRef.current = saved;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
const buildState = useCallback3(() => {
|
|
1461
|
+
const buildState = useCallback4(() => {
|
|
1178
1462
|
const childStates = {
|
|
1179
1463
|
...loadedChildStatesRef.current
|
|
1180
1464
|
};
|
|
1181
1465
|
if (ctx) {
|
|
1182
1466
|
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
1467
|
+
if (opts.shouldIncludeChildState && !opts.shouldIncludeChildState(checkId, entry.pageIndex)) {
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1183
1470
|
const handle = entry.handle;
|
|
1184
1471
|
if (handle.getCurrentState) {
|
|
1185
|
-
|
|
1186
|
-
|
|
1472
|
+
const live = handle.getCurrentState();
|
|
1473
|
+
const loaded = loadedChildStatesRef.current[checkId];
|
|
1474
|
+
if (loaded !== void 0 && isEmptyResumeState(live)) {
|
|
1475
|
+
childStates[checkId] = loaded;
|
|
1476
|
+
} else {
|
|
1477
|
+
childStates[checkId] = live;
|
|
1478
|
+
if (!isEmptyResumeState(live)) {
|
|
1479
|
+
delete loadedChildStatesRef.current[checkId];
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1187
1482
|
}
|
|
1188
1483
|
}
|
|
1189
1484
|
}
|
|
1190
|
-
return
|
|
1485
|
+
return createCompoundResumeState3({
|
|
1191
1486
|
activePageIndex: clampCompoundPageIndex(opts.index, opts.pageCount),
|
|
1192
1487
|
childStates
|
|
1193
1488
|
});
|
|
1194
|
-
}, [ctx, opts.index, opts.pageCount]);
|
|
1489
|
+
}, [ctx, opts.index, opts.pageCount, opts.shouldIncludeChildState]);
|
|
1195
1490
|
const buildStateRef = useRef7(buildState);
|
|
1196
1491
|
buildStateRef.current = buildState;
|
|
1197
1492
|
const transformStateRef = useRef7(opts.transformState);
|
|
1198
1493
|
transformStateRef.current = opts.transformState;
|
|
1494
|
+
const onCompoundResumeRef = useRef7(opts.onCompoundResume);
|
|
1495
|
+
onCompoundResumeRef.current = opts.onCompoundResume;
|
|
1199
1496
|
const persistNowRef = useRef7(() => {
|
|
1200
1497
|
});
|
|
1201
|
-
const finalizeHydration =
|
|
1498
|
+
const finalizeHydration = useCallback4(
|
|
1202
1499
|
(childStates) => {
|
|
1203
1500
|
loadedChildStatesRef.current = {
|
|
1204
1501
|
...loadedChildStatesRef.current,
|
|
@@ -1206,11 +1503,20 @@ function useCompoundPersistence(opts) {
|
|
|
1206
1503
|
};
|
|
1207
1504
|
skipSaveUntilHydratedRef.current = false;
|
|
1208
1505
|
pendingChildResumeRef.current = null;
|
|
1209
|
-
|
|
1506
|
+
hydrationRetryRef.current = 0;
|
|
1507
|
+
postResumePersistPendingRef.current = true;
|
|
1508
|
+
requestAnimationFrame(() => {
|
|
1509
|
+
requestAnimationFrame(() => {
|
|
1510
|
+
postResumePersistPendingRef.current = false;
|
|
1511
|
+
persistNowRef.current();
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1210
1514
|
},
|
|
1211
1515
|
[]
|
|
1212
1516
|
);
|
|
1213
|
-
const
|
|
1517
|
+
const applyPendingChildResumeRef = useRef7(() => {
|
|
1518
|
+
});
|
|
1519
|
+
const applyPendingChildResume = useCallback4(() => {
|
|
1214
1520
|
const pending = pendingChildResumeRef.current;
|
|
1215
1521
|
if (!pending || !ctx) return;
|
|
1216
1522
|
const handles = ctx.getHandles();
|
|
@@ -1219,30 +1525,57 @@ function useCompoundPersistence(opts) {
|
|
|
1219
1525
|
alreadyResumed: resumedChildKeysRef.current
|
|
1220
1526
|
});
|
|
1221
1527
|
if (!applied) {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
finalizeHydration(registeredOnly2);
|
|
1528
|
+
const registerable = registerablePendingKeys(pending.childStates);
|
|
1529
|
+
const missing = registerable.filter((k) => !handles.has(k));
|
|
1530
|
+
if (missing.length > 0 && hydrationRetryRef.current < MAX_HYDRATION_RETRIES) {
|
|
1531
|
+
hydrationRetryRef.current += 1;
|
|
1532
|
+
requestAnimationFrame(() => applyPendingChildResumeRef.current());
|
|
1228
1533
|
return;
|
|
1229
1534
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1535
|
+
if (missing.length > 0 && isDevEnvironment()) {
|
|
1536
|
+
console.warn(
|
|
1537
|
+
`[lessonkit] Compound hydration: ${missing.length} child state(s) not restored (missing handles: ${missing.join(", ")})`
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
lessonkitCtx?.config?.observability?.onCompoundHydrationPartial?.({
|
|
1541
|
+
compoundId: opts.compoundId,
|
|
1542
|
+
missingCheckIds: missing
|
|
1543
|
+
});
|
|
1544
|
+
for (const key of missing) {
|
|
1545
|
+
const state = pending.childStates[key];
|
|
1546
|
+
if (state) {
|
|
1547
|
+
loadedChildStatesRef.current[key] = state;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
|
|
1551
|
+
resumeChildHandles(handles, registeredOnly2, {
|
|
1552
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
1553
|
+
});
|
|
1554
|
+
finalizeHydration({
|
|
1555
|
+
...loadedChildStatesRef.current,
|
|
1556
|
+
...registeredOnly2
|
|
1240
1557
|
});
|
|
1241
1558
|
return;
|
|
1242
1559
|
}
|
|
1243
1560
|
const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
|
|
1244
1561
|
finalizeHydration(registeredOnly);
|
|
1245
1562
|
}, [ctx, finalizeHydration]);
|
|
1563
|
+
applyPendingChildResumeRef.current = applyPendingChildResume;
|
|
1564
|
+
useLayoutEffect(() => {
|
|
1565
|
+
if (!opts.enabled || !opts.courseId) return;
|
|
1566
|
+
markCompoundHydrated(compoundHydrationKey(opts.courseId, opts.compoundId));
|
|
1567
|
+
const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
|
|
1568
|
+
if (!saved) return;
|
|
1569
|
+
if (Object.keys(saved.childStates).length > 0) {
|
|
1570
|
+
loadedChildStatesRef.current = { ...saved.childStates };
|
|
1571
|
+
skipSaveUntilHydratedRef.current = true;
|
|
1572
|
+
pendingChildResumeRef.current = saved;
|
|
1573
|
+
}
|
|
1574
|
+
const clamped = clampCompoundPageIndex(saved.activePageIndex, opts.pageCount);
|
|
1575
|
+
onCompoundResumeRef.current?.({ ...saved, activePageIndex: clamped });
|
|
1576
|
+
opts.setIndex(clamped);
|
|
1577
|
+
queueMicrotask(() => applyPendingChildResumeRef.current());
|
|
1578
|
+
}, [hydrationKey, opts.courseId, opts.compoundId, opts.enabled, opts.pageCount, storage]);
|
|
1246
1579
|
const saveResume = useCompoundResume({
|
|
1247
1580
|
courseId: opts.courseId,
|
|
1248
1581
|
compoundId: opts.compoundId,
|
|
@@ -1252,29 +1585,65 @@ function useCompoundPersistence(opts) {
|
|
|
1252
1585
|
const clamped = clampCompoundPageIndex(state.activePageIndex, opts.pageCount);
|
|
1253
1586
|
loadedChildStatesRef.current = { ...state.childStates };
|
|
1254
1587
|
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
1588
|
+
onCompoundResumeRef.current?.({ ...state, activePageIndex: clamped });
|
|
1255
1589
|
opts.setIndex(clamped);
|
|
1256
1590
|
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
1591
|
+
hydrationRetryRef.current = 0;
|
|
1257
1592
|
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
1258
1593
|
queueMicrotask(() => applyPendingChildResume());
|
|
1259
1594
|
}
|
|
1260
1595
|
});
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1596
|
+
const buildBestEffortState = useCallback4(() => {
|
|
1597
|
+
const childStates = {
|
|
1598
|
+
...loadedChildStatesRef.current
|
|
1599
|
+
};
|
|
1600
|
+
if (ctx) {
|
|
1601
|
+
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
1602
|
+
if (opts.shouldIncludeChildState && !opts.shouldIncludeChildState(checkId, entry.pageIndex)) {
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const handle = entry.handle;
|
|
1606
|
+
if (handle.getCurrentState) {
|
|
1607
|
+
const live = handle.getCurrentState();
|
|
1608
|
+
if (!isEmptyResumeState(live)) {
|
|
1609
|
+
childStates[checkId] = live;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const built = createCompoundResumeState3({
|
|
1615
|
+
activePageIndex: clampCompoundPageIndex(opts.index, opts.pageCount),
|
|
1616
|
+
childStates
|
|
1617
|
+
});
|
|
1618
|
+
return transformStateRef.current ? transformStateRef.current(built) : built;
|
|
1619
|
+
}, [ctx, opts.index, opts.pageCount, opts.shouldIncludeChildState]);
|
|
1620
|
+
const persistNow = useCallback4(
|
|
1621
|
+
(options) => {
|
|
1622
|
+
if (!opts.enabled || !opts.courseId) return;
|
|
1623
|
+
if (options?.forceDuringHydration) {
|
|
1624
|
+
saveResume(buildBestEffortState());
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (skipSaveUntilHydratedRef.current) return;
|
|
1628
|
+
if (postResumePersistPendingRef.current) return;
|
|
1629
|
+
const built = buildStateRef.current();
|
|
1630
|
+
const state = transformStateRef.current ? transformStateRef.current(built) : built;
|
|
1631
|
+
saveResume(state);
|
|
1632
|
+
},
|
|
1633
|
+
[opts.enabled, opts.courseId, saveResume, buildBestEffortState]
|
|
1634
|
+
);
|
|
1268
1635
|
useEffect7(() => {
|
|
1269
1636
|
persistNowRef.current = persistNow;
|
|
1270
1637
|
}, [persistNow]);
|
|
1271
|
-
const notifyImperativeResume =
|
|
1638
|
+
const notifyImperativeResume = useCallback4(
|
|
1272
1639
|
(state) => {
|
|
1273
1640
|
const clamped = clampCompoundPageIndex(state.activePageIndex, opts.pageCount);
|
|
1274
1641
|
loadedChildStatesRef.current = { ...state.childStates };
|
|
1275
1642
|
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
1643
|
+
onCompoundResumeRef.current?.({ ...state, activePageIndex: clamped });
|
|
1276
1644
|
opts.setIndex(clamped);
|
|
1277
1645
|
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
1646
|
+
hydrationRetryRef.current = 0;
|
|
1278
1647
|
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
1279
1648
|
queueMicrotask(() => applyPendingChildResume());
|
|
1280
1649
|
},
|
|
@@ -1294,17 +1663,22 @@ function useCompoundPersistence(opts) {
|
|
|
1294
1663
|
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
1295
1664
|
useEffect7(() => {
|
|
1296
1665
|
persistNow();
|
|
1297
|
-
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
1666
|
+
}, [persistNow, opts.index, opts.pageCount, handlesVersion, opts.persistTrigger]);
|
|
1298
1667
|
useEffect7(() => {
|
|
1299
1668
|
if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
|
|
1300
1669
|
const flushOnExit = () => {
|
|
1301
|
-
if (document.visibilityState === "hidden")
|
|
1670
|
+
if (document.visibilityState === "hidden") {
|
|
1671
|
+
persistNow({ forceDuringHydration: skipSaveUntilHydratedRef.current });
|
|
1672
|
+
}
|
|
1302
1673
|
};
|
|
1303
1674
|
document.addEventListener("visibilitychange", flushOnExit);
|
|
1304
|
-
|
|
1675
|
+
const flushOnPageHide = () => {
|
|
1676
|
+
persistNow({ forceDuringHydration: skipSaveUntilHydratedRef.current });
|
|
1677
|
+
};
|
|
1678
|
+
window.addEventListener("pagehide", flushOnPageHide);
|
|
1305
1679
|
return () => {
|
|
1306
1680
|
document.removeEventListener("visibilitychange", flushOnExit);
|
|
1307
|
-
window.removeEventListener("pagehide",
|
|
1681
|
+
window.removeEventListener("pagehide", flushOnPageHide);
|
|
1308
1682
|
};
|
|
1309
1683
|
}, [opts.enabled, opts.courseId, persistNow]);
|
|
1310
1684
|
}
|
|
@@ -1320,7 +1694,9 @@ function useCompoundShell(opts) {
|
|
|
1320
1694
|
setIndex: opts.setIndex,
|
|
1321
1695
|
enabled: opts.persistEnabled,
|
|
1322
1696
|
storage: opts.storage,
|
|
1323
|
-
transformState: opts.transformState
|
|
1697
|
+
transformState: opts.transformState,
|
|
1698
|
+
persistTrigger: opts.persistTrigger,
|
|
1699
|
+
onCompoundResume: opts.onCompoundResume
|
|
1324
1700
|
});
|
|
1325
1701
|
const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
|
|
1326
1702
|
const visibleIndex = clampCompoundPageIndex2(opts.index, opts.pageCount);
|
|
@@ -1347,13 +1723,19 @@ function useCompoundInitialIndex(opts) {
|
|
|
1347
1723
|
);
|
|
1348
1724
|
}
|
|
1349
1725
|
|
|
1350
|
-
// src/compound/
|
|
1351
|
-
var
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1726
|
+
// src/compound/requireCompoundBlockId.ts
|
|
1727
|
+
var MissingCompoundBlockIdError = class extends Error {
|
|
1728
|
+
constructor(componentName) {
|
|
1729
|
+
super(
|
|
1730
|
+
`[lessonkit] <${componentName}> requires a unique blockId when session.persistCompoundState is enabled`
|
|
1731
|
+
);
|
|
1732
|
+
this.name = "MissingCompoundBlockIdError";
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
function requireCompoundBlockIdWhenPersisting(opts) {
|
|
1736
|
+
if (opts.persistEnabled && !opts.blockId) {
|
|
1737
|
+
throw new MissingCompoundBlockIdError(opts.componentName);
|
|
1738
|
+
}
|
|
1357
1739
|
}
|
|
1358
1740
|
|
|
1359
1741
|
// src/blocks/AssessmentSequence.tsx
|
|
@@ -1362,7 +1744,10 @@ var AssessmentSequenceInner = forwardRef6(
|
|
|
1362
1744
|
function AssessmentSequenceInner2(props, ref) {
|
|
1363
1745
|
const { compoundId, childArray, index, setIndex, persistEnabled } = props;
|
|
1364
1746
|
const sequential = props.sequential !== false;
|
|
1747
|
+
const requireAnswerBeforeNext = props.requireAnswerBeforeNext !== false;
|
|
1365
1748
|
const { config } = useLessonkit();
|
|
1749
|
+
const registry = useCompoundRegistry();
|
|
1750
|
+
const handlesVersion = useCompoundHandlesVersion();
|
|
1366
1751
|
const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
|
|
1367
1752
|
courseId: config.courseId,
|
|
1368
1753
|
compoundId,
|
|
@@ -1374,6 +1759,21 @@ var AssessmentSequenceInner = forwardRef6(
|
|
|
1374
1759
|
enableSolutionsButton: props.enableSolutionsButton
|
|
1375
1760
|
});
|
|
1376
1761
|
validateCompoundChildren("AssessmentSequence", props.children);
|
|
1762
|
+
const activeStepAnswered = useMemo7(() => {
|
|
1763
|
+
if (!requireAnswerBeforeNext || !registry) return true;
|
|
1764
|
+
let handlesForStep = 0;
|
|
1765
|
+
for (const entry of registry.getRegisteredHandles().values()) {
|
|
1766
|
+
if (entry.pageIndex !== visibleIndex) continue;
|
|
1767
|
+
handlesForStep += 1;
|
|
1768
|
+
if (!entry.handle.getAnswerGiven()) return false;
|
|
1769
|
+
}
|
|
1770
|
+
if (handlesForStep === 0) {
|
|
1771
|
+
const child = childArray[visibleIndex];
|
|
1772
|
+
const childProps = child?.props;
|
|
1773
|
+
if (child && typeof childProps?.checkId === "string") return false;
|
|
1774
|
+
}
|
|
1775
|
+
return true;
|
|
1776
|
+
}, [childArray, handlesVersion, registry, requireAnswerBeforeNext, visibleIndex]);
|
|
1377
1777
|
if (!sequential) {
|
|
1378
1778
|
return /* @__PURE__ */ jsx6("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
|
|
1379
1779
|
}
|
|
@@ -1401,7 +1801,7 @@ var AssessmentSequenceInner = forwardRef6(
|
|
|
1401
1801
|
{
|
|
1402
1802
|
type: "button",
|
|
1403
1803
|
"data-testid": "sequence-next",
|
|
1404
|
-
disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
|
|
1804
|
+
disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0 || !activeStepAnswered,
|
|
1405
1805
|
onClick: goNext,
|
|
1406
1806
|
children: "Next"
|
|
1407
1807
|
}
|
|
@@ -1417,22 +1817,22 @@ var AssessmentSequence = forwardRef6(
|
|
|
1417
1817
|
if (!props.blockId && !autoCompoundIdRef.current) {
|
|
1418
1818
|
autoCompoundIdRef.current = deriveId(`assessment-sequence-${reactInstanceId}`);
|
|
1419
1819
|
}
|
|
1420
|
-
const compoundId = useMemo7(
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1820
|
+
const compoundId = useMemo7(() => {
|
|
1821
|
+
if (props.blockId) {
|
|
1822
|
+
return normalizeComponentId(props.blockId, "blockId");
|
|
1823
|
+
}
|
|
1824
|
+
return autoCompoundIdRef.current ?? deriveId(`assessment-sequence-${reactInstanceId}`);
|
|
1825
|
+
}, [props.blockId, reactInstanceId]);
|
|
1424
1826
|
const childArray = React7.Children.toArray(props.children).filter(
|
|
1425
1827
|
React7.isValidElement
|
|
1426
1828
|
);
|
|
1427
1829
|
const { config, storage } = useLessonkit();
|
|
1428
1830
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
});
|
|
1435
|
-
}, [persistEnabled, props.blockId]);
|
|
1831
|
+
requireCompoundBlockIdWhenPersisting({
|
|
1832
|
+
persistEnabled,
|
|
1833
|
+
blockId: props.blockId,
|
|
1834
|
+
componentName: "AssessmentSequence"
|
|
1835
|
+
});
|
|
1436
1836
|
const initialIndex = useCompoundInitialIndex({
|
|
1437
1837
|
courseId: config.courseId,
|
|
1438
1838
|
compoundId,
|
|
@@ -1441,7 +1841,7 @@ var AssessmentSequence = forwardRef6(
|
|
|
1441
1841
|
storage
|
|
1442
1842
|
});
|
|
1443
1843
|
const [index, setIndex] = useState6(initialIndex);
|
|
1444
|
-
const setIndexStable =
|
|
1844
|
+
const setIndexStable = useCallback5((i) => setIndex(i), []);
|
|
1445
1845
|
useEffect8(() => {
|
|
1446
1846
|
setIndex(initialIndex);
|
|
1447
1847
|
}, [config.courseId, compoundId, initialIndex]);
|
|
@@ -1477,14 +1877,180 @@ function Heading(props) {
|
|
|
1477
1877
|
}
|
|
1478
1878
|
setLessonkitBlockType(Heading, "Heading");
|
|
1479
1879
|
|
|
1880
|
+
// src/blocks/embedSecurity.ts
|
|
1881
|
+
var BLOCKED_SANDBOX_TOKENS = /* @__PURE__ */ new Set([
|
|
1882
|
+
"allow-top-navigation",
|
|
1883
|
+
"allow-top-navigation-by-user-activation",
|
|
1884
|
+
"allow-modals",
|
|
1885
|
+
"allow-downloads",
|
|
1886
|
+
"allow-popups-to-escape-sandbox"
|
|
1887
|
+
]);
|
|
1888
|
+
var ALLOWED_SANDBOX_TOKENS = /* @__PURE__ */ new Set([
|
|
1889
|
+
"allow-forms",
|
|
1890
|
+
"allow-popups",
|
|
1891
|
+
"allow-presentation"
|
|
1892
|
+
]);
|
|
1893
|
+
var DEFAULT_SANDBOX = "allow-scripts";
|
|
1894
|
+
function isProductionEmbedBuild() {
|
|
1895
|
+
try {
|
|
1896
|
+
if (import.meta.env?.PROD === true) return true;
|
|
1897
|
+
} catch {
|
|
1898
|
+
}
|
|
1899
|
+
const g = globalThis;
|
|
1900
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
1901
|
+
}
|
|
1902
|
+
function allowedEmbedSchemes() {
|
|
1903
|
+
return isProductionEmbedBuild() ? /* @__PURE__ */ new Set(["https:"]) : /* @__PURE__ */ new Set(["https:", "http:"]);
|
|
1904
|
+
}
|
|
1905
|
+
function normalizeHostname(hostname) {
|
|
1906
|
+
return hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
1907
|
+
}
|
|
1908
|
+
function expandIpv4Literal(hostname) {
|
|
1909
|
+
if (/^\d+$/.test(hostname)) {
|
|
1910
|
+
const value2 = Number(hostname);
|
|
1911
|
+
if (!Number.isInteger(value2) || value2 < 0 || value2 > 4294967295) return null;
|
|
1912
|
+
return `${value2 >>> 24 & 255}.${value2 >>> 16 & 255}.${value2 >>> 8 & 255}.${value2 & 255}`;
|
|
1913
|
+
}
|
|
1914
|
+
if (!/^\d+(?:\.\d+){1,3}$/.test(hostname)) return null;
|
|
1915
|
+
const parts = hostname.split(".").map((part) => Number(part));
|
|
1916
|
+
if (parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
|
|
1917
|
+
let value = 0;
|
|
1918
|
+
for (const part of parts) {
|
|
1919
|
+
value = value << 8 | part;
|
|
1920
|
+
}
|
|
1921
|
+
value <<= (4 - parts.length) * 8;
|
|
1922
|
+
return `${value >>> 24 & 255}.${value >>> 16 & 255}.${value >>> 8 & 255}.${value & 255}`;
|
|
1923
|
+
}
|
|
1924
|
+
function canonicalHostnameForBlocklist(hostname) {
|
|
1925
|
+
const normalized = normalizeHostname(hostname);
|
|
1926
|
+
return expandIpv4Literal(normalized) ?? normalized;
|
|
1927
|
+
}
|
|
1928
|
+
function isIpv4MappedAddress(hostname) {
|
|
1929
|
+
const match = hostname.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
1930
|
+
return match?.[1] ?? null;
|
|
1931
|
+
}
|
|
1932
|
+
function isLoopbackHost(hostname) {
|
|
1933
|
+
const ipv4Mapped = isIpv4MappedAddress(hostname);
|
|
1934
|
+
if (ipv4Mapped) return isLoopbackHost(ipv4Mapped);
|
|
1935
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname) || hostname === "::1" || hostname === "0.0.0.0";
|
|
1936
|
+
}
|
|
1937
|
+
function isIpv6UniqueLocalHost(hostname) {
|
|
1938
|
+
return /^f[cd][0-9a-f]{0,2}:/i.test(hostname);
|
|
1939
|
+
}
|
|
1940
|
+
function isLinkLocalOrMetadataHost(hostname) {
|
|
1941
|
+
if (hostname === "169.254.169.254") return true;
|
|
1942
|
+
if (/^169\.254\./.test(hostname)) return true;
|
|
1943
|
+
if (/^fe80:/i.test(hostname)) return true;
|
|
1944
|
+
return false;
|
|
1945
|
+
}
|
|
1946
|
+
function isRfc1918Host(hostname) {
|
|
1947
|
+
const ipv4Mapped = isIpv4MappedAddress(hostname);
|
|
1948
|
+
if (ipv4Mapped) return isRfc1918Host(ipv4Mapped);
|
|
1949
|
+
if (/^10\./.test(hostname)) return true;
|
|
1950
|
+
if (/^192\.168\./.test(hostname)) return true;
|
|
1951
|
+
const parts = hostname.split(".").map(Number);
|
|
1952
|
+
if (parts.length === 4 && parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
function isBlockedHost(hostname, allowedHosts) {
|
|
1956
|
+
const normalized = normalizeHostname(hostname);
|
|
1957
|
+
const canonical = canonicalHostnameForBlocklist(hostname);
|
|
1958
|
+
if (allowedHosts?.some((host) => {
|
|
1959
|
+
const allowedNormalized = normalizeHostname(host);
|
|
1960
|
+
return canonicalHostnameForBlocklist(host) === canonical || allowedNormalized === normalized;
|
|
1961
|
+
})) {
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
if (!isProductionEmbedBuild()) return false;
|
|
1965
|
+
return isLoopbackHost(canonical) || isLinkLocalOrMetadataHost(canonical) || isRfc1918Host(canonical) || isIpv6UniqueLocalHost(normalized);
|
|
1966
|
+
}
|
|
1967
|
+
function resolveAllowedUrl(src, options) {
|
|
1968
|
+
const trimmed = src.trim();
|
|
1969
|
+
if (!trimmed) return null;
|
|
1970
|
+
try {
|
|
1971
|
+
const base = typeof window !== "undefined" ? window.location.href : "https://example.com/";
|
|
1972
|
+
const url = new URL(trimmed, base);
|
|
1973
|
+
if (!allowedEmbedSchemes().has(url.protocol)) return null;
|
|
1974
|
+
if (isBlockedHost(url.hostname, options?.allowedHosts)) return null;
|
|
1975
|
+
if (typeof window !== "undefined") {
|
|
1976
|
+
const pageOrigin = window.location.origin;
|
|
1977
|
+
const isAbsolute = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed) || trimmed.startsWith("//");
|
|
1978
|
+
if (!isAbsolute && url.origin !== pageOrigin) return null;
|
|
1979
|
+
if (trimmed.startsWith("//") && url.origin !== pageOrigin) return null;
|
|
1980
|
+
}
|
|
1981
|
+
url.username = "";
|
|
1982
|
+
url.password = "";
|
|
1983
|
+
return url.href;
|
|
1984
|
+
} catch {
|
|
1985
|
+
return null;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
function resolveEmbedSrc(src, options) {
|
|
1989
|
+
return resolveAllowedUrl(src, options);
|
|
1990
|
+
}
|
|
1991
|
+
function resolveMediaSrc(src, options) {
|
|
1992
|
+
if (src === void 0) return null;
|
|
1993
|
+
return resolveAllowedUrl(src, options);
|
|
1994
|
+
}
|
|
1995
|
+
function buildEmbedSandbox(allow, options) {
|
|
1996
|
+
const tokens = /* @__PURE__ */ new Set([DEFAULT_SANDBOX]);
|
|
1997
|
+
if (allow) {
|
|
1998
|
+
for (const raw of allow.split(/\s+/)) {
|
|
1999
|
+
const token = raw.trim();
|
|
2000
|
+
if (!token || BLOCKED_SANDBOX_TOKENS.has(token)) continue;
|
|
2001
|
+
if (ALLOWED_SANDBOX_TOKENS.has(token)) tokens.add(token);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (options?.restrictPopupsInProduction !== false && isProductionEmbedBuild()) {
|
|
2005
|
+
tokens.delete("allow-popups");
|
|
2006
|
+
}
|
|
2007
|
+
return [...tokens].join(" ");
|
|
2008
|
+
}
|
|
2009
|
+
function telemetryEmbedSrc(src) {
|
|
2010
|
+
try {
|
|
2011
|
+
const url = new URL(src);
|
|
2012
|
+
url.username = "";
|
|
2013
|
+
url.password = "";
|
|
2014
|
+
url.search = "";
|
|
2015
|
+
url.hash = "";
|
|
2016
|
+
return `${url.origin}${url.pathname}`;
|
|
2017
|
+
} catch {
|
|
2018
|
+
return src;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
function resolveEmbedAspectRatio(aspectRatio) {
|
|
2022
|
+
if (!aspectRatio) return void 0;
|
|
2023
|
+
const trimmed = aspectRatio.trim();
|
|
2024
|
+
if (!/^\d+(\.\d+)?\s*\/\s*\d+(\.\d+)?$/.test(trimmed)) return void 0;
|
|
2025
|
+
const [numRaw, denRaw] = trimmed.split("/").map((part) => part.trim());
|
|
2026
|
+
const num = Number(numRaw);
|
|
2027
|
+
const den = Number(denRaw);
|
|
2028
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || num <= 0 || den <= 0) return void 0;
|
|
2029
|
+
return trimmed;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1480
2032
|
// src/blocks/Image.tsx
|
|
1481
2033
|
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
1482
2034
|
function Image(props) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
2035
|
+
const { config } = useLessonkit();
|
|
2036
|
+
const resolvedSrc = resolveMediaSrc(props.src, {
|
|
2037
|
+
allowedHosts: config.embed?.allowedHosts
|
|
2038
|
+
});
|
|
2039
|
+
if (!resolvedSrc) {
|
|
2040
|
+
return /* @__PURE__ */ jsx9(
|
|
2041
|
+
"figure",
|
|
2042
|
+
{
|
|
2043
|
+
"data-lk-block-id": props.blockId,
|
|
2044
|
+
"data-testid": props.blockId ? `image-${props.blockId}` : "image",
|
|
2045
|
+
children: /* @__PURE__ */ jsx9("p", { role: "alert", children: "This image URL is not allowed." })
|
|
2046
|
+
}
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
return /* @__PURE__ */ jsx9(
|
|
2050
|
+
"img",
|
|
2051
|
+
{
|
|
2052
|
+
src: resolvedSrc,
|
|
2053
|
+
alt: props.alt,
|
|
1488
2054
|
"data-lk-block-id": props.blockId,
|
|
1489
2055
|
"data-testid": props.blockId ? `image-${props.blockId}` : "image",
|
|
1490
2056
|
style: { maxWidth: "100%", height: "auto" }
|
|
@@ -1497,10 +2063,21 @@ setLessonkitBlockType(Image, "Image");
|
|
|
1497
2063
|
import { useMemo as useMemo8 } from "react";
|
|
1498
2064
|
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1499
2065
|
function Video(props) {
|
|
2066
|
+
const { config } = useLessonkit();
|
|
1500
2067
|
const blockId = useMemo8(
|
|
1501
2068
|
() => normalizeComponentId(props.blockId, "blockId"),
|
|
1502
2069
|
[props.blockId]
|
|
1503
2070
|
);
|
|
2071
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
2072
|
+
const resolvedSrc = resolveMediaSrc(props.src, mediaOptions);
|
|
2073
|
+
const resolvedPoster = resolveMediaSrc(props.poster, mediaOptions);
|
|
2074
|
+
const resolvedCaptions = resolveMediaSrc(props.captions, mediaOptions);
|
|
2075
|
+
if (!resolvedSrc) {
|
|
2076
|
+
return /* @__PURE__ */ jsxs7("section", { "aria-label": props.title ?? "Video", "data-lk-block-id": blockId, "data-testid": "video", children: [
|
|
2077
|
+
props.title ? /* @__PURE__ */ jsx10("h3", { "data-testid": "video-title", children: props.title }) : null,
|
|
2078
|
+
/* @__PURE__ */ jsx10("p", { role: "alert", "data-testid": "video-blocked", children: "This video URL is not allowed." })
|
|
2079
|
+
] });
|
|
2080
|
+
}
|
|
1504
2081
|
return /* @__PURE__ */ jsxs7("section", { "aria-label": props.title ?? "Video", "data-lk-block-id": blockId, "data-testid": "video", children: [
|
|
1505
2082
|
props.title ? /* @__PURE__ */ jsx10("h3", { "data-testid": "video-title", children: props.title }) : null,
|
|
1506
2083
|
/* @__PURE__ */ jsx10(
|
|
@@ -1508,11 +2085,20 @@ function Video(props) {
|
|
|
1508
2085
|
{
|
|
1509
2086
|
controls: true,
|
|
1510
2087
|
preload: "metadata",
|
|
1511
|
-
poster:
|
|
1512
|
-
src:
|
|
2088
|
+
poster: resolvedPoster ?? void 0,
|
|
2089
|
+
src: resolvedSrc,
|
|
1513
2090
|
"data-testid": "video-player",
|
|
1514
2091
|
style: { maxWidth: "100%" },
|
|
1515
|
-
children:
|
|
2092
|
+
children: resolvedCaptions ? /* @__PURE__ */ jsx10(
|
|
2093
|
+
"track",
|
|
2094
|
+
{
|
|
2095
|
+
kind: "captions",
|
|
2096
|
+
src: resolvedCaptions,
|
|
2097
|
+
srcLang: "en",
|
|
2098
|
+
label: "Captions",
|
|
2099
|
+
default: true
|
|
2100
|
+
}
|
|
2101
|
+
) : null
|
|
1516
2102
|
}
|
|
1517
2103
|
)
|
|
1518
2104
|
] });
|
|
@@ -1555,7 +2141,7 @@ function Page(props) {
|
|
|
1555
2141
|
setLessonkitBlockType(Page, "Page");
|
|
1556
2142
|
|
|
1557
2143
|
// src/blocks/InteractiveBook.tsx
|
|
1558
|
-
import React11, { forwardRef as forwardRef7, useCallback as
|
|
2144
|
+
import React11, { forwardRef as forwardRef7, useCallback as useCallback6, useEffect as useEffect10, useMemo as useMemo9, useState as useState7 } from "react";
|
|
1559
2145
|
import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1560
2146
|
var InteractiveBookInner = forwardRef7(
|
|
1561
2147
|
function InteractiveBookInner2(props, ref) {
|
|
@@ -1637,15 +2223,20 @@ var InteractiveBookInner = forwardRef7(
|
|
|
1637
2223
|
}
|
|
1638
2224
|
);
|
|
1639
2225
|
var InteractiveBook = forwardRef7(function InteractiveBook2(props, ref) {
|
|
1640
|
-
const blockId = useMemo9(
|
|
1641
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
1642
|
-
[props.blockId]
|
|
1643
|
-
);
|
|
1644
2226
|
const pages = React11.Children.toArray(props.children).filter(
|
|
1645
2227
|
React11.isValidElement
|
|
1646
2228
|
);
|
|
1647
2229
|
const { config, storage } = useLessonkit();
|
|
1648
2230
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2231
|
+
requireCompoundBlockIdWhenPersisting({
|
|
2232
|
+
persistEnabled,
|
|
2233
|
+
blockId: props.blockId,
|
|
2234
|
+
componentName: "InteractiveBook"
|
|
2235
|
+
});
|
|
2236
|
+
const blockId = useMemo9(
|
|
2237
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2238
|
+
[props.blockId]
|
|
2239
|
+
);
|
|
1649
2240
|
const initialIndex = useCompoundInitialIndex({
|
|
1650
2241
|
courseId: config.courseId,
|
|
1651
2242
|
compoundId: blockId,
|
|
@@ -1654,7 +2245,7 @@ var InteractiveBook = forwardRef7(function InteractiveBook2(props, ref) {
|
|
|
1654
2245
|
storage
|
|
1655
2246
|
});
|
|
1656
2247
|
const [index, setIndex] = useState7(initialIndex);
|
|
1657
|
-
const setIndexStable =
|
|
2248
|
+
const setIndexStable = useCallback6((i) => setIndex(i), []);
|
|
1658
2249
|
useEffect10(() => {
|
|
1659
2250
|
setIndex(initialIndex);
|
|
1660
2251
|
}, [config.courseId, blockId, initialIndex]);
|
|
@@ -1709,7 +2300,7 @@ function Slide(props) {
|
|
|
1709
2300
|
setLessonkitBlockType(Slide, "Slide");
|
|
1710
2301
|
|
|
1711
2302
|
// src/blocks/SlideDeck.tsx
|
|
1712
|
-
import React13, { forwardRef as forwardRef8, useCallback as
|
|
2303
|
+
import React13, { forwardRef as forwardRef8, useCallback as useCallback7, useEffect as useEffect13, useMemo as useMemo10, useRef as useRef9, useState as useState8 } from "react";
|
|
1713
2304
|
|
|
1714
2305
|
// src/compound/useCompoundKeyboardNav.ts
|
|
1715
2306
|
import { useEffect as useEffect12 } from "react";
|
|
@@ -1786,7 +2377,7 @@ var SlideDeckInner = forwardRef8(function SlideDeckInner2(props, ref) {
|
|
|
1786
2377
|
persistEnabled,
|
|
1787
2378
|
ref
|
|
1788
2379
|
});
|
|
1789
|
-
const setIndexStable =
|
|
2380
|
+
const setIndexStable = useCallback7((i) => setIndex(i), [setIndex]);
|
|
1790
2381
|
useCompoundKeyboardNav({
|
|
1791
2382
|
containerRef,
|
|
1792
2383
|
visibleIndex,
|
|
@@ -1869,15 +2460,20 @@ var SlideDeckInner = forwardRef8(function SlideDeckInner2(props, ref) {
|
|
|
1869
2460
|
);
|
|
1870
2461
|
});
|
|
1871
2462
|
var SlideDeck = forwardRef8(function SlideDeck2(props, ref) {
|
|
1872
|
-
const blockId = useMemo10(
|
|
1873
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
1874
|
-
[props.blockId]
|
|
1875
|
-
);
|
|
1876
2463
|
const slides = React13.Children.toArray(props.children).filter(
|
|
1877
2464
|
React13.isValidElement
|
|
1878
2465
|
);
|
|
1879
2466
|
const { config, storage } = useLessonkit();
|
|
1880
2467
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2468
|
+
requireCompoundBlockIdWhenPersisting({
|
|
2469
|
+
persistEnabled,
|
|
2470
|
+
blockId: props.blockId,
|
|
2471
|
+
componentName: "SlideDeck"
|
|
2472
|
+
});
|
|
2473
|
+
const blockId = useMemo10(
|
|
2474
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2475
|
+
[props.blockId]
|
|
2476
|
+
);
|
|
1881
2477
|
const initialIndex = useCompoundInitialIndex({
|
|
1882
2478
|
courseId: config.courseId,
|
|
1883
2479
|
compoundId: blockId,
|
|
@@ -1886,7 +2482,7 @@ var SlideDeck = forwardRef8(function SlideDeck2(props, ref) {
|
|
|
1886
2482
|
storage
|
|
1887
2483
|
});
|
|
1888
2484
|
const [index, setIndex] = useState8(initialIndex);
|
|
1889
|
-
const setIndexStable =
|
|
2485
|
+
const setIndexStable = useCallback7((i) => setIndex(i), []);
|
|
1890
2486
|
useEffect13(() => {
|
|
1891
2487
|
setIndex(initialIndex);
|
|
1892
2488
|
}, [config.courseId, blockId, initialIndex]);
|
|
@@ -1953,37 +2549,19 @@ function TimedCue(props) {
|
|
|
1953
2549
|
setLessonkitBlockType(TimedCue, "TimedCue");
|
|
1954
2550
|
|
|
1955
2551
|
// src/blocks/InteractiveVideo.tsx
|
|
1956
|
-
import React15, { forwardRef as forwardRef9, useCallback as
|
|
2552
|
+
import React15, { forwardRef as forwardRef9, useCallback as useCallback8, useEffect as useEffect15, useMemo as useMemo11, useRef as useRef11, useState as useState9 } from "react";
|
|
1957
2553
|
import { loadCompoundState as loadCompoundState3 } from "@lessonkit/core";
|
|
1958
|
-
|
|
1959
|
-
// src/compound/useCompoundVideoShell.ts
|
|
1960
|
-
import "@lessonkit/core";
|
|
1961
|
-
var IV_META_KEY = "__lk_iv__";
|
|
1962
|
-
function readInteractiveVideoMeta(childStates) {
|
|
1963
|
-
const raw = childStates[IV_META_KEY];
|
|
1964
|
-
if (!raw || typeof raw !== "object") return null;
|
|
1965
|
-
const currentTime = typeof raw.currentTime === "number" ? raw.currentTime : 0;
|
|
1966
|
-
const completedCueIndices = Array.isArray(raw.completedCueIndices) ? raw.completedCueIndices.filter((n) => typeof n === "number") : [];
|
|
1967
|
-
return { currentTime, completedCueIndices };
|
|
1968
|
-
}
|
|
1969
|
-
function mergeVideoMetaIntoState(state, meta) {
|
|
1970
|
-
return {
|
|
1971
|
-
...state,
|
|
1972
|
-
childStates: {
|
|
1973
|
-
...state.childStates,
|
|
1974
|
-
[IV_META_KEY]: meta
|
|
1975
|
-
}
|
|
1976
|
-
};
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
// src/blocks/InteractiveVideo.tsx
|
|
1980
2554
|
import { Fragment, jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2555
|
+
function sortCuesByTime(cues) {
|
|
2556
|
+
return [...cues].sort((a, b) => (a.props.atSeconds ?? 0) - (b.props.atSeconds ?? 0));
|
|
2557
|
+
}
|
|
1981
2558
|
function loadVideoMeta(storage, courseId, blockId, enabled) {
|
|
1982
|
-
|
|
2559
|
+
const empty = { currentTime: 0, completedCueIndices: [], firedCueIndices: [] };
|
|
2560
|
+
if (!enabled || !courseId) return empty;
|
|
1983
2561
|
const saved = loadCompoundState3(storage, courseId, blockId);
|
|
1984
|
-
if (!saved) return
|
|
2562
|
+
if (!saved) return empty;
|
|
1985
2563
|
const meta = readInteractiveVideoMeta(saved.childStates);
|
|
1986
|
-
return meta ??
|
|
2564
|
+
return meta ?? empty;
|
|
1987
2565
|
}
|
|
1988
2566
|
function getCueChildCheckId(cue) {
|
|
1989
2567
|
const child = React15.Children.only(cue.props.children);
|
|
@@ -2000,29 +2578,59 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2000
2578
|
validateCompoundChildren("InteractiveVideo", cues);
|
|
2001
2579
|
const { config, track, storage } = useLessonkit();
|
|
2002
2580
|
const lessonId = useEnclosingLessonId();
|
|
2581
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
2582
|
+
const resolvedSrc = resolveMediaSrc(props.src, mediaOptions);
|
|
2583
|
+
const resolvedPoster = resolveMediaSrc(props.poster, mediaOptions);
|
|
2584
|
+
const resolvedCaptions = resolveMediaSrc(props.captions, mediaOptions);
|
|
2003
2585
|
const videoRef = useRef11(null);
|
|
2586
|
+
const lastKnownTimeRef = useRef11(initialMeta.currentTime);
|
|
2004
2587
|
const completedCuesRef = useRef11(new Set(initialMeta.completedCueIndices));
|
|
2005
2588
|
const [completedCues, setCompletedCues] = useState9(
|
|
2006
2589
|
() => new Set(initialMeta.completedCueIndices)
|
|
2007
2590
|
);
|
|
2008
2591
|
const [overlayActive, setOverlayActive] = useState9(false);
|
|
2009
|
-
const firedCuesRef = useRef11(
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
[cues]
|
|
2592
|
+
const firedCuesRef = useRef11(
|
|
2593
|
+
new Set(
|
|
2594
|
+
initialMeta.firedCueIndices.length > 0 ? initialMeta.firedCueIndices : initialMeta.completedCueIndices
|
|
2595
|
+
)
|
|
2014
2596
|
);
|
|
2597
|
+
const resumeOverlayCheckedRef = useRef11(false);
|
|
2598
|
+
const [persistTrigger, setPersistTrigger] = useState9(0);
|
|
2599
|
+
const lastPersistTimeRef = useRef11(0);
|
|
2600
|
+
const sortedCues = cues;
|
|
2015
2601
|
useEffect15(() => {
|
|
2016
2602
|
completedCuesRef.current = completedCues;
|
|
2017
2603
|
}, [completedCues]);
|
|
2018
|
-
const transformState =
|
|
2019
|
-
(state) =>
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2604
|
+
const transformState = useCallback8(
|
|
2605
|
+
(state) => {
|
|
2606
|
+
const liveTime = videoRef.current?.currentTime;
|
|
2607
|
+
const currentTime = Math.max(
|
|
2608
|
+
lastKnownTimeRef.current,
|
|
2609
|
+
typeof liveTime === "number" && Number.isFinite(liveTime) ? liveTime : 0
|
|
2610
|
+
);
|
|
2611
|
+
return mergeVideoMetaIntoState(state, {
|
|
2612
|
+
currentTime,
|
|
2613
|
+
completedCueIndices: [...completedCuesRef.current],
|
|
2614
|
+
firedCueIndices: [...firedCuesRef.current]
|
|
2615
|
+
});
|
|
2616
|
+
},
|
|
2617
|
+
[]
|
|
2024
2618
|
);
|
|
2025
|
-
const
|
|
2619
|
+
const applyVideoMetaFromState = useCallback8((state) => {
|
|
2620
|
+
const meta = readInteractiveVideoMeta(state.childStates);
|
|
2621
|
+
if (!meta) return;
|
|
2622
|
+
lastKnownTimeRef.current = meta.currentTime;
|
|
2623
|
+
completedCuesRef.current = new Set(meta.completedCueIndices);
|
|
2624
|
+
firedCuesRef.current = new Set(
|
|
2625
|
+
meta.firedCueIndices.length > 0 ? meta.firedCueIndices : meta.completedCueIndices
|
|
2626
|
+
);
|
|
2627
|
+
setCompletedCues(new Set(meta.completedCueIndices));
|
|
2628
|
+
const video = videoRef.current;
|
|
2629
|
+
if (video && meta.currentTime > 0) {
|
|
2630
|
+
video.currentTime = meta.currentTime;
|
|
2631
|
+
}
|
|
2632
|
+
}, []);
|
|
2633
|
+
const { visibleIndex, ctx } = useCompoundShell({
|
|
2026
2634
|
courseId: config.courseId,
|
|
2027
2635
|
compoundId: blockId,
|
|
2028
2636
|
pageCount: sortedCues.length,
|
|
@@ -2031,10 +2639,12 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2031
2639
|
persistEnabled,
|
|
2032
2640
|
ref,
|
|
2033
2641
|
storage,
|
|
2034
|
-
transformState
|
|
2642
|
+
transformState,
|
|
2643
|
+
persistTrigger,
|
|
2644
|
+
onCompoundResume: applyVideoMetaFromState
|
|
2035
2645
|
});
|
|
2036
|
-
const activeCue = sortedCues[
|
|
2037
|
-
const cueCanContinue =
|
|
2646
|
+
const activeCue = sortedCues[visibleIndex];
|
|
2647
|
+
const cueCanContinue = useCallback8(
|
|
2038
2648
|
(cue) => {
|
|
2039
2649
|
if (!cue || !cueRequiresAnswer(cue)) return true;
|
|
2040
2650
|
const checkId = getCueChildCheckId(cue);
|
|
@@ -2058,8 +2668,8 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2058
2668
|
if (!hasSavedProgress) return;
|
|
2059
2669
|
const video = videoRef.current;
|
|
2060
2670
|
if (!video) return;
|
|
2061
|
-
const cue = sortedCues[
|
|
2062
|
-
if (!cue || completedCues.has(
|
|
2671
|
+
const cue = sortedCues[visibleIndex];
|
|
2672
|
+
if (!cue || completedCues.has(visibleIndex)) return;
|
|
2063
2673
|
setOverlayActive(true);
|
|
2064
2674
|
video.pause();
|
|
2065
2675
|
const at = cue.props.atSeconds ?? 0;
|
|
@@ -2071,13 +2681,14 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2071
2681
|
completedCues,
|
|
2072
2682
|
config.courseId,
|
|
2073
2683
|
index,
|
|
2684
|
+
visibleIndex,
|
|
2074
2685
|
initialMeta.completedCueIndices.length,
|
|
2075
2686
|
initialMeta.currentTime,
|
|
2076
2687
|
persistEnabled,
|
|
2077
2688
|
sortedCues,
|
|
2078
2689
|
storage
|
|
2079
2690
|
]);
|
|
2080
|
-
const mandatoryIncompleteBefore =
|
|
2691
|
+
const mandatoryIncompleteBefore = useCallback8(
|
|
2081
2692
|
(time) => {
|
|
2082
2693
|
for (let i = 0; i < sortedCues.length; i++) {
|
|
2083
2694
|
const cue = sortedCues[i];
|
|
@@ -2088,7 +2699,7 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2088
2699
|
},
|
|
2089
2700
|
[sortedCues, completedCues]
|
|
2090
2701
|
);
|
|
2091
|
-
const activateCue =
|
|
2702
|
+
const activateCue = useCallback8(
|
|
2092
2703
|
(i) => {
|
|
2093
2704
|
const cue = sortedCues[i];
|
|
2094
2705
|
if (!cue || firedCuesRef.current.has(i)) return;
|
|
@@ -2110,6 +2721,12 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2110
2721
|
const video = videoRef.current;
|
|
2111
2722
|
if (!video || overlayActive) return;
|
|
2112
2723
|
const t = video.currentTime;
|
|
2724
|
+
lastKnownTimeRef.current = Math.max(lastKnownTimeRef.current, t);
|
|
2725
|
+
const now = Date.now();
|
|
2726
|
+
if (now - lastPersistTimeRef.current >= 5e3) {
|
|
2727
|
+
lastPersistTimeRef.current = now;
|
|
2728
|
+
setPersistTrigger((n) => n + 1);
|
|
2729
|
+
}
|
|
2113
2730
|
const blockSeek = mandatoryIncompleteBefore(t);
|
|
2114
2731
|
if (blockSeek !== null && t > blockSeek + 0.5) {
|
|
2115
2732
|
video.currentTime = blockSeek;
|
|
@@ -2125,10 +2742,10 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2125
2742
|
}
|
|
2126
2743
|
};
|
|
2127
2744
|
const completeCue = () => {
|
|
2128
|
-
const cue = sortedCues[
|
|
2745
|
+
const cue = sortedCues[visibleIndex];
|
|
2129
2746
|
if (!cue || !cueCanContinue(cue)) return;
|
|
2130
2747
|
setCompletedCues((prev) => {
|
|
2131
|
-
const next = /* @__PURE__ */ new Set([...prev,
|
|
2748
|
+
const next = /* @__PURE__ */ new Set([...prev, visibleIndex]);
|
|
2132
2749
|
completedCuesRef.current = next;
|
|
2133
2750
|
return next;
|
|
2134
2751
|
});
|
|
@@ -2138,7 +2755,7 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2138
2755
|
"video_segment_completed",
|
|
2139
2756
|
{
|
|
2140
2757
|
blockId,
|
|
2141
|
-
segmentIndex:
|
|
2758
|
+
segmentIndex: visibleIndex,
|
|
2142
2759
|
atSeconds: cue.props.atSeconds ?? 0,
|
|
2143
2760
|
segmentLabel: cue.props.label
|
|
2144
2761
|
},
|
|
@@ -2157,12 +2774,12 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2157
2774
|
" ",
|
|
2158
2775
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
2159
2776
|
] }) : null,
|
|
2160
|
-
/* @__PURE__ */ jsx16("div", { style: { position: "relative" }, children: /* @__PURE__ */ jsx16(
|
|
2777
|
+
!resolvedSrc ? /* @__PURE__ */ jsx16("p", { role: "alert", "data-testid": "interactive-video-blocked", children: "This video URL is not allowed." }) : /* @__PURE__ */ jsx16("div", { style: { position: "relative" }, children: /* @__PURE__ */ jsx16(
|
|
2161
2778
|
"video",
|
|
2162
2779
|
{
|
|
2163
2780
|
ref: videoRef,
|
|
2164
|
-
src:
|
|
2165
|
-
poster:
|
|
2781
|
+
src: resolvedSrc,
|
|
2782
|
+
poster: resolvedPoster ?? void 0,
|
|
2166
2783
|
controls: true,
|
|
2167
2784
|
"data-testid": "interactive-video-player",
|
|
2168
2785
|
onTimeUpdate,
|
|
@@ -2174,13 +2791,22 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2174
2791
|
video.currentTime = blockSeek;
|
|
2175
2792
|
}
|
|
2176
2793
|
},
|
|
2177
|
-
children:
|
|
2794
|
+
children: resolvedCaptions ? /* @__PURE__ */ jsx16(
|
|
2795
|
+
"track",
|
|
2796
|
+
{
|
|
2797
|
+
kind: "captions",
|
|
2798
|
+
src: resolvedCaptions,
|
|
2799
|
+
srcLang: "en",
|
|
2800
|
+
label: "Captions",
|
|
2801
|
+
default: true
|
|
2802
|
+
}
|
|
2803
|
+
) : null
|
|
2178
2804
|
}
|
|
2179
2805
|
) }),
|
|
2180
2806
|
/* @__PURE__ */ jsx16("div", { "data-testid": "interactive-video-cues", children: sortedCues.map(
|
|
2181
2807
|
(cue, i) => React15.cloneElement(cue, {
|
|
2182
2808
|
key: cue.key ?? i,
|
|
2183
|
-
hidden: !overlayActive || i !==
|
|
2809
|
+
hidden: !overlayActive || i !== visibleIndex,
|
|
2184
2810
|
cueIndex: i,
|
|
2185
2811
|
parentType: "InteractiveVideo"
|
|
2186
2812
|
})
|
|
@@ -2203,15 +2829,21 @@ var InteractiveVideoInner = forwardRef9(function InteractiveVideoInner2(props, r
|
|
|
2203
2829
|
});
|
|
2204
2830
|
var InteractiveVideo = forwardRef9(
|
|
2205
2831
|
function InteractiveVideo2(props, ref) {
|
|
2206
|
-
const blockId = useMemo11(
|
|
2207
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2208
|
-
[props.blockId]
|
|
2209
|
-
);
|
|
2210
2832
|
const cues = React15.Children.toArray(props.children).filter(
|
|
2211
2833
|
React15.isValidElement
|
|
2212
2834
|
);
|
|
2835
|
+
const sortedCues = useMemo11(() => sortCuesByTime(cues), [cues]);
|
|
2213
2836
|
const { config, storage } = useLessonkit();
|
|
2214
2837
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2838
|
+
requireCompoundBlockIdWhenPersisting({
|
|
2839
|
+
persistEnabled,
|
|
2840
|
+
blockId: props.blockId,
|
|
2841
|
+
componentName: "InteractiveVideo"
|
|
2842
|
+
});
|
|
2843
|
+
const blockId = useMemo11(
|
|
2844
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2845
|
+
[props.blockId]
|
|
2846
|
+
);
|
|
2215
2847
|
const initialMeta = useMemo11(
|
|
2216
2848
|
() => loadVideoMeta(storage, config.courseId, blockId, persistEnabled),
|
|
2217
2849
|
[storage, config.courseId, blockId, persistEnabled]
|
|
@@ -2219,22 +2851,22 @@ var InteractiveVideo = forwardRef9(
|
|
|
2219
2851
|
const initialIndex = useCompoundInitialIndex({
|
|
2220
2852
|
courseId: config.courseId,
|
|
2221
2853
|
compoundId: blockId,
|
|
2222
|
-
pageCount:
|
|
2854
|
+
pageCount: sortedCues.length,
|
|
2223
2855
|
persistEnabled,
|
|
2224
2856
|
storage
|
|
2225
2857
|
});
|
|
2226
2858
|
const [index, setIndex] = useState9(initialIndex);
|
|
2227
|
-
const setIndexStable =
|
|
2859
|
+
const setIndexStable = useCallback8((i) => setIndex(i), []);
|
|
2228
2860
|
useEffect15(() => {
|
|
2229
2861
|
setIndex(initialIndex);
|
|
2230
|
-
}, [config.courseId, blockId, initialIndex]);
|
|
2862
|
+
}, [config.courseId, blockId, initialIndex, sortedCues.length]);
|
|
2231
2863
|
return /* @__PURE__ */ jsx16(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx16(
|
|
2232
2864
|
InteractiveVideoInner,
|
|
2233
2865
|
{
|
|
2234
2866
|
...props,
|
|
2235
2867
|
ref,
|
|
2236
2868
|
blockId,
|
|
2237
|
-
cues,
|
|
2869
|
+
cues: sortedCues,
|
|
2238
2870
|
index,
|
|
2239
2871
|
setIndex,
|
|
2240
2872
|
persistEnabled,
|
|
@@ -2247,10 +2879,18 @@ setLessonkitBlockType(InteractiveVideo, "InteractiveVideo");
|
|
|
2247
2879
|
|
|
2248
2880
|
// src/blocks/Summary.tsx
|
|
2249
2881
|
import { forwardRef as forwardRef10, useEffect as useEffect16, useMemo as useMemo12, useRef as useRef12, useState as useState10 } from "react";
|
|
2882
|
+
|
|
2883
|
+
// src/assessment/shouldReplayResumeTelemetry.ts
|
|
2884
|
+
function shouldReplayResumeTelemetry(config) {
|
|
2885
|
+
return config?.tracking?.replayResumeEvents === true;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// src/blocks/Summary.tsx
|
|
2250
2889
|
import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2251
2890
|
var INTERACTION6 = "summary";
|
|
2252
2891
|
function SummaryInner(props, ref) {
|
|
2253
2892
|
const checkId = useMemo12(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2893
|
+
const { config } = useLessonkit();
|
|
2254
2894
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2255
2895
|
const [selectedIndices, setSelectedIndices] = useState10([]);
|
|
2256
2896
|
const [passed, setPassed] = useState10(false);
|
|
@@ -2278,7 +2918,7 @@ function SummaryInner(props, ref) {
|
|
|
2278
2918
|
const handle = useMemo12(
|
|
2279
2919
|
() => buildAssessmentHandle({
|
|
2280
2920
|
checkId,
|
|
2281
|
-
getScore: () =>
|
|
2921
|
+
getScore: () => score,
|
|
2282
2922
|
getMaxScore: () => maxScore,
|
|
2283
2923
|
getAnswerGiven: () => selectedIndices.length > 0,
|
|
2284
2924
|
resetTask: reset,
|
|
@@ -2289,7 +2929,7 @@ function SummaryInner(props, ref) {
|
|
|
2289
2929
|
interactionType: INTERACTION6,
|
|
2290
2930
|
response: selected,
|
|
2291
2931
|
correct: passedThreshold,
|
|
2292
|
-
score
|
|
2932
|
+
score,
|
|
2293
2933
|
maxScore
|
|
2294
2934
|
}),
|
|
2295
2935
|
getCurrentState: () => ({ selectedIndices, passed, checked }),
|
|
@@ -2299,34 +2939,48 @@ function SummaryInner(props, ref) {
|
|
|
2299
2939
|
nextIndices = [...state.selectedIndices];
|
|
2300
2940
|
} else if (Array.isArray(state.selected)) {
|
|
2301
2941
|
const legacy = state.selected;
|
|
2942
|
+
if (isDevEnvironment()) {
|
|
2943
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2944
|
+
for (const text of props.statements) {
|
|
2945
|
+
if (seen.has(text)) {
|
|
2946
|
+
console.warn(
|
|
2947
|
+
"[lessonkit] Summary: duplicate statement strings; legacy selected resume may be ambiguous",
|
|
2948
|
+
text
|
|
2949
|
+
);
|
|
2950
|
+
break;
|
|
2951
|
+
}
|
|
2952
|
+
seen.add(text);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2302
2955
|
nextIndices = legacy.map((text) => props.statements.indexOf(text)).filter((i) => i >= 0);
|
|
2303
2956
|
}
|
|
2304
2957
|
setSelectedIndices(nextIndices);
|
|
2305
2958
|
const nextSelected = nextIndices.map((i) => props.statements[i] ?? "");
|
|
2306
2959
|
const nextIsCorrect = nextSelected.length === props.correct.length && nextSelected.every((s, i) => s === props.correct[i]);
|
|
2307
2960
|
const nextScore = nextIsCorrect ? maxScore : 0;
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2961
|
+
const nextPassedThreshold = meetsPassingThreshold(
|
|
2962
|
+
nextScore,
|
|
2963
|
+
maxScore,
|
|
2964
|
+
props.passingScore
|
|
2965
|
+
);
|
|
2966
|
+
setPassed(nextPassedThreshold);
|
|
2967
|
+
completedRef.current = nextPassedThreshold;
|
|
2968
|
+
if (nextPassedThreshold && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
2969
|
+
telemetryReplayedRef.current = true;
|
|
2970
|
+
assessment.answer({
|
|
2971
|
+
checkId,
|
|
2972
|
+
interactionType: INTERACTION6,
|
|
2973
|
+
response: nextSelected,
|
|
2974
|
+
correct: nextPassedThreshold
|
|
2975
|
+
});
|
|
2976
|
+
assessment.complete({
|
|
2977
|
+
checkId,
|
|
2978
|
+
interactionType: INTERACTION6,
|
|
2979
|
+
score: nextScore,
|
|
2980
|
+
maxScore,
|
|
2981
|
+
passingScore: props.passingScore ?? maxScore
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2330
2984
|
readBooleanStateField(state, "checked", setChecked);
|
|
2331
2985
|
}
|
|
2332
2986
|
}),
|
|
@@ -2334,14 +2988,16 @@ function SummaryInner(props, ref) {
|
|
|
2334
2988
|
assessment,
|
|
2335
2989
|
checkId,
|
|
2336
2990
|
checked,
|
|
2991
|
+
config,
|
|
2337
2992
|
maxScore,
|
|
2338
2993
|
passed,
|
|
2339
2994
|
passedThreshold,
|
|
2995
|
+
props.correct,
|
|
2340
2996
|
props.passingScore,
|
|
2341
2997
|
props.statements,
|
|
2342
2998
|
score,
|
|
2343
2999
|
selected,
|
|
2344
|
-
selectedIndices
|
|
3000
|
+
selectedIndices
|
|
2345
3001
|
]
|
|
2346
3002
|
);
|
|
2347
3003
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
@@ -2364,9 +3020,9 @@ function SummaryInner(props, ref) {
|
|
|
2364
3020
|
response: selected,
|
|
2365
3021
|
correct: passedThreshold
|
|
2366
3022
|
});
|
|
2367
|
-
if (passedThreshold && !completedRef.current) {
|
|
3023
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
2368
3024
|
completedRef.current = true;
|
|
2369
|
-
setPassed(true);
|
|
3025
|
+
if (passedThreshold) setPassed(true);
|
|
2370
3026
|
assessment.complete({
|
|
2371
3027
|
checkId,
|
|
2372
3028
|
interactionType: INTERACTION6,
|
|
@@ -2444,8 +3100,34 @@ function buildDeck(pairs) {
|
|
|
2444
3100
|
);
|
|
2445
3101
|
return shuffleCards(cards);
|
|
2446
3102
|
}
|
|
3103
|
+
function rebuildCardsFromKeys(pairs, cardKeys) {
|
|
3104
|
+
const pairMap = new Map(pairs.map((pair) => [pair.id, pair]));
|
|
3105
|
+
if (cardKeys.length !== pairs.length * 2) return null;
|
|
3106
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3107
|
+
const cards = [];
|
|
3108
|
+
for (const cardKey of cardKeys) {
|
|
3109
|
+
if (seen.has(cardKey)) return null;
|
|
3110
|
+
seen.add(cardKey);
|
|
3111
|
+
const match = /^(.+)-([01])$/.exec(cardKey);
|
|
3112
|
+
if (!match) return null;
|
|
3113
|
+
const pairId = match[1];
|
|
3114
|
+
const copy = Number(match[2]);
|
|
3115
|
+
if (copy !== 0 && copy !== 1) return null;
|
|
3116
|
+
const pair = pairMap.get(pairId);
|
|
3117
|
+
if (!pair) return null;
|
|
3118
|
+
cards.push({
|
|
3119
|
+
cardKey,
|
|
3120
|
+
pairId: pair.id,
|
|
3121
|
+
label: pair.label,
|
|
3122
|
+
imageSrc: pair.imageSrc
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
return cards;
|
|
3126
|
+
}
|
|
2447
3127
|
function ImagePairingInner(props, ref) {
|
|
2448
3128
|
const checkId = useMemo13(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3129
|
+
const { config } = useLessonkit();
|
|
3130
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
2449
3131
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2450
3132
|
const pairsKey = props.pairs.map((p) => p.id).join("\0");
|
|
2451
3133
|
const [cards, setCards] = useState11(() => buildDeck(props.pairs));
|
|
@@ -2453,9 +3135,15 @@ function ImagePairingInner(props, ref) {
|
|
|
2453
3135
|
const [revealed, setRevealed] = useState11(() => /* @__PURE__ */ new Set());
|
|
2454
3136
|
const [keyboardSelection, setKeyboardSelection] = useState11(null);
|
|
2455
3137
|
const [passed, setPassed] = useState11(false);
|
|
3138
|
+
const [submitted, setSubmitted] = useState11(false);
|
|
2456
3139
|
const completedRef = useRef13(false);
|
|
2457
3140
|
const telemetryReplayedRef = useRef13(false);
|
|
3141
|
+
const mismatchTimeoutRef = useRef13(null);
|
|
2458
3142
|
const reset = () => {
|
|
3143
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
3144
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
3145
|
+
mismatchTimeoutRef.current = null;
|
|
3146
|
+
}
|
|
2459
3147
|
completedRef.current = false;
|
|
2460
3148
|
telemetryReplayedRef.current = false;
|
|
2461
3149
|
setCards(buildDeck(props.pairs));
|
|
@@ -2463,10 +3151,19 @@ function ImagePairingInner(props, ref) {
|
|
|
2463
3151
|
setRevealed(/* @__PURE__ */ new Set());
|
|
2464
3152
|
setKeyboardSelection(null);
|
|
2465
3153
|
setPassed(false);
|
|
3154
|
+
setSubmitted(false);
|
|
2466
3155
|
};
|
|
2467
3156
|
useEffect17(() => {
|
|
2468
3157
|
reset();
|
|
2469
3158
|
}, [checkId, pairsKey]);
|
|
3159
|
+
useEffect17(
|
|
3160
|
+
() => () => {
|
|
3161
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
3162
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
3163
|
+
}
|
|
3164
|
+
},
|
|
3165
|
+
[]
|
|
3166
|
+
);
|
|
2470
3167
|
const totalPairs = props.pairs.length;
|
|
2471
3168
|
const matchedCount = matched.size;
|
|
2472
3169
|
const maxScore = totalPairs || 1;
|
|
@@ -2474,25 +3171,34 @@ function ImagePairingInner(props, ref) {
|
|
|
2474
3171
|
const allMatched = totalPairs > 0 && matchedCount === totalPairs;
|
|
2475
3172
|
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
2476
3173
|
const completeIfReady = (nextMatched) => {
|
|
2477
|
-
if (
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
3174
|
+
if (totalPairs === 0 || completedRef.current) return;
|
|
3175
|
+
const finalScore = nextMatched.size;
|
|
3176
|
+
const finalPassed = meetsPassingThreshold(finalScore, maxScore, props.passingScore);
|
|
3177
|
+
if (!finalPassed && nextMatched.size < totalPairs) return;
|
|
3178
|
+
completeWithScore(nextMatched, finalScore, finalPassed);
|
|
3179
|
+
};
|
|
3180
|
+
const completeWithScore = (nextMatched, finalScore, finalPassed) => {
|
|
3181
|
+
if (completedRef.current) return;
|
|
3182
|
+
completedRef.current = true;
|
|
3183
|
+
setSubmitted(true);
|
|
3184
|
+
setPassed(finalPassed);
|
|
3185
|
+
assessment.answer({
|
|
3186
|
+
checkId,
|
|
3187
|
+
interactionType: INTERACTION7,
|
|
3188
|
+
response: { matchedPairIds: [...nextMatched] },
|
|
3189
|
+
correct: finalPassed
|
|
3190
|
+
});
|
|
3191
|
+
assessment.complete({
|
|
3192
|
+
checkId,
|
|
3193
|
+
interactionType: INTERACTION7,
|
|
3194
|
+
score: finalScore,
|
|
3195
|
+
maxScore,
|
|
3196
|
+
passingScore: props.passingScore ?? maxScore
|
|
3197
|
+
});
|
|
3198
|
+
};
|
|
3199
|
+
const finishAttempt = () => {
|
|
3200
|
+
if (completedRef.current || matchedCount === 0) return;
|
|
3201
|
+
completeWithScore(matched, matchedCount, passedThreshold);
|
|
2496
3202
|
};
|
|
2497
3203
|
const tryMatch = (firstKey, secondKey) => {
|
|
2498
3204
|
if (firstKey === secondKey) return;
|
|
@@ -2509,7 +3215,11 @@ function ImagePairingInner(props, ref) {
|
|
|
2509
3215
|
setRevealed(/* @__PURE__ */ new Set());
|
|
2510
3216
|
setKeyboardSelection(null);
|
|
2511
3217
|
} else {
|
|
2512
|
-
|
|
3218
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
3219
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
3220
|
+
}
|
|
3221
|
+
mismatchTimeoutRef.current = window.setTimeout(() => {
|
|
3222
|
+
mismatchTimeoutRef.current = null;
|
|
2513
3223
|
setRevealed((prev) => {
|
|
2514
3224
|
const next = new Set(prev);
|
|
2515
3225
|
next.delete(firstKey);
|
|
@@ -2552,17 +3262,22 @@ function ImagePairingInner(props, ref) {
|
|
|
2552
3262
|
checkId,
|
|
2553
3263
|
interactionType: INTERACTION7,
|
|
2554
3264
|
response: { matchedPairIds: [...matched] },
|
|
2555
|
-
correct:
|
|
3265
|
+
correct: passedThreshold,
|
|
2556
3266
|
score,
|
|
2557
3267
|
maxScore
|
|
2558
3268
|
}),
|
|
2559
3269
|
getCurrentState: () => ({
|
|
3270
|
+
cardKeys: cards.map((card) => card.cardKey),
|
|
2560
3271
|
matched: [...matched],
|
|
2561
3272
|
revealed: [...revealed],
|
|
2562
3273
|
keyboardSelection,
|
|
2563
3274
|
passed
|
|
2564
3275
|
}),
|
|
2565
3276
|
resume: (state) => {
|
|
3277
|
+
if (Array.isArray(state.cardKeys)) {
|
|
3278
|
+
const restored = rebuildCardsFromKeys(props.pairs, state.cardKeys);
|
|
3279
|
+
if (restored) setCards(restored);
|
|
3280
|
+
}
|
|
2566
3281
|
if (Array.isArray(state.matched)) setMatched(new Set(state.matched));
|
|
2567
3282
|
if (Array.isArray(state.revealed)) setRevealed(new Set(state.revealed));
|
|
2568
3283
|
const sel = state.keyboardSelection;
|
|
@@ -2570,15 +3285,20 @@ function ImagePairingInner(props, ref) {
|
|
|
2570
3285
|
readBooleanStateField(state, "passed", (value) => {
|
|
2571
3286
|
setPassed(value);
|
|
2572
3287
|
completedRef.current = value;
|
|
2573
|
-
if (value && !telemetryReplayedRef.current) {
|
|
3288
|
+
if (value && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
2574
3289
|
telemetryReplayedRef.current = true;
|
|
2575
3290
|
const matchedIds = Array.isArray(state.matched) ? state.matched : [...matched];
|
|
2576
3291
|
const finalScore = matchedIds.length;
|
|
3292
|
+
const finalPassed = meetsPassingThreshold(
|
|
3293
|
+
finalScore,
|
|
3294
|
+
maxScore,
|
|
3295
|
+
props.passingScore
|
|
3296
|
+
);
|
|
2577
3297
|
assessment.answer({
|
|
2578
3298
|
checkId,
|
|
2579
3299
|
interactionType: INTERACTION7,
|
|
2580
3300
|
response: { matchedPairIds: matchedIds },
|
|
2581
|
-
correct:
|
|
3301
|
+
correct: finalPassed
|
|
2582
3302
|
});
|
|
2583
3303
|
assessment.complete({
|
|
2584
3304
|
checkId,
|
|
@@ -2591,7 +3311,7 @@ function ImagePairingInner(props, ref) {
|
|
|
2591
3311
|
});
|
|
2592
3312
|
}
|
|
2593
3313
|
}),
|
|
2594
|
-
[allMatched, checkId, keyboardSelection, matched, matchedCount, maxScore, passed, passedThreshold, revealed, score]
|
|
3314
|
+
[allMatched, assessment, cards, checkId, config, keyboardSelection, matched, matchedCount, maxScore, passed, passedThreshold, props.pairs, props.passingScore, revealed, score]
|
|
2595
3315
|
);
|
|
2596
3316
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2597
3317
|
return /* @__PURE__ */ jsxs15("section", { "aria-label": "Image Pairing", "data-lk-check-id": checkId, "data-testid": "image-pairing", children: [
|
|
@@ -2600,6 +3320,7 @@ function ImagePairingInner(props, ref) {
|
|
|
2600
3320
|
const isMatched = matched.has(card.pairId);
|
|
2601
3321
|
const isRevealed = isMatched || revealed.has(card.cardKey);
|
|
2602
3322
|
const isSelected = keyboardSelection === card.cardKey;
|
|
3323
|
+
const resolvedCardSrc = resolveMediaSrc(card.imageSrc, mediaOptions);
|
|
2603
3324
|
return /* @__PURE__ */ jsx18(
|
|
2604
3325
|
"button",
|
|
2605
3326
|
{
|
|
@@ -2616,7 +3337,14 @@ function ImagePairingInner(props, ref) {
|
|
|
2616
3337
|
border: isSelected ? "2px solid currentColor" : "1px solid currentColor"
|
|
2617
3338
|
},
|
|
2618
3339
|
children: isRevealed ? /* @__PURE__ */ jsxs15(Fragment2, { children: [
|
|
2619
|
-
/* @__PURE__ */ jsx18(
|
|
3340
|
+
resolvedCardSrc ? /* @__PURE__ */ jsx18(
|
|
3341
|
+
"img",
|
|
3342
|
+
{
|
|
3343
|
+
src: resolvedCardSrc,
|
|
3344
|
+
alt: card.label,
|
|
3345
|
+
style: { maxWidth: "5rem", maxHeight: "5rem" }
|
|
3346
|
+
}
|
|
3347
|
+
) : /* @__PURE__ */ jsx18("span", { "aria-hidden": "true", children: "!" }),
|
|
2620
3348
|
/* @__PURE__ */ jsx18("span", { className: "lk-visually-hidden", children: card.label })
|
|
2621
3349
|
] }) : "?"
|
|
2622
3350
|
},
|
|
@@ -2629,6 +3357,7 @@ function ImagePairingInner(props, ref) {
|
|
|
2629
3357
|
totalPairs,
|
|
2630
3358
|
" pairs matched"
|
|
2631
3359
|
] }),
|
|
3360
|
+
props.enableRetry === false && matchedCount > 0 && !submitted ? /* @__PURE__ */ jsx18("button", { type: "button", "data-testid": "image-pairing-finish", onClick: finishAttempt, children: "Submit" }) : null,
|
|
2632
3361
|
props.enableRetry && passed ? /* @__PURE__ */ jsx18("button", { type: "button", "data-testid": "image-pairing-retry", onClick: reset, children: "Try again" }) : null
|
|
2633
3362
|
] });
|
|
2634
3363
|
}
|
|
@@ -2644,6 +3373,8 @@ import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
|
2644
3373
|
var INTERACTION8 = "imageSequencing";
|
|
2645
3374
|
function ImageSequencingInner(props, ref) {
|
|
2646
3375
|
const checkId = useMemo14(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3376
|
+
const { config } = useLessonkit();
|
|
3377
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
2647
3378
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2648
3379
|
const imagesKey = props.images.map((i) => i.id).join("\0");
|
|
2649
3380
|
const orderKey = props.correctOrder.join("\0");
|
|
@@ -2680,9 +3411,9 @@ function ImageSequencingInner(props, ref) {
|
|
|
2680
3411
|
const handle = useMemo14(
|
|
2681
3412
|
() => buildAssessmentHandle({
|
|
2682
3413
|
checkId,
|
|
2683
|
-
getScore: () =>
|
|
3414
|
+
getScore: () => score,
|
|
2684
3415
|
getMaxScore: () => maxScore,
|
|
2685
|
-
getAnswerGiven: () =>
|
|
3416
|
+
getAnswerGiven: () => checked,
|
|
2686
3417
|
resetTask: reset,
|
|
2687
3418
|
showSolutions: () => {
|
|
2688
3419
|
},
|
|
@@ -2691,7 +3422,7 @@ function ImageSequencingInner(props, ref) {
|
|
|
2691
3422
|
interactionType: INTERACTION8,
|
|
2692
3423
|
response: order,
|
|
2693
3424
|
correct: passedThreshold,
|
|
2694
|
-
score
|
|
3425
|
+
score,
|
|
2695
3426
|
maxScore
|
|
2696
3427
|
}),
|
|
2697
3428
|
getCurrentState: () => ({ order, passed, checked }),
|
|
@@ -2704,7 +3435,7 @@ function ImageSequencingInner(props, ref) {
|
|
|
2704
3435
|
readBooleanStateField(state, "passed", (value) => {
|
|
2705
3436
|
setPassed(value);
|
|
2706
3437
|
completedRef.current = value;
|
|
2707
|
-
if (value && !telemetryReplayedRef.current) {
|
|
3438
|
+
if (value && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
2708
3439
|
telemetryReplayedRef.current = true;
|
|
2709
3440
|
const nextIsCorrect = nextOrder.every((id, i) => id === props.correctOrder[i]);
|
|
2710
3441
|
const nextScore = nextIsCorrect ? maxScore : 0;
|
|
@@ -2726,7 +3457,7 @@ function ImageSequencingInner(props, ref) {
|
|
|
2726
3457
|
readBooleanStateField(state, "checked", setChecked);
|
|
2727
3458
|
}
|
|
2728
3459
|
}),
|
|
2729
|
-
[checkId, checked, maxScore, order, passed, passedThreshold, score]
|
|
3460
|
+
[assessment, checkId, checked, config, maxScore, order, passed, passedThreshold, props.correctOrder, props.passingScore, score]
|
|
2730
3461
|
);
|
|
2731
3462
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2732
3463
|
const check = () => {
|
|
@@ -2737,9 +3468,9 @@ function ImageSequencingInner(props, ref) {
|
|
|
2737
3468
|
response: order,
|
|
2738
3469
|
correct: passedThreshold
|
|
2739
3470
|
});
|
|
2740
|
-
if (passedThreshold && !completedRef.current) {
|
|
3471
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
2741
3472
|
completedRef.current = true;
|
|
2742
|
-
setPassed(true);
|
|
3473
|
+
if (passedThreshold) setPassed(true);
|
|
2743
3474
|
assessment.complete({
|
|
2744
3475
|
checkId,
|
|
2745
3476
|
interactionType: INTERACTION8,
|
|
@@ -2754,8 +3485,16 @@ function ImageSequencingInner(props, ref) {
|
|
|
2754
3485
|
/* @__PURE__ */ jsx19("ol", { "data-testid": "image-sequencing-list", children: order.map((id, index) => {
|
|
2755
3486
|
const image = props.images.find((i) => i.id === id);
|
|
2756
3487
|
if (!image) return null;
|
|
3488
|
+
const resolvedSrc = resolveMediaSrc(image.src, mediaOptions);
|
|
2757
3489
|
return /* @__PURE__ */ jsxs16("li", { "data-testid": `sequencing-item-${id}`, children: [
|
|
2758
|
-
/* @__PURE__ */ jsx19(
|
|
3490
|
+
resolvedSrc ? /* @__PURE__ */ jsx19(
|
|
3491
|
+
"img",
|
|
3492
|
+
{
|
|
3493
|
+
src: resolvedSrc,
|
|
3494
|
+
alt: image.alt,
|
|
3495
|
+
style: { maxWidth: "8rem", verticalAlign: "middle" }
|
|
3496
|
+
}
|
|
3497
|
+
) : /* @__PURE__ */ jsx19("span", { "aria-hidden": "true", children: "!" }),
|
|
2759
3498
|
/* @__PURE__ */ jsx19(
|
|
2760
3499
|
"button",
|
|
2761
3500
|
{
|
|
@@ -2803,11 +3542,12 @@ var ImageSequencing = forwardRef12(
|
|
|
2803
3542
|
setLessonkitBlockType(ImageSequencing, "ImageSequencing");
|
|
2804
3543
|
|
|
2805
3544
|
// src/blocks/ArithmeticQuiz.tsx
|
|
2806
|
-
import { forwardRef as forwardRef13, useCallback as
|
|
3545
|
+
import { forwardRef as forwardRef13, useCallback as useCallback9, useEffect as useEffect19, useMemo as useMemo15, useRef as useRef15, useState as useState13 } from "react";
|
|
2807
3546
|
import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2808
3547
|
var INTERACTION9 = "arithmeticQuiz";
|
|
2809
3548
|
function ArithmeticQuizInner(props, ref) {
|
|
2810
3549
|
const checkId = useMemo15(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3550
|
+
const { config } = useLessonkit();
|
|
2811
3551
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2812
3552
|
const problemsKey = props.problems.map((p) => `${p.question}\0${p.answer}`).join("|");
|
|
2813
3553
|
const [answers, setAnswers] = useState13(
|
|
@@ -2838,7 +3578,7 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
2838
3578
|
const maxScore = props.problems.length || 1;
|
|
2839
3579
|
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
2840
3580
|
const allFilled = props.problems.every((_, i) => (answers[i] ?? "").trim().length > 0);
|
|
2841
|
-
const runCheck =
|
|
3581
|
+
const runCheck = useCallback9(
|
|
2842
3582
|
(force = false) => {
|
|
2843
3583
|
if (!force && !allFilled) return;
|
|
2844
3584
|
setChecked(true);
|
|
@@ -2848,9 +3588,9 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
2848
3588
|
response: answers,
|
|
2849
3589
|
correct: passedThreshold
|
|
2850
3590
|
});
|
|
2851
|
-
if (passedThreshold && !completedRef.current) {
|
|
3591
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
2852
3592
|
completedRef.current = true;
|
|
2853
|
-
setPassed(
|
|
3593
|
+
setPassed(passedThreshold);
|
|
2854
3594
|
assessment.complete({
|
|
2855
3595
|
checkId,
|
|
2856
3596
|
interactionType: INTERACTION9,
|
|
@@ -2874,7 +3614,7 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
2874
3614
|
const handle = useMemo15(
|
|
2875
3615
|
() => buildAssessmentHandle({
|
|
2876
3616
|
checkId,
|
|
2877
|
-
getScore: () =>
|
|
3617
|
+
getScore: () => score,
|
|
2878
3618
|
getMaxScore: () => maxScore,
|
|
2879
3619
|
getAnswerGiven: () => allFilled,
|
|
2880
3620
|
resetTask: reset,
|
|
@@ -2885,7 +3625,7 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
2885
3625
|
interactionType: INTERACTION9,
|
|
2886
3626
|
response: answers,
|
|
2887
3627
|
correct: passedThreshold,
|
|
2888
|
-
score
|
|
3628
|
+
score,
|
|
2889
3629
|
maxScore
|
|
2890
3630
|
}),
|
|
2891
3631
|
getCurrentState: () => ({ answers, passed, checked, timeLeft }),
|
|
@@ -2899,17 +3639,18 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
2899
3639
|
readBooleanStateField(state, "passed", (value) => {
|
|
2900
3640
|
setPassed(value);
|
|
2901
3641
|
completedRef.current = value;
|
|
2902
|
-
if (value && !telemetryReplayedRef.current) {
|
|
3642
|
+
if (value && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
2903
3643
|
telemetryReplayedRef.current = true;
|
|
2904
3644
|
let nextScore = 0;
|
|
2905
3645
|
props.problems.forEach((p, i) => {
|
|
2906
3646
|
if ((nextAnswers[i] ?? "").trim() === p.answer.trim()) nextScore += 1;
|
|
2907
3647
|
});
|
|
3648
|
+
const replayCorrect = nextScore >= (props.passingScore ?? maxScore);
|
|
2908
3649
|
assessment.answer({
|
|
2909
3650
|
checkId,
|
|
2910
3651
|
interactionType: INTERACTION9,
|
|
2911
3652
|
response: nextAnswers,
|
|
2912
|
-
correct:
|
|
3653
|
+
correct: replayCorrect
|
|
2913
3654
|
});
|
|
2914
3655
|
assessment.complete({
|
|
2915
3656
|
checkId,
|
|
@@ -2924,7 +3665,7 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
2924
3665
|
if (typeof state.timeLeft === "number") setTimeLeft(state.timeLeft);
|
|
2925
3666
|
}
|
|
2926
3667
|
}),
|
|
2927
|
-
[allFilled, answers, checkId, checked, maxScore, passed, passedThreshold, score, timeLeft]
|
|
3668
|
+
[allFilled, answers, checkId, checked, config, maxScore, passed, passedThreshold, props.problems, props.passingScore, score, timeLeft]
|
|
2928
3669
|
);
|
|
2929
3670
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2930
3671
|
const onInput = (index, value) => {
|
|
@@ -2988,6 +3729,7 @@ import { jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
|
2988
3729
|
var INTERACTION10 = "essay";
|
|
2989
3730
|
function EssayInner(props, ref) {
|
|
2990
3731
|
const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3732
|
+
const { config } = useLessonkit();
|
|
2991
3733
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2992
3734
|
const [text, setText] = useState14("");
|
|
2993
3735
|
const [submitted, setSubmitted] = useState14(false);
|
|
@@ -3027,16 +3769,22 @@ function EssayInner(props, ref) {
|
|
|
3027
3769
|
const nextText = readStringField(state, "text");
|
|
3028
3770
|
if (typeof nextText === "string") setText(nextText);
|
|
3029
3771
|
readBooleanStateField(state, "submitted", (value) => {
|
|
3772
|
+
const textVal = typeof nextText === "string" ? nextText : text;
|
|
3773
|
+
const meetsMin = textVal.trim().length >= minLength;
|
|
3774
|
+
if (value && !meetsMin) {
|
|
3775
|
+
setSubmitted(false);
|
|
3776
|
+
completedRef.current = false;
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3030
3779
|
setSubmitted(value);
|
|
3031
3780
|
completedRef.current = value;
|
|
3032
|
-
if (value && !telemetryReplayedRef.current) {
|
|
3781
|
+
if (value && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
3033
3782
|
telemetryReplayedRef.current = true;
|
|
3034
|
-
const response = typeof nextText === "string" ? nextText : text;
|
|
3035
3783
|
assessment.answer({
|
|
3036
3784
|
checkId,
|
|
3037
3785
|
interactionType: INTERACTION10,
|
|
3038
3786
|
question: props.question,
|
|
3039
|
-
response,
|
|
3787
|
+
response: textVal,
|
|
3040
3788
|
correct: false
|
|
3041
3789
|
});
|
|
3042
3790
|
assessment.complete({
|
|
@@ -3050,7 +3798,7 @@ function EssayInner(props, ref) {
|
|
|
3050
3798
|
});
|
|
3051
3799
|
}
|
|
3052
3800
|
}),
|
|
3053
|
-
[checkId, meetsMinLength, props.question, submitted, text]
|
|
3801
|
+
[assessment, checkId, config, meetsMinLength, minLength, props.passingScore, props.question, submitted, text]
|
|
3054
3802
|
);
|
|
3055
3803
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3056
3804
|
const submit = () => {
|
|
@@ -3201,7 +3949,7 @@ function Questionnaire(props) {
|
|
|
3201
3949
|
setLessonkitBlockType(Questionnaire, "Questionnaire");
|
|
3202
3950
|
|
|
3203
3951
|
// src/blocks/MemoryGame.tsx
|
|
3204
|
-
import { useEffect as useEffect22, useMemo as useMemo18, useState as useState16 } from "react";
|
|
3952
|
+
import { useEffect as useEffect22, useMemo as useMemo18, useRef as useRef17, useState as useState16 } from "react";
|
|
3205
3953
|
import { jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
3206
3954
|
function shuffleCards2(cards) {
|
|
3207
3955
|
const next = [...cards];
|
|
@@ -3228,16 +3976,29 @@ function MemoryGame(props) {
|
|
|
3228
3976
|
const [revealed, setRevealed] = useState16(() => /* @__PURE__ */ new Set());
|
|
3229
3977
|
const [selection, setSelection] = useState16(null);
|
|
3230
3978
|
const [complete, setComplete] = useState16(false);
|
|
3979
|
+
const mismatchTimeoutRef = useRef17(null);
|
|
3231
3980
|
const { track } = useLessonkit();
|
|
3232
3981
|
const lessonId = useEnclosingLessonId();
|
|
3233
3982
|
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
3234
3983
|
useEffect22(() => {
|
|
3984
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
3985
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
3986
|
+
mismatchTimeoutRef.current = null;
|
|
3987
|
+
}
|
|
3235
3988
|
setCards(buildDeck2(props.pairs));
|
|
3236
3989
|
setMatched(/* @__PURE__ */ new Set());
|
|
3237
3990
|
setRevealed(/* @__PURE__ */ new Set());
|
|
3238
3991
|
setSelection(null);
|
|
3239
3992
|
setComplete(false);
|
|
3240
3993
|
}, [props.blockId, pairsKey]);
|
|
3994
|
+
useEffect22(
|
|
3995
|
+
() => () => {
|
|
3996
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
3997
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
3998
|
+
}
|
|
3999
|
+
},
|
|
4000
|
+
[]
|
|
4001
|
+
);
|
|
3241
4002
|
const cardIndexByKey = useMemo18(
|
|
3242
4003
|
() => Object.fromEntries(cards.map((c, i) => [c.cardKey, i])),
|
|
3243
4004
|
[cards]
|
|
@@ -3267,7 +4028,11 @@ function MemoryGame(props) {
|
|
|
3267
4028
|
setRevealed(/* @__PURE__ */ new Set());
|
|
3268
4029
|
setSelection(null);
|
|
3269
4030
|
} else {
|
|
3270
|
-
|
|
4031
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
4032
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
4033
|
+
}
|
|
4034
|
+
mismatchTimeoutRef.current = window.setTimeout(() => {
|
|
4035
|
+
mismatchTimeoutRef.current = null;
|
|
3271
4036
|
setRevealed((prev) => {
|
|
3272
4037
|
const next = new Set(prev);
|
|
3273
4038
|
next.delete(firstKey);
|
|
@@ -3341,7 +4106,7 @@ function MemoryGame(props) {
|
|
|
3341
4106
|
setLessonkitBlockType(MemoryGame, "MemoryGame");
|
|
3342
4107
|
|
|
3343
4108
|
// src/blocks/InformationWall.tsx
|
|
3344
|
-
import { useEffect as useEffect23, useMemo as useMemo19, useRef as
|
|
4109
|
+
import { useEffect as useEffect23, useMemo as useMemo19, useRef as useRef18, useState as useState17 } from "react";
|
|
3345
4110
|
import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
3346
4111
|
function InformationWall(props) {
|
|
3347
4112
|
const blockId = useMemo19(
|
|
@@ -3352,7 +4117,7 @@ function InformationWall(props) {
|
|
|
3352
4117
|
const { track } = useLessonkit();
|
|
3353
4118
|
const lessonId = useEnclosingLessonId();
|
|
3354
4119
|
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
3355
|
-
const debounceRef =
|
|
4120
|
+
const debounceRef = useRef18(null);
|
|
3356
4121
|
const filtered = useMemo19(() => {
|
|
3357
4122
|
const q = query.trim().toLowerCase();
|
|
3358
4123
|
if (!q) return props.panels;
|
|
@@ -3424,10 +4189,16 @@ function usePrefersReducedMotion() {
|
|
|
3424
4189
|
function ParallaxSlideshow(props) {
|
|
3425
4190
|
const [index, setIndex] = useState18(0);
|
|
3426
4191
|
const reducedMotion = usePrefersReducedMotion();
|
|
3427
|
-
const { track } = useLessonkit();
|
|
4192
|
+
const { track, config } = useLessonkit();
|
|
3428
4193
|
const lessonId = useEnclosingLessonId();
|
|
3429
4194
|
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
3430
4195
|
const slide = props.slides[index];
|
|
4196
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
4197
|
+
const resolvedImageSrc = slide?.imageSrc ? resolveMediaSrc(slide.imageSrc, mediaOptions) : null;
|
|
4198
|
+
useEffect24(() => {
|
|
4199
|
+
if (props.slides.length < 1) return;
|
|
4200
|
+
setIndex((current) => Math.min(current, props.slides.length - 1));
|
|
4201
|
+
}, [props.slides.length]);
|
|
3431
4202
|
useEffect24(() => {
|
|
3432
4203
|
track(
|
|
3433
4204
|
"parallax_slide_viewed",
|
|
@@ -3454,22 +4225,23 @@ function ParallaxSlideshow(props) {
|
|
|
3454
4225
|
"data-testid": `parallax-slide-${index}`,
|
|
3455
4226
|
style: reducedMotion ? void 0 : {
|
|
3456
4227
|
backgroundAttachment: "fixed",
|
|
3457
|
-
backgroundImage:
|
|
4228
|
+
backgroundImage: resolvedImageSrc ? `url("${resolvedImageSrc}")` : void 0,
|
|
3458
4229
|
backgroundPosition: "center",
|
|
3459
4230
|
backgroundSize: "cover",
|
|
3460
4231
|
minHeight: "12rem",
|
|
3461
4232
|
padding: "1rem"
|
|
3462
4233
|
},
|
|
3463
4234
|
children: [
|
|
3464
|
-
reducedMotion &&
|
|
4235
|
+
reducedMotion && resolvedImageSrc ? /* @__PURE__ */ jsx25(
|
|
3465
4236
|
"img",
|
|
3466
4237
|
{
|
|
3467
|
-
src:
|
|
4238
|
+
src: resolvedImageSrc,
|
|
3468
4239
|
alt: "",
|
|
3469
4240
|
"data-testid": "parallax-slide-image",
|
|
3470
4241
|
style: { maxWidth: "100%" }
|
|
3471
4242
|
}
|
|
3472
4243
|
) : null,
|
|
4244
|
+
!reducedMotion && slide.imageSrc && !resolvedImageSrc ? /* @__PURE__ */ jsx25("p", { role: "alert", children: "This image URL is not allowed." }) : null,
|
|
3473
4245
|
/* @__PURE__ */ jsx25("h3", { "data-testid": "parallax-slide-title", children: slide.title }),
|
|
3474
4246
|
/* @__PURE__ */ jsx25("p", { "data-testid": "parallax-slide-body", children: slide.body })
|
|
3475
4247
|
]
|
|
@@ -3658,8 +4430,9 @@ import { useState as useState22 } from "react";
|
|
|
3658
4430
|
import { jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
3659
4431
|
function ImageHotspots(props) {
|
|
3660
4432
|
const [active, setActive] = useState22(null);
|
|
3661
|
-
const { track } = useLessonkit();
|
|
4433
|
+
const { track, config } = useLessonkit();
|
|
3662
4434
|
const lessonId = useEnclosingLessonId();
|
|
4435
|
+
const resolvedSrc = resolveMediaSrc(props.src, { allowedHosts: config.embed?.allowedHosts });
|
|
3663
4436
|
const open = (hotspotId) => {
|
|
3664
4437
|
setActive(hotspotId);
|
|
3665
4438
|
track(
|
|
@@ -3670,7 +4443,7 @@ function ImageHotspots(props) {
|
|
|
3670
4443
|
};
|
|
3671
4444
|
return /* @__PURE__ */ jsxs26("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
|
|
3672
4445
|
/* @__PURE__ */ jsxs26("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3673
|
-
/* @__PURE__ */ jsx29("img", { src:
|
|
4446
|
+
resolvedSrc ? /* @__PURE__ */ jsx29("img", { src: resolvedSrc, alt: props.alt, style: { maxWidth: "100%" } }) : /* @__PURE__ */ jsx29("p", { role: "alert", children: "This image URL is not allowed." }),
|
|
3674
4447
|
props.hotspots.map((h) => /* @__PURE__ */ jsx29(
|
|
3675
4448
|
"button",
|
|
3676
4449
|
{
|
|
@@ -3703,9 +4476,11 @@ import { useState as useState23 } from "react";
|
|
|
3703
4476
|
import { jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
|
|
3704
4477
|
function ImageSlider(props) {
|
|
3705
4478
|
const [index, setIndex] = useState23(0);
|
|
3706
|
-
const { track } = useLessonkit();
|
|
4479
|
+
const { track, config } = useLessonkit();
|
|
3707
4480
|
const lessonId = useEnclosingLessonId();
|
|
3708
4481
|
const slide = props.slides[index];
|
|
4482
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
4483
|
+
const resolvedSrc = slide ? resolveMediaSrc(slide.src, mediaOptions) : null;
|
|
3709
4484
|
if (!slide) return null;
|
|
3710
4485
|
const goTo = (next) => {
|
|
3711
4486
|
setIndex(next);
|
|
@@ -3716,7 +4491,7 @@ function ImageSlider(props) {
|
|
|
3716
4491
|
);
|
|
3717
4492
|
};
|
|
3718
4493
|
return /* @__PURE__ */ jsxs27("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
3719
|
-
/* @__PURE__ */ jsx30("img", { src:
|
|
4494
|
+
resolvedSrc ? /* @__PURE__ */ jsx30("img", { src: resolvedSrc, alt: slide.alt, style: { maxWidth: "100%" } }) : /* @__PURE__ */ jsx30("p", { role: "alert", children: "This image URL is not allowed." }),
|
|
3720
4495
|
slide.caption ? /* @__PURE__ */ jsx30("p", { children: slide.caption }) : null,
|
|
3721
4496
|
/* @__PURE__ */ jsxs27("nav", { "aria-label": "Slide navigation", children: [
|
|
3722
4497
|
/* @__PURE__ */ jsx30(
|
|
@@ -3750,14 +4525,16 @@ function ImageSlider(props) {
|
|
|
3750
4525
|
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
3751
4526
|
|
|
3752
4527
|
// src/blocks/FindHotspot.tsx
|
|
3753
|
-
import { forwardRef as forwardRef15, useEffect as useEffect25, useMemo as useMemo20, useRef as
|
|
4528
|
+
import { forwardRef as forwardRef15, useEffect as useEffect25, useMemo as useMemo20, useRef as useRef19, useState as useState24 } from "react";
|
|
3754
4529
|
import { jsx as jsx31, jsxs as jsxs28 } from "react/jsx-runtime";
|
|
3755
4530
|
var INTERACTION11 = "findHotspot";
|
|
3756
4531
|
function FindHotspotInner(props, ref) {
|
|
3757
4532
|
const checkId = useMemo20(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4533
|
+
const { config } = useLessonkit();
|
|
4534
|
+
const resolvedSrc = resolveMediaSrc(props.src, { allowedHosts: config.embed?.allowedHosts });
|
|
3758
4535
|
const [selected, setSelected] = useState24(null);
|
|
3759
4536
|
const [checked, setChecked] = useState24(false);
|
|
3760
|
-
const telemetryReplayedRef =
|
|
4537
|
+
const telemetryReplayedRef = useRef19(false);
|
|
3761
4538
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3762
4539
|
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
3763
4540
|
useEffect25(() => {
|
|
@@ -3790,7 +4567,7 @@ function FindHotspotInner(props, ref) {
|
|
|
3790
4567
|
checkId,
|
|
3791
4568
|
getScore: () => checked && correct ? 1 : 0,
|
|
3792
4569
|
getMaxScore: () => 1,
|
|
3793
|
-
getAnswerGiven: () =>
|
|
4570
|
+
getAnswerGiven: () => checked,
|
|
3794
4571
|
resetTask: () => {
|
|
3795
4572
|
setSelected(null);
|
|
3796
4573
|
setChecked(false);
|
|
@@ -3820,10 +4597,12 @@ function FindHotspotInner(props, ref) {
|
|
|
3820
4597
|
setChecked(value);
|
|
3821
4598
|
});
|
|
3822
4599
|
const nextCorrect = nextSelected === props.correctTargetId;
|
|
3823
|
-
|
|
4600
|
+
if (config.tracking?.replayResumeEvents === true) {
|
|
4601
|
+
replayTelemetry(nextSelected, nextChecked, nextCorrect);
|
|
4602
|
+
}
|
|
3824
4603
|
}
|
|
3825
4604
|
}),
|
|
3826
|
-
[assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
|
|
4605
|
+
[assessment, checkId, checked, config.tracking?.replayResumeEvents, correct, props.correctTargetId, props.passingScore, props.targets, selected]
|
|
3827
4606
|
);
|
|
3828
4607
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3829
4608
|
const selectTarget = (id) => {
|
|
@@ -3847,11 +4626,19 @@ function FindHotspotInner(props, ref) {
|
|
|
3847
4626
|
maxScore: 1,
|
|
3848
4627
|
passingScore: props.passingScore ?? 1
|
|
3849
4628
|
});
|
|
4629
|
+
} else if (props.enableRetry === false) {
|
|
4630
|
+
assessment.complete({
|
|
4631
|
+
checkId,
|
|
4632
|
+
interactionType: INTERACTION11,
|
|
4633
|
+
score: 0,
|
|
4634
|
+
maxScore: 1,
|
|
4635
|
+
passingScore: props.passingScore ?? 1
|
|
4636
|
+
});
|
|
3850
4637
|
}
|
|
3851
4638
|
};
|
|
3852
4639
|
return /* @__PURE__ */ jsxs28("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
3853
4640
|
/* @__PURE__ */ jsxs28("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3854
|
-
/* @__PURE__ */ jsx31("img", { src:
|
|
4641
|
+
resolvedSrc ? /* @__PURE__ */ jsx31("img", { src: resolvedSrc, alt: props.alt, style: { maxWidth: "100%" } }) : /* @__PURE__ */ jsx31("p", { role: "alert", children: "This image URL is not allowed." }),
|
|
3855
4642
|
props.targets.map((t) => /* @__PURE__ */ jsx31(
|
|
3856
4643
|
"button",
|
|
3857
4644
|
{
|
|
@@ -3882,14 +4669,26 @@ var FindHotspot = forwardRef15(function FindHotspot2(props, ref) {
|
|
|
3882
4669
|
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
3883
4670
|
|
|
3884
4671
|
// src/blocks/FindMultipleHotspots.tsx
|
|
3885
|
-
import { forwardRef as forwardRef16, useMemo as useMemo21, useState as useState25 } from "react";
|
|
4672
|
+
import { forwardRef as forwardRef16, useEffect as useEffect26, useMemo as useMemo21, useState as useState25 } from "react";
|
|
3886
4673
|
import { jsx as jsx32, jsxs as jsxs29 } from "react/jsx-runtime";
|
|
3887
4674
|
var INTERACTION12 = "findMultipleHotspots";
|
|
3888
4675
|
function FindMultipleHotspotsInner(props, ref) {
|
|
3889
4676
|
const checkId = useMemo21(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4677
|
+
const { config } = useLessonkit();
|
|
4678
|
+
const resolvedSrc = resolveMediaSrc(props.src, { allowedHosts: config.embed?.allowedHosts });
|
|
3890
4679
|
const [selected, setSelected] = useState25(/* @__PURE__ */ new Set());
|
|
3891
4680
|
const [checked, setChecked] = useState25(false);
|
|
3892
4681
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4682
|
+
const correctSet = useMemo21(
|
|
4683
|
+
() => new Set(props.correctTargetIds),
|
|
4684
|
+
[props.correctTargetIds]
|
|
4685
|
+
);
|
|
4686
|
+
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
4687
|
+
const correctIdsKey = props.correctTargetIds.join("\0");
|
|
4688
|
+
useEffect26(() => {
|
|
4689
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
4690
|
+
setChecked(false);
|
|
4691
|
+
}, [checkId, correctIdsKey, targetIdsKey]);
|
|
3893
4692
|
const toggle = (id) => {
|
|
3894
4693
|
setSelected((prev) => {
|
|
3895
4694
|
const next = new Set(prev);
|
|
@@ -3899,13 +4698,26 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3899
4698
|
});
|
|
3900
4699
|
setChecked(false);
|
|
3901
4700
|
};
|
|
3902
|
-
const
|
|
4701
|
+
const maxScore = props.correctTargetIds.length || 1;
|
|
4702
|
+
const score = props.correctTargetIds.filter((id) => selected.has(id)).length;
|
|
4703
|
+
const wrongSelected = [...selected].filter((id) => !correctSet.has(id)).length;
|
|
4704
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore) && wrongSelected === 0;
|
|
4705
|
+
const isFactuallyCorrect = (sel) => {
|
|
4706
|
+
const wrong = [...sel].filter((id) => !correctSet.has(id)).length;
|
|
4707
|
+
const matched = props.correctTargetIds.filter((id) => sel.has(id)).length;
|
|
4708
|
+
return wrong === 0 && sel.size === props.correctTargetIds.length && matched === maxScore;
|
|
4709
|
+
};
|
|
4710
|
+
const factualCorrect = checked ? isFactuallyCorrect(selected) : false;
|
|
4711
|
+
const validTargetIds = useMemo21(
|
|
4712
|
+
() => new Set(props.targets.map((t) => t.id)),
|
|
4713
|
+
[props.targets]
|
|
4714
|
+
);
|
|
3903
4715
|
const handle = useMemo21(
|
|
3904
4716
|
() => buildAssessmentHandle({
|
|
3905
4717
|
checkId,
|
|
3906
|
-
getScore: () =>
|
|
3907
|
-
getMaxScore: () =>
|
|
3908
|
-
getAnswerGiven: () =>
|
|
4718
|
+
getScore: () => score,
|
|
4719
|
+
getMaxScore: () => maxScore,
|
|
4720
|
+
getAnswerGiven: () => checked,
|
|
3909
4721
|
resetTask: () => {
|
|
3910
4722
|
setSelected(/* @__PURE__ */ new Set());
|
|
3911
4723
|
setChecked(false);
|
|
@@ -3915,42 +4727,68 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3915
4727
|
checkId,
|
|
3916
4728
|
interactionType: INTERACTION12,
|
|
3917
4729
|
response: [...selected],
|
|
3918
|
-
correct: checked ?
|
|
3919
|
-
score: checked
|
|
3920
|
-
maxScore
|
|
4730
|
+
correct: checked ? factualCorrect : void 0,
|
|
4731
|
+
score: checked ? score : 0,
|
|
4732
|
+
maxScore
|
|
3921
4733
|
}),
|
|
3922
4734
|
getCurrentState: () => ({ selected: [...selected], checked }),
|
|
3923
4735
|
resume: (state) => {
|
|
3924
4736
|
const raw = state.selected;
|
|
3925
|
-
if (Array.isArray(raw))
|
|
4737
|
+
if (Array.isArray(raw)) {
|
|
4738
|
+
setSelected(
|
|
4739
|
+
new Set(
|
|
4740
|
+
raw.filter(
|
|
4741
|
+
(id) => typeof id === "string" && validTargetIds.has(id)
|
|
4742
|
+
)
|
|
4743
|
+
)
|
|
4744
|
+
);
|
|
4745
|
+
}
|
|
3926
4746
|
readBooleanStateField(state, "checked", setChecked);
|
|
3927
4747
|
}
|
|
3928
4748
|
}),
|
|
3929
|
-
[
|
|
4749
|
+
[
|
|
4750
|
+
checkId,
|
|
4751
|
+
checked,
|
|
4752
|
+
factualCorrect,
|
|
4753
|
+
maxScore,
|
|
4754
|
+
props.correctTargetIds,
|
|
4755
|
+
score,
|
|
4756
|
+
selected,
|
|
4757
|
+
validTargetIds
|
|
4758
|
+
]
|
|
3930
4759
|
);
|
|
3931
4760
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3932
4761
|
const submit = () => {
|
|
3933
4762
|
if (selected.size === 0 || checked) return;
|
|
4763
|
+
const correctAtSubmit = isFactuallyCorrect(selected);
|
|
3934
4764
|
setChecked(true);
|
|
3935
4765
|
assessment.answer({
|
|
3936
4766
|
checkId,
|
|
3937
4767
|
interactionType: INTERACTION12,
|
|
3938
4768
|
response: [...selected],
|
|
3939
|
-
correct
|
|
4769
|
+
correct: correctAtSubmit
|
|
3940
4770
|
});
|
|
3941
|
-
if (
|
|
4771
|
+
if (passedThreshold) {
|
|
3942
4772
|
assessment.complete({
|
|
3943
4773
|
checkId,
|
|
3944
4774
|
interactionType: INTERACTION12,
|
|
3945
|
-
score
|
|
3946
|
-
maxScore
|
|
3947
|
-
passingScore: props.passingScore ??
|
|
4775
|
+
score,
|
|
4776
|
+
maxScore,
|
|
4777
|
+
passingScore: props.passingScore ?? maxScore
|
|
4778
|
+
});
|
|
4779
|
+
} else if (props.enableRetry === false) {
|
|
4780
|
+
assessment.complete({
|
|
4781
|
+
checkId,
|
|
4782
|
+
interactionType: INTERACTION12,
|
|
4783
|
+
score,
|
|
4784
|
+
maxScore,
|
|
4785
|
+
passingScore: props.passingScore ?? maxScore
|
|
3948
4786
|
});
|
|
3949
4787
|
}
|
|
3950
4788
|
};
|
|
3951
4789
|
return /* @__PURE__ */ jsxs29("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
3952
4790
|
/* @__PURE__ */ jsxs29("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3953
|
-
/* @__PURE__ */ jsx32("img", { src:
|
|
4791
|
+
resolvedSrc ? /* @__PURE__ */ jsx32("img", { src: resolvedSrc, alt: props.alt, style: { maxWidth: "100%" } }) : /* @__PURE__ */ jsx32("p", { role: "alert", children: "This image URL is not allowed." }),
|
|
3954
4792
|
props.targets.map((t) => /* @__PURE__ */ jsx32(
|
|
3955
4793
|
"button",
|
|
3956
4794
|
{
|
|
@@ -3971,7 +4809,7 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3971
4809
|
))
|
|
3972
4810
|
] }),
|
|
3973
4811
|
/* @__PURE__ */ jsx32("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
3974
|
-
checked ? /* @__PURE__ */ jsx32("p", { role: "status", children:
|
|
4812
|
+
checked ? /* @__PURE__ */ jsx32("p", { role: "status", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
3975
4813
|
] });
|
|
3976
4814
|
}
|
|
3977
4815
|
var FindMultipleHotspotsInnerForwarded = forwardRef16(FindMultipleHotspotsInner);
|
|
@@ -3982,6 +4820,679 @@ var FindMultipleHotspots = forwardRef16(
|
|
|
3982
4820
|
);
|
|
3983
4821
|
setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
|
|
3984
4822
|
|
|
4823
|
+
// src/compound/useBranchingScenario.tsx
|
|
4824
|
+
import { createContext, useContext as useContext3 } from "react";
|
|
4825
|
+
import { jsx as jsx33 } from "react/jsx-runtime";
|
|
4826
|
+
var BranchingScenarioContext = createContext(null);
|
|
4827
|
+
function BranchingScenarioProvider(props) {
|
|
4828
|
+
return /* @__PURE__ */ jsx33(BranchingScenarioContext.Provider, { value: props.value, children: props.children });
|
|
4829
|
+
}
|
|
4830
|
+
function useBranchingScenario() {
|
|
4831
|
+
const ctx = useContext3(BranchingScenarioContext);
|
|
4832
|
+
if (!ctx) {
|
|
4833
|
+
throw new Error("useBranchingScenario must be used within BranchingScenario");
|
|
4834
|
+
}
|
|
4835
|
+
return ctx;
|
|
4836
|
+
}
|
|
4837
|
+
function useBranchingScenarioOptional() {
|
|
4838
|
+
return useContext3(BranchingScenarioContext);
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4841
|
+
// src/blocks/BranchingScenario.tsx
|
|
4842
|
+
import React34, { forwardRef as forwardRef17, useCallback as useCallback11, useEffect as useEffect27, useLayoutEffect as useLayoutEffect2, useMemo as useMemo22, useRef as useRef20, useState as useState26 } from "react";
|
|
4843
|
+
import { clampCompoundPageIndex as clampCompoundPageIndex3 } from "@lessonkit/core";
|
|
4844
|
+
|
|
4845
|
+
// src/compound/useCompoundBranchHandle.ts
|
|
4846
|
+
import { useCallback as useCallback10, useImperativeHandle } from "react";
|
|
4847
|
+
import { createCompoundResumeState as createCompoundResumeState4 } from "@lessonkit/core";
|
|
4848
|
+
function useCompoundBranchHandle(ref, opts) {
|
|
4849
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
4850
|
+
const {
|
|
4851
|
+
activePageIndex,
|
|
4852
|
+
getRegisteredHandles,
|
|
4853
|
+
visitedNodeIndices,
|
|
4854
|
+
choiceScores,
|
|
4855
|
+
meta,
|
|
4856
|
+
maxChoiceScore = 0,
|
|
4857
|
+
onResetMeta,
|
|
4858
|
+
enableSolutionsButton
|
|
4859
|
+
} = opts;
|
|
4860
|
+
const filterVisited = useCallback10(
|
|
4861
|
+
(handles) => {
|
|
4862
|
+
const filtered = [];
|
|
4863
|
+
for (const entry of handles) {
|
|
4864
|
+
const { pageIndex } = entry;
|
|
4865
|
+
if (pageIndex === void 0) continue;
|
|
4866
|
+
if (visitedNodeIndices.has(pageIndex)) filtered.push(entry);
|
|
4867
|
+
}
|
|
4868
|
+
return filtered;
|
|
4869
|
+
},
|
|
4870
|
+
[visitedNodeIndices]
|
|
4871
|
+
);
|
|
4872
|
+
useImperativeHandle(
|
|
4873
|
+
ref,
|
|
4874
|
+
() => ({
|
|
4875
|
+
getScore: () => {
|
|
4876
|
+
const assessment = aggregateAssessmentScores(filterVisited(getRegisteredHandles().values()));
|
|
4877
|
+
return assessment.score + sumChoiceScores(choiceScores);
|
|
4878
|
+
},
|
|
4879
|
+
getMaxScore: () => {
|
|
4880
|
+
const assessment = aggregateAssessmentScores(filterVisited(getRegisteredHandles().values()));
|
|
4881
|
+
return assessment.maxScore + maxChoiceScore;
|
|
4882
|
+
},
|
|
4883
|
+
getAnswerGiven: () => aggregateAssessmentScores(filterVisited(getRegisteredHandles().values())).allAnswered,
|
|
4884
|
+
resetTask: () => {
|
|
4885
|
+
onResetMeta();
|
|
4886
|
+
for (const entry of filterVisited(getRegisteredHandles().values())) {
|
|
4887
|
+
entry.handle.resetTask();
|
|
4888
|
+
}
|
|
4889
|
+
},
|
|
4890
|
+
showSolutions: () => {
|
|
4891
|
+
if (!enableSolutionsButton) return;
|
|
4892
|
+
for (const entry of filterVisited(getRegisteredHandles().values())) {
|
|
4893
|
+
entry.handle.showSolutions();
|
|
4894
|
+
}
|
|
4895
|
+
},
|
|
4896
|
+
getCurrentState: () => {
|
|
4897
|
+
const childStates = {};
|
|
4898
|
+
for (const [checkId, entry] of getRegisteredHandles()) {
|
|
4899
|
+
const { pageIndex } = entry;
|
|
4900
|
+
if (pageIndex === void 0 || !visitedNodeIndices.has(pageIndex)) continue;
|
|
4901
|
+
if (entry.handle.getCurrentState) {
|
|
4902
|
+
childStates[checkId] = entry.handle.getCurrentState();
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
return mergeBranchMetaIntoState(
|
|
4906
|
+
createCompoundResumeState4({ activePageIndex, childStates }),
|
|
4907
|
+
meta
|
|
4908
|
+
);
|
|
4909
|
+
},
|
|
4910
|
+
resume: (state) => {
|
|
4911
|
+
bridgeRef?.current?.notifyImperativeResume(state);
|
|
4912
|
+
}
|
|
4913
|
+
}),
|
|
4914
|
+
[
|
|
4915
|
+
activePageIndex,
|
|
4916
|
+
bridgeRef,
|
|
4917
|
+
choiceScores,
|
|
4918
|
+
enableSolutionsButton,
|
|
4919
|
+
filterVisited,
|
|
4920
|
+
getRegisteredHandles,
|
|
4921
|
+
meta,
|
|
4922
|
+
maxChoiceScore,
|
|
4923
|
+
onResetMeta,
|
|
4924
|
+
visitedNodeIndices
|
|
4925
|
+
]
|
|
4926
|
+
);
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
// src/compound/validateBranchGraph.ts
|
|
4930
|
+
import React33 from "react";
|
|
4931
|
+
import { validateBranchGraph } from "@lessonkit/core";
|
|
4932
|
+
function extractBranchGraph(nodes) {
|
|
4933
|
+
return nodes.map((node) => {
|
|
4934
|
+
const choices = [];
|
|
4935
|
+
React33.Children.forEach(node.props.children, (child) => {
|
|
4936
|
+
if (!React33.isValidElement(child)) return;
|
|
4937
|
+
if (getLessonkitBlockType(child.type) !== "BranchChoice") return;
|
|
4938
|
+
const targetNodeId = child.props.targetNodeId;
|
|
4939
|
+
if (typeof targetNodeId === "string") {
|
|
4940
|
+
choices.push({ targetNodeId: normalizeComponentId(targetNodeId, "blockId") });
|
|
4941
|
+
}
|
|
4942
|
+
});
|
|
4943
|
+
return {
|
|
4944
|
+
nodeId: normalizeComponentId(node.props.nodeId, "blockId"),
|
|
4945
|
+
terminal: Boolean(node.props.terminal),
|
|
4946
|
+
choices
|
|
4947
|
+
};
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
function validateBranchGraphAtMount(startNodeId, nodes, strict) {
|
|
4951
|
+
const graph = extractBranchGraph(nodes);
|
|
4952
|
+
const result = validateBranchGraph(startNodeId, graph.map(({ nodeId, choices }) => ({ nodeId, choices })));
|
|
4953
|
+
for (const node of graph) {
|
|
4954
|
+
if (node.terminal && node.choices.length > 0) {
|
|
4955
|
+
const msg = `[lessonkit] BranchingScenario: terminal node "${node.nodeId}" must not contain BranchChoice children`;
|
|
4956
|
+
if (strict || !isDevEnvironment()) {
|
|
4957
|
+
throw new Error(msg);
|
|
4958
|
+
}
|
|
4959
|
+
console.warn(msg);
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
for (const issue of result.issues) {
|
|
4963
|
+
const msg = `[lessonkit] BranchingScenario: ${issue.message}`;
|
|
4964
|
+
if (strict || !isDevEnvironment()) {
|
|
4965
|
+
throw new Error(msg);
|
|
4966
|
+
}
|
|
4967
|
+
console.warn(msg);
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
function buildNodeIndexMap(nodes) {
|
|
4971
|
+
const map = /* @__PURE__ */ new Map();
|
|
4972
|
+
nodes.forEach((node, index) => {
|
|
4973
|
+
map.set(normalizeComponentId(node.props.nodeId, "blockId"), index);
|
|
4974
|
+
});
|
|
4975
|
+
return map;
|
|
4976
|
+
}
|
|
4977
|
+
function buildNodeLabels(nodes) {
|
|
4978
|
+
const map = /* @__PURE__ */ new Map();
|
|
4979
|
+
for (const node of nodes) {
|
|
4980
|
+
const nodeId = normalizeComponentId(node.props.nodeId, "blockId");
|
|
4981
|
+
map.set(nodeId, node.props.title ?? nodeId);
|
|
4982
|
+
}
|
|
4983
|
+
return map;
|
|
4984
|
+
}
|
|
4985
|
+
function filterBranchNodeContent(children) {
|
|
4986
|
+
const filtered = [];
|
|
4987
|
+
React33.Children.forEach(children, (child) => {
|
|
4988
|
+
if (!React33.isValidElement(child)) {
|
|
4989
|
+
filtered.push(child);
|
|
4990
|
+
return;
|
|
4991
|
+
}
|
|
4992
|
+
if (getLessonkitBlockType(child.type) === "BranchChoice") return;
|
|
4993
|
+
filtered.push(child);
|
|
4994
|
+
});
|
|
4995
|
+
return filtered;
|
|
4996
|
+
}
|
|
4997
|
+
function nodeHasChoices(node) {
|
|
4998
|
+
let hasChoice = false;
|
|
4999
|
+
React33.Children.forEach(node.props.children, (child) => {
|
|
5000
|
+
if (!React33.isValidElement(child)) return;
|
|
5001
|
+
if (getLessonkitBlockType(child.type) === "BranchChoice") hasChoice = true;
|
|
5002
|
+
});
|
|
5003
|
+
return hasChoice;
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
// src/blocks/BranchingScenario.tsx
|
|
5007
|
+
import { jsx as jsx34, jsxs as jsxs30 } from "react/jsx-runtime";
|
|
5008
|
+
var BranchingScenarioInner = forwardRef17(function BranchingScenarioInner2(props, ref) {
|
|
5009
|
+
const { blockId, nodes, persistEnabled, startNodeId } = props;
|
|
5010
|
+
validateCompoundChildren("BranchingScenario", nodes);
|
|
5011
|
+
useLayoutEffect2(() => {
|
|
5012
|
+
validateBranchGraphAtMount(startNodeId, nodes);
|
|
5013
|
+
}, [startNodeId, nodes]);
|
|
5014
|
+
const { config, track, storage } = useLessonkit();
|
|
5015
|
+
const lessonId = useEnclosingLessonId();
|
|
5016
|
+
const ctx = useCompoundRegistry();
|
|
5017
|
+
const nodeIndexMap = useMemo22(() => buildNodeIndexMap(nodes), [nodes]);
|
|
5018
|
+
const nodeLabels = useMemo22(() => buildNodeLabels(nodes), [nodes]);
|
|
5019
|
+
const [meta, setMeta] = useState26(() => createInitialBranchMeta(startNodeId));
|
|
5020
|
+
const metaRef = useRef20(meta);
|
|
5021
|
+
const branchViewedRef = useRef20(/* @__PURE__ */ new Set());
|
|
5022
|
+
const legacyResumeWarnedRef = useRef20(false);
|
|
5023
|
+
const commitMeta = useCallback11((next) => {
|
|
5024
|
+
metaRef.current = next;
|
|
5025
|
+
setMeta(next);
|
|
5026
|
+
}, []);
|
|
5027
|
+
const activeIndex = nodeIndexMap.get(meta.activeNodeId) ?? 0;
|
|
5028
|
+
const visitedNodeIndices = useMemo22(() => {
|
|
5029
|
+
const indices = /* @__PURE__ */ new Set();
|
|
5030
|
+
for (const nodeId of meta.visitedNodeIds) {
|
|
5031
|
+
const i = nodeIndexMap.get(nodeId);
|
|
5032
|
+
if (i !== void 0) indices.add(i);
|
|
5033
|
+
}
|
|
5034
|
+
return indices;
|
|
5035
|
+
}, [meta.visitedNodeIds, nodeIndexMap]);
|
|
5036
|
+
const syncBranchViewedRef = useCallback11(
|
|
5037
|
+
(restoredMeta) => {
|
|
5038
|
+
const next = /* @__PURE__ */ new Set();
|
|
5039
|
+
for (const nodeId of restoredMeta.visitedNodeIds) {
|
|
5040
|
+
if (nodeId !== restoredMeta.activeNodeId) {
|
|
5041
|
+
next.add(`${blockId}:${nodeId}`);
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
branchViewedRef.current = next;
|
|
5045
|
+
},
|
|
5046
|
+
[blockId]
|
|
5047
|
+
);
|
|
5048
|
+
const applyResumeState = useCallback11(
|
|
5049
|
+
(state) => {
|
|
5050
|
+
const fromMeta = readBranchingScenarioMeta(state.childStates);
|
|
5051
|
+
if (fromMeta) {
|
|
5052
|
+
const sanitized = sanitizeBranchMeta(fromMeta, nodeIndexMap, startNodeId);
|
|
5053
|
+
commitMeta(sanitized);
|
|
5054
|
+
syncBranchViewedRef(sanitized);
|
|
5055
|
+
return;
|
|
5056
|
+
}
|
|
5057
|
+
const hasChildCheckStates = Object.keys(state.childStates).some((k) => k !== BS_META_KEY);
|
|
5058
|
+
const clampedIndex = clampCompoundPageIndex3(state.activePageIndex, nodes.length);
|
|
5059
|
+
const nodeAtIndex = nodes[clampedIndex];
|
|
5060
|
+
if (nodeAtIndex || hasChildCheckStates) {
|
|
5061
|
+
const nodeId = nodeAtIndex?.props.nodeId ?? startNodeId;
|
|
5062
|
+
const visitedNodeIds = [startNodeId];
|
|
5063
|
+
if (nodeId !== startNodeId) visitedNodeIds.push(nodeId);
|
|
5064
|
+
const legacyMeta = sanitizeBranchMeta(
|
|
5065
|
+
{ activeNodeId: nodeId, visitedNodeIds, choiceScores: {} },
|
|
5066
|
+
nodeIndexMap,
|
|
5067
|
+
startNodeId
|
|
5068
|
+
);
|
|
5069
|
+
commitMeta(legacyMeta);
|
|
5070
|
+
syncBranchViewedRef(legacyMeta);
|
|
5071
|
+
if (!legacyResumeWarnedRef.current && isDevEnvironment() && (hasChildCheckStates || state.activePageIndex !== 0)) {
|
|
5072
|
+
legacyResumeWarnedRef.current = true;
|
|
5073
|
+
console.warn(
|
|
5074
|
+
"[lessonkit] BranchingScenario: legacy save without branch meta; restored via activePageIndex and child states"
|
|
5075
|
+
);
|
|
5076
|
+
}
|
|
5077
|
+
return;
|
|
5078
|
+
}
|
|
5079
|
+
if (!legacyResumeWarnedRef.current && isDevEnvironment() && (state.activePageIndex !== 0 || Object.keys(state.childStates).length > 0)) {
|
|
5080
|
+
legacyResumeWarnedRef.current = true;
|
|
5081
|
+
console.warn(
|
|
5082
|
+
"[lessonkit] BranchingScenario: legacy save without branch meta; starting at startNodeId"
|
|
5083
|
+
);
|
|
5084
|
+
}
|
|
5085
|
+
const fresh = sanitizeBranchMeta(createInitialBranchMeta(startNodeId), nodeIndexMap, startNodeId);
|
|
5086
|
+
commitMeta(fresh);
|
|
5087
|
+
syncBranchViewedRef(fresh);
|
|
5088
|
+
},
|
|
5089
|
+
[commitMeta, nodeIndexMap, nodes, startNodeId, syncBranchViewedRef]
|
|
5090
|
+
);
|
|
5091
|
+
const resetBranchMeta = useCallback11(() => {
|
|
5092
|
+
commitMeta(createInitialBranchMeta(startNodeId));
|
|
5093
|
+
branchViewedRef.current = /* @__PURE__ */ new Set();
|
|
5094
|
+
}, [commitMeta, startNodeId]);
|
|
5095
|
+
const transformState = useCallback11(
|
|
5096
|
+
(state) => mergeBranchMetaIntoState(state, metaRef.current),
|
|
5097
|
+
[]
|
|
5098
|
+
);
|
|
5099
|
+
const shouldIncludeChildState = useCallback11(
|
|
5100
|
+
(_checkId, pageIndex) => pageIndex !== void 0 && visitedNodeIndices.has(pageIndex),
|
|
5101
|
+
[visitedNodeIndices]
|
|
5102
|
+
);
|
|
5103
|
+
useCompoundPersistence({
|
|
5104
|
+
courseId: config.courseId,
|
|
5105
|
+
compoundId: blockId,
|
|
5106
|
+
pageCount: nodes.length,
|
|
5107
|
+
index: activeIndex,
|
|
5108
|
+
setIndex: () => {
|
|
5109
|
+
},
|
|
5110
|
+
enabled: persistEnabled,
|
|
5111
|
+
storage,
|
|
5112
|
+
transformState,
|
|
5113
|
+
onCompoundResume: applyResumeState,
|
|
5114
|
+
shouldIncludeChildState
|
|
5115
|
+
});
|
|
5116
|
+
const maxChoiceScoreOnPath = useMemo22(() => {
|
|
5117
|
+
if (!meta.choiceScores) return 0;
|
|
5118
|
+
const branchFromIds = /* @__PURE__ */ new Set();
|
|
5119
|
+
for (const key of Object.keys(meta.choiceScores)) {
|
|
5120
|
+
const fromId = key.split(":")[0];
|
|
5121
|
+
if (fromId) branchFromIds.add(fromId);
|
|
5122
|
+
}
|
|
5123
|
+
let total = 0;
|
|
5124
|
+
for (const node of nodes) {
|
|
5125
|
+
const nodeId = node.props.nodeId;
|
|
5126
|
+
if (!branchFromIds.has(nodeId)) continue;
|
|
5127
|
+
let maxWeight = 0;
|
|
5128
|
+
let found = false;
|
|
5129
|
+
for (const child of React34.Children.toArray(node.props.children)) {
|
|
5130
|
+
if (!React34.isValidElement(child)) continue;
|
|
5131
|
+
const weight = child.props.scoreWeight;
|
|
5132
|
+
if (typeof weight === "number" && Number.isFinite(weight)) {
|
|
5133
|
+
found = true;
|
|
5134
|
+
maxWeight = Math.max(maxWeight, weight);
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
if (found) total += maxWeight;
|
|
5138
|
+
}
|
|
5139
|
+
return total;
|
|
5140
|
+
}, [meta.choiceScores, nodes]);
|
|
5141
|
+
useCompoundBranchHandle(ref, {
|
|
5142
|
+
activePageIndex: activeIndex,
|
|
5143
|
+
getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
|
|
5144
|
+
visitedNodeIndices,
|
|
5145
|
+
choiceScores: meta.choiceScores ?? {},
|
|
5146
|
+
meta,
|
|
5147
|
+
maxChoiceScore: maxChoiceScoreOnPath,
|
|
5148
|
+
onResetMeta: resetBranchMeta,
|
|
5149
|
+
enableSolutionsButton: props.enableSolutionsButton
|
|
5150
|
+
});
|
|
5151
|
+
const activeNode = nodes[activeIndex];
|
|
5152
|
+
const isTerminal = Boolean(activeNode?.props.terminal) || (activeNode ? !nodeHasChoices(activeNode) && meta.activeNodeId !== startNodeId : false);
|
|
5153
|
+
const visitedLabels = useMemo22(
|
|
5154
|
+
() => meta.visitedNodeIds.map((id) => ({
|
|
5155
|
+
nodeId: id,
|
|
5156
|
+
label: nodeLabels.get(id) ?? id
|
|
5157
|
+
})),
|
|
5158
|
+
[meta.visitedNodeIds, nodeLabels]
|
|
5159
|
+
);
|
|
5160
|
+
useEffect27(() => {
|
|
5161
|
+
if (!lessonId || !activeNode) return;
|
|
5162
|
+
const dedupeKey = `${blockId}:${meta.activeNodeId}`;
|
|
5163
|
+
if (branchViewedRef.current.has(dedupeKey)) return;
|
|
5164
|
+
branchViewedRef.current.add(dedupeKey);
|
|
5165
|
+
track(
|
|
5166
|
+
"branch_node_viewed",
|
|
5167
|
+
{
|
|
5168
|
+
blockId,
|
|
5169
|
+
nodeId: meta.activeNodeId,
|
|
5170
|
+
nodeIndex: activeIndex,
|
|
5171
|
+
nodeTitle: activeNode.props.title
|
|
5172
|
+
},
|
|
5173
|
+
{ lessonId }
|
|
5174
|
+
);
|
|
5175
|
+
}, [activeIndex, activeNode, blockId, lessonId, meta.activeNodeId, track]);
|
|
5176
|
+
const navigateToNode = useCallback11(
|
|
5177
|
+
(opts) => {
|
|
5178
|
+
const toNodeId = normalizeComponentId(opts.toNodeId, "blockId");
|
|
5179
|
+
const fromNodeId = normalizeComponentId(opts.fromNodeId, "blockId");
|
|
5180
|
+
if (!nodeIndexMap.has(toNodeId)) {
|
|
5181
|
+
if (isDevEnvironment()) {
|
|
5182
|
+
console.warn(
|
|
5183
|
+
`[lessonkit] BranchingScenario: unknown targetNodeId "${toNodeId}" from "${fromNodeId}"`
|
|
5184
|
+
);
|
|
5185
|
+
}
|
|
5186
|
+
return;
|
|
5187
|
+
}
|
|
5188
|
+
const activeNodeId = metaRef.current.activeNodeId;
|
|
5189
|
+
if (fromNodeId !== activeNodeId) {
|
|
5190
|
+
if (isDevEnvironment()) {
|
|
5191
|
+
console.warn(
|
|
5192
|
+
`[lessonkit] BranchingScenario: navigateToNode from "${fromNodeId}" but active node is "${activeNodeId}"`
|
|
5193
|
+
);
|
|
5194
|
+
}
|
|
5195
|
+
return;
|
|
5196
|
+
}
|
|
5197
|
+
if (lessonId) {
|
|
5198
|
+
track(
|
|
5199
|
+
"branch_selected",
|
|
5200
|
+
{
|
|
5201
|
+
blockId,
|
|
5202
|
+
fromNodeId,
|
|
5203
|
+
toNodeId,
|
|
5204
|
+
label: opts.label,
|
|
5205
|
+
scoreWeight: opts.scoreWeight
|
|
5206
|
+
},
|
|
5207
|
+
{ lessonId }
|
|
5208
|
+
);
|
|
5209
|
+
}
|
|
5210
|
+
setMeta((prev) => {
|
|
5211
|
+
const choiceScores = applyChoiceScoreUpdate(
|
|
5212
|
+
prev.choiceScores,
|
|
5213
|
+
fromNodeId,
|
|
5214
|
+
toNodeId,
|
|
5215
|
+
opts.scoreWeight
|
|
5216
|
+
);
|
|
5217
|
+
const visited = prev.visitedNodeIds.includes(toNodeId) ? prev.visitedNodeIds : [...prev.visitedNodeIds, toNodeId];
|
|
5218
|
+
const next = sanitizeBranchMeta(
|
|
5219
|
+
{
|
|
5220
|
+
activeNodeId: toNodeId,
|
|
5221
|
+
visitedNodeIds: visited,
|
|
5222
|
+
choiceScores
|
|
5223
|
+
},
|
|
5224
|
+
nodeIndexMap,
|
|
5225
|
+
startNodeId
|
|
5226
|
+
);
|
|
5227
|
+
metaRef.current = next;
|
|
5228
|
+
return next;
|
|
5229
|
+
});
|
|
5230
|
+
},
|
|
5231
|
+
[blockId, lessonId, nodeIndexMap, startNodeId, track]
|
|
5232
|
+
);
|
|
5233
|
+
const choicesLocked = isTerminal;
|
|
5234
|
+
const contextValue = useMemo22(
|
|
5235
|
+
() => ({
|
|
5236
|
+
compoundBlockId: blockId,
|
|
5237
|
+
activeNodeId: meta.activeNodeId,
|
|
5238
|
+
visitedNodeIds: meta.visitedNodeIds,
|
|
5239
|
+
visitedLabels: visitedLabels.map((entry) => entry.label),
|
|
5240
|
+
navigateToNode,
|
|
5241
|
+
isTerminal,
|
|
5242
|
+
choicesLocked
|
|
5243
|
+
}),
|
|
5244
|
+
[blockId, choicesLocked, isTerminal, meta.activeNodeId, meta.visitedNodeIds, navigateToNode, visitedLabels]
|
|
5245
|
+
);
|
|
5246
|
+
const pathScore = ctx ? Array.from(ctx.getRegisteredHandles().values()).filter((h) => h.pageIndex !== void 0 && visitedNodeIndices.has(h.pageIndex)).reduce((s, h) => s + h.handle.getScore(), 0) + sumChoiceScores(meta.choiceScores) : 0;
|
|
5247
|
+
const pathMaxScore = ctx ? Array.from(ctx.getRegisteredHandles().values()).filter((h) => h.pageIndex !== void 0 && visitedNodeIndices.has(h.pageIndex)).reduce((s, h) => s + h.handle.getMaxScore(), 0) + maxChoiceScoreOnPath : 0;
|
|
5248
|
+
return /* @__PURE__ */ jsx34(BranchingScenarioProvider, { value: contextValue, children: /* @__PURE__ */ jsxs30("section", { "aria-label": props.title, "data-testid": "branching-scenario", "data-lk-block-id": blockId, children: [
|
|
5249
|
+
/* @__PURE__ */ jsx34("h3", { children: props.title }),
|
|
5250
|
+
props.showPathScore && ctx ? /* @__PURE__ */ jsxs30("p", { "data-testid": "branch-score", children: [
|
|
5251
|
+
"Score: ",
|
|
5252
|
+
pathScore,
|
|
5253
|
+
" / ",
|
|
5254
|
+
pathMaxScore
|
|
5255
|
+
] }) : null,
|
|
5256
|
+
props.showPathRecap && isTerminal && meta.visitedNodeIds.length > 0 ? /* @__PURE__ */ jsxs30("aside", { "data-testid": "branch-path-recap", "aria-label": "Your path", children: [
|
|
5257
|
+
/* @__PURE__ */ jsx34("h4", { children: "Your path" }),
|
|
5258
|
+
/* @__PURE__ */ jsx34("ol", { children: visitedLabels.map((entry) => /* @__PURE__ */ jsx34("li", { children: entry.label }, entry.nodeId)) })
|
|
5259
|
+
] }) : null,
|
|
5260
|
+
/* @__PURE__ */ jsx34("div", { "data-testid": "branching-scenario-active-node", children: nodes.map((node, i) => {
|
|
5261
|
+
const content = React34.Children.map(node.props.children, (child) => {
|
|
5262
|
+
if (!React34.isValidElement(child)) return child;
|
|
5263
|
+
if (getLessonkitBlockType(child.type) !== "BranchChoice") return child;
|
|
5264
|
+
return React34.cloneElement(child, {
|
|
5265
|
+
fromNodeId: node.props.nodeId
|
|
5266
|
+
});
|
|
5267
|
+
});
|
|
5268
|
+
return React34.cloneElement(node, {
|
|
5269
|
+
key: node.key ?? node.props.nodeId,
|
|
5270
|
+
hidden: i !== activeIndex,
|
|
5271
|
+
nodeIndex: i,
|
|
5272
|
+
children: content
|
|
5273
|
+
});
|
|
5274
|
+
}) })
|
|
5275
|
+
] }) });
|
|
5276
|
+
});
|
|
5277
|
+
var BranchingScenario = forwardRef17(
|
|
5278
|
+
function BranchingScenario2(props, ref) {
|
|
5279
|
+
const nodes = React34.Children.toArray(props.children).filter(
|
|
5280
|
+
React34.isValidElement
|
|
5281
|
+
);
|
|
5282
|
+
const { config } = useLessonkit();
|
|
5283
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
5284
|
+
requireCompoundBlockIdWhenPersisting({
|
|
5285
|
+
persistEnabled,
|
|
5286
|
+
blockId: props.blockId,
|
|
5287
|
+
componentName: "BranchingScenario"
|
|
5288
|
+
});
|
|
5289
|
+
const blockId = useMemo22(
|
|
5290
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
5291
|
+
[props.blockId]
|
|
5292
|
+
);
|
|
5293
|
+
const startNodeId = useMemo22(
|
|
5294
|
+
() => normalizeComponentId(props.startNodeId, "blockId"),
|
|
5295
|
+
[props.startNodeId]
|
|
5296
|
+
);
|
|
5297
|
+
const nodeIndexMap = useMemo22(() => buildNodeIndexMap(nodes), [nodes]);
|
|
5298
|
+
const initialIndex = nodeIndexMap.get(startNodeId) ?? 0;
|
|
5299
|
+
const hydrationKey = `${config.courseId ?? "no-course"}:${blockId}`;
|
|
5300
|
+
const setIndexStable = useCallback11(() => {
|
|
5301
|
+
}, []);
|
|
5302
|
+
return /* @__PURE__ */ jsx34(CompoundProvider, { activePageIndex: initialIndex, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx34(
|
|
5303
|
+
BranchingScenarioInner,
|
|
5304
|
+
{
|
|
5305
|
+
...props,
|
|
5306
|
+
startNodeId,
|
|
5307
|
+
ref,
|
|
5308
|
+
blockId,
|
|
5309
|
+
nodes,
|
|
5310
|
+
persistEnabled
|
|
5311
|
+
},
|
|
5312
|
+
hydrationKey
|
|
5313
|
+
) });
|
|
5314
|
+
}
|
|
5315
|
+
);
|
|
5316
|
+
setLessonkitBlockType(BranchingScenario, "BranchingScenario");
|
|
5317
|
+
|
|
5318
|
+
// src/blocks/BranchNode.tsx
|
|
5319
|
+
import "react";
|
|
5320
|
+
import { jsx as jsx35, jsxs as jsxs31 } from "react/jsx-runtime";
|
|
5321
|
+
function BranchNode(props) {
|
|
5322
|
+
validateCompoundChildren("BranchNode", filterBranchNodeContent(props.children));
|
|
5323
|
+
return /* @__PURE__ */ jsxs31(
|
|
5324
|
+
"section",
|
|
5325
|
+
{
|
|
5326
|
+
"aria-label": props.title ?? props.nodeId,
|
|
5327
|
+
"data-lk-node-id": props.nodeId,
|
|
5328
|
+
"data-testid": `branch-node-${props.nodeId}`,
|
|
5329
|
+
hidden: props.hidden ? true : void 0,
|
|
5330
|
+
style: props.hidden ? { display: "none" } : void 0,
|
|
5331
|
+
children: [
|
|
5332
|
+
props.title ? /* @__PURE__ */ jsx35("h4", { children: props.title }) : null,
|
|
5333
|
+
/* @__PURE__ */ jsx35(CompoundPageIndexProvider, { pageIndex: props.nodeIndex ?? 0, children: /* @__PURE__ */ jsx35("div", { children: props.children }) })
|
|
5334
|
+
]
|
|
5335
|
+
}
|
|
5336
|
+
);
|
|
5337
|
+
}
|
|
5338
|
+
setLessonkitBlockType(BranchNode, "BranchNode");
|
|
5339
|
+
|
|
5340
|
+
// src/blocks/BranchChoice.tsx
|
|
5341
|
+
import { useId as useId4 } from "react";
|
|
5342
|
+
import { jsx as jsx36 } from "react/jsx-runtime";
|
|
5343
|
+
function BranchChoice(props) {
|
|
5344
|
+
const ctx = useBranchingScenarioOptional();
|
|
5345
|
+
const groupId = useId4();
|
|
5346
|
+
const fromNodeId = props.fromNodeId ?? ctx?.activeNodeId ?? "";
|
|
5347
|
+
const isActiveNode = ctx ? ctx.activeNodeId === fromNodeId : true;
|
|
5348
|
+
const locked = ctx?.choicesLocked ?? false;
|
|
5349
|
+
const onSelect = () => {
|
|
5350
|
+
if (!ctx || !fromNodeId || props.disabled || locked || !isActiveNode) return;
|
|
5351
|
+
ctx.navigateToNode({
|
|
5352
|
+
fromNodeId,
|
|
5353
|
+
toNodeId: props.targetNodeId,
|
|
5354
|
+
label: props.label,
|
|
5355
|
+
scoreWeight: props.scoreWeight
|
|
5356
|
+
});
|
|
5357
|
+
};
|
|
5358
|
+
return /* @__PURE__ */ jsx36(
|
|
5359
|
+
"button",
|
|
5360
|
+
{
|
|
5361
|
+
type: "button",
|
|
5362
|
+
role: "radio",
|
|
5363
|
+
"aria-checked": false,
|
|
5364
|
+
"aria-labelledby": groupId,
|
|
5365
|
+
"data-testid": `branch-choice-${props.targetNodeId}`,
|
|
5366
|
+
disabled: props.disabled || locked || !isActiveNode,
|
|
5367
|
+
onClick: onSelect,
|
|
5368
|
+
children: /* @__PURE__ */ jsx36("span", { id: groupId, children: props.label })
|
|
5369
|
+
}
|
|
5370
|
+
);
|
|
5371
|
+
}
|
|
5372
|
+
setLessonkitBlockType(BranchChoice, "BranchChoice");
|
|
5373
|
+
|
|
5374
|
+
// src/blocks/Embed.tsx
|
|
5375
|
+
import { useEffect as useEffect28 } from "react";
|
|
5376
|
+
import { jsx as jsx37, jsxs as jsxs32 } from "react/jsx-runtime";
|
|
5377
|
+
function Embed(props) {
|
|
5378
|
+
const blockId = normalizeComponentId(props.blockId, "blockId");
|
|
5379
|
+
const { config, track } = useLessonkit();
|
|
5380
|
+
const lessonId = useEnclosingLessonId();
|
|
5381
|
+
const resolvedSrc = resolveEmbedSrc(props.src, {
|
|
5382
|
+
allowedHosts: config.embed?.allowedHosts
|
|
5383
|
+
});
|
|
5384
|
+
const sandbox = buildEmbedSandbox(props.allow, {
|
|
5385
|
+
restrictPopupsInProduction: config.embed?.restrictPopupsInProduction ?? true
|
|
5386
|
+
});
|
|
5387
|
+
const aspectRatio = resolveEmbedAspectRatio(props.aspectRatio);
|
|
5388
|
+
useEffect28(() => {
|
|
5389
|
+
if (!resolvedSrc) return;
|
|
5390
|
+
track(
|
|
5391
|
+
"interaction",
|
|
5392
|
+
{ kind: "embed_viewed", blockId, src: telemetryEmbedSrc(resolvedSrc) },
|
|
5393
|
+
lessonId ? { lessonId } : void 0
|
|
5394
|
+
);
|
|
5395
|
+
}, [blockId, lessonId, resolvedSrc, track]);
|
|
5396
|
+
if (!resolvedSrc) {
|
|
5397
|
+
return /* @__PURE__ */ jsx37("figure", { "data-lk-block-id": blockId, "data-testid": `embed-${blockId}`, children: /* @__PURE__ */ jsx37("p", { role: "alert", children: "This embed URL is not allowed." }) });
|
|
5398
|
+
}
|
|
5399
|
+
return /* @__PURE__ */ jsxs32("figure", { "data-lk-block-id": blockId, "data-testid": `embed-${blockId}`, children: [
|
|
5400
|
+
/* @__PURE__ */ jsx37(
|
|
5401
|
+
"iframe",
|
|
5402
|
+
{
|
|
5403
|
+
title: props.title,
|
|
5404
|
+
src: resolvedSrc,
|
|
5405
|
+
sandbox,
|
|
5406
|
+
referrerPolicy: "no-referrer",
|
|
5407
|
+
style: aspectRatio ? { aspectRatio, width: "100%", border: 0 } : { width: "100%", border: 0 }
|
|
5408
|
+
}
|
|
5409
|
+
),
|
|
5410
|
+
/* @__PURE__ */ jsx37("figcaption", { className: "lk-visually-hidden", children: props.title })
|
|
5411
|
+
] });
|
|
5412
|
+
}
|
|
5413
|
+
setLessonkitBlockType(Embed, "Embed");
|
|
5414
|
+
|
|
5415
|
+
// src/blocks/Chart.tsx
|
|
5416
|
+
import { useEffect as useEffect29, useMemo as useMemo23 } from "react";
|
|
5417
|
+
|
|
5418
|
+
// src/blocks/chartUtils.ts
|
|
5419
|
+
function normalizeChartType(type) {
|
|
5420
|
+
if (type === "bar" || type === "pie") return type;
|
|
5421
|
+
if (type !== void 0 && isDevEnvironment()) {
|
|
5422
|
+
console.warn(`[lessonkit] Chart: unknown type "${type}"; rendering data table only.`);
|
|
5423
|
+
}
|
|
5424
|
+
return type === void 0 ? "pie" : "table";
|
|
5425
|
+
}
|
|
5426
|
+
function normalizeChartData(data) {
|
|
5427
|
+
if (!Array.isArray(data)) return [];
|
|
5428
|
+
const rows = [];
|
|
5429
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
5430
|
+
const row = data[i];
|
|
5431
|
+
if (!row || typeof row !== "object") continue;
|
|
5432
|
+
const label = typeof row.label === "string" ? row.label : `Item ${i + 1}`;
|
|
5433
|
+
const raw = Number(row.value);
|
|
5434
|
+
const value = Number.isFinite(raw) && raw >= 0 ? raw : 0;
|
|
5435
|
+
rows.push({ label, value, key: `${label}-${i}` });
|
|
5436
|
+
}
|
|
5437
|
+
return rows;
|
|
5438
|
+
}
|
|
5439
|
+
function chartMaxValue(rows) {
|
|
5440
|
+
if (rows.length === 0) return 1;
|
|
5441
|
+
return Math.max(...rows.map((row) => row.value), 1);
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5444
|
+
// src/blocks/Chart.tsx
|
|
5445
|
+
import { jsx as jsx38, jsxs as jsxs33 } from "react/jsx-runtime";
|
|
5446
|
+
function Chart(props) {
|
|
5447
|
+
const blockId = normalizeComponentId(props.blockId, "blockId");
|
|
5448
|
+
const { track } = useLessonkit();
|
|
5449
|
+
const lessonId = useEnclosingLessonId();
|
|
5450
|
+
const chartType = normalizeChartType(props.type);
|
|
5451
|
+
const rows = useMemo23(() => normalizeChartData(props.data), [props.data]);
|
|
5452
|
+
const max = useMemo23(() => chartMaxValue(rows), [rows]);
|
|
5453
|
+
useEffect29(() => {
|
|
5454
|
+
track(
|
|
5455
|
+
"interaction",
|
|
5456
|
+
{ kind: "chart_viewed", blockId, chartType },
|
|
5457
|
+
lessonId ? { lessonId } : void 0
|
|
5458
|
+
);
|
|
5459
|
+
}, [blockId, chartType, lessonId, track]);
|
|
5460
|
+
return /* @__PURE__ */ jsxs33("figure", { "data-lk-block-id": blockId, "data-testid": `chart-${blockId}`, children: [
|
|
5461
|
+
props.title ? /* @__PURE__ */ jsx38("figcaption", { children: props.title }) : null,
|
|
5462
|
+
rows.length === 0 ? /* @__PURE__ */ jsx38("p", { "data-testid": "chart-empty", children: "No chart data." }) : chartType === "table" ? null : chartType === "bar" ? /* @__PURE__ */ jsx38("div", { role: "img", "aria-label": props.title ?? "Bar chart", "aria-describedby": `${blockId}-table`, children: rows.map((datum) => /* @__PURE__ */ jsxs33("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
|
|
5463
|
+
/* @__PURE__ */ jsx38("span", { style: { minWidth: "6rem" }, children: datum.label }),
|
|
5464
|
+
/* @__PURE__ */ jsx38(
|
|
5465
|
+
"div",
|
|
5466
|
+
{
|
|
5467
|
+
style: {
|
|
5468
|
+
height: "1rem",
|
|
5469
|
+
width: `${datum.value / max * 100}%`,
|
|
5470
|
+
background: "var(--lk-color-primary, #2563eb)"
|
|
5471
|
+
},
|
|
5472
|
+
"aria-hidden": true
|
|
5473
|
+
}
|
|
5474
|
+
),
|
|
5475
|
+
/* @__PURE__ */ jsx38("span", { children: datum.value })
|
|
5476
|
+
] }, datum.key)) }) : /* @__PURE__ */ jsx38("ul", { role: "list", "aria-label": props.title ?? "Pie chart segments", children: rows.map((datum) => /* @__PURE__ */ jsxs33("li", { children: [
|
|
5477
|
+
datum.label,
|
|
5478
|
+
": ",
|
|
5479
|
+
datum.value
|
|
5480
|
+
] }, datum.key)) }),
|
|
5481
|
+
/* @__PURE__ */ jsxs33("table", { id: `${blockId}-table`, children: [
|
|
5482
|
+
/* @__PURE__ */ jsx38("caption", { children: props.title ?? "Chart data" }),
|
|
5483
|
+
/* @__PURE__ */ jsx38("thead", { children: /* @__PURE__ */ jsxs33("tr", { children: [
|
|
5484
|
+
/* @__PURE__ */ jsx38("th", { scope: "col", children: "Label" }),
|
|
5485
|
+
/* @__PURE__ */ jsx38("th", { scope: "col", children: "Value" })
|
|
5486
|
+
] }) }),
|
|
5487
|
+
/* @__PURE__ */ jsx38("tbody", { children: rows.map((datum) => /* @__PURE__ */ jsxs33("tr", { children: [
|
|
5488
|
+
/* @__PURE__ */ jsx38("th", { scope: "row", children: datum.label }),
|
|
5489
|
+
/* @__PURE__ */ jsx38("td", { children: datum.value })
|
|
5490
|
+
] }, datum.key)) })
|
|
5491
|
+
] })
|
|
5492
|
+
] });
|
|
5493
|
+
}
|
|
5494
|
+
setLessonkitBlockType(Chart, "Chart");
|
|
5495
|
+
|
|
3985
5496
|
export {
|
|
3986
5497
|
TrueFalse,
|
|
3987
5498
|
MarkTheWords,
|
|
@@ -4014,5 +5525,11 @@ export {
|
|
|
4014
5525
|
ImageHotspots,
|
|
4015
5526
|
ImageSlider,
|
|
4016
5527
|
FindHotspot,
|
|
4017
|
-
FindMultipleHotspots
|
|
5528
|
+
FindMultipleHotspots,
|
|
5529
|
+
useBranchingScenario,
|
|
5530
|
+
BranchingScenario,
|
|
5531
|
+
BranchNode,
|
|
5532
|
+
BranchChoice,
|
|
5533
|
+
Embed,
|
|
5534
|
+
Chart
|
|
4018
5535
|
};
|