@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
package/dist/blocks-entry.cjs
CHANGED
|
@@ -33,9 +33,14 @@ __export(blocks_entry_exports, {
|
|
|
33
33
|
Accordion: () => Accordion,
|
|
34
34
|
ArithmeticQuiz: () => ArithmeticQuiz,
|
|
35
35
|
AssessmentSequence: () => AssessmentSequence,
|
|
36
|
+
BranchChoice: () => BranchChoice,
|
|
37
|
+
BranchNode: () => BranchNode,
|
|
38
|
+
BranchingScenario: () => BranchingScenario,
|
|
39
|
+
Chart: () => Chart,
|
|
36
40
|
DialogCards: () => DialogCards,
|
|
37
41
|
DragAndDrop: () => DragAndDrop,
|
|
38
42
|
DragTheWords: () => DragTheWords,
|
|
43
|
+
Embed: () => Embed,
|
|
39
44
|
Essay: () => Essay,
|
|
40
45
|
FillInTheBlanks: () => FillInTheBlanks,
|
|
41
46
|
FindHotspot: () => FindHotspot,
|
|
@@ -336,9 +341,6 @@ var import_core10 = require("@lessonkit/core");
|
|
|
336
341
|
// src/runtime/observability.ts
|
|
337
342
|
var import_xapi = require("@lessonkit/xapi");
|
|
338
343
|
|
|
339
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
340
|
-
var import_xapi5 = require("@lessonkit/xapi");
|
|
341
|
-
|
|
342
344
|
// src/runtime/emitTelemetry.ts
|
|
343
345
|
var import_core4 = require("@lessonkit/core");
|
|
344
346
|
|
|
@@ -349,36 +351,45 @@ var import_xapi2 = require("@lessonkit/xapi");
|
|
|
349
351
|
// src/runtime/lxpackBridge.ts
|
|
350
352
|
var import_bridge = require("@lessonkit/lxpack/bridge");
|
|
351
353
|
|
|
352
|
-
// src/runtime/
|
|
353
|
-
var
|
|
354
|
+
// src/runtime/courseStartedPipeline.ts
|
|
355
|
+
var import_xapi3 = require("@lessonkit/xapi");
|
|
354
356
|
|
|
355
|
-
// src/
|
|
356
|
-
var
|
|
357
|
+
// src/runtime/plugins.ts
|
|
358
|
+
var import_core5 = require("@lessonkit/core");
|
|
359
|
+
function buildPluginContext(opts) {
|
|
360
|
+
return (0, import_core5.buildPluginContext)(opts);
|
|
361
|
+
}
|
|
357
362
|
|
|
358
|
-
// src/runtime/
|
|
363
|
+
// src/runtime/session.ts
|
|
359
364
|
var import_core6 = require("@lessonkit/core");
|
|
360
365
|
|
|
361
|
-
// src/
|
|
362
|
-
|
|
366
|
+
// src/provider/courseStarted/emit.ts
|
|
367
|
+
function createCourseStartedFlightScope() {
|
|
368
|
+
return {
|
|
369
|
+
trackingFlights: /* @__PURE__ */ new Map(),
|
|
370
|
+
emitFlights: /* @__PURE__ */ new Map()
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
var defaultFlightScope = createCourseStartedFlightScope();
|
|
363
374
|
|
|
364
|
-
// src/
|
|
375
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
376
|
+
var import_xapi5 = require("@lessonkit/xapi");
|
|
377
|
+
|
|
378
|
+
// src/runtime/ports.ts
|
|
365
379
|
var import_core7 = require("@lessonkit/core");
|
|
366
380
|
|
|
367
|
-
// src/
|
|
368
|
-
var
|
|
381
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
382
|
+
var import_core11 = require("@lessonkit/core");
|
|
369
383
|
|
|
370
|
-
// src/runtime/
|
|
384
|
+
// src/runtime/progress.ts
|
|
371
385
|
var import_core8 = require("@lessonkit/core");
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
386
|
+
|
|
387
|
+
// src/runtime/xapi.ts
|
|
388
|
+
var import_xapi4 = require("@lessonkit/xapi");
|
|
375
389
|
|
|
376
390
|
// src/runtime/telemetry.ts
|
|
377
391
|
var import_core9 = require("@lessonkit/core");
|
|
378
392
|
|
|
379
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
380
|
-
var defaultStorage = (0, import_core5.createSessionStoragePort)();
|
|
381
|
-
|
|
382
393
|
// src/context.tsx
|
|
383
394
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
384
395
|
var LessonkitContext = (0, import_react8.createContext)(null);
|
|
@@ -418,14 +429,15 @@ function meetsPassingThreshold(score, maxScore, passingScore) {
|
|
|
418
429
|
}
|
|
419
430
|
function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
|
|
420
431
|
const maxScore = custom?.maxScore ?? fallbackMax;
|
|
432
|
+
const hasNumericScore = custom?.score != null && Number.isFinite(custom.score);
|
|
433
|
+
if (hasNumericScore) {
|
|
434
|
+
const passed2 = custom.passed !== void 0 ? custom.passed : meetsPassingThreshold(custom.score, maxScore, passingScore);
|
|
435
|
+
return { score: custom.score, maxScore, passed: passed2 };
|
|
436
|
+
}
|
|
421
437
|
if (custom?.passed !== void 0) {
|
|
422
|
-
const score2 = custom.passed ?
|
|
438
|
+
const score2 = custom.passed ? maxScore : 0;
|
|
423
439
|
return { score: score2, maxScore, passed: custom.passed };
|
|
424
440
|
}
|
|
425
|
-
if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
426
|
-
const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
427
|
-
return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
|
|
428
|
-
}
|
|
429
441
|
const score = fallbackCorrect ? maxScore : 0;
|
|
430
442
|
const passed = meetsPassingThreshold(score, maxScore, passingScore);
|
|
431
443
|
return { score, maxScore, passed };
|
|
@@ -500,7 +512,7 @@ function TrueFalseInner(props, ref) {
|
|
|
500
512
|
if (passed) {
|
|
501
513
|
return { score: completedScore ?? maxScore, maxScore };
|
|
502
514
|
}
|
|
503
|
-
if (selectionCorrect) {
|
|
515
|
+
if (selected !== null && selectionCorrect) {
|
|
504
516
|
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
505
517
|
}
|
|
506
518
|
return { score: 0, maxScore };
|
|
@@ -572,7 +584,9 @@ function TrueFalseInner(props, ref) {
|
|
|
572
584
|
if (nextPassed) {
|
|
573
585
|
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
574
586
|
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
575
|
-
|
|
587
|
+
if (config.tracking?.replayResumeEvents === true) {
|
|
588
|
+
replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
|
|
589
|
+
}
|
|
576
590
|
}
|
|
577
591
|
}
|
|
578
592
|
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
@@ -588,7 +602,8 @@ function TrueFalseInner(props, ref) {
|
|
|
588
602
|
props.question,
|
|
589
603
|
selected,
|
|
590
604
|
selectionCorrect,
|
|
591
|
-
showSolutions
|
|
605
|
+
showSolutions,
|
|
606
|
+
config.tracking?.replayResumeEvents
|
|
592
607
|
]
|
|
593
608
|
);
|
|
594
609
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
@@ -617,6 +632,17 @@ function TrueFalseInner(props, ref) {
|
|
|
617
632
|
maxScore: scored.maxScore,
|
|
618
633
|
passingScore: props.passingScore ?? scored.maxScore
|
|
619
634
|
});
|
|
635
|
+
} else if (!scored.passed && props.enableRetry === false && !completedRef.current) {
|
|
636
|
+
completedRef.current = true;
|
|
637
|
+
setCompletedScore(scored.score);
|
|
638
|
+
setCompletedMaxScore(scored.maxScore);
|
|
639
|
+
assessment.complete({
|
|
640
|
+
checkId,
|
|
641
|
+
interactionType: INTERACTION,
|
|
642
|
+
score: scored.score,
|
|
643
|
+
maxScore: scored.maxScore,
|
|
644
|
+
passingScore: props.passingScore ?? scored.maxScore
|
|
645
|
+
});
|
|
620
646
|
}
|
|
621
647
|
};
|
|
622
648
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
@@ -682,11 +708,13 @@ function MarkTheWordsInner(props, ref) {
|
|
|
682
708
|
);
|
|
683
709
|
const [marked, setMarked] = (0, import_react13.useState)(() => /* @__PURE__ */ new Set());
|
|
684
710
|
const [passed, setPassed] = (0, import_react13.useState)(false);
|
|
711
|
+
const [submitted, setSubmitted] = (0, import_react13.useState)(false);
|
|
685
712
|
const [showSolutions, setShowSolutions] = (0, import_react13.useState)(false);
|
|
686
713
|
const completedRef = (0, import_react13.useRef)(false);
|
|
687
714
|
const reset = () => {
|
|
688
715
|
completedRef.current = false;
|
|
689
716
|
setPassed(false);
|
|
717
|
+
setSubmitted(false);
|
|
690
718
|
setMarked(/* @__PURE__ */ new Set());
|
|
691
719
|
setShowSolutions(false);
|
|
692
720
|
};
|
|
@@ -744,6 +772,27 @@ function MarkTheWordsInner(props, ref) {
|
|
|
744
772
|
return next;
|
|
745
773
|
});
|
|
746
774
|
};
|
|
775
|
+
const submitMarks = () => {
|
|
776
|
+
if (!hasTargets || completedRef.current || marked.size === 0) return;
|
|
777
|
+
completedRef.current = true;
|
|
778
|
+
setSubmitted(true);
|
|
779
|
+
const didPass = passedThreshold;
|
|
780
|
+
if (didPass) setPassed(true);
|
|
781
|
+
assessment.answer({
|
|
782
|
+
checkId,
|
|
783
|
+
interactionType: INTERACTION2,
|
|
784
|
+
question: props.text,
|
|
785
|
+
response: [...marked].map((i) => tokens[i]),
|
|
786
|
+
correct: didPass
|
|
787
|
+
});
|
|
788
|
+
assessment.complete({
|
|
789
|
+
checkId,
|
|
790
|
+
interactionType: INTERACTION2,
|
|
791
|
+
score,
|
|
792
|
+
maxScore,
|
|
793
|
+
passingScore: props.passingScore ?? maxScore
|
|
794
|
+
});
|
|
795
|
+
};
|
|
747
796
|
(0, import_react13.useEffect)(() => {
|
|
748
797
|
if (!hasTargets) {
|
|
749
798
|
if (isDevEnvironment()) {
|
|
@@ -754,8 +803,10 @@ function MarkTheWordsInner(props, ref) {
|
|
|
754
803
|
}
|
|
755
804
|
return;
|
|
756
805
|
}
|
|
806
|
+
if (props.enableRetry === false) return;
|
|
757
807
|
if (!passedThreshold || completedRef.current) return;
|
|
758
808
|
completedRef.current = true;
|
|
809
|
+
setSubmitted(true);
|
|
759
810
|
setPassed(true);
|
|
760
811
|
assessment.answer({
|
|
761
812
|
checkId,
|
|
@@ -778,6 +829,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
778
829
|
marked,
|
|
779
830
|
maxScore,
|
|
780
831
|
passedThreshold,
|
|
832
|
+
props.enableRetry,
|
|
781
833
|
props.passingScore,
|
|
782
834
|
props.correctWords,
|
|
783
835
|
props.text,
|
|
@@ -815,7 +867,8 @@ function MarkTheWordsInner(props, ref) {
|
|
|
815
867
|
i
|
|
816
868
|
);
|
|
817
869
|
}) }),
|
|
818
|
-
|
|
870
|
+
passedThreshold ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
871
|
+
props.enableRetry === false && hasTargets && marked.size > 0 && !submitted ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", "data-testid": "mark-the-words-submit", onClick: submitMarks, children: "Submit" }) : null,
|
|
819
872
|
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
820
873
|
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
821
874
|
] });
|
|
@@ -856,11 +909,43 @@ function parseTemplate(template) {
|
|
|
856
909
|
blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
|
|
857
910
|
};
|
|
858
911
|
}
|
|
912
|
+
function normalizeBlanks(blanks) {
|
|
913
|
+
return blanks.map((b) => ({ id: b.id.trim(), answer: b.answer.trim() })).filter((b) => b.id.length > 0 && b.answer.length > 0);
|
|
914
|
+
}
|
|
915
|
+
function resolveBlanks(template, explicitBlanks) {
|
|
916
|
+
const parsed = parseTemplate(template);
|
|
917
|
+
if (!explicitBlanks) {
|
|
918
|
+
return { parts: parsed.parts, blanks: parsed.blanks };
|
|
919
|
+
}
|
|
920
|
+
const normalized = normalizeBlanks(explicitBlanks);
|
|
921
|
+
if (normalized.length !== parsed.blanks.length) {
|
|
922
|
+
if (isDevEnvironment()) {
|
|
923
|
+
console.warn(
|
|
924
|
+
"[lessonkit] FillInTheBlanks: blanks length does not match template; using parsed blanks",
|
|
925
|
+
{ templateBlanks: parsed.blanks.length, explicitBlanks: normalized.length }
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
return { parts: parsed.parts, blanks: parsed.blanks };
|
|
929
|
+
}
|
|
930
|
+
let blankIdx = 0;
|
|
931
|
+
const interleavedParts = parsed.parts.map((part) => {
|
|
932
|
+
if (part.startsWith("blank-")) {
|
|
933
|
+
const id = normalized[blankIdx]?.id;
|
|
934
|
+
blankIdx += 1;
|
|
935
|
+
return id ?? part;
|
|
936
|
+
}
|
|
937
|
+
return part;
|
|
938
|
+
});
|
|
939
|
+
return { parts: interleavedParts, blanks: normalized };
|
|
940
|
+
}
|
|
859
941
|
function FillInTheBlanksInner(props, ref) {
|
|
860
942
|
const checkId = (0, import_react14.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
861
943
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
862
|
-
const
|
|
863
|
-
const blanks =
|
|
944
|
+
const { config } = useLessonkit();
|
|
945
|
+
const { parts, blanks } = (0, import_react14.useMemo)(
|
|
946
|
+
() => resolveBlanks(props.template, props.blanks),
|
|
947
|
+
[props.template, props.blanks]
|
|
948
|
+
);
|
|
864
949
|
const [values, setValues] = (0, import_react14.useState)(
|
|
865
950
|
() => Object.fromEntries(blanks.map((b) => [b.id, ""]))
|
|
866
951
|
);
|
|
@@ -959,13 +1044,14 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
959
1044
|
blanks.forEach((b) => {
|
|
960
1045
|
if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
|
|
961
1046
|
});
|
|
962
|
-
|
|
1047
|
+
if (config.tracking?.replayResumeEvents === true) {
|
|
1048
|
+
replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
|
|
1049
|
+
}
|
|
963
1050
|
}
|
|
964
1051
|
}),
|
|
965
|
-
[allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
|
|
1052
|
+
[allFilled, assessment, blanks, checkId, config.tracking?.replayResumeEvents, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
|
|
966
1053
|
);
|
|
967
|
-
|
|
968
|
-
const check = () => {
|
|
1054
|
+
const check = (0, import_react14.useCallback)(() => {
|
|
969
1055
|
if (!hasBlanks) {
|
|
970
1056
|
if (isDevEnvironment()) {
|
|
971
1057
|
console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
|
|
@@ -973,7 +1059,7 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
973
1059
|
return;
|
|
974
1060
|
}
|
|
975
1061
|
if (!allFilled) return;
|
|
976
|
-
if (passed) return;
|
|
1062
|
+
if (passed && !props.enableRetry) return;
|
|
977
1063
|
const snapshot = JSON.stringify(values);
|
|
978
1064
|
if (checkSnapshotRef.current === snapshot) return;
|
|
979
1065
|
checkSnapshotRef.current = snapshot;
|
|
@@ -986,9 +1072,9 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
986
1072
|
response: values,
|
|
987
1073
|
correct: passedThreshold
|
|
988
1074
|
});
|
|
989
|
-
if (passedThreshold && !completedRef.current) {
|
|
1075
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
990
1076
|
completedRef.current = true;
|
|
991
|
-
setPassed(true);
|
|
1077
|
+
if (passedThreshold) setPassed(true);
|
|
992
1078
|
assessment.complete({
|
|
993
1079
|
checkId,
|
|
994
1080
|
interactionType: INTERACTION3,
|
|
@@ -997,7 +1083,22 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
997
1083
|
passingScore: props.passingScore ?? maxScore
|
|
998
1084
|
});
|
|
999
1085
|
}
|
|
1000
|
-
}
|
|
1086
|
+
}, [
|
|
1087
|
+
allFilled,
|
|
1088
|
+
assessment,
|
|
1089
|
+
blanks.length,
|
|
1090
|
+
checkId,
|
|
1091
|
+
hasBlanks,
|
|
1092
|
+
maxScore,
|
|
1093
|
+
passed,
|
|
1094
|
+
passedThreshold,
|
|
1095
|
+
props.enableRetry,
|
|
1096
|
+
props.passingScore,
|
|
1097
|
+
props.template,
|
|
1098
|
+
score,
|
|
1099
|
+
values
|
|
1100
|
+
]);
|
|
1101
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1001
1102
|
(0, import_react14.useEffect)(() => {
|
|
1002
1103
|
if (!allFilled) {
|
|
1003
1104
|
answeredRef.current = false;
|
|
@@ -1007,14 +1108,17 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1007
1108
|
}, [allFilled]);
|
|
1008
1109
|
(0, import_react14.useEffect)(() => {
|
|
1009
1110
|
if (props.autoCheck && allFilled && !passed) check();
|
|
1010
|
-
}, [allFilled,
|
|
1111
|
+
}, [allFilled, check, passed, props.autoCheck]);
|
|
1011
1112
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1012
1113
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
1013
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children:
|
|
1114
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: parts.map((part, i) => {
|
|
1014
1115
|
const blank = blanks.find((b) => b.id === part);
|
|
1015
1116
|
if (!blank) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react14.default.Fragment, { children: part }, i);
|
|
1016
1117
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { margin: "0 0.25em" }, children: [
|
|
1017
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.
|
|
1118
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "lk-visually-hidden", children: [
|
|
1119
|
+
"Blank ",
|
|
1120
|
+
blank.id
|
|
1121
|
+
] }),
|
|
1018
1122
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1019
1123
|
"input",
|
|
1020
1124
|
{
|
|
@@ -1031,7 +1135,16 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1031
1135
|
)
|
|
1032
1136
|
] }, blank.id);
|
|
1033
1137
|
}) }),
|
|
1034
|
-
!props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1138
|
+
!props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1139
|
+
"button",
|
|
1140
|
+
{
|
|
1141
|
+
type: "button",
|
|
1142
|
+
"data-testid": "check-blanks",
|
|
1143
|
+
disabled: !allFilled || passed && !props.enableRetry,
|
|
1144
|
+
onClick: check,
|
|
1145
|
+
children: "Check"
|
|
1146
|
+
}
|
|
1147
|
+
) : null,
|
|
1035
1148
|
!hasBlanks ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
1036
1149
|
submitted ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
1037
1150
|
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
@@ -1056,6 +1169,7 @@ function parseZones(template) {
|
|
|
1056
1169
|
function DragTheWordsInner(props, ref) {
|
|
1057
1170
|
const checkId = (0, import_react15.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1058
1171
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1172
|
+
const { config } = useLessonkit();
|
|
1059
1173
|
const { parts, answers } = (0, import_react15.useMemo)(() => parseZones(props.template), [props.template]);
|
|
1060
1174
|
const [zones, setZones] = (0, import_react15.useState)(
|
|
1061
1175
|
() => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
|
|
@@ -1160,10 +1274,12 @@ function DragTheWordsInner(props, ref) {
|
|
|
1160
1274
|
answers.forEach((ans, i) => {
|
|
1161
1275
|
if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
|
|
1162
1276
|
});
|
|
1163
|
-
|
|
1277
|
+
if (config.tracking?.replayResumeEvents === true) {
|
|
1278
|
+
replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
|
|
1279
|
+
}
|
|
1164
1280
|
}
|
|
1165
1281
|
}),
|
|
1166
|
-
[allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
|
|
1282
|
+
[allFilled, answers, assessment, checkId, config.tracking?.replayResumeEvents, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
|
|
1167
1283
|
);
|
|
1168
1284
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1169
1285
|
const placeInZone = (zoneId, word) => {
|
|
@@ -1206,9 +1322,9 @@ function DragTheWordsInner(props, ref) {
|
|
|
1206
1322
|
response: zones,
|
|
1207
1323
|
correct: passedThreshold
|
|
1208
1324
|
});
|
|
1209
|
-
if (passedThreshold && !completedRef.current) {
|
|
1325
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
1210
1326
|
completedRef.current = true;
|
|
1211
|
-
setPassed(true);
|
|
1327
|
+
if (passedThreshold) setPassed(true);
|
|
1212
1328
|
assessment.complete({
|
|
1213
1329
|
checkId,
|
|
1214
1330
|
interactionType: INTERACTION4,
|
|
@@ -1270,7 +1386,16 @@ function DragTheWordsInner(props, ref) {
|
|
|
1270
1386
|
part
|
|
1271
1387
|
);
|
|
1272
1388
|
}) }),
|
|
1273
|
-
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1389
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1390
|
+
"button",
|
|
1391
|
+
{
|
|
1392
|
+
type: "button",
|
|
1393
|
+
"data-testid": "check-drag-words",
|
|
1394
|
+
disabled: !allFilled || passed && !props.enableRetry,
|
|
1395
|
+
onClick: check,
|
|
1396
|
+
children: "Check"
|
|
1397
|
+
}
|
|
1398
|
+
),
|
|
1274
1399
|
!hasZones ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
1275
1400
|
submitted ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
1276
1401
|
] });
|
|
@@ -1284,6 +1409,37 @@ var DragTheWords = (0, import_react15.forwardRef)(function DragTheWords2(props,
|
|
|
1284
1409
|
var import_react16 = require("react");
|
|
1285
1410
|
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1286
1411
|
var INTERACTION5 = "dragAndDrop";
|
|
1412
|
+
function normalizeDragAndDropState(rawAssignments, rawPool, items, targets) {
|
|
1413
|
+
const itemIds = new Set(items.map((i) => i.id));
|
|
1414
|
+
const targetIds = targets.map((t) => t.id);
|
|
1415
|
+
const assignments = Object.fromEntries(targetIds.map((id) => [id, ""]));
|
|
1416
|
+
if (rawAssignments && typeof rawAssignments === "object") {
|
|
1417
|
+
for (const targetId of targetIds) {
|
|
1418
|
+
const value = rawAssignments[targetId];
|
|
1419
|
+
if (typeof value === "string" && (value === "" || itemIds.has(value))) {
|
|
1420
|
+
assignments[targetId] = value;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const assigned = new Set(Object.values(assignments).filter(Boolean));
|
|
1425
|
+
const pool = [];
|
|
1426
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1427
|
+
if (Array.isArray(rawPool)) {
|
|
1428
|
+
for (const id of rawPool) {
|
|
1429
|
+
if (typeof id === "string" && itemIds.has(id) && !assigned.has(id) && !seen.has(id)) {
|
|
1430
|
+
pool.push(id);
|
|
1431
|
+
seen.add(id);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
for (const item of items) {
|
|
1436
|
+
if (!assigned.has(item.id) && !seen.has(item.id)) {
|
|
1437
|
+
pool.push(item.id);
|
|
1438
|
+
seen.add(item.id);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return { assignments, pool };
|
|
1442
|
+
}
|
|
1287
1443
|
function DragAndDropInner(props, ref) {
|
|
1288
1444
|
const checkId = (0, import_react16.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1289
1445
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
@@ -1333,11 +1489,14 @@ function DragAndDropInner(props, ref) {
|
|
|
1333
1489
|
}),
|
|
1334
1490
|
getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
|
|
1335
1491
|
resume: (state) => {
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1492
|
+
const normalized = normalizeDragAndDropState(
|
|
1493
|
+
state.assignments,
|
|
1494
|
+
state.pool,
|
|
1495
|
+
props.items,
|
|
1496
|
+
props.targets
|
|
1497
|
+
);
|
|
1498
|
+
setAssignments(normalized.assignments);
|
|
1499
|
+
setPool(normalized.pool);
|
|
1341
1500
|
readBooleanStateField(state, "passed", (value) => {
|
|
1342
1501
|
setPassed(value);
|
|
1343
1502
|
completedRef.current = value;
|
|
@@ -1370,9 +1529,9 @@ function DragAndDropInner(props, ref) {
|
|
|
1370
1529
|
response: assignments,
|
|
1371
1530
|
correct: passedThreshold
|
|
1372
1531
|
});
|
|
1373
|
-
if (passedThreshold && !completedRef.current) {
|
|
1532
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
1374
1533
|
completedRef.current = true;
|
|
1375
|
-
setPassed(true);
|
|
1534
|
+
if (passedThreshold) setPassed(true);
|
|
1376
1535
|
assessment.complete({
|
|
1377
1536
|
checkId,
|
|
1378
1537
|
interactionType: INTERACTION5,
|
|
@@ -1435,7 +1594,16 @@ function DragAndDropInner(props, ref) {
|
|
|
1435
1594
|
)
|
|
1436
1595
|
] }, target.id);
|
|
1437
1596
|
}) }),
|
|
1438
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1597
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1598
|
+
"button",
|
|
1599
|
+
{
|
|
1600
|
+
type: "button",
|
|
1601
|
+
"data-testid": "check-drag-drop",
|
|
1602
|
+
disabled: !hasTargets || !allFilled || passed && !props.enableRetry,
|
|
1603
|
+
onClick: check,
|
|
1604
|
+
children: "Check"
|
|
1605
|
+
}
|
|
1606
|
+
),
|
|
1439
1607
|
checked ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
1440
1608
|
] });
|
|
1441
1609
|
}
|
|
@@ -1446,11 +1614,11 @@ var DragAndDrop = (0, import_react16.forwardRef)(function DragAndDrop2(props, re
|
|
|
1446
1614
|
|
|
1447
1615
|
// src/blocks/AssessmentSequence.tsx
|
|
1448
1616
|
var import_react22 = __toESM(require("react"), 1);
|
|
1449
|
-
var
|
|
1617
|
+
var import_core19 = require("@lessonkit/core");
|
|
1450
1618
|
|
|
1451
1619
|
// src/compound/useCompoundShell.ts
|
|
1452
1620
|
var import_react20 = require("react");
|
|
1453
|
-
var
|
|
1621
|
+
var import_core17 = require("@lessonkit/core");
|
|
1454
1622
|
|
|
1455
1623
|
// src/compound/useCompoundNavigation.ts
|
|
1456
1624
|
var import_react17 = require("react");
|
|
@@ -1474,28 +1642,132 @@ function useCompoundNavigation(pageCount, index, setIndex) {
|
|
|
1474
1642
|
|
|
1475
1643
|
// src/compound/useCompoundPersistence.ts
|
|
1476
1644
|
var import_react19 = require("react");
|
|
1477
|
-
var
|
|
1645
|
+
var import_core16 = require("@lessonkit/core");
|
|
1646
|
+
|
|
1647
|
+
// src/compound/compoundHydration.ts
|
|
1648
|
+
var hydratedKeys = /* @__PURE__ */ new Set();
|
|
1649
|
+
function compoundHydrationKey(courseId, compoundId) {
|
|
1650
|
+
return `${courseId}:${compoundId}`;
|
|
1651
|
+
}
|
|
1652
|
+
function markCompoundHydrated(key) {
|
|
1653
|
+
hydratedKeys.add(key);
|
|
1654
|
+
}
|
|
1655
|
+
function isCompoundHydrated(key) {
|
|
1656
|
+
return hydratedKeys.has(key);
|
|
1657
|
+
}
|
|
1658
|
+
function clearCompoundHydrated(key) {
|
|
1659
|
+
hydratedKeys.delete(key);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// src/compound/useCompoundBranchShell.ts
|
|
1663
|
+
var import_core12 = require("@lessonkit/core");
|
|
1664
|
+
var BS_META_KEY = "__lk_bs__";
|
|
1665
|
+
function createInitialBranchMeta(startNodeId) {
|
|
1666
|
+
return { activeNodeId: startNodeId, visitedNodeIds: [startNodeId] };
|
|
1667
|
+
}
|
|
1668
|
+
function readBranchingScenarioMeta(childStates) {
|
|
1669
|
+
const raw = childStates[BS_META_KEY];
|
|
1670
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1671
|
+
const activeNodeId = typeof raw.activeNodeId === "string" ? raw.activeNodeId : "";
|
|
1672
|
+
const visitedNodeIds = Array.isArray(raw.visitedNodeIds) ? raw.visitedNodeIds.filter((id) => typeof id === "string") : [];
|
|
1673
|
+
const choiceScores = raw.choiceScores && typeof raw.choiceScores === "object" && !Array.isArray(raw.choiceScores) ? raw.choiceScores : void 0;
|
|
1674
|
+
if (!activeNodeId) return null;
|
|
1675
|
+
return { activeNodeId, visitedNodeIds, choiceScores };
|
|
1676
|
+
}
|
|
1677
|
+
function sanitizeBranchMeta(meta, nodeIndexMap, startNodeId, validChoiceKeys) {
|
|
1678
|
+
const knownIds = new Set(nodeIndexMap.keys());
|
|
1679
|
+
const activeNodeId = knownIds.has(meta.activeNodeId) ? meta.activeNodeId : startNodeId;
|
|
1680
|
+
const visitedNodeIds = meta.visitedNodeIds.filter((id) => knownIds.has(id));
|
|
1681
|
+
if (!visitedNodeIds.includes(activeNodeId)) {
|
|
1682
|
+
visitedNodeIds.push(activeNodeId);
|
|
1683
|
+
}
|
|
1684
|
+
if (visitedNodeIds.length === 0) {
|
|
1685
|
+
visitedNodeIds.push(startNodeId);
|
|
1686
|
+
}
|
|
1687
|
+
const choiceScores = meta.choiceScores ? Object.fromEntries(
|
|
1688
|
+
Object.entries(meta.choiceScores).filter(
|
|
1689
|
+
([key]) => validChoiceKeys ? validChoiceKeys.has(key) : knownIds.has(key.split(":")[0] ?? "")
|
|
1690
|
+
)
|
|
1691
|
+
) : void 0;
|
|
1692
|
+
return {
|
|
1693
|
+
activeNodeId,
|
|
1694
|
+
visitedNodeIds,
|
|
1695
|
+
...Object.keys(choiceScores ?? {}).length > 0 ? { choiceScores } : {}
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function mergeBranchMetaIntoState(state, meta) {
|
|
1699
|
+
return {
|
|
1700
|
+
...state,
|
|
1701
|
+
childStates: {
|
|
1702
|
+
...state.childStates,
|
|
1703
|
+
[BS_META_KEY]: meta
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function choiceScoreKey(fromNodeId, toNodeId) {
|
|
1708
|
+
return `${fromNodeId}:${toNodeId}`;
|
|
1709
|
+
}
|
|
1710
|
+
function applyChoiceScoreUpdate(prev, fromNodeId, toNodeId, scoreWeight) {
|
|
1711
|
+
if (scoreWeight === void 0) return prev;
|
|
1712
|
+
const next = { ...prev ?? {} };
|
|
1713
|
+
for (const key of Object.keys(next)) {
|
|
1714
|
+
if (key.startsWith(`${fromNodeId}:`)) {
|
|
1715
|
+
delete next[key];
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
next[choiceScoreKey(fromNodeId, toNodeId)] = scoreWeight;
|
|
1719
|
+
return next;
|
|
1720
|
+
}
|
|
1721
|
+
function sumChoiceScores(choiceScores) {
|
|
1722
|
+
if (!choiceScores) return 0;
|
|
1723
|
+
return Object.values(choiceScores).reduce((sum, value) => sum + (Number.isFinite(value) ? value : 0), 0);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/compound/useCompoundVideoShell.ts
|
|
1727
|
+
var import_core13 = require("@lessonkit/core");
|
|
1728
|
+
var IV_META_KEY = "__lk_iv__";
|
|
1729
|
+
function readInteractiveVideoMeta(childStates) {
|
|
1730
|
+
const raw = childStates[IV_META_KEY];
|
|
1731
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1732
|
+
const currentTime = typeof raw.currentTime === "number" ? raw.currentTime : 0;
|
|
1733
|
+
const completedCueIndices = Array.isArray(raw.completedCueIndices) ? raw.completedCueIndices.filter((n) => typeof n === "number") : [];
|
|
1734
|
+
const firedCueIndices = Array.isArray(raw.firedCueIndices) ? raw.firedCueIndices.filter((n) => typeof n === "number") : completedCueIndices;
|
|
1735
|
+
return { currentTime, completedCueIndices, firedCueIndices };
|
|
1736
|
+
}
|
|
1737
|
+
function mergeVideoMetaIntoState(state, meta) {
|
|
1738
|
+
return {
|
|
1739
|
+
...state,
|
|
1740
|
+
childStates: {
|
|
1741
|
+
...state.childStates,
|
|
1742
|
+
[IV_META_KEY]: meta
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1478
1746
|
|
|
1479
1747
|
// src/compound/resumeChildHandles.ts
|
|
1480
|
-
|
|
1748
|
+
var DEFAULT_PRESERVED_CHILD_STATE_KEYS = /* @__PURE__ */ new Set([BS_META_KEY, IV_META_KEY]);
|
|
1749
|
+
function registerablePendingKeys(childStates, preserveKeys = DEFAULT_PRESERVED_CHILD_STATE_KEYS) {
|
|
1750
|
+
return Object.keys(childStates).filter((key) => !preserveKeys.has(key));
|
|
1751
|
+
}
|
|
1752
|
+
function filterRegisteredChildStates(handles, childStates, preserveKeys = DEFAULT_PRESERVED_CHILD_STATE_KEYS) {
|
|
1481
1753
|
const filtered = {};
|
|
1482
1754
|
for (const [key, value] of Object.entries(childStates)) {
|
|
1483
|
-
if (handles.has(key)) {
|
|
1755
|
+
if (preserveKeys.has(key) || handles.has(key)) {
|
|
1484
1756
|
filtered[key] = value;
|
|
1485
1757
|
}
|
|
1486
1758
|
}
|
|
1487
1759
|
return filtered;
|
|
1488
1760
|
}
|
|
1489
1761
|
function resumeChildHandles(handles, childStates, opts) {
|
|
1490
|
-
const pendingKeys = Object.keys(childStates);
|
|
1491
1762
|
const alreadyResumed = opts?.alreadyResumed;
|
|
1492
|
-
|
|
1763
|
+
const registerableKeys = registerablePendingKeys(childStates);
|
|
1764
|
+
if (opts?.waitForHandles && registerableKeys.length > 0) {
|
|
1493
1765
|
if (handles.size === 0) return false;
|
|
1494
|
-
const registeredPending =
|
|
1766
|
+
const registeredPending = registerableKeys.filter((k) => handles.has(k));
|
|
1495
1767
|
if (registeredPending.length === 0) {
|
|
1496
1768
|
return false;
|
|
1497
1769
|
}
|
|
1498
|
-
if (registeredPending.length <
|
|
1770
|
+
if (registeredPending.length < registerableKeys.length) {
|
|
1499
1771
|
for (const key of registeredPending) {
|
|
1500
1772
|
if (alreadyResumed?.has(key)) continue;
|
|
1501
1773
|
const handle = handles.get(key);
|
|
@@ -1521,24 +1793,36 @@ function resumeChildHandles(handles, childStates, opts) {
|
|
|
1521
1793
|
|
|
1522
1794
|
// src/compound/useCompoundResume.ts
|
|
1523
1795
|
var import_react18 = require("react");
|
|
1524
|
-
var
|
|
1525
|
-
var
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1796
|
+
var import_core14 = require("@lessonkit/core");
|
|
1797
|
+
var import_core15 = require("@lessonkit/core");
|
|
1798
|
+
|
|
1799
|
+
// src/compound/compoundLoadOpts.ts
|
|
1800
|
+
function compoundLoadOpts(ctx, compoundId) {
|
|
1801
|
+
const onCorruptHook = ctx?.config?.observability?.onCompoundResumeCorrupt;
|
|
1802
|
+
if (!onCorruptHook) return void 0;
|
|
1803
|
+
return {
|
|
1804
|
+
onCorrupt: () => onCorruptHook({ compoundId, corrupt: true }),
|
|
1805
|
+
onDroppedChildKeys: (droppedChildKeys) => onCorruptHook({ compoundId, droppedChildKeys })
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/compound/useCompoundResume.ts
|
|
1810
|
+
var warnedCompoundPersistFailures = /* @__PURE__ */ new Set();
|
|
1811
|
+
function warnCompoundPersistFailure(compoundId) {
|
|
1812
|
+
if (warnedCompoundPersistFailures.has(compoundId) || !isDevEnvironment()) return;
|
|
1813
|
+
warnedCompoundPersistFailures.add(compoundId);
|
|
1530
1814
|
console.warn(
|
|
1531
|
-
|
|
1815
|
+
`[lessonkit] compound resume state for "${compoundId}" could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload.`
|
|
1532
1816
|
);
|
|
1533
1817
|
}
|
|
1534
1818
|
function useCompoundResume(opts) {
|
|
1535
1819
|
const lessonkitCtx = (0, import_react18.useContext)(LessonkitContext);
|
|
1536
|
-
const storageRef = (0, import_react18.useRef)(opts.storage ?? lessonkitCtx?.storage ?? (0,
|
|
1820
|
+
const storageRef = (0, import_react18.useRef)(opts.storage ?? lessonkitCtx?.storage ?? (0, import_core15.createSessionStoragePort)());
|
|
1537
1821
|
const resumedRef = (0, import_react18.useRef)(false);
|
|
1538
1822
|
const resumeKeyRef = (0, import_react18.useRef)("");
|
|
1539
1823
|
const prevEnabledRef = (0, import_react18.useRef)(opts.enabled);
|
|
1540
1824
|
(0, import_react18.useEffect)(() => {
|
|
1541
|
-
storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? (0,
|
|
1825
|
+
storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? (0, import_core15.createSessionStoragePort)();
|
|
1542
1826
|
}, [opts.storage, lessonkitCtx?.storage]);
|
|
1543
1827
|
(0, import_react18.useEffect)(() => {
|
|
1544
1828
|
if (!prevEnabledRef.current && opts.enabled) {
|
|
@@ -1551,7 +1835,16 @@ function useCompoundResume(opts) {
|
|
|
1551
1835
|
resumedRef.current = false;
|
|
1552
1836
|
}
|
|
1553
1837
|
if (!opts.enabled || !opts.courseId || resumedRef.current) return;
|
|
1554
|
-
|
|
1838
|
+
if (isCompoundHydrated(key)) {
|
|
1839
|
+
resumedRef.current = true;
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
const saved = (0, import_core14.loadCompoundState)(
|
|
1843
|
+
storageRef.current,
|
|
1844
|
+
opts.courseId,
|
|
1845
|
+
opts.compoundId,
|
|
1846
|
+
compoundLoadOpts(lessonkitCtx, opts.compoundId)
|
|
1847
|
+
);
|
|
1555
1848
|
if (saved) {
|
|
1556
1849
|
resumedRef.current = true;
|
|
1557
1850
|
opts.onResume?.(saved);
|
|
@@ -1560,26 +1853,31 @@ function useCompoundResume(opts) {
|
|
|
1560
1853
|
return (0, import_react18.useCallback)(
|
|
1561
1854
|
(state) => {
|
|
1562
1855
|
if (!opts.enabled || !opts.courseId) return;
|
|
1563
|
-
const persisted = (0,
|
|
1564
|
-
if (!persisted) warnCompoundPersistFailure();
|
|
1856
|
+
const persisted = (0, import_core14.saveCompoundState)(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
1857
|
+
if (!persisted) warnCompoundPersistFailure(opts.compoundId);
|
|
1565
1858
|
},
|
|
1566
1859
|
[opts.enabled, opts.courseId, opts.compoundId]
|
|
1567
1860
|
);
|
|
1568
1861
|
}
|
|
1569
1862
|
|
|
1570
1863
|
// src/compound/useCompoundPersistence.ts
|
|
1571
|
-
|
|
1864
|
+
var MAX_HYDRATION_RETRIES = 10;
|
|
1865
|
+
function isEmptyResumeState(state) {
|
|
1866
|
+
if (!state || typeof state !== "object") return true;
|
|
1867
|
+
return Object.keys(state).length === 0;
|
|
1868
|
+
}
|
|
1869
|
+
function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = (0, import_core16.createSessionStoragePort)()) {
|
|
1572
1870
|
if (!enabled || !courseId || pageCount < 1) return 0;
|
|
1573
|
-
const saved = (0,
|
|
1871
|
+
const saved = (0, import_core16.loadCompoundState)(storage, courseId, compoundId);
|
|
1574
1872
|
if (!saved) return 0;
|
|
1575
|
-
return (0,
|
|
1873
|
+
return (0, import_core16.clampCompoundPageIndex)(saved.activePageIndex, pageCount);
|
|
1576
1874
|
}
|
|
1577
1875
|
function stripOrphanChildStates(handles, childStates) {
|
|
1578
1876
|
return filterRegisteredChildStates(handles, childStates);
|
|
1579
1877
|
}
|
|
1580
1878
|
function useCompoundPersistence(opts) {
|
|
1581
1879
|
const lessonkitCtx = (0, import_react19.useContext)(LessonkitContext);
|
|
1582
|
-
const storage = opts.storage ?? lessonkitCtx?.storage ?? (0,
|
|
1880
|
+
const storage = opts.storage ?? lessonkitCtx?.storage ?? (0, import_core16.createSessionStoragePort)();
|
|
1583
1881
|
const ctx = useCompoundRegistry();
|
|
1584
1882
|
const handlesVersion = useCompoundHandlesVersion();
|
|
1585
1883
|
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
@@ -1587,25 +1885,21 @@ function useCompoundPersistence(opts) {
|
|
|
1587
1885
|
const resumedChildKeysRef = (0, import_react19.useRef)(/* @__PURE__ */ new Set());
|
|
1588
1886
|
const loadedChildStatesRef = (0, import_react19.useRef)({});
|
|
1589
1887
|
const skipSaveUntilHydratedRef = (0, import_react19.useRef)(false);
|
|
1888
|
+
const postResumePersistPendingRef = (0, import_react19.useRef)(false);
|
|
1590
1889
|
const hydrationKeyRef = (0, import_react19.useRef)("");
|
|
1591
|
-
const
|
|
1890
|
+
const hydrationRetryRef = (0, import_react19.useRef)(0);
|
|
1592
1891
|
const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
1593
1892
|
if (hydrationKeyRef.current !== hydrationKey) {
|
|
1893
|
+
if (hydrationKeyRef.current) {
|
|
1894
|
+
clearCompoundHydrated(hydrationKeyRef.current);
|
|
1895
|
+
}
|
|
1594
1896
|
hydrationKeyRef.current = hydrationKey;
|
|
1595
|
-
hydrationInitRef.current = false;
|
|
1596
1897
|
loadedChildStatesRef.current = {};
|
|
1597
1898
|
skipSaveUntilHydratedRef.current = false;
|
|
1899
|
+
postResumePersistPendingRef.current = false;
|
|
1598
1900
|
pendingChildResumeRef.current = null;
|
|
1599
1901
|
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
1600
|
-
|
|
1601
|
-
if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
|
|
1602
|
-
hydrationInitRef.current = true;
|
|
1603
|
-
const saved = (0, import_core14.loadCompoundState)(storage, opts.courseId, opts.compoundId);
|
|
1604
|
-
if (saved && Object.keys(saved.childStates).length > 0) {
|
|
1605
|
-
loadedChildStatesRef.current = { ...saved.childStates };
|
|
1606
|
-
skipSaveUntilHydratedRef.current = true;
|
|
1607
|
-
pendingChildResumeRef.current = saved;
|
|
1608
|
-
}
|
|
1902
|
+
hydrationRetryRef.current = 0;
|
|
1609
1903
|
}
|
|
1610
1904
|
const buildState = (0, import_react19.useCallback)(() => {
|
|
1611
1905
|
const childStates = {
|
|
@@ -1613,22 +1907,35 @@ function useCompoundPersistence(opts) {
|
|
|
1613
1907
|
};
|
|
1614
1908
|
if (ctx) {
|
|
1615
1909
|
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
1910
|
+
if (opts.shouldIncludeChildState && !opts.shouldIncludeChildState(checkId, entry.pageIndex)) {
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1616
1913
|
const handle = entry.handle;
|
|
1617
1914
|
if (handle.getCurrentState) {
|
|
1618
|
-
|
|
1619
|
-
|
|
1915
|
+
const live = handle.getCurrentState();
|
|
1916
|
+
const loaded = loadedChildStatesRef.current[checkId];
|
|
1917
|
+
if (loaded !== void 0 && isEmptyResumeState(live)) {
|
|
1918
|
+
childStates[checkId] = loaded;
|
|
1919
|
+
} else {
|
|
1920
|
+
childStates[checkId] = live;
|
|
1921
|
+
if (!isEmptyResumeState(live)) {
|
|
1922
|
+
delete loadedChildStatesRef.current[checkId];
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1620
1925
|
}
|
|
1621
1926
|
}
|
|
1622
1927
|
}
|
|
1623
|
-
return (0,
|
|
1624
|
-
activePageIndex: (0,
|
|
1928
|
+
return (0, import_core16.createCompoundResumeState)({
|
|
1929
|
+
activePageIndex: (0, import_core16.clampCompoundPageIndex)(opts.index, opts.pageCount),
|
|
1625
1930
|
childStates
|
|
1626
1931
|
});
|
|
1627
|
-
}, [ctx, opts.index, opts.pageCount]);
|
|
1932
|
+
}, [ctx, opts.index, opts.pageCount, opts.shouldIncludeChildState]);
|
|
1628
1933
|
const buildStateRef = (0, import_react19.useRef)(buildState);
|
|
1629
1934
|
buildStateRef.current = buildState;
|
|
1630
1935
|
const transformStateRef = (0, import_react19.useRef)(opts.transformState);
|
|
1631
1936
|
transformStateRef.current = opts.transformState;
|
|
1937
|
+
const onCompoundResumeRef = (0, import_react19.useRef)(opts.onCompoundResume);
|
|
1938
|
+
onCompoundResumeRef.current = opts.onCompoundResume;
|
|
1632
1939
|
const persistNowRef = (0, import_react19.useRef)(() => {
|
|
1633
1940
|
});
|
|
1634
1941
|
const finalizeHydration = (0, import_react19.useCallback)(
|
|
@@ -1639,10 +1946,19 @@ function useCompoundPersistence(opts) {
|
|
|
1639
1946
|
};
|
|
1640
1947
|
skipSaveUntilHydratedRef.current = false;
|
|
1641
1948
|
pendingChildResumeRef.current = null;
|
|
1642
|
-
|
|
1949
|
+
hydrationRetryRef.current = 0;
|
|
1950
|
+
postResumePersistPendingRef.current = true;
|
|
1951
|
+
requestAnimationFrame(() => {
|
|
1952
|
+
requestAnimationFrame(() => {
|
|
1953
|
+
postResumePersistPendingRef.current = false;
|
|
1954
|
+
persistNowRef.current();
|
|
1955
|
+
});
|
|
1956
|
+
});
|
|
1643
1957
|
},
|
|
1644
1958
|
[]
|
|
1645
1959
|
);
|
|
1960
|
+
const applyPendingChildResumeRef = (0, import_react19.useRef)(() => {
|
|
1961
|
+
});
|
|
1646
1962
|
const applyPendingChildResume = (0, import_react19.useCallback)(() => {
|
|
1647
1963
|
const pending = pendingChildResumeRef.current;
|
|
1648
1964
|
if (!pending || !ctx) return;
|
|
@@ -1652,62 +1968,125 @@ function useCompoundPersistence(opts) {
|
|
|
1652
1968
|
alreadyResumed: resumedChildKeysRef.current
|
|
1653
1969
|
});
|
|
1654
1970
|
if (!applied) {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
finalizeHydration(registeredOnly2);
|
|
1971
|
+
const registerable = registerablePendingKeys(pending.childStates);
|
|
1972
|
+
const missing = registerable.filter((k) => !handles.has(k));
|
|
1973
|
+
if (missing.length > 0 && hydrationRetryRef.current < MAX_HYDRATION_RETRIES) {
|
|
1974
|
+
hydrationRetryRef.current += 1;
|
|
1975
|
+
requestAnimationFrame(() => applyPendingChildResumeRef.current());
|
|
1661
1976
|
return;
|
|
1662
1977
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1978
|
+
if (missing.length > 0 && isDevEnvironment()) {
|
|
1979
|
+
console.warn(
|
|
1980
|
+
`[lessonkit] Compound hydration: ${missing.length} child state(s) not restored (missing handles: ${missing.join(", ")})`
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
lessonkitCtx?.config?.observability?.onCompoundHydrationPartial?.({
|
|
1984
|
+
compoundId: opts.compoundId,
|
|
1985
|
+
missingCheckIds: missing
|
|
1986
|
+
});
|
|
1987
|
+
for (const key of missing) {
|
|
1988
|
+
const state = pending.childStates[key];
|
|
1989
|
+
if (state) {
|
|
1990
|
+
loadedChildStatesRef.current[key] = state;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
|
|
1994
|
+
resumeChildHandles(handles, registeredOnly2, {
|
|
1995
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
1996
|
+
});
|
|
1997
|
+
finalizeHydration({
|
|
1998
|
+
...loadedChildStatesRef.current,
|
|
1999
|
+
...registeredOnly2
|
|
1673
2000
|
});
|
|
1674
2001
|
return;
|
|
1675
2002
|
}
|
|
1676
2003
|
const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
|
|
1677
2004
|
finalizeHydration(registeredOnly);
|
|
1678
2005
|
}, [ctx, finalizeHydration]);
|
|
2006
|
+
applyPendingChildResumeRef.current = applyPendingChildResume;
|
|
2007
|
+
(0, import_react19.useLayoutEffect)(() => {
|
|
2008
|
+
if (!opts.enabled || !opts.courseId) return;
|
|
2009
|
+
markCompoundHydrated(compoundHydrationKey(opts.courseId, opts.compoundId));
|
|
2010
|
+
const saved = (0, import_core16.loadCompoundState)(storage, opts.courseId, opts.compoundId);
|
|
2011
|
+
if (!saved) return;
|
|
2012
|
+
if (Object.keys(saved.childStates).length > 0) {
|
|
2013
|
+
loadedChildStatesRef.current = { ...saved.childStates };
|
|
2014
|
+
skipSaveUntilHydratedRef.current = true;
|
|
2015
|
+
pendingChildResumeRef.current = saved;
|
|
2016
|
+
}
|
|
2017
|
+
const clamped = (0, import_core16.clampCompoundPageIndex)(saved.activePageIndex, opts.pageCount);
|
|
2018
|
+
onCompoundResumeRef.current?.({ ...saved, activePageIndex: clamped });
|
|
2019
|
+
opts.setIndex(clamped);
|
|
2020
|
+
queueMicrotask(() => applyPendingChildResumeRef.current());
|
|
2021
|
+
}, [hydrationKey, opts.courseId, opts.compoundId, opts.enabled, opts.pageCount, storage]);
|
|
1679
2022
|
const saveResume = useCompoundResume({
|
|
1680
2023
|
courseId: opts.courseId,
|
|
1681
2024
|
compoundId: opts.compoundId,
|
|
1682
2025
|
enabled: opts.enabled,
|
|
1683
2026
|
storage,
|
|
1684
2027
|
onResume: (state) => {
|
|
1685
|
-
const clamped = (0,
|
|
2028
|
+
const clamped = (0, import_core16.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
|
|
1686
2029
|
loadedChildStatesRef.current = { ...state.childStates };
|
|
1687
2030
|
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2031
|
+
onCompoundResumeRef.current?.({ ...state, activePageIndex: clamped });
|
|
1688
2032
|
opts.setIndex(clamped);
|
|
1689
2033
|
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2034
|
+
hydrationRetryRef.current = 0;
|
|
1690
2035
|
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
1691
2036
|
queueMicrotask(() => applyPendingChildResume());
|
|
1692
2037
|
}
|
|
1693
2038
|
});
|
|
1694
|
-
const
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2039
|
+
const buildBestEffortState = (0, import_react19.useCallback)(() => {
|
|
2040
|
+
const childStates = {
|
|
2041
|
+
...loadedChildStatesRef.current
|
|
2042
|
+
};
|
|
2043
|
+
if (ctx) {
|
|
2044
|
+
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
2045
|
+
if (opts.shouldIncludeChildState && !opts.shouldIncludeChildState(checkId, entry.pageIndex)) {
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
const handle = entry.handle;
|
|
2049
|
+
if (handle.getCurrentState) {
|
|
2050
|
+
const live = handle.getCurrentState();
|
|
2051
|
+
if (!isEmptyResumeState(live)) {
|
|
2052
|
+
childStates[checkId] = live;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
const built = (0, import_core16.createCompoundResumeState)({
|
|
2058
|
+
activePageIndex: (0, import_core16.clampCompoundPageIndex)(opts.index, opts.pageCount),
|
|
2059
|
+
childStates
|
|
2060
|
+
});
|
|
2061
|
+
return transformStateRef.current ? transformStateRef.current(built) : built;
|
|
2062
|
+
}, [ctx, opts.index, opts.pageCount, opts.shouldIncludeChildState]);
|
|
2063
|
+
const persistNow = (0, import_react19.useCallback)(
|
|
2064
|
+
(options) => {
|
|
2065
|
+
if (!opts.enabled || !opts.courseId) return;
|
|
2066
|
+
if (options?.forceDuringHydration) {
|
|
2067
|
+
saveResume(buildBestEffortState());
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
if (skipSaveUntilHydratedRef.current) return;
|
|
2071
|
+
if (postResumePersistPendingRef.current) return;
|
|
2072
|
+
const built = buildStateRef.current();
|
|
2073
|
+
const state = transformStateRef.current ? transformStateRef.current(built) : built;
|
|
2074
|
+
saveResume(state);
|
|
2075
|
+
},
|
|
2076
|
+
[opts.enabled, opts.courseId, saveResume, buildBestEffortState]
|
|
2077
|
+
);
|
|
1701
2078
|
(0, import_react19.useEffect)(() => {
|
|
1702
2079
|
persistNowRef.current = persistNow;
|
|
1703
2080
|
}, [persistNow]);
|
|
1704
2081
|
const notifyImperativeResume = (0, import_react19.useCallback)(
|
|
1705
2082
|
(state) => {
|
|
1706
|
-
const clamped = (0,
|
|
2083
|
+
const clamped = (0, import_core16.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
|
|
1707
2084
|
loadedChildStatesRef.current = { ...state.childStates };
|
|
1708
2085
|
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2086
|
+
onCompoundResumeRef.current?.({ ...state, activePageIndex: clamped });
|
|
1709
2087
|
opts.setIndex(clamped);
|
|
1710
2088
|
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2089
|
+
hydrationRetryRef.current = 0;
|
|
1711
2090
|
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
1712
2091
|
queueMicrotask(() => applyPendingChildResume());
|
|
1713
2092
|
},
|
|
@@ -1727,17 +2106,22 @@ function useCompoundPersistence(opts) {
|
|
|
1727
2106
|
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
1728
2107
|
(0, import_react19.useEffect)(() => {
|
|
1729
2108
|
persistNow();
|
|
1730
|
-
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
2109
|
+
}, [persistNow, opts.index, opts.pageCount, handlesVersion, opts.persistTrigger]);
|
|
1731
2110
|
(0, import_react19.useEffect)(() => {
|
|
1732
2111
|
if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
|
|
1733
2112
|
const flushOnExit = () => {
|
|
1734
|
-
if (document.visibilityState === "hidden")
|
|
2113
|
+
if (document.visibilityState === "hidden") {
|
|
2114
|
+
persistNow({ forceDuringHydration: skipSaveUntilHydratedRef.current });
|
|
2115
|
+
}
|
|
1735
2116
|
};
|
|
1736
2117
|
document.addEventListener("visibilitychange", flushOnExit);
|
|
1737
|
-
|
|
2118
|
+
const flushOnPageHide = () => {
|
|
2119
|
+
persistNow({ forceDuringHydration: skipSaveUntilHydratedRef.current });
|
|
2120
|
+
};
|
|
2121
|
+
window.addEventListener("pagehide", flushOnPageHide);
|
|
1738
2122
|
return () => {
|
|
1739
2123
|
document.removeEventListener("visibilitychange", flushOnExit);
|
|
1740
|
-
window.removeEventListener("pagehide",
|
|
2124
|
+
window.removeEventListener("pagehide", flushOnPageHide);
|
|
1741
2125
|
};
|
|
1742
2126
|
}, [opts.enabled, opts.courseId, persistNow]);
|
|
1743
2127
|
}
|
|
@@ -1753,10 +2137,12 @@ function useCompoundShell(opts) {
|
|
|
1753
2137
|
setIndex: opts.setIndex,
|
|
1754
2138
|
enabled: opts.persistEnabled,
|
|
1755
2139
|
storage: opts.storage,
|
|
1756
|
-
transformState: opts.transformState
|
|
2140
|
+
transformState: opts.transformState,
|
|
2141
|
+
persistTrigger: opts.persistTrigger,
|
|
2142
|
+
onCompoundResume: opts.onCompoundResume
|
|
1757
2143
|
});
|
|
1758
2144
|
const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
|
|
1759
|
-
const visibleIndex = (0,
|
|
2145
|
+
const visibleIndex = (0, import_core17.clampCompoundPageIndex)(opts.index, opts.pageCount);
|
|
1760
2146
|
useCompoundHandleRef(opts.ref, {
|
|
1761
2147
|
activePageIndex: visibleIndex,
|
|
1762
2148
|
setActivePageIndex: opts.setIndex,
|
|
@@ -1782,7 +2168,7 @@ function useCompoundInitialIndex(opts) {
|
|
|
1782
2168
|
|
|
1783
2169
|
// src/compound/validateChildren.ts
|
|
1784
2170
|
var import_react21 = __toESM(require("react"), 1);
|
|
1785
|
-
var
|
|
2171
|
+
var import_core18 = require("@lessonkit/core");
|
|
1786
2172
|
|
|
1787
2173
|
// src/compound/blockType.ts
|
|
1788
2174
|
var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
|
|
@@ -1810,7 +2196,9 @@ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
|
1810
2196
|
"SlideDeck",
|
|
1811
2197
|
"TimedCue",
|
|
1812
2198
|
"InteractiveVideo",
|
|
1813
|
-
"AssessmentSequence"
|
|
2199
|
+
"AssessmentSequence",
|
|
2200
|
+
"BranchingScenario",
|
|
2201
|
+
"BranchNode"
|
|
1814
2202
|
]);
|
|
1815
2203
|
function warnOrThrow(msg, strict) {
|
|
1816
2204
|
if (strict) throw new Error(msg);
|
|
@@ -1829,7 +2217,7 @@ function validateNode(parent, node, depth, strict) {
|
|
|
1829
2217
|
}
|
|
1830
2218
|
return;
|
|
1831
2219
|
}
|
|
1832
|
-
if (!(0,
|
|
2220
|
+
if (!(0, import_core18.isChildTypeAllowed)(parent, blockType)) {
|
|
1833
2221
|
const key = `${parent}:${blockType}`;
|
|
1834
2222
|
if (!warnedPairs.has(key)) {
|
|
1835
2223
|
warnedPairs.add(key);
|
|
@@ -1839,7 +2227,7 @@ function validateNode(parent, node, depth, strict) {
|
|
|
1839
2227
|
}
|
|
1840
2228
|
}
|
|
1841
2229
|
if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
|
|
1842
|
-
const maxDepth =
|
|
2230
|
+
const maxDepth = import_core18.COMPOUND_MAX_NESTING_DEPTH[parent];
|
|
1843
2231
|
if (depth >= maxDepth) {
|
|
1844
2232
|
warnOrThrow(
|
|
1845
2233
|
`[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
|
|
@@ -1854,7 +2242,7 @@ function validateNode(parent, node, depth, strict) {
|
|
|
1854
2242
|
} else if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
1855
2243
|
validateSubtreeForForbidden(
|
|
1856
2244
|
child.props.children,
|
|
1857
|
-
|
|
2245
|
+
import_core18.ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
1858
2246
|
strict
|
|
1859
2247
|
);
|
|
1860
2248
|
}
|
|
@@ -1882,23 +2270,29 @@ function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
|
1882
2270
|
});
|
|
1883
2271
|
}
|
|
1884
2272
|
function validateAccordionSections(sections, strict) {
|
|
1885
|
-
|
|
2273
|
+
const enforceStrict = strict ?? !isDevEnvironment();
|
|
1886
2274
|
for (const section of sections) {
|
|
1887
|
-
validateSubtreeForForbidden(section.content,
|
|
2275
|
+
validateSubtreeForForbidden(section.content, import_core18.ACCORDION_FORBIDDEN_CHILD_TYPES, enforceStrict);
|
|
1888
2276
|
}
|
|
1889
2277
|
}
|
|
1890
2278
|
function validateCompoundChildren(parent, children, strict) {
|
|
1891
|
-
|
|
1892
|
-
validateNode(parent, children, 0,
|
|
2279
|
+
const enforceStrict = strict ?? !isDevEnvironment();
|
|
2280
|
+
validateNode(parent, children, 0, enforceStrict);
|
|
1893
2281
|
}
|
|
1894
2282
|
|
|
1895
|
-
// src/compound/
|
|
1896
|
-
var
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2283
|
+
// src/compound/requireCompoundBlockId.ts
|
|
2284
|
+
var MissingCompoundBlockIdError = class extends Error {
|
|
2285
|
+
constructor(componentName) {
|
|
2286
|
+
super(
|
|
2287
|
+
`[lessonkit] <${componentName}> requires a unique blockId when session.persistCompoundState is enabled`
|
|
2288
|
+
);
|
|
2289
|
+
this.name = "MissingCompoundBlockIdError";
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
function requireCompoundBlockIdWhenPersisting(opts) {
|
|
2293
|
+
if (opts.persistEnabled && !opts.blockId) {
|
|
2294
|
+
throw new MissingCompoundBlockIdError(opts.componentName);
|
|
2295
|
+
}
|
|
1902
2296
|
}
|
|
1903
2297
|
|
|
1904
2298
|
// src/blocks/AssessmentSequence.tsx
|
|
@@ -1907,7 +2301,10 @@ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
|
|
|
1907
2301
|
function AssessmentSequenceInner2(props, ref) {
|
|
1908
2302
|
const { compoundId, childArray, index, setIndex, persistEnabled } = props;
|
|
1909
2303
|
const sequential = props.sequential !== false;
|
|
2304
|
+
const requireAnswerBeforeNext = props.requireAnswerBeforeNext !== false;
|
|
1910
2305
|
const { config } = useLessonkit();
|
|
2306
|
+
const registry = useCompoundRegistry();
|
|
2307
|
+
const handlesVersion = useCompoundHandlesVersion();
|
|
1911
2308
|
const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
|
|
1912
2309
|
courseId: config.courseId,
|
|
1913
2310
|
compoundId,
|
|
@@ -1919,6 +2316,21 @@ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
|
|
|
1919
2316
|
enableSolutionsButton: props.enableSolutionsButton
|
|
1920
2317
|
});
|
|
1921
2318
|
validateCompoundChildren("AssessmentSequence", props.children);
|
|
2319
|
+
const activeStepAnswered = (0, import_react22.useMemo)(() => {
|
|
2320
|
+
if (!requireAnswerBeforeNext || !registry) return true;
|
|
2321
|
+
let handlesForStep = 0;
|
|
2322
|
+
for (const entry of registry.getRegisteredHandles().values()) {
|
|
2323
|
+
if (entry.pageIndex !== visibleIndex) continue;
|
|
2324
|
+
handlesForStep += 1;
|
|
2325
|
+
if (!entry.handle.getAnswerGiven()) return false;
|
|
2326
|
+
}
|
|
2327
|
+
if (handlesForStep === 0) {
|
|
2328
|
+
const child = childArray[visibleIndex];
|
|
2329
|
+
const childProps = child?.props;
|
|
2330
|
+
if (child && typeof childProps?.checkId === "string") return false;
|
|
2331
|
+
}
|
|
2332
|
+
return true;
|
|
2333
|
+
}, [childArray, handlesVersion, registry, requireAnswerBeforeNext, visibleIndex]);
|
|
1922
2334
|
if (!sequential) {
|
|
1923
2335
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
|
|
1924
2336
|
}
|
|
@@ -1946,7 +2358,7 @@ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
|
|
|
1946
2358
|
{
|
|
1947
2359
|
type: "button",
|
|
1948
2360
|
"data-testid": "sequence-next",
|
|
1949
|
-
disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
|
|
2361
|
+
disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0 || !activeStepAnswered,
|
|
1950
2362
|
onClick: goNext,
|
|
1951
2363
|
children: "Next"
|
|
1952
2364
|
}
|
|
@@ -1960,24 +2372,24 @@ var AssessmentSequence = (0, import_react22.forwardRef)(
|
|
|
1960
2372
|
const reactInstanceId = (0, import_react22.useId)();
|
|
1961
2373
|
const autoCompoundIdRef = (0, import_react22.useRef)(null);
|
|
1962
2374
|
if (!props.blockId && !autoCompoundIdRef.current) {
|
|
1963
|
-
autoCompoundIdRef.current = (0,
|
|
2375
|
+
autoCompoundIdRef.current = (0, import_core19.deriveId)(`assessment-sequence-${reactInstanceId}`);
|
|
1964
2376
|
}
|
|
1965
|
-
const compoundId = (0, import_react22.useMemo)(
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2377
|
+
const compoundId = (0, import_react22.useMemo)(() => {
|
|
2378
|
+
if (props.blockId) {
|
|
2379
|
+
return normalizeComponentId(props.blockId, "blockId");
|
|
2380
|
+
}
|
|
2381
|
+
return autoCompoundIdRef.current ?? (0, import_core19.deriveId)(`assessment-sequence-${reactInstanceId}`);
|
|
2382
|
+
}, [props.blockId, reactInstanceId]);
|
|
1969
2383
|
const childArray = import_react22.default.Children.toArray(props.children).filter(
|
|
1970
2384
|
import_react22.default.isValidElement
|
|
1971
2385
|
);
|
|
1972
2386
|
const { config, storage } = useLessonkit();
|
|
1973
2387
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
1974
|
-
(
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
});
|
|
1980
|
-
}, [persistEnabled, props.blockId]);
|
|
2388
|
+
requireCompoundBlockIdWhenPersisting({
|
|
2389
|
+
persistEnabled,
|
|
2390
|
+
blockId: props.blockId,
|
|
2391
|
+
componentName: "AssessmentSequence"
|
|
2392
|
+
});
|
|
1981
2393
|
const initialIndex = useCompoundInitialIndex({
|
|
1982
2394
|
courseId: config.courseId,
|
|
1983
2395
|
compoundId,
|
|
@@ -2022,13 +2434,180 @@ function Heading(props) {
|
|
|
2022
2434
|
}
|
|
2023
2435
|
setLessonkitBlockType(Heading, "Heading");
|
|
2024
2436
|
|
|
2437
|
+
// src/blocks/embedSecurity.ts
|
|
2438
|
+
var import_meta = {};
|
|
2439
|
+
var BLOCKED_SANDBOX_TOKENS = /* @__PURE__ */ new Set([
|
|
2440
|
+
"allow-top-navigation",
|
|
2441
|
+
"allow-top-navigation-by-user-activation",
|
|
2442
|
+
"allow-modals",
|
|
2443
|
+
"allow-downloads",
|
|
2444
|
+
"allow-popups-to-escape-sandbox"
|
|
2445
|
+
]);
|
|
2446
|
+
var ALLOWED_SANDBOX_TOKENS = /* @__PURE__ */ new Set([
|
|
2447
|
+
"allow-forms",
|
|
2448
|
+
"allow-popups",
|
|
2449
|
+
"allow-presentation"
|
|
2450
|
+
]);
|
|
2451
|
+
var DEFAULT_SANDBOX = "allow-scripts";
|
|
2452
|
+
function isProductionEmbedBuild() {
|
|
2453
|
+
try {
|
|
2454
|
+
if (import_meta.env?.PROD === true) return true;
|
|
2455
|
+
} catch {
|
|
2456
|
+
}
|
|
2457
|
+
const g = globalThis;
|
|
2458
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
2459
|
+
}
|
|
2460
|
+
function allowedEmbedSchemes() {
|
|
2461
|
+
return isProductionEmbedBuild() ? /* @__PURE__ */ new Set(["https:"]) : /* @__PURE__ */ new Set(["https:", "http:"]);
|
|
2462
|
+
}
|
|
2463
|
+
function normalizeHostname(hostname) {
|
|
2464
|
+
return hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
2465
|
+
}
|
|
2466
|
+
function expandIpv4Literal(hostname) {
|
|
2467
|
+
if (/^\d+$/.test(hostname)) {
|
|
2468
|
+
const value2 = Number(hostname);
|
|
2469
|
+
if (!Number.isInteger(value2) || value2 < 0 || value2 > 4294967295) return null;
|
|
2470
|
+
return `${value2 >>> 24 & 255}.${value2 >>> 16 & 255}.${value2 >>> 8 & 255}.${value2 & 255}`;
|
|
2471
|
+
}
|
|
2472
|
+
if (!/^\d+(?:\.\d+){1,3}$/.test(hostname)) return null;
|
|
2473
|
+
const parts = hostname.split(".").map((part) => Number(part));
|
|
2474
|
+
if (parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
|
|
2475
|
+
let value = 0;
|
|
2476
|
+
for (const part of parts) {
|
|
2477
|
+
value = value << 8 | part;
|
|
2478
|
+
}
|
|
2479
|
+
value <<= (4 - parts.length) * 8;
|
|
2480
|
+
return `${value >>> 24 & 255}.${value >>> 16 & 255}.${value >>> 8 & 255}.${value & 255}`;
|
|
2481
|
+
}
|
|
2482
|
+
function canonicalHostnameForBlocklist(hostname) {
|
|
2483
|
+
const normalized = normalizeHostname(hostname);
|
|
2484
|
+
return expandIpv4Literal(normalized) ?? normalized;
|
|
2485
|
+
}
|
|
2486
|
+
function isIpv4MappedAddress(hostname) {
|
|
2487
|
+
const match = hostname.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
2488
|
+
return match?.[1] ?? null;
|
|
2489
|
+
}
|
|
2490
|
+
function isLoopbackHost(hostname) {
|
|
2491
|
+
const ipv4Mapped = isIpv4MappedAddress(hostname);
|
|
2492
|
+
if (ipv4Mapped) return isLoopbackHost(ipv4Mapped);
|
|
2493
|
+
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";
|
|
2494
|
+
}
|
|
2495
|
+
function isIpv6UniqueLocalHost(hostname) {
|
|
2496
|
+
return /^f[cd][0-9a-f]{0,2}:/i.test(hostname);
|
|
2497
|
+
}
|
|
2498
|
+
function isLinkLocalOrMetadataHost(hostname) {
|
|
2499
|
+
if (hostname === "169.254.169.254") return true;
|
|
2500
|
+
if (/^169\.254\./.test(hostname)) return true;
|
|
2501
|
+
if (/^fe80:/i.test(hostname)) return true;
|
|
2502
|
+
return false;
|
|
2503
|
+
}
|
|
2504
|
+
function isRfc1918Host(hostname) {
|
|
2505
|
+
const ipv4Mapped = isIpv4MappedAddress(hostname);
|
|
2506
|
+
if (ipv4Mapped) return isRfc1918Host(ipv4Mapped);
|
|
2507
|
+
if (/^10\./.test(hostname)) return true;
|
|
2508
|
+
if (/^192\.168\./.test(hostname)) return true;
|
|
2509
|
+
const parts = hostname.split(".").map(Number);
|
|
2510
|
+
if (parts.length === 4 && parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
|
2511
|
+
return false;
|
|
2512
|
+
}
|
|
2513
|
+
function isBlockedHost(hostname, allowedHosts) {
|
|
2514
|
+
const normalized = normalizeHostname(hostname);
|
|
2515
|
+
const canonical = canonicalHostnameForBlocklist(hostname);
|
|
2516
|
+
if (allowedHosts?.some((host) => {
|
|
2517
|
+
const allowedNormalized = normalizeHostname(host);
|
|
2518
|
+
return canonicalHostnameForBlocklist(host) === canonical || allowedNormalized === normalized;
|
|
2519
|
+
})) {
|
|
2520
|
+
return false;
|
|
2521
|
+
}
|
|
2522
|
+
if (!isProductionEmbedBuild()) return false;
|
|
2523
|
+
return isLoopbackHost(canonical) || isLinkLocalOrMetadataHost(canonical) || isRfc1918Host(canonical) || isIpv6UniqueLocalHost(normalized);
|
|
2524
|
+
}
|
|
2525
|
+
function resolveAllowedUrl(src, options) {
|
|
2526
|
+
const trimmed = src.trim();
|
|
2527
|
+
if (!trimmed) return null;
|
|
2528
|
+
try {
|
|
2529
|
+
const base = typeof window !== "undefined" ? window.location.href : "https://example.com/";
|
|
2530
|
+
const url = new URL(trimmed, base);
|
|
2531
|
+
if (!allowedEmbedSchemes().has(url.protocol)) return null;
|
|
2532
|
+
if (isBlockedHost(url.hostname, options?.allowedHosts)) return null;
|
|
2533
|
+
if (typeof window !== "undefined") {
|
|
2534
|
+
const pageOrigin = window.location.origin;
|
|
2535
|
+
const isAbsolute = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed) || trimmed.startsWith("//");
|
|
2536
|
+
if (!isAbsolute && url.origin !== pageOrigin) return null;
|
|
2537
|
+
if (trimmed.startsWith("//") && url.origin !== pageOrigin) return null;
|
|
2538
|
+
}
|
|
2539
|
+
url.username = "";
|
|
2540
|
+
url.password = "";
|
|
2541
|
+
return url.href;
|
|
2542
|
+
} catch {
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
function resolveEmbedSrc(src, options) {
|
|
2547
|
+
return resolveAllowedUrl(src, options);
|
|
2548
|
+
}
|
|
2549
|
+
function resolveMediaSrc(src, options) {
|
|
2550
|
+
if (src === void 0) return null;
|
|
2551
|
+
return resolveAllowedUrl(src, options);
|
|
2552
|
+
}
|
|
2553
|
+
function buildEmbedSandbox(allow, options) {
|
|
2554
|
+
const tokens = /* @__PURE__ */ new Set([DEFAULT_SANDBOX]);
|
|
2555
|
+
if (allow) {
|
|
2556
|
+
for (const raw of allow.split(/\s+/)) {
|
|
2557
|
+
const token = raw.trim();
|
|
2558
|
+
if (!token || BLOCKED_SANDBOX_TOKENS.has(token)) continue;
|
|
2559
|
+
if (ALLOWED_SANDBOX_TOKENS.has(token)) tokens.add(token);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
if (options?.restrictPopupsInProduction !== false && isProductionEmbedBuild()) {
|
|
2563
|
+
tokens.delete("allow-popups");
|
|
2564
|
+
}
|
|
2565
|
+
return [...tokens].join(" ");
|
|
2566
|
+
}
|
|
2567
|
+
function telemetryEmbedSrc(src) {
|
|
2568
|
+
try {
|
|
2569
|
+
const url = new URL(src);
|
|
2570
|
+
url.username = "";
|
|
2571
|
+
url.password = "";
|
|
2572
|
+
url.search = "";
|
|
2573
|
+
url.hash = "";
|
|
2574
|
+
return `${url.origin}${url.pathname}`;
|
|
2575
|
+
} catch {
|
|
2576
|
+
return src;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
function resolveEmbedAspectRatio(aspectRatio) {
|
|
2580
|
+
if (!aspectRatio) return void 0;
|
|
2581
|
+
const trimmed = aspectRatio.trim();
|
|
2582
|
+
if (!/^\d+(\.\d+)?\s*\/\s*\d+(\.\d+)?$/.test(trimmed)) return void 0;
|
|
2583
|
+
const [numRaw, denRaw] = trimmed.split("/").map((part) => part.trim());
|
|
2584
|
+
const num = Number(numRaw);
|
|
2585
|
+
const den = Number(denRaw);
|
|
2586
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || num <= 0 || den <= 0) return void 0;
|
|
2587
|
+
return trimmed;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2025
2590
|
// src/blocks/Image.tsx
|
|
2026
2591
|
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
2027
2592
|
function Image(props) {
|
|
2593
|
+
const { config } = useLessonkit();
|
|
2594
|
+
const resolvedSrc = resolveMediaSrc(props.src, {
|
|
2595
|
+
allowedHosts: config.embed?.allowedHosts
|
|
2596
|
+
});
|
|
2597
|
+
if (!resolvedSrc) {
|
|
2598
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2599
|
+
"figure",
|
|
2600
|
+
{
|
|
2601
|
+
"data-lk-block-id": props.blockId,
|
|
2602
|
+
"data-testid": props.blockId ? `image-${props.blockId}` : "image",
|
|
2603
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { role: "alert", children: "This image URL is not allowed." })
|
|
2604
|
+
}
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2028
2607
|
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2029
2608
|
"img",
|
|
2030
2609
|
{
|
|
2031
|
-
src:
|
|
2610
|
+
src: resolvedSrc,
|
|
2032
2611
|
alt: props.alt,
|
|
2033
2612
|
"data-lk-block-id": props.blockId,
|
|
2034
2613
|
"data-testid": props.blockId ? `image-${props.blockId}` : "image",
|
|
@@ -2042,10 +2621,21 @@ setLessonkitBlockType(Image, "Image");
|
|
|
2042
2621
|
var import_react24 = require("react");
|
|
2043
2622
|
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
2044
2623
|
function Video(props) {
|
|
2624
|
+
const { config } = useLessonkit();
|
|
2045
2625
|
const blockId = (0, import_react24.useMemo)(
|
|
2046
2626
|
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2047
2627
|
[props.blockId]
|
|
2048
2628
|
);
|
|
2629
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
2630
|
+
const resolvedSrc = resolveMediaSrc(props.src, mediaOptions);
|
|
2631
|
+
const resolvedPoster = resolveMediaSrc(props.poster, mediaOptions);
|
|
2632
|
+
const resolvedCaptions = resolveMediaSrc(props.captions, mediaOptions);
|
|
2633
|
+
if (!resolvedSrc) {
|
|
2634
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("section", { "aria-label": props.title ?? "Video", "data-lk-block-id": blockId, "data-testid": "video", children: [
|
|
2635
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { "data-testid": "video-title", children: props.title }) : null,
|
|
2636
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { role: "alert", "data-testid": "video-blocked", children: "This video URL is not allowed." })
|
|
2637
|
+
] });
|
|
2638
|
+
}
|
|
2049
2639
|
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("section", { "aria-label": props.title ?? "Video", "data-lk-block-id": blockId, "data-testid": "video", children: [
|
|
2050
2640
|
props.title ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { "data-testid": "video-title", children: props.title }) : null,
|
|
2051
2641
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
@@ -2053,11 +2643,20 @@ function Video(props) {
|
|
|
2053
2643
|
{
|
|
2054
2644
|
controls: true,
|
|
2055
2645
|
preload: "metadata",
|
|
2056
|
-
poster:
|
|
2057
|
-
src:
|
|
2646
|
+
poster: resolvedPoster ?? void 0,
|
|
2647
|
+
src: resolvedSrc,
|
|
2058
2648
|
"data-testid": "video-player",
|
|
2059
2649
|
style: { maxWidth: "100%" },
|
|
2060
|
-
children:
|
|
2650
|
+
children: resolvedCaptions ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2651
|
+
"track",
|
|
2652
|
+
{
|
|
2653
|
+
kind: "captions",
|
|
2654
|
+
src: resolvedCaptions,
|
|
2655
|
+
srcLang: "en",
|
|
2656
|
+
label: "Captions",
|
|
2657
|
+
default: true
|
|
2658
|
+
}
|
|
2659
|
+
) : null
|
|
2061
2660
|
}
|
|
2062
2661
|
)
|
|
2063
2662
|
] });
|
|
@@ -2182,15 +2781,20 @@ var InteractiveBookInner = (0, import_react26.forwardRef)(
|
|
|
2182
2781
|
}
|
|
2183
2782
|
);
|
|
2184
2783
|
var InteractiveBook = (0, import_react26.forwardRef)(function InteractiveBook2(props, ref) {
|
|
2185
|
-
const blockId = (0, import_react26.useMemo)(
|
|
2186
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2187
|
-
[props.blockId]
|
|
2188
|
-
);
|
|
2189
2784
|
const pages = import_react26.default.Children.toArray(props.children).filter(
|
|
2190
2785
|
import_react26.default.isValidElement
|
|
2191
2786
|
);
|
|
2192
2787
|
const { config, storage } = useLessonkit();
|
|
2193
2788
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2789
|
+
requireCompoundBlockIdWhenPersisting({
|
|
2790
|
+
persistEnabled,
|
|
2791
|
+
blockId: props.blockId,
|
|
2792
|
+
componentName: "InteractiveBook"
|
|
2793
|
+
});
|
|
2794
|
+
const blockId = (0, import_react26.useMemo)(
|
|
2795
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2796
|
+
[props.blockId]
|
|
2797
|
+
);
|
|
2194
2798
|
const initialIndex = useCompoundInitialIndex({
|
|
2195
2799
|
courseId: config.courseId,
|
|
2196
2800
|
compoundId: blockId,
|
|
@@ -2414,15 +3018,20 @@ var SlideDeckInner = (0, import_react29.forwardRef)(function SlideDeckInner2(pro
|
|
|
2414
3018
|
);
|
|
2415
3019
|
});
|
|
2416
3020
|
var SlideDeck = (0, import_react29.forwardRef)(function SlideDeck2(props, ref) {
|
|
2417
|
-
const blockId = (0, import_react29.useMemo)(
|
|
2418
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2419
|
-
[props.blockId]
|
|
2420
|
-
);
|
|
2421
3021
|
const slides = import_react29.default.Children.toArray(props.children).filter(
|
|
2422
3022
|
import_react29.default.isValidElement
|
|
2423
3023
|
);
|
|
2424
3024
|
const { config, storage } = useLessonkit();
|
|
2425
3025
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3026
|
+
requireCompoundBlockIdWhenPersisting({
|
|
3027
|
+
persistEnabled,
|
|
3028
|
+
blockId: props.blockId,
|
|
3029
|
+
componentName: "SlideDeck"
|
|
3030
|
+
});
|
|
3031
|
+
const blockId = (0, import_react29.useMemo)(
|
|
3032
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3033
|
+
[props.blockId]
|
|
3034
|
+
);
|
|
2426
3035
|
const initialIndex = useCompoundInitialIndex({
|
|
2427
3036
|
courseId: config.courseId,
|
|
2428
3037
|
compoundId: blockId,
|
|
@@ -2499,36 +3108,18 @@ setLessonkitBlockType(TimedCue, "TimedCue");
|
|
|
2499
3108
|
|
|
2500
3109
|
// src/blocks/InteractiveVideo.tsx
|
|
2501
3110
|
var import_react31 = __toESM(require("react"), 1);
|
|
2502
|
-
var
|
|
2503
|
-
|
|
2504
|
-
// src/compound/useCompoundVideoShell.ts
|
|
2505
|
-
var import_core18 = require("@lessonkit/core");
|
|
2506
|
-
var IV_META_KEY = "__lk_iv__";
|
|
2507
|
-
function readInteractiveVideoMeta(childStates) {
|
|
2508
|
-
const raw = childStates[IV_META_KEY];
|
|
2509
|
-
if (!raw || typeof raw !== "object") return null;
|
|
2510
|
-
const currentTime = typeof raw.currentTime === "number" ? raw.currentTime : 0;
|
|
2511
|
-
const completedCueIndices = Array.isArray(raw.completedCueIndices) ? raw.completedCueIndices.filter((n) => typeof n === "number") : [];
|
|
2512
|
-
return { currentTime, completedCueIndices };
|
|
2513
|
-
}
|
|
2514
|
-
function mergeVideoMetaIntoState(state, meta) {
|
|
2515
|
-
return {
|
|
2516
|
-
...state,
|
|
2517
|
-
childStates: {
|
|
2518
|
-
...state.childStates,
|
|
2519
|
-
[IV_META_KEY]: meta
|
|
2520
|
-
}
|
|
2521
|
-
};
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
// src/blocks/InteractiveVideo.tsx
|
|
3111
|
+
var import_core20 = require("@lessonkit/core");
|
|
2525
3112
|
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
3113
|
+
function sortCuesByTime(cues) {
|
|
3114
|
+
return [...cues].sort((a, b) => (a.props.atSeconds ?? 0) - (b.props.atSeconds ?? 0));
|
|
3115
|
+
}
|
|
2526
3116
|
function loadVideoMeta(storage, courseId, blockId, enabled) {
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
3117
|
+
const empty = { currentTime: 0, completedCueIndices: [], firedCueIndices: [] };
|
|
3118
|
+
if (!enabled || !courseId) return empty;
|
|
3119
|
+
const saved = (0, import_core20.loadCompoundState)(storage, courseId, blockId);
|
|
3120
|
+
if (!saved) return empty;
|
|
2530
3121
|
const meta = readInteractiveVideoMeta(saved.childStates);
|
|
2531
|
-
return meta ??
|
|
3122
|
+
return meta ?? empty;
|
|
2532
3123
|
}
|
|
2533
3124
|
function getCueChildCheckId(cue) {
|
|
2534
3125
|
const child = import_react31.default.Children.only(cue.props.children);
|
|
@@ -2545,29 +3136,59 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2545
3136
|
validateCompoundChildren("InteractiveVideo", cues);
|
|
2546
3137
|
const { config, track, storage } = useLessonkit();
|
|
2547
3138
|
const lessonId = useEnclosingLessonId();
|
|
3139
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
3140
|
+
const resolvedSrc = resolveMediaSrc(props.src, mediaOptions);
|
|
3141
|
+
const resolvedPoster = resolveMediaSrc(props.poster, mediaOptions);
|
|
3142
|
+
const resolvedCaptions = resolveMediaSrc(props.captions, mediaOptions);
|
|
2548
3143
|
const videoRef = (0, import_react31.useRef)(null);
|
|
3144
|
+
const lastKnownTimeRef = (0, import_react31.useRef)(initialMeta.currentTime);
|
|
2549
3145
|
const completedCuesRef = (0, import_react31.useRef)(new Set(initialMeta.completedCueIndices));
|
|
2550
3146
|
const [completedCues, setCompletedCues] = (0, import_react31.useState)(
|
|
2551
3147
|
() => new Set(initialMeta.completedCueIndices)
|
|
2552
3148
|
);
|
|
2553
3149
|
const [overlayActive, setOverlayActive] = (0, import_react31.useState)(false);
|
|
2554
|
-
const firedCuesRef = (0, import_react31.useRef)(
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
[cues]
|
|
3150
|
+
const firedCuesRef = (0, import_react31.useRef)(
|
|
3151
|
+
new Set(
|
|
3152
|
+
initialMeta.firedCueIndices.length > 0 ? initialMeta.firedCueIndices : initialMeta.completedCueIndices
|
|
3153
|
+
)
|
|
2559
3154
|
);
|
|
3155
|
+
const resumeOverlayCheckedRef = (0, import_react31.useRef)(false);
|
|
3156
|
+
const [persistTrigger, setPersistTrigger] = (0, import_react31.useState)(0);
|
|
3157
|
+
const lastPersistTimeRef = (0, import_react31.useRef)(0);
|
|
3158
|
+
const sortedCues = cues;
|
|
2560
3159
|
(0, import_react31.useEffect)(() => {
|
|
2561
3160
|
completedCuesRef.current = completedCues;
|
|
2562
3161
|
}, [completedCues]);
|
|
2563
3162
|
const transformState = (0, import_react31.useCallback)(
|
|
2564
|
-
(state) =>
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
3163
|
+
(state) => {
|
|
3164
|
+
const liveTime = videoRef.current?.currentTime;
|
|
3165
|
+
const currentTime = Math.max(
|
|
3166
|
+
lastKnownTimeRef.current,
|
|
3167
|
+
typeof liveTime === "number" && Number.isFinite(liveTime) ? liveTime : 0
|
|
3168
|
+
);
|
|
3169
|
+
return mergeVideoMetaIntoState(state, {
|
|
3170
|
+
currentTime,
|
|
3171
|
+
completedCueIndices: [...completedCuesRef.current],
|
|
3172
|
+
firedCueIndices: [...firedCuesRef.current]
|
|
3173
|
+
});
|
|
3174
|
+
},
|
|
3175
|
+
[]
|
|
2569
3176
|
);
|
|
2570
|
-
const
|
|
3177
|
+
const applyVideoMetaFromState = (0, import_react31.useCallback)((state) => {
|
|
3178
|
+
const meta = readInteractiveVideoMeta(state.childStates);
|
|
3179
|
+
if (!meta) return;
|
|
3180
|
+
lastKnownTimeRef.current = meta.currentTime;
|
|
3181
|
+
completedCuesRef.current = new Set(meta.completedCueIndices);
|
|
3182
|
+
firedCuesRef.current = new Set(
|
|
3183
|
+
meta.firedCueIndices.length > 0 ? meta.firedCueIndices : meta.completedCueIndices
|
|
3184
|
+
);
|
|
3185
|
+
setCompletedCues(new Set(meta.completedCueIndices));
|
|
3186
|
+
const video = videoRef.current;
|
|
3187
|
+
if (video && meta.currentTime > 0) {
|
|
3188
|
+
video.currentTime = meta.currentTime;
|
|
3189
|
+
}
|
|
3190
|
+
}, []);
|
|
3191
|
+
const { visibleIndex, ctx } = useCompoundShell({
|
|
2571
3192
|
courseId: config.courseId,
|
|
2572
3193
|
compoundId: blockId,
|
|
2573
3194
|
pageCount: sortedCues.length,
|
|
@@ -2576,9 +3197,11 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2576
3197
|
persistEnabled,
|
|
2577
3198
|
ref,
|
|
2578
3199
|
storage,
|
|
2579
|
-
transformState
|
|
3200
|
+
transformState,
|
|
3201
|
+
persistTrigger,
|
|
3202
|
+
onCompoundResume: applyVideoMetaFromState
|
|
2580
3203
|
});
|
|
2581
|
-
const activeCue = sortedCues[
|
|
3204
|
+
const activeCue = sortedCues[visibleIndex];
|
|
2582
3205
|
const cueCanContinue = (0, import_react31.useCallback)(
|
|
2583
3206
|
(cue) => {
|
|
2584
3207
|
if (!cue || !cueRequiresAnswer(cue)) return true;
|
|
@@ -2599,12 +3222,12 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2599
3222
|
(0, import_react31.useEffect)(() => {
|
|
2600
3223
|
if (resumeOverlayCheckedRef.current || sortedCues.length === 0) return;
|
|
2601
3224
|
resumeOverlayCheckedRef.current = true;
|
|
2602
|
-
const hasSavedProgress = initialMeta.currentTime > 0 || initialMeta.completedCueIndices.length > 0 || persistEnabled && config.courseId && (0,
|
|
3225
|
+
const hasSavedProgress = initialMeta.currentTime > 0 || initialMeta.completedCueIndices.length > 0 || persistEnabled && config.courseId && (0, import_core20.loadCompoundState)(storage, config.courseId, blockId) !== null;
|
|
2603
3226
|
if (!hasSavedProgress) return;
|
|
2604
3227
|
const video = videoRef.current;
|
|
2605
3228
|
if (!video) return;
|
|
2606
|
-
const cue = sortedCues[
|
|
2607
|
-
if (!cue || completedCues.has(
|
|
3229
|
+
const cue = sortedCues[visibleIndex];
|
|
3230
|
+
if (!cue || completedCues.has(visibleIndex)) return;
|
|
2608
3231
|
setOverlayActive(true);
|
|
2609
3232
|
video.pause();
|
|
2610
3233
|
const at = cue.props.atSeconds ?? 0;
|
|
@@ -2616,6 +3239,7 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2616
3239
|
completedCues,
|
|
2617
3240
|
config.courseId,
|
|
2618
3241
|
index,
|
|
3242
|
+
visibleIndex,
|
|
2619
3243
|
initialMeta.completedCueIndices.length,
|
|
2620
3244
|
initialMeta.currentTime,
|
|
2621
3245
|
persistEnabled,
|
|
@@ -2655,6 +3279,12 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2655
3279
|
const video = videoRef.current;
|
|
2656
3280
|
if (!video || overlayActive) return;
|
|
2657
3281
|
const t = video.currentTime;
|
|
3282
|
+
lastKnownTimeRef.current = Math.max(lastKnownTimeRef.current, t);
|
|
3283
|
+
const now = Date.now();
|
|
3284
|
+
if (now - lastPersistTimeRef.current >= 5e3) {
|
|
3285
|
+
lastPersistTimeRef.current = now;
|
|
3286
|
+
setPersistTrigger((n) => n + 1);
|
|
3287
|
+
}
|
|
2658
3288
|
const blockSeek = mandatoryIncompleteBefore(t);
|
|
2659
3289
|
if (blockSeek !== null && t > blockSeek + 0.5) {
|
|
2660
3290
|
video.currentTime = blockSeek;
|
|
@@ -2670,10 +3300,10 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2670
3300
|
}
|
|
2671
3301
|
};
|
|
2672
3302
|
const completeCue = () => {
|
|
2673
|
-
const cue = sortedCues[
|
|
3303
|
+
const cue = sortedCues[visibleIndex];
|
|
2674
3304
|
if (!cue || !cueCanContinue(cue)) return;
|
|
2675
3305
|
setCompletedCues((prev) => {
|
|
2676
|
-
const next = /* @__PURE__ */ new Set([...prev,
|
|
3306
|
+
const next = /* @__PURE__ */ new Set([...prev, visibleIndex]);
|
|
2677
3307
|
completedCuesRef.current = next;
|
|
2678
3308
|
return next;
|
|
2679
3309
|
});
|
|
@@ -2683,7 +3313,7 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2683
3313
|
"video_segment_completed",
|
|
2684
3314
|
{
|
|
2685
3315
|
blockId,
|
|
2686
|
-
segmentIndex:
|
|
3316
|
+
segmentIndex: visibleIndex,
|
|
2687
3317
|
atSeconds: cue.props.atSeconds ?? 0,
|
|
2688
3318
|
segmentLabel: cue.props.label
|
|
2689
3319
|
},
|
|
@@ -2702,12 +3332,12 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2702
3332
|
" ",
|
|
2703
3333
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
2704
3334
|
] }) : null,
|
|
2705
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { style: { position: "relative" }, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3335
|
+
!resolvedSrc ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { role: "alert", "data-testid": "interactive-video-blocked", children: "This video URL is not allowed." }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { style: { position: "relative" }, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
2706
3336
|
"video",
|
|
2707
3337
|
{
|
|
2708
3338
|
ref: videoRef,
|
|
2709
|
-
src:
|
|
2710
|
-
poster:
|
|
3339
|
+
src: resolvedSrc,
|
|
3340
|
+
poster: resolvedPoster ?? void 0,
|
|
2711
3341
|
controls: true,
|
|
2712
3342
|
"data-testid": "interactive-video-player",
|
|
2713
3343
|
onTimeUpdate,
|
|
@@ -2719,13 +3349,22 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2719
3349
|
video.currentTime = blockSeek;
|
|
2720
3350
|
}
|
|
2721
3351
|
},
|
|
2722
|
-
children:
|
|
3352
|
+
children: resolvedCaptions ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3353
|
+
"track",
|
|
3354
|
+
{
|
|
3355
|
+
kind: "captions",
|
|
3356
|
+
src: resolvedCaptions,
|
|
3357
|
+
srcLang: "en",
|
|
3358
|
+
label: "Captions",
|
|
3359
|
+
default: true
|
|
3360
|
+
}
|
|
3361
|
+
) : null
|
|
2723
3362
|
}
|
|
2724
3363
|
) }),
|
|
2725
3364
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "data-testid": "interactive-video-cues", children: sortedCues.map(
|
|
2726
3365
|
(cue, i) => import_react31.default.cloneElement(cue, {
|
|
2727
3366
|
key: cue.key ?? i,
|
|
2728
|
-
hidden: !overlayActive || i !==
|
|
3367
|
+
hidden: !overlayActive || i !== visibleIndex,
|
|
2729
3368
|
cueIndex: i,
|
|
2730
3369
|
parentType: "InteractiveVideo"
|
|
2731
3370
|
})
|
|
@@ -2748,15 +3387,21 @@ var InteractiveVideoInner = (0, import_react31.forwardRef)(function InteractiveV
|
|
|
2748
3387
|
});
|
|
2749
3388
|
var InteractiveVideo = (0, import_react31.forwardRef)(
|
|
2750
3389
|
function InteractiveVideo2(props, ref) {
|
|
2751
|
-
const blockId = (0, import_react31.useMemo)(
|
|
2752
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2753
|
-
[props.blockId]
|
|
2754
|
-
);
|
|
2755
3390
|
const cues = import_react31.default.Children.toArray(props.children).filter(
|
|
2756
3391
|
import_react31.default.isValidElement
|
|
2757
3392
|
);
|
|
3393
|
+
const sortedCues = (0, import_react31.useMemo)(() => sortCuesByTime(cues), [cues]);
|
|
2758
3394
|
const { config, storage } = useLessonkit();
|
|
2759
3395
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3396
|
+
requireCompoundBlockIdWhenPersisting({
|
|
3397
|
+
persistEnabled,
|
|
3398
|
+
blockId: props.blockId,
|
|
3399
|
+
componentName: "InteractiveVideo"
|
|
3400
|
+
});
|
|
3401
|
+
const blockId = (0, import_react31.useMemo)(
|
|
3402
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3403
|
+
[props.blockId]
|
|
3404
|
+
);
|
|
2760
3405
|
const initialMeta = (0, import_react31.useMemo)(
|
|
2761
3406
|
() => loadVideoMeta(storage, config.courseId, blockId, persistEnabled),
|
|
2762
3407
|
[storage, config.courseId, blockId, persistEnabled]
|
|
@@ -2764,7 +3409,7 @@ var InteractiveVideo = (0, import_react31.forwardRef)(
|
|
|
2764
3409
|
const initialIndex = useCompoundInitialIndex({
|
|
2765
3410
|
courseId: config.courseId,
|
|
2766
3411
|
compoundId: blockId,
|
|
2767
|
-
pageCount:
|
|
3412
|
+
pageCount: sortedCues.length,
|
|
2768
3413
|
persistEnabled,
|
|
2769
3414
|
storage
|
|
2770
3415
|
});
|
|
@@ -2772,14 +3417,14 @@ var InteractiveVideo = (0, import_react31.forwardRef)(
|
|
|
2772
3417
|
const setIndexStable = (0, import_react31.useCallback)((i) => setIndex(i), []);
|
|
2773
3418
|
(0, import_react31.useEffect)(() => {
|
|
2774
3419
|
setIndex(initialIndex);
|
|
2775
|
-
}, [config.courseId, blockId, initialIndex]);
|
|
3420
|
+
}, [config.courseId, blockId, initialIndex, sortedCues.length]);
|
|
2776
3421
|
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
2777
3422
|
InteractiveVideoInner,
|
|
2778
3423
|
{
|
|
2779
3424
|
...props,
|
|
2780
3425
|
ref,
|
|
2781
3426
|
blockId,
|
|
2782
|
-
cues,
|
|
3427
|
+
cues: sortedCues,
|
|
2783
3428
|
index,
|
|
2784
3429
|
setIndex,
|
|
2785
3430
|
persistEnabled,
|
|
@@ -2792,10 +3437,18 @@ setLessonkitBlockType(InteractiveVideo, "InteractiveVideo");
|
|
|
2792
3437
|
|
|
2793
3438
|
// src/blocks/Summary.tsx
|
|
2794
3439
|
var import_react32 = require("react");
|
|
3440
|
+
|
|
3441
|
+
// src/assessment/shouldReplayResumeTelemetry.ts
|
|
3442
|
+
function shouldReplayResumeTelemetry(config) {
|
|
3443
|
+
return config?.tracking?.replayResumeEvents === true;
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
// src/blocks/Summary.tsx
|
|
2795
3447
|
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
2796
3448
|
var INTERACTION6 = "summary";
|
|
2797
3449
|
function SummaryInner(props, ref) {
|
|
2798
3450
|
const checkId = (0, import_react32.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3451
|
+
const { config } = useLessonkit();
|
|
2799
3452
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2800
3453
|
const [selectedIndices, setSelectedIndices] = (0, import_react32.useState)([]);
|
|
2801
3454
|
const [passed, setPassed] = (0, import_react32.useState)(false);
|
|
@@ -2823,7 +3476,7 @@ function SummaryInner(props, ref) {
|
|
|
2823
3476
|
const handle = (0, import_react32.useMemo)(
|
|
2824
3477
|
() => buildAssessmentHandle({
|
|
2825
3478
|
checkId,
|
|
2826
|
-
getScore: () =>
|
|
3479
|
+
getScore: () => score,
|
|
2827
3480
|
getMaxScore: () => maxScore,
|
|
2828
3481
|
getAnswerGiven: () => selectedIndices.length > 0,
|
|
2829
3482
|
resetTask: reset,
|
|
@@ -2834,7 +3487,7 @@ function SummaryInner(props, ref) {
|
|
|
2834
3487
|
interactionType: INTERACTION6,
|
|
2835
3488
|
response: selected,
|
|
2836
3489
|
correct: passedThreshold,
|
|
2837
|
-
score
|
|
3490
|
+
score,
|
|
2838
3491
|
maxScore
|
|
2839
3492
|
}),
|
|
2840
3493
|
getCurrentState: () => ({ selectedIndices, passed, checked }),
|
|
@@ -2844,34 +3497,48 @@ function SummaryInner(props, ref) {
|
|
|
2844
3497
|
nextIndices = [...state.selectedIndices];
|
|
2845
3498
|
} else if (Array.isArray(state.selected)) {
|
|
2846
3499
|
const legacy = state.selected;
|
|
3500
|
+
if (isDevEnvironment()) {
|
|
3501
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3502
|
+
for (const text of props.statements) {
|
|
3503
|
+
if (seen.has(text)) {
|
|
3504
|
+
console.warn(
|
|
3505
|
+
"[lessonkit] Summary: duplicate statement strings; legacy selected resume may be ambiguous",
|
|
3506
|
+
text
|
|
3507
|
+
);
|
|
3508
|
+
break;
|
|
3509
|
+
}
|
|
3510
|
+
seen.add(text);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
2847
3513
|
nextIndices = legacy.map((text) => props.statements.indexOf(text)).filter((i) => i >= 0);
|
|
2848
3514
|
}
|
|
2849
3515
|
setSelectedIndices(nextIndices);
|
|
2850
3516
|
const nextSelected = nextIndices.map((i) => props.statements[i] ?? "");
|
|
2851
3517
|
const nextIsCorrect = nextSelected.length === props.correct.length && nextSelected.every((s, i) => s === props.correct[i]);
|
|
2852
3518
|
const nextScore = nextIsCorrect ? maxScore : 0;
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
3519
|
+
const nextPassedThreshold = meetsPassingThreshold(
|
|
3520
|
+
nextScore,
|
|
3521
|
+
maxScore,
|
|
3522
|
+
props.passingScore
|
|
3523
|
+
);
|
|
3524
|
+
setPassed(nextPassedThreshold);
|
|
3525
|
+
completedRef.current = nextPassedThreshold;
|
|
3526
|
+
if (nextPassedThreshold && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
3527
|
+
telemetryReplayedRef.current = true;
|
|
3528
|
+
assessment.answer({
|
|
3529
|
+
checkId,
|
|
3530
|
+
interactionType: INTERACTION6,
|
|
3531
|
+
response: nextSelected,
|
|
3532
|
+
correct: nextPassedThreshold
|
|
3533
|
+
});
|
|
3534
|
+
assessment.complete({
|
|
3535
|
+
checkId,
|
|
3536
|
+
interactionType: INTERACTION6,
|
|
3537
|
+
score: nextScore,
|
|
3538
|
+
maxScore,
|
|
3539
|
+
passingScore: props.passingScore ?? maxScore
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
2875
3542
|
readBooleanStateField(state, "checked", setChecked);
|
|
2876
3543
|
}
|
|
2877
3544
|
}),
|
|
@@ -2879,14 +3546,16 @@ function SummaryInner(props, ref) {
|
|
|
2879
3546
|
assessment,
|
|
2880
3547
|
checkId,
|
|
2881
3548
|
checked,
|
|
3549
|
+
config,
|
|
2882
3550
|
maxScore,
|
|
2883
3551
|
passed,
|
|
2884
3552
|
passedThreshold,
|
|
3553
|
+
props.correct,
|
|
2885
3554
|
props.passingScore,
|
|
2886
3555
|
props.statements,
|
|
2887
3556
|
score,
|
|
2888
3557
|
selected,
|
|
2889
|
-
selectedIndices
|
|
3558
|
+
selectedIndices
|
|
2890
3559
|
]
|
|
2891
3560
|
);
|
|
2892
3561
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
@@ -2909,9 +3578,9 @@ function SummaryInner(props, ref) {
|
|
|
2909
3578
|
response: selected,
|
|
2910
3579
|
correct: passedThreshold
|
|
2911
3580
|
});
|
|
2912
|
-
if (passedThreshold && !completedRef.current) {
|
|
3581
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
2913
3582
|
completedRef.current = true;
|
|
2914
|
-
setPassed(true);
|
|
3583
|
+
if (passedThreshold) setPassed(true);
|
|
2915
3584
|
assessment.complete({
|
|
2916
3585
|
checkId,
|
|
2917
3586
|
interactionType: INTERACTION6,
|
|
@@ -2989,8 +3658,34 @@ function buildDeck(pairs) {
|
|
|
2989
3658
|
);
|
|
2990
3659
|
return shuffleCards(cards);
|
|
2991
3660
|
}
|
|
3661
|
+
function rebuildCardsFromKeys(pairs, cardKeys) {
|
|
3662
|
+
const pairMap = new Map(pairs.map((pair) => [pair.id, pair]));
|
|
3663
|
+
if (cardKeys.length !== pairs.length * 2) return null;
|
|
3664
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3665
|
+
const cards = [];
|
|
3666
|
+
for (const cardKey of cardKeys) {
|
|
3667
|
+
if (seen.has(cardKey)) return null;
|
|
3668
|
+
seen.add(cardKey);
|
|
3669
|
+
const match = /^(.+)-([01])$/.exec(cardKey);
|
|
3670
|
+
if (!match) return null;
|
|
3671
|
+
const pairId = match[1];
|
|
3672
|
+
const copy = Number(match[2]);
|
|
3673
|
+
if (copy !== 0 && copy !== 1) return null;
|
|
3674
|
+
const pair = pairMap.get(pairId);
|
|
3675
|
+
if (!pair) return null;
|
|
3676
|
+
cards.push({
|
|
3677
|
+
cardKey,
|
|
3678
|
+
pairId: pair.id,
|
|
3679
|
+
label: pair.label,
|
|
3680
|
+
imageSrc: pair.imageSrc
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
return cards;
|
|
3684
|
+
}
|
|
2992
3685
|
function ImagePairingInner(props, ref) {
|
|
2993
3686
|
const checkId = (0, import_react33.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3687
|
+
const { config } = useLessonkit();
|
|
3688
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
2994
3689
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2995
3690
|
const pairsKey = props.pairs.map((p) => p.id).join("\0");
|
|
2996
3691
|
const [cards, setCards] = (0, import_react33.useState)(() => buildDeck(props.pairs));
|
|
@@ -2998,9 +3693,15 @@ function ImagePairingInner(props, ref) {
|
|
|
2998
3693
|
const [revealed, setRevealed] = (0, import_react33.useState)(() => /* @__PURE__ */ new Set());
|
|
2999
3694
|
const [keyboardSelection, setKeyboardSelection] = (0, import_react33.useState)(null);
|
|
3000
3695
|
const [passed, setPassed] = (0, import_react33.useState)(false);
|
|
3696
|
+
const [submitted, setSubmitted] = (0, import_react33.useState)(false);
|
|
3001
3697
|
const completedRef = (0, import_react33.useRef)(false);
|
|
3002
3698
|
const telemetryReplayedRef = (0, import_react33.useRef)(false);
|
|
3699
|
+
const mismatchTimeoutRef = (0, import_react33.useRef)(null);
|
|
3003
3700
|
const reset = () => {
|
|
3701
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
3702
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
3703
|
+
mismatchTimeoutRef.current = null;
|
|
3704
|
+
}
|
|
3004
3705
|
completedRef.current = false;
|
|
3005
3706
|
telemetryReplayedRef.current = false;
|
|
3006
3707
|
setCards(buildDeck(props.pairs));
|
|
@@ -3008,10 +3709,19 @@ function ImagePairingInner(props, ref) {
|
|
|
3008
3709
|
setRevealed(/* @__PURE__ */ new Set());
|
|
3009
3710
|
setKeyboardSelection(null);
|
|
3010
3711
|
setPassed(false);
|
|
3712
|
+
setSubmitted(false);
|
|
3011
3713
|
};
|
|
3012
3714
|
(0, import_react33.useEffect)(() => {
|
|
3013
3715
|
reset();
|
|
3014
3716
|
}, [checkId, pairsKey]);
|
|
3717
|
+
(0, import_react33.useEffect)(
|
|
3718
|
+
() => () => {
|
|
3719
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
3720
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
3721
|
+
}
|
|
3722
|
+
},
|
|
3723
|
+
[]
|
|
3724
|
+
);
|
|
3015
3725
|
const totalPairs = props.pairs.length;
|
|
3016
3726
|
const matchedCount = matched.size;
|
|
3017
3727
|
const maxScore = totalPairs || 1;
|
|
@@ -3019,25 +3729,34 @@ function ImagePairingInner(props, ref) {
|
|
|
3019
3729
|
const allMatched = totalPairs > 0 && matchedCount === totalPairs;
|
|
3020
3730
|
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
3021
3731
|
const completeIfReady = (nextMatched) => {
|
|
3022
|
-
if (
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3732
|
+
if (totalPairs === 0 || completedRef.current) return;
|
|
3733
|
+
const finalScore = nextMatched.size;
|
|
3734
|
+
const finalPassed = meetsPassingThreshold(finalScore, maxScore, props.passingScore);
|
|
3735
|
+
if (!finalPassed && nextMatched.size < totalPairs) return;
|
|
3736
|
+
completeWithScore(nextMatched, finalScore, finalPassed);
|
|
3737
|
+
};
|
|
3738
|
+
const completeWithScore = (nextMatched, finalScore, finalPassed) => {
|
|
3739
|
+
if (completedRef.current) return;
|
|
3740
|
+
completedRef.current = true;
|
|
3741
|
+
setSubmitted(true);
|
|
3742
|
+
setPassed(finalPassed);
|
|
3743
|
+
assessment.answer({
|
|
3744
|
+
checkId,
|
|
3745
|
+
interactionType: INTERACTION7,
|
|
3746
|
+
response: { matchedPairIds: [...nextMatched] },
|
|
3747
|
+
correct: finalPassed
|
|
3748
|
+
});
|
|
3749
|
+
assessment.complete({
|
|
3750
|
+
checkId,
|
|
3751
|
+
interactionType: INTERACTION7,
|
|
3752
|
+
score: finalScore,
|
|
3753
|
+
maxScore,
|
|
3754
|
+
passingScore: props.passingScore ?? maxScore
|
|
3755
|
+
});
|
|
3756
|
+
};
|
|
3757
|
+
const finishAttempt = () => {
|
|
3758
|
+
if (completedRef.current || matchedCount === 0) return;
|
|
3759
|
+
completeWithScore(matched, matchedCount, passedThreshold);
|
|
3041
3760
|
};
|
|
3042
3761
|
const tryMatch = (firstKey, secondKey) => {
|
|
3043
3762
|
if (firstKey === secondKey) return;
|
|
@@ -3054,7 +3773,11 @@ function ImagePairingInner(props, ref) {
|
|
|
3054
3773
|
setRevealed(/* @__PURE__ */ new Set());
|
|
3055
3774
|
setKeyboardSelection(null);
|
|
3056
3775
|
} else {
|
|
3057
|
-
|
|
3776
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
3777
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
3778
|
+
}
|
|
3779
|
+
mismatchTimeoutRef.current = window.setTimeout(() => {
|
|
3780
|
+
mismatchTimeoutRef.current = null;
|
|
3058
3781
|
setRevealed((prev) => {
|
|
3059
3782
|
const next = new Set(prev);
|
|
3060
3783
|
next.delete(firstKey);
|
|
@@ -3097,17 +3820,22 @@ function ImagePairingInner(props, ref) {
|
|
|
3097
3820
|
checkId,
|
|
3098
3821
|
interactionType: INTERACTION7,
|
|
3099
3822
|
response: { matchedPairIds: [...matched] },
|
|
3100
|
-
correct:
|
|
3823
|
+
correct: passedThreshold,
|
|
3101
3824
|
score,
|
|
3102
3825
|
maxScore
|
|
3103
3826
|
}),
|
|
3104
3827
|
getCurrentState: () => ({
|
|
3828
|
+
cardKeys: cards.map((card) => card.cardKey),
|
|
3105
3829
|
matched: [...matched],
|
|
3106
3830
|
revealed: [...revealed],
|
|
3107
3831
|
keyboardSelection,
|
|
3108
3832
|
passed
|
|
3109
3833
|
}),
|
|
3110
3834
|
resume: (state) => {
|
|
3835
|
+
if (Array.isArray(state.cardKeys)) {
|
|
3836
|
+
const restored = rebuildCardsFromKeys(props.pairs, state.cardKeys);
|
|
3837
|
+
if (restored) setCards(restored);
|
|
3838
|
+
}
|
|
3111
3839
|
if (Array.isArray(state.matched)) setMatched(new Set(state.matched));
|
|
3112
3840
|
if (Array.isArray(state.revealed)) setRevealed(new Set(state.revealed));
|
|
3113
3841
|
const sel = state.keyboardSelection;
|
|
@@ -3115,15 +3843,20 @@ function ImagePairingInner(props, ref) {
|
|
|
3115
3843
|
readBooleanStateField(state, "passed", (value) => {
|
|
3116
3844
|
setPassed(value);
|
|
3117
3845
|
completedRef.current = value;
|
|
3118
|
-
if (value && !telemetryReplayedRef.current) {
|
|
3846
|
+
if (value && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
3119
3847
|
telemetryReplayedRef.current = true;
|
|
3120
3848
|
const matchedIds = Array.isArray(state.matched) ? state.matched : [...matched];
|
|
3121
3849
|
const finalScore = matchedIds.length;
|
|
3850
|
+
const finalPassed = meetsPassingThreshold(
|
|
3851
|
+
finalScore,
|
|
3852
|
+
maxScore,
|
|
3853
|
+
props.passingScore
|
|
3854
|
+
);
|
|
3122
3855
|
assessment.answer({
|
|
3123
3856
|
checkId,
|
|
3124
3857
|
interactionType: INTERACTION7,
|
|
3125
3858
|
response: { matchedPairIds: matchedIds },
|
|
3126
|
-
correct:
|
|
3859
|
+
correct: finalPassed
|
|
3127
3860
|
});
|
|
3128
3861
|
assessment.complete({
|
|
3129
3862
|
checkId,
|
|
@@ -3136,7 +3869,7 @@ function ImagePairingInner(props, ref) {
|
|
|
3136
3869
|
});
|
|
3137
3870
|
}
|
|
3138
3871
|
}),
|
|
3139
|
-
[allMatched, checkId, keyboardSelection, matched, matchedCount, maxScore, passed, passedThreshold, revealed, score]
|
|
3872
|
+
[allMatched, assessment, cards, checkId, config, keyboardSelection, matched, matchedCount, maxScore, passed, passedThreshold, props.pairs, props.passingScore, revealed, score]
|
|
3140
3873
|
);
|
|
3141
3874
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3142
3875
|
return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": "Image Pairing", "data-lk-check-id": checkId, "data-testid": "image-pairing", children: [
|
|
@@ -3145,6 +3878,7 @@ function ImagePairingInner(props, ref) {
|
|
|
3145
3878
|
const isMatched = matched.has(card.pairId);
|
|
3146
3879
|
const isRevealed = isMatched || revealed.has(card.cardKey);
|
|
3147
3880
|
const isSelected = keyboardSelection === card.cardKey;
|
|
3881
|
+
const resolvedCardSrc = resolveMediaSrc(card.imageSrc, mediaOptions);
|
|
3148
3882
|
return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
3149
3883
|
"button",
|
|
3150
3884
|
{
|
|
@@ -3161,7 +3895,14 @@ function ImagePairingInner(props, ref) {
|
|
|
3161
3895
|
border: isSelected ? "2px solid currentColor" : "1px solid currentColor"
|
|
3162
3896
|
},
|
|
3163
3897
|
children: isRevealed ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
|
|
3164
|
-
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
3898
|
+
resolvedCardSrc ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
3899
|
+
"img",
|
|
3900
|
+
{
|
|
3901
|
+
src: resolvedCardSrc,
|
|
3902
|
+
alt: card.label,
|
|
3903
|
+
style: { maxWidth: "5rem", maxHeight: "5rem" }
|
|
3904
|
+
}
|
|
3905
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { "aria-hidden": "true", children: "!" }),
|
|
3165
3906
|
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "lk-visually-hidden", children: card.label })
|
|
3166
3907
|
] }) : "?"
|
|
3167
3908
|
},
|
|
@@ -3174,6 +3915,7 @@ function ImagePairingInner(props, ref) {
|
|
|
3174
3915
|
totalPairs,
|
|
3175
3916
|
" pairs matched"
|
|
3176
3917
|
] }),
|
|
3918
|
+
props.enableRetry === false && matchedCount > 0 && !submitted ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", "data-testid": "image-pairing-finish", onClick: finishAttempt, children: "Submit" }) : null,
|
|
3177
3919
|
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", "data-testid": "image-pairing-retry", onClick: reset, children: "Try again" }) : null
|
|
3178
3920
|
] });
|
|
3179
3921
|
}
|
|
@@ -3189,6 +3931,8 @@ var import_jsx_runtime24 = require("react/jsx-runtime");
|
|
|
3189
3931
|
var INTERACTION8 = "imageSequencing";
|
|
3190
3932
|
function ImageSequencingInner(props, ref) {
|
|
3191
3933
|
const checkId = (0, import_react34.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3934
|
+
const { config } = useLessonkit();
|
|
3935
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
3192
3936
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3193
3937
|
const imagesKey = props.images.map((i) => i.id).join("\0");
|
|
3194
3938
|
const orderKey = props.correctOrder.join("\0");
|
|
@@ -3225,9 +3969,9 @@ function ImageSequencingInner(props, ref) {
|
|
|
3225
3969
|
const handle = (0, import_react34.useMemo)(
|
|
3226
3970
|
() => buildAssessmentHandle({
|
|
3227
3971
|
checkId,
|
|
3228
|
-
getScore: () =>
|
|
3972
|
+
getScore: () => score,
|
|
3229
3973
|
getMaxScore: () => maxScore,
|
|
3230
|
-
getAnswerGiven: () =>
|
|
3974
|
+
getAnswerGiven: () => checked,
|
|
3231
3975
|
resetTask: reset,
|
|
3232
3976
|
showSolutions: () => {
|
|
3233
3977
|
},
|
|
@@ -3236,7 +3980,7 @@ function ImageSequencingInner(props, ref) {
|
|
|
3236
3980
|
interactionType: INTERACTION8,
|
|
3237
3981
|
response: order,
|
|
3238
3982
|
correct: passedThreshold,
|
|
3239
|
-
score
|
|
3983
|
+
score,
|
|
3240
3984
|
maxScore
|
|
3241
3985
|
}),
|
|
3242
3986
|
getCurrentState: () => ({ order, passed, checked }),
|
|
@@ -3249,7 +3993,7 @@ function ImageSequencingInner(props, ref) {
|
|
|
3249
3993
|
readBooleanStateField(state, "passed", (value) => {
|
|
3250
3994
|
setPassed(value);
|
|
3251
3995
|
completedRef.current = value;
|
|
3252
|
-
if (value && !telemetryReplayedRef.current) {
|
|
3996
|
+
if (value && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
3253
3997
|
telemetryReplayedRef.current = true;
|
|
3254
3998
|
const nextIsCorrect = nextOrder.every((id, i) => id === props.correctOrder[i]);
|
|
3255
3999
|
const nextScore = nextIsCorrect ? maxScore : 0;
|
|
@@ -3271,7 +4015,7 @@ function ImageSequencingInner(props, ref) {
|
|
|
3271
4015
|
readBooleanStateField(state, "checked", setChecked);
|
|
3272
4016
|
}
|
|
3273
4017
|
}),
|
|
3274
|
-
[checkId, checked, maxScore, order, passed, passedThreshold, score]
|
|
4018
|
+
[assessment, checkId, checked, config, maxScore, order, passed, passedThreshold, props.correctOrder, props.passingScore, score]
|
|
3275
4019
|
);
|
|
3276
4020
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3277
4021
|
const check = () => {
|
|
@@ -3282,9 +4026,9 @@ function ImageSequencingInner(props, ref) {
|
|
|
3282
4026
|
response: order,
|
|
3283
4027
|
correct: passedThreshold
|
|
3284
4028
|
});
|
|
3285
|
-
if (passedThreshold && !completedRef.current) {
|
|
4029
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
3286
4030
|
completedRef.current = true;
|
|
3287
|
-
setPassed(true);
|
|
4031
|
+
if (passedThreshold) setPassed(true);
|
|
3288
4032
|
assessment.complete({
|
|
3289
4033
|
checkId,
|
|
3290
4034
|
interactionType: INTERACTION8,
|
|
@@ -3299,8 +4043,16 @@ function ImageSequencingInner(props, ref) {
|
|
|
3299
4043
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("ol", { "data-testid": "image-sequencing-list", children: order.map((id, index) => {
|
|
3300
4044
|
const image = props.images.find((i) => i.id === id);
|
|
3301
4045
|
if (!image) return null;
|
|
4046
|
+
const resolvedSrc = resolveMediaSrc(image.src, mediaOptions);
|
|
3302
4047
|
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("li", { "data-testid": `sequencing-item-${id}`, children: [
|
|
3303
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4048
|
+
resolvedSrc ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4049
|
+
"img",
|
|
4050
|
+
{
|
|
4051
|
+
src: resolvedSrc,
|
|
4052
|
+
alt: image.alt,
|
|
4053
|
+
style: { maxWidth: "8rem", verticalAlign: "middle" }
|
|
4054
|
+
}
|
|
4055
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { "aria-hidden": "true", children: "!" }),
|
|
3304
4056
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3305
4057
|
"button",
|
|
3306
4058
|
{
|
|
@@ -3353,6 +4105,7 @@ var import_jsx_runtime25 = require("react/jsx-runtime");
|
|
|
3353
4105
|
var INTERACTION9 = "arithmeticQuiz";
|
|
3354
4106
|
function ArithmeticQuizInner(props, ref) {
|
|
3355
4107
|
const checkId = (0, import_react35.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4108
|
+
const { config } = useLessonkit();
|
|
3356
4109
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3357
4110
|
const problemsKey = props.problems.map((p) => `${p.question}\0${p.answer}`).join("|");
|
|
3358
4111
|
const [answers, setAnswers] = (0, import_react35.useState)(
|
|
@@ -3393,9 +4146,9 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
3393
4146
|
response: answers,
|
|
3394
4147
|
correct: passedThreshold
|
|
3395
4148
|
});
|
|
3396
|
-
if (passedThreshold && !completedRef.current) {
|
|
4149
|
+
if ((passedThreshold || props.enableRetry === false) && !completedRef.current) {
|
|
3397
4150
|
completedRef.current = true;
|
|
3398
|
-
setPassed(
|
|
4151
|
+
setPassed(passedThreshold);
|
|
3399
4152
|
assessment.complete({
|
|
3400
4153
|
checkId,
|
|
3401
4154
|
interactionType: INTERACTION9,
|
|
@@ -3419,7 +4172,7 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
3419
4172
|
const handle = (0, import_react35.useMemo)(
|
|
3420
4173
|
() => buildAssessmentHandle({
|
|
3421
4174
|
checkId,
|
|
3422
|
-
getScore: () =>
|
|
4175
|
+
getScore: () => score,
|
|
3423
4176
|
getMaxScore: () => maxScore,
|
|
3424
4177
|
getAnswerGiven: () => allFilled,
|
|
3425
4178
|
resetTask: reset,
|
|
@@ -3430,7 +4183,7 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
3430
4183
|
interactionType: INTERACTION9,
|
|
3431
4184
|
response: answers,
|
|
3432
4185
|
correct: passedThreshold,
|
|
3433
|
-
score
|
|
4186
|
+
score,
|
|
3434
4187
|
maxScore
|
|
3435
4188
|
}),
|
|
3436
4189
|
getCurrentState: () => ({ answers, passed, checked, timeLeft }),
|
|
@@ -3444,17 +4197,18 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
3444
4197
|
readBooleanStateField(state, "passed", (value) => {
|
|
3445
4198
|
setPassed(value);
|
|
3446
4199
|
completedRef.current = value;
|
|
3447
|
-
if (value && !telemetryReplayedRef.current) {
|
|
4200
|
+
if (value && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
3448
4201
|
telemetryReplayedRef.current = true;
|
|
3449
4202
|
let nextScore = 0;
|
|
3450
4203
|
props.problems.forEach((p, i) => {
|
|
3451
4204
|
if ((nextAnswers[i] ?? "").trim() === p.answer.trim()) nextScore += 1;
|
|
3452
4205
|
});
|
|
4206
|
+
const replayCorrect = nextScore >= (props.passingScore ?? maxScore);
|
|
3453
4207
|
assessment.answer({
|
|
3454
4208
|
checkId,
|
|
3455
4209
|
interactionType: INTERACTION9,
|
|
3456
4210
|
response: nextAnswers,
|
|
3457
|
-
correct:
|
|
4211
|
+
correct: replayCorrect
|
|
3458
4212
|
});
|
|
3459
4213
|
assessment.complete({
|
|
3460
4214
|
checkId,
|
|
@@ -3469,7 +4223,7 @@ function ArithmeticQuizInner(props, ref) {
|
|
|
3469
4223
|
if (typeof state.timeLeft === "number") setTimeLeft(state.timeLeft);
|
|
3470
4224
|
}
|
|
3471
4225
|
}),
|
|
3472
|
-
[allFilled, answers, checkId, checked, maxScore, passed, passedThreshold, score, timeLeft]
|
|
4226
|
+
[allFilled, answers, checkId, checked, config, maxScore, passed, passedThreshold, props.problems, props.passingScore, score, timeLeft]
|
|
3473
4227
|
);
|
|
3474
4228
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3475
4229
|
const onInput = (index, value) => {
|
|
@@ -3533,6 +4287,7 @@ var import_jsx_runtime26 = require("react/jsx-runtime");
|
|
|
3533
4287
|
var INTERACTION10 = "essay";
|
|
3534
4288
|
function EssayInner(props, ref) {
|
|
3535
4289
|
const checkId = (0, import_react36.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4290
|
+
const { config } = useLessonkit();
|
|
3536
4291
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3537
4292
|
const [text, setText] = (0, import_react36.useState)("");
|
|
3538
4293
|
const [submitted, setSubmitted] = (0, import_react36.useState)(false);
|
|
@@ -3572,16 +4327,22 @@ function EssayInner(props, ref) {
|
|
|
3572
4327
|
const nextText = readStringField(state, "text");
|
|
3573
4328
|
if (typeof nextText === "string") setText(nextText);
|
|
3574
4329
|
readBooleanStateField(state, "submitted", (value) => {
|
|
4330
|
+
const textVal = typeof nextText === "string" ? nextText : text;
|
|
4331
|
+
const meetsMin = textVal.trim().length >= minLength;
|
|
4332
|
+
if (value && !meetsMin) {
|
|
4333
|
+
setSubmitted(false);
|
|
4334
|
+
completedRef.current = false;
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
3575
4337
|
setSubmitted(value);
|
|
3576
4338
|
completedRef.current = value;
|
|
3577
|
-
if (value && !telemetryReplayedRef.current) {
|
|
4339
|
+
if (value && !telemetryReplayedRef.current && shouldReplayResumeTelemetry(config)) {
|
|
3578
4340
|
telemetryReplayedRef.current = true;
|
|
3579
|
-
const response = typeof nextText === "string" ? nextText : text;
|
|
3580
4341
|
assessment.answer({
|
|
3581
4342
|
checkId,
|
|
3582
4343
|
interactionType: INTERACTION10,
|
|
3583
4344
|
question: props.question,
|
|
3584
|
-
response,
|
|
4345
|
+
response: textVal,
|
|
3585
4346
|
correct: false
|
|
3586
4347
|
});
|
|
3587
4348
|
assessment.complete({
|
|
@@ -3595,7 +4356,7 @@ function EssayInner(props, ref) {
|
|
|
3595
4356
|
});
|
|
3596
4357
|
}
|
|
3597
4358
|
}),
|
|
3598
|
-
[checkId, meetsMinLength, props.question, submitted, text]
|
|
4359
|
+
[assessment, checkId, config, meetsMinLength, minLength, props.passingScore, props.question, submitted, text]
|
|
3599
4360
|
);
|
|
3600
4361
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3601
4362
|
const submit = () => {
|
|
@@ -3773,16 +4534,29 @@ function MemoryGame(props) {
|
|
|
3773
4534
|
const [revealed, setRevealed] = (0, import_react38.useState)(() => /* @__PURE__ */ new Set());
|
|
3774
4535
|
const [selection, setSelection] = (0, import_react38.useState)(null);
|
|
3775
4536
|
const [complete, setComplete] = (0, import_react38.useState)(false);
|
|
4537
|
+
const mismatchTimeoutRef = (0, import_react38.useRef)(null);
|
|
3776
4538
|
const { track } = useLessonkit();
|
|
3777
4539
|
const lessonId = useEnclosingLessonId();
|
|
3778
4540
|
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
3779
4541
|
(0, import_react38.useEffect)(() => {
|
|
4542
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
4543
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
4544
|
+
mismatchTimeoutRef.current = null;
|
|
4545
|
+
}
|
|
3780
4546
|
setCards(buildDeck2(props.pairs));
|
|
3781
4547
|
setMatched(/* @__PURE__ */ new Set());
|
|
3782
4548
|
setRevealed(/* @__PURE__ */ new Set());
|
|
3783
4549
|
setSelection(null);
|
|
3784
4550
|
setComplete(false);
|
|
3785
4551
|
}, [props.blockId, pairsKey]);
|
|
4552
|
+
(0, import_react38.useEffect)(
|
|
4553
|
+
() => () => {
|
|
4554
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
4555
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
4556
|
+
}
|
|
4557
|
+
},
|
|
4558
|
+
[]
|
|
4559
|
+
);
|
|
3786
4560
|
const cardIndexByKey = (0, import_react38.useMemo)(
|
|
3787
4561
|
() => Object.fromEntries(cards.map((c, i) => [c.cardKey, i])),
|
|
3788
4562
|
[cards]
|
|
@@ -3812,7 +4586,11 @@ function MemoryGame(props) {
|
|
|
3812
4586
|
setRevealed(/* @__PURE__ */ new Set());
|
|
3813
4587
|
setSelection(null);
|
|
3814
4588
|
} else {
|
|
3815
|
-
|
|
4589
|
+
if (mismatchTimeoutRef.current !== null) {
|
|
4590
|
+
window.clearTimeout(mismatchTimeoutRef.current);
|
|
4591
|
+
}
|
|
4592
|
+
mismatchTimeoutRef.current = window.setTimeout(() => {
|
|
4593
|
+
mismatchTimeoutRef.current = null;
|
|
3816
4594
|
setRevealed((prev) => {
|
|
3817
4595
|
const next = new Set(prev);
|
|
3818
4596
|
next.delete(firstKey);
|
|
@@ -3969,10 +4747,16 @@ function usePrefersReducedMotion() {
|
|
|
3969
4747
|
function ParallaxSlideshow(props) {
|
|
3970
4748
|
const [index, setIndex] = (0, import_react40.useState)(0);
|
|
3971
4749
|
const reducedMotion = usePrefersReducedMotion();
|
|
3972
|
-
const { track } = useLessonkit();
|
|
4750
|
+
const { track, config } = useLessonkit();
|
|
3973
4751
|
const lessonId = useEnclosingLessonId();
|
|
3974
4752
|
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
3975
4753
|
const slide = props.slides[index];
|
|
4754
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
4755
|
+
const resolvedImageSrc = slide?.imageSrc ? resolveMediaSrc(slide.imageSrc, mediaOptions) : null;
|
|
4756
|
+
(0, import_react40.useEffect)(() => {
|
|
4757
|
+
if (props.slides.length < 1) return;
|
|
4758
|
+
setIndex((current) => Math.min(current, props.slides.length - 1));
|
|
4759
|
+
}, [props.slides.length]);
|
|
3976
4760
|
(0, import_react40.useEffect)(() => {
|
|
3977
4761
|
track(
|
|
3978
4762
|
"parallax_slide_viewed",
|
|
@@ -3999,22 +4783,23 @@ function ParallaxSlideshow(props) {
|
|
|
3999
4783
|
"data-testid": `parallax-slide-${index}`,
|
|
4000
4784
|
style: reducedMotion ? void 0 : {
|
|
4001
4785
|
backgroundAttachment: "fixed",
|
|
4002
|
-
backgroundImage:
|
|
4786
|
+
backgroundImage: resolvedImageSrc ? `url("${resolvedImageSrc}")` : void 0,
|
|
4003
4787
|
backgroundPosition: "center",
|
|
4004
4788
|
backgroundSize: "cover",
|
|
4005
4789
|
minHeight: "12rem",
|
|
4006
4790
|
padding: "1rem"
|
|
4007
4791
|
},
|
|
4008
4792
|
children: [
|
|
4009
|
-
reducedMotion &&
|
|
4793
|
+
reducedMotion && resolvedImageSrc ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
|
|
4010
4794
|
"img",
|
|
4011
4795
|
{
|
|
4012
|
-
src:
|
|
4796
|
+
src: resolvedImageSrc,
|
|
4013
4797
|
alt: "",
|
|
4014
4798
|
"data-testid": "parallax-slide-image",
|
|
4015
4799
|
style: { maxWidth: "100%" }
|
|
4016
4800
|
}
|
|
4017
4801
|
) : null,
|
|
4802
|
+
!reducedMotion && slide.imageSrc && !resolvedImageSrc ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { role: "alert", children: "This image URL is not allowed." }) : null,
|
|
4018
4803
|
/* @__PURE__ */ (0, import_jsx_runtime30.jsx)("h3", { "data-testid": "parallax-slide-title", children: slide.title }),
|
|
4019
4804
|
/* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { "data-testid": "parallax-slide-body", children: slide.body })
|
|
4020
4805
|
]
|
|
@@ -4203,8 +4988,9 @@ var import_react44 = require("react");
|
|
|
4203
4988
|
var import_jsx_runtime34 = require("react/jsx-runtime");
|
|
4204
4989
|
function ImageHotspots(props) {
|
|
4205
4990
|
const [active, setActive] = (0, import_react44.useState)(null);
|
|
4206
|
-
const { track } = useLessonkit();
|
|
4991
|
+
const { track, config } = useLessonkit();
|
|
4207
4992
|
const lessonId = useEnclosingLessonId();
|
|
4993
|
+
const resolvedSrc = resolveMediaSrc(props.src, { allowedHosts: config.embed?.allowedHosts });
|
|
4208
4994
|
const open = (hotspotId) => {
|
|
4209
4995
|
setActive(hotspotId);
|
|
4210
4996
|
track(
|
|
@@ -4215,7 +5001,7 @@ function ImageHotspots(props) {
|
|
|
4215
5001
|
};
|
|
4216
5002
|
return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
|
|
4217
5003
|
/* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4218
|
-
/* @__PURE__ */ (0, import_jsx_runtime34.jsx)("img", { src:
|
|
5004
|
+
resolvedSrc ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("img", { src: resolvedSrc, alt: props.alt, style: { maxWidth: "100%" } }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { role: "alert", children: "This image URL is not allowed." }),
|
|
4219
5005
|
props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
|
|
4220
5006
|
"button",
|
|
4221
5007
|
{
|
|
@@ -4248,9 +5034,11 @@ var import_react45 = require("react");
|
|
|
4248
5034
|
var import_jsx_runtime35 = require("react/jsx-runtime");
|
|
4249
5035
|
function ImageSlider(props) {
|
|
4250
5036
|
const [index, setIndex] = (0, import_react45.useState)(0);
|
|
4251
|
-
const { track } = useLessonkit();
|
|
5037
|
+
const { track, config } = useLessonkit();
|
|
4252
5038
|
const lessonId = useEnclosingLessonId();
|
|
4253
5039
|
const slide = props.slides[index];
|
|
5040
|
+
const mediaOptions = { allowedHosts: config.embed?.allowedHosts };
|
|
5041
|
+
const resolvedSrc = slide ? resolveMediaSrc(slide.src, mediaOptions) : null;
|
|
4254
5042
|
if (!slide) return null;
|
|
4255
5043
|
const goTo = (next) => {
|
|
4256
5044
|
setIndex(next);
|
|
@@ -4261,7 +5049,7 @@ function ImageSlider(props) {
|
|
|
4261
5049
|
);
|
|
4262
5050
|
};
|
|
4263
5051
|
return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
4264
|
-
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)("img", { src:
|
|
5052
|
+
resolvedSrc ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("img", { src: resolvedSrc, alt: slide.alt, style: { maxWidth: "100%" } }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { role: "alert", children: "This image URL is not allowed." }),
|
|
4265
5053
|
slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { children: slide.caption }) : null,
|
|
4266
5054
|
/* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("nav", { "aria-label": "Slide navigation", children: [
|
|
4267
5055
|
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
|
|
@@ -4300,6 +5088,8 @@ var import_jsx_runtime36 = require("react/jsx-runtime");
|
|
|
4300
5088
|
var INTERACTION11 = "findHotspot";
|
|
4301
5089
|
function FindHotspotInner(props, ref) {
|
|
4302
5090
|
const checkId = (0, import_react46.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
5091
|
+
const { config } = useLessonkit();
|
|
5092
|
+
const resolvedSrc = resolveMediaSrc(props.src, { allowedHosts: config.embed?.allowedHosts });
|
|
4303
5093
|
const [selected, setSelected] = (0, import_react46.useState)(null);
|
|
4304
5094
|
const [checked, setChecked] = (0, import_react46.useState)(false);
|
|
4305
5095
|
const telemetryReplayedRef = (0, import_react46.useRef)(false);
|
|
@@ -4335,7 +5125,7 @@ function FindHotspotInner(props, ref) {
|
|
|
4335
5125
|
checkId,
|
|
4336
5126
|
getScore: () => checked && correct ? 1 : 0,
|
|
4337
5127
|
getMaxScore: () => 1,
|
|
4338
|
-
getAnswerGiven: () =>
|
|
5128
|
+
getAnswerGiven: () => checked,
|
|
4339
5129
|
resetTask: () => {
|
|
4340
5130
|
setSelected(null);
|
|
4341
5131
|
setChecked(false);
|
|
@@ -4365,10 +5155,12 @@ function FindHotspotInner(props, ref) {
|
|
|
4365
5155
|
setChecked(value);
|
|
4366
5156
|
});
|
|
4367
5157
|
const nextCorrect = nextSelected === props.correctTargetId;
|
|
4368
|
-
|
|
5158
|
+
if (config.tracking?.replayResumeEvents === true) {
|
|
5159
|
+
replayTelemetry(nextSelected, nextChecked, nextCorrect);
|
|
5160
|
+
}
|
|
4369
5161
|
}
|
|
4370
5162
|
}),
|
|
4371
|
-
[assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
|
|
5163
|
+
[assessment, checkId, checked, config.tracking?.replayResumeEvents, correct, props.correctTargetId, props.passingScore, props.targets, selected]
|
|
4372
5164
|
);
|
|
4373
5165
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4374
5166
|
const selectTarget = (id) => {
|
|
@@ -4392,11 +5184,19 @@ function FindHotspotInner(props, ref) {
|
|
|
4392
5184
|
maxScore: 1,
|
|
4393
5185
|
passingScore: props.passingScore ?? 1
|
|
4394
5186
|
});
|
|
5187
|
+
} else if (props.enableRetry === false) {
|
|
5188
|
+
assessment.complete({
|
|
5189
|
+
checkId,
|
|
5190
|
+
interactionType: INTERACTION11,
|
|
5191
|
+
score: 0,
|
|
5192
|
+
maxScore: 1,
|
|
5193
|
+
passingScore: props.passingScore ?? 1
|
|
5194
|
+
});
|
|
4395
5195
|
}
|
|
4396
5196
|
};
|
|
4397
5197
|
return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
4398
5198
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4399
|
-
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { src:
|
|
5199
|
+
resolvedSrc ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { src: resolvedSrc, alt: props.alt, style: { maxWidth: "100%" } }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { role: "alert", children: "This image URL is not allowed." }),
|
|
4400
5200
|
props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
4401
5201
|
"button",
|
|
4402
5202
|
{
|
|
@@ -4432,9 +5232,21 @@ var import_jsx_runtime37 = require("react/jsx-runtime");
|
|
|
4432
5232
|
var INTERACTION12 = "findMultipleHotspots";
|
|
4433
5233
|
function FindMultipleHotspotsInner(props, ref) {
|
|
4434
5234
|
const checkId = (0, import_react47.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
5235
|
+
const { config } = useLessonkit();
|
|
5236
|
+
const resolvedSrc = resolveMediaSrc(props.src, { allowedHosts: config.embed?.allowedHosts });
|
|
4435
5237
|
const [selected, setSelected] = (0, import_react47.useState)(/* @__PURE__ */ new Set());
|
|
4436
5238
|
const [checked, setChecked] = (0, import_react47.useState)(false);
|
|
4437
5239
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
5240
|
+
const correctSet = (0, import_react47.useMemo)(
|
|
5241
|
+
() => new Set(props.correctTargetIds),
|
|
5242
|
+
[props.correctTargetIds]
|
|
5243
|
+
);
|
|
5244
|
+
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
5245
|
+
const correctIdsKey = props.correctTargetIds.join("\0");
|
|
5246
|
+
(0, import_react47.useEffect)(() => {
|
|
5247
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
5248
|
+
setChecked(false);
|
|
5249
|
+
}, [checkId, correctIdsKey, targetIdsKey]);
|
|
4438
5250
|
const toggle = (id) => {
|
|
4439
5251
|
setSelected((prev) => {
|
|
4440
5252
|
const next = new Set(prev);
|
|
@@ -4444,13 +5256,26 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4444
5256
|
});
|
|
4445
5257
|
setChecked(false);
|
|
4446
5258
|
};
|
|
4447
|
-
const
|
|
5259
|
+
const maxScore = props.correctTargetIds.length || 1;
|
|
5260
|
+
const score = props.correctTargetIds.filter((id) => selected.has(id)).length;
|
|
5261
|
+
const wrongSelected = [...selected].filter((id) => !correctSet.has(id)).length;
|
|
5262
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore) && wrongSelected === 0;
|
|
5263
|
+
const isFactuallyCorrect = (sel) => {
|
|
5264
|
+
const wrong = [...sel].filter((id) => !correctSet.has(id)).length;
|
|
5265
|
+
const matched = props.correctTargetIds.filter((id) => sel.has(id)).length;
|
|
5266
|
+
return wrong === 0 && sel.size === props.correctTargetIds.length && matched === maxScore;
|
|
5267
|
+
};
|
|
5268
|
+
const factualCorrect = checked ? isFactuallyCorrect(selected) : false;
|
|
5269
|
+
const validTargetIds = (0, import_react47.useMemo)(
|
|
5270
|
+
() => new Set(props.targets.map((t) => t.id)),
|
|
5271
|
+
[props.targets]
|
|
5272
|
+
);
|
|
4448
5273
|
const handle = (0, import_react47.useMemo)(
|
|
4449
5274
|
() => buildAssessmentHandle({
|
|
4450
5275
|
checkId,
|
|
4451
|
-
getScore: () =>
|
|
4452
|
-
getMaxScore: () =>
|
|
4453
|
-
getAnswerGiven: () =>
|
|
5276
|
+
getScore: () => score,
|
|
5277
|
+
getMaxScore: () => maxScore,
|
|
5278
|
+
getAnswerGiven: () => checked,
|
|
4454
5279
|
resetTask: () => {
|
|
4455
5280
|
setSelected(/* @__PURE__ */ new Set());
|
|
4456
5281
|
setChecked(false);
|
|
@@ -4460,42 +5285,68 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4460
5285
|
checkId,
|
|
4461
5286
|
interactionType: INTERACTION12,
|
|
4462
5287
|
response: [...selected],
|
|
4463
|
-
correct: checked ?
|
|
4464
|
-
score: checked
|
|
4465
|
-
maxScore
|
|
5288
|
+
correct: checked ? factualCorrect : void 0,
|
|
5289
|
+
score: checked ? score : 0,
|
|
5290
|
+
maxScore
|
|
4466
5291
|
}),
|
|
4467
5292
|
getCurrentState: () => ({ selected: [...selected], checked }),
|
|
4468
5293
|
resume: (state) => {
|
|
4469
5294
|
const raw = state.selected;
|
|
4470
|
-
if (Array.isArray(raw))
|
|
5295
|
+
if (Array.isArray(raw)) {
|
|
5296
|
+
setSelected(
|
|
5297
|
+
new Set(
|
|
5298
|
+
raw.filter(
|
|
5299
|
+
(id) => typeof id === "string" && validTargetIds.has(id)
|
|
5300
|
+
)
|
|
5301
|
+
)
|
|
5302
|
+
);
|
|
5303
|
+
}
|
|
4471
5304
|
readBooleanStateField(state, "checked", setChecked);
|
|
4472
5305
|
}
|
|
4473
5306
|
}),
|
|
4474
|
-
[
|
|
5307
|
+
[
|
|
5308
|
+
checkId,
|
|
5309
|
+
checked,
|
|
5310
|
+
factualCorrect,
|
|
5311
|
+
maxScore,
|
|
5312
|
+
props.correctTargetIds,
|
|
5313
|
+
score,
|
|
5314
|
+
selected,
|
|
5315
|
+
validTargetIds
|
|
5316
|
+
]
|
|
4475
5317
|
);
|
|
4476
5318
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4477
5319
|
const submit = () => {
|
|
4478
5320
|
if (selected.size === 0 || checked) return;
|
|
5321
|
+
const correctAtSubmit = isFactuallyCorrect(selected);
|
|
4479
5322
|
setChecked(true);
|
|
4480
5323
|
assessment.answer({
|
|
4481
5324
|
checkId,
|
|
4482
5325
|
interactionType: INTERACTION12,
|
|
4483
5326
|
response: [...selected],
|
|
4484
|
-
correct
|
|
5327
|
+
correct: correctAtSubmit
|
|
4485
5328
|
});
|
|
4486
|
-
if (
|
|
5329
|
+
if (passedThreshold) {
|
|
4487
5330
|
assessment.complete({
|
|
4488
5331
|
checkId,
|
|
4489
5332
|
interactionType: INTERACTION12,
|
|
4490
|
-
score
|
|
4491
|
-
maxScore
|
|
4492
|
-
passingScore: props.passingScore ??
|
|
5333
|
+
score,
|
|
5334
|
+
maxScore,
|
|
5335
|
+
passingScore: props.passingScore ?? maxScore
|
|
5336
|
+
});
|
|
5337
|
+
} else if (props.enableRetry === false) {
|
|
5338
|
+
assessment.complete({
|
|
5339
|
+
checkId,
|
|
5340
|
+
interactionType: INTERACTION12,
|
|
5341
|
+
score,
|
|
5342
|
+
maxScore,
|
|
5343
|
+
passingScore: props.passingScore ?? maxScore
|
|
4493
5344
|
});
|
|
4494
5345
|
}
|
|
4495
5346
|
};
|
|
4496
5347
|
return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
4497
5348
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4498
|
-
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("img", { src:
|
|
5349
|
+
resolvedSrc ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("img", { src: resolvedSrc, alt: props.alt, style: { maxWidth: "100%" } }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { role: "alert", children: "This image URL is not allowed." }),
|
|
4499
5350
|
props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
4500
5351
|
"button",
|
|
4501
5352
|
{
|
|
@@ -4516,7 +5367,7 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4516
5367
|
))
|
|
4517
5368
|
] }),
|
|
4518
5369
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
4519
|
-
checked ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { role: "status", children:
|
|
5370
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { role: "status", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
4520
5371
|
] });
|
|
4521
5372
|
}
|
|
4522
5373
|
var FindMultipleHotspotsInnerForwarded = (0, import_react47.forwardRef)(FindMultipleHotspotsInner);
|
|
@@ -4526,14 +5377,685 @@ var FindMultipleHotspots = (0, import_react47.forwardRef)(
|
|
|
4526
5377
|
}
|
|
4527
5378
|
);
|
|
4528
5379
|
setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
|
|
5380
|
+
|
|
5381
|
+
// src/blocks/BranchingScenario.tsx
|
|
5382
|
+
var import_react51 = __toESM(require("react"), 1);
|
|
5383
|
+
var import_core23 = require("@lessonkit/core");
|
|
5384
|
+
|
|
5385
|
+
// src/compound/useCompoundBranchHandle.ts
|
|
5386
|
+
var import_react48 = require("react");
|
|
5387
|
+
var import_core21 = require("@lessonkit/core");
|
|
5388
|
+
function useCompoundBranchHandle(ref, opts) {
|
|
5389
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
5390
|
+
const {
|
|
5391
|
+
activePageIndex,
|
|
5392
|
+
getRegisteredHandles,
|
|
5393
|
+
visitedNodeIndices,
|
|
5394
|
+
choiceScores,
|
|
5395
|
+
meta,
|
|
5396
|
+
maxChoiceScore = 0,
|
|
5397
|
+
onResetMeta,
|
|
5398
|
+
enableSolutionsButton
|
|
5399
|
+
} = opts;
|
|
5400
|
+
const filterVisited = (0, import_react48.useCallback)(
|
|
5401
|
+
(handles) => {
|
|
5402
|
+
const filtered = [];
|
|
5403
|
+
for (const entry of handles) {
|
|
5404
|
+
const { pageIndex } = entry;
|
|
5405
|
+
if (pageIndex === void 0) continue;
|
|
5406
|
+
if (visitedNodeIndices.has(pageIndex)) filtered.push(entry);
|
|
5407
|
+
}
|
|
5408
|
+
return filtered;
|
|
5409
|
+
},
|
|
5410
|
+
[visitedNodeIndices]
|
|
5411
|
+
);
|
|
5412
|
+
(0, import_react48.useImperativeHandle)(
|
|
5413
|
+
ref,
|
|
5414
|
+
() => ({
|
|
5415
|
+
getScore: () => {
|
|
5416
|
+
const assessment = aggregateAssessmentScores(filterVisited(getRegisteredHandles().values()));
|
|
5417
|
+
return assessment.score + sumChoiceScores(choiceScores);
|
|
5418
|
+
},
|
|
5419
|
+
getMaxScore: () => {
|
|
5420
|
+
const assessment = aggregateAssessmentScores(filterVisited(getRegisteredHandles().values()));
|
|
5421
|
+
return assessment.maxScore + maxChoiceScore;
|
|
5422
|
+
},
|
|
5423
|
+
getAnswerGiven: () => aggregateAssessmentScores(filterVisited(getRegisteredHandles().values())).allAnswered,
|
|
5424
|
+
resetTask: () => {
|
|
5425
|
+
onResetMeta();
|
|
5426
|
+
for (const entry of filterVisited(getRegisteredHandles().values())) {
|
|
5427
|
+
entry.handle.resetTask();
|
|
5428
|
+
}
|
|
5429
|
+
},
|
|
5430
|
+
showSolutions: () => {
|
|
5431
|
+
if (!enableSolutionsButton) return;
|
|
5432
|
+
for (const entry of filterVisited(getRegisteredHandles().values())) {
|
|
5433
|
+
entry.handle.showSolutions();
|
|
5434
|
+
}
|
|
5435
|
+
},
|
|
5436
|
+
getCurrentState: () => {
|
|
5437
|
+
const childStates = {};
|
|
5438
|
+
for (const [checkId, entry] of getRegisteredHandles()) {
|
|
5439
|
+
const { pageIndex } = entry;
|
|
5440
|
+
if (pageIndex === void 0 || !visitedNodeIndices.has(pageIndex)) continue;
|
|
5441
|
+
if (entry.handle.getCurrentState) {
|
|
5442
|
+
childStates[checkId] = entry.handle.getCurrentState();
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
return mergeBranchMetaIntoState(
|
|
5446
|
+
(0, import_core21.createCompoundResumeState)({ activePageIndex, childStates }),
|
|
5447
|
+
meta
|
|
5448
|
+
);
|
|
5449
|
+
},
|
|
5450
|
+
resume: (state) => {
|
|
5451
|
+
bridgeRef?.current?.notifyImperativeResume(state);
|
|
5452
|
+
}
|
|
5453
|
+
}),
|
|
5454
|
+
[
|
|
5455
|
+
activePageIndex,
|
|
5456
|
+
bridgeRef,
|
|
5457
|
+
choiceScores,
|
|
5458
|
+
enableSolutionsButton,
|
|
5459
|
+
filterVisited,
|
|
5460
|
+
getRegisteredHandles,
|
|
5461
|
+
meta,
|
|
5462
|
+
maxChoiceScore,
|
|
5463
|
+
onResetMeta,
|
|
5464
|
+
visitedNodeIndices
|
|
5465
|
+
]
|
|
5466
|
+
);
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
// src/compound/useBranchingScenario.tsx
|
|
5470
|
+
var import_react49 = require("react");
|
|
5471
|
+
var import_jsx_runtime38 = require("react/jsx-runtime");
|
|
5472
|
+
var BranchingScenarioContext = (0, import_react49.createContext)(null);
|
|
5473
|
+
function BranchingScenarioProvider(props) {
|
|
5474
|
+
return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(BranchingScenarioContext.Provider, { value: props.value, children: props.children });
|
|
5475
|
+
}
|
|
5476
|
+
function useBranchingScenarioOptional() {
|
|
5477
|
+
return (0, import_react49.useContext)(BranchingScenarioContext);
|
|
5478
|
+
}
|
|
5479
|
+
|
|
5480
|
+
// src/compound/validateBranchGraph.ts
|
|
5481
|
+
var import_react50 = __toESM(require("react"), 1);
|
|
5482
|
+
var import_core22 = require("@lessonkit/core");
|
|
5483
|
+
function extractBranchGraph(nodes) {
|
|
5484
|
+
return nodes.map((node) => {
|
|
5485
|
+
const choices = [];
|
|
5486
|
+
import_react50.default.Children.forEach(node.props.children, (child) => {
|
|
5487
|
+
if (!import_react50.default.isValidElement(child)) return;
|
|
5488
|
+
if (getLessonkitBlockType(child.type) !== "BranchChoice") return;
|
|
5489
|
+
const targetNodeId = child.props.targetNodeId;
|
|
5490
|
+
if (typeof targetNodeId === "string") {
|
|
5491
|
+
choices.push({ targetNodeId: normalizeComponentId(targetNodeId, "blockId") });
|
|
5492
|
+
}
|
|
5493
|
+
});
|
|
5494
|
+
return {
|
|
5495
|
+
nodeId: normalizeComponentId(node.props.nodeId, "blockId"),
|
|
5496
|
+
terminal: Boolean(node.props.terminal),
|
|
5497
|
+
choices
|
|
5498
|
+
};
|
|
5499
|
+
});
|
|
5500
|
+
}
|
|
5501
|
+
function validateBranchGraphAtMount(startNodeId, nodes, strict) {
|
|
5502
|
+
const graph = extractBranchGraph(nodes);
|
|
5503
|
+
const result = (0, import_core22.validateBranchGraph)(startNodeId, graph.map(({ nodeId, choices }) => ({ nodeId, choices })));
|
|
5504
|
+
for (const node of graph) {
|
|
5505
|
+
if (node.terminal && node.choices.length > 0) {
|
|
5506
|
+
const msg = `[lessonkit] BranchingScenario: terminal node "${node.nodeId}" must not contain BranchChoice children`;
|
|
5507
|
+
if (strict || !isDevEnvironment()) {
|
|
5508
|
+
throw new Error(msg);
|
|
5509
|
+
}
|
|
5510
|
+
console.warn(msg);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
for (const issue of result.issues) {
|
|
5514
|
+
const msg = `[lessonkit] BranchingScenario: ${issue.message}`;
|
|
5515
|
+
if (strict || !isDevEnvironment()) {
|
|
5516
|
+
throw new Error(msg);
|
|
5517
|
+
}
|
|
5518
|
+
console.warn(msg);
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
function buildNodeIndexMap(nodes) {
|
|
5522
|
+
const map = /* @__PURE__ */ new Map();
|
|
5523
|
+
nodes.forEach((node, index) => {
|
|
5524
|
+
map.set(normalizeComponentId(node.props.nodeId, "blockId"), index);
|
|
5525
|
+
});
|
|
5526
|
+
return map;
|
|
5527
|
+
}
|
|
5528
|
+
function buildNodeLabels(nodes) {
|
|
5529
|
+
const map = /* @__PURE__ */ new Map();
|
|
5530
|
+
for (const node of nodes) {
|
|
5531
|
+
const nodeId = normalizeComponentId(node.props.nodeId, "blockId");
|
|
5532
|
+
map.set(nodeId, node.props.title ?? nodeId);
|
|
5533
|
+
}
|
|
5534
|
+
return map;
|
|
5535
|
+
}
|
|
5536
|
+
function filterBranchNodeContent(children) {
|
|
5537
|
+
const filtered = [];
|
|
5538
|
+
import_react50.default.Children.forEach(children, (child) => {
|
|
5539
|
+
if (!import_react50.default.isValidElement(child)) {
|
|
5540
|
+
filtered.push(child);
|
|
5541
|
+
return;
|
|
5542
|
+
}
|
|
5543
|
+
if (getLessonkitBlockType(child.type) === "BranchChoice") return;
|
|
5544
|
+
filtered.push(child);
|
|
5545
|
+
});
|
|
5546
|
+
return filtered;
|
|
5547
|
+
}
|
|
5548
|
+
function nodeHasChoices(node) {
|
|
5549
|
+
let hasChoice = false;
|
|
5550
|
+
import_react50.default.Children.forEach(node.props.children, (child) => {
|
|
5551
|
+
if (!import_react50.default.isValidElement(child)) return;
|
|
5552
|
+
if (getLessonkitBlockType(child.type) === "BranchChoice") hasChoice = true;
|
|
5553
|
+
});
|
|
5554
|
+
return hasChoice;
|
|
5555
|
+
}
|
|
5556
|
+
|
|
5557
|
+
// src/blocks/BranchingScenario.tsx
|
|
5558
|
+
var import_jsx_runtime39 = require("react/jsx-runtime");
|
|
5559
|
+
var BranchingScenarioInner = (0, import_react51.forwardRef)(function BranchingScenarioInner2(props, ref) {
|
|
5560
|
+
const { blockId, nodes, persistEnabled, startNodeId } = props;
|
|
5561
|
+
validateCompoundChildren("BranchingScenario", nodes);
|
|
5562
|
+
(0, import_react51.useLayoutEffect)(() => {
|
|
5563
|
+
validateBranchGraphAtMount(startNodeId, nodes);
|
|
5564
|
+
}, [startNodeId, nodes]);
|
|
5565
|
+
const { config, track, storage } = useLessonkit();
|
|
5566
|
+
const lessonId = useEnclosingLessonId();
|
|
5567
|
+
const ctx = useCompoundRegistry();
|
|
5568
|
+
const nodeIndexMap = (0, import_react51.useMemo)(() => buildNodeIndexMap(nodes), [nodes]);
|
|
5569
|
+
const nodeLabels = (0, import_react51.useMemo)(() => buildNodeLabels(nodes), [nodes]);
|
|
5570
|
+
const [meta, setMeta] = (0, import_react51.useState)(() => createInitialBranchMeta(startNodeId));
|
|
5571
|
+
const metaRef = (0, import_react51.useRef)(meta);
|
|
5572
|
+
const branchViewedRef = (0, import_react51.useRef)(/* @__PURE__ */ new Set());
|
|
5573
|
+
const legacyResumeWarnedRef = (0, import_react51.useRef)(false);
|
|
5574
|
+
const commitMeta = (0, import_react51.useCallback)((next) => {
|
|
5575
|
+
metaRef.current = next;
|
|
5576
|
+
setMeta(next);
|
|
5577
|
+
}, []);
|
|
5578
|
+
const activeIndex = nodeIndexMap.get(meta.activeNodeId) ?? 0;
|
|
5579
|
+
const visitedNodeIndices = (0, import_react51.useMemo)(() => {
|
|
5580
|
+
const indices = /* @__PURE__ */ new Set();
|
|
5581
|
+
for (const nodeId of meta.visitedNodeIds) {
|
|
5582
|
+
const i = nodeIndexMap.get(nodeId);
|
|
5583
|
+
if (i !== void 0) indices.add(i);
|
|
5584
|
+
}
|
|
5585
|
+
return indices;
|
|
5586
|
+
}, [meta.visitedNodeIds, nodeIndexMap]);
|
|
5587
|
+
const syncBranchViewedRef = (0, import_react51.useCallback)(
|
|
5588
|
+
(restoredMeta) => {
|
|
5589
|
+
const next = /* @__PURE__ */ new Set();
|
|
5590
|
+
for (const nodeId of restoredMeta.visitedNodeIds) {
|
|
5591
|
+
if (nodeId !== restoredMeta.activeNodeId) {
|
|
5592
|
+
next.add(`${blockId}:${nodeId}`);
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
branchViewedRef.current = next;
|
|
5596
|
+
},
|
|
5597
|
+
[blockId]
|
|
5598
|
+
);
|
|
5599
|
+
const applyResumeState = (0, import_react51.useCallback)(
|
|
5600
|
+
(state) => {
|
|
5601
|
+
const fromMeta = readBranchingScenarioMeta(state.childStates);
|
|
5602
|
+
if (fromMeta) {
|
|
5603
|
+
const sanitized = sanitizeBranchMeta(fromMeta, nodeIndexMap, startNodeId);
|
|
5604
|
+
commitMeta(sanitized);
|
|
5605
|
+
syncBranchViewedRef(sanitized);
|
|
5606
|
+
return;
|
|
5607
|
+
}
|
|
5608
|
+
const hasChildCheckStates = Object.keys(state.childStates).some((k) => k !== BS_META_KEY);
|
|
5609
|
+
const clampedIndex = (0, import_core23.clampCompoundPageIndex)(state.activePageIndex, nodes.length);
|
|
5610
|
+
const nodeAtIndex = nodes[clampedIndex];
|
|
5611
|
+
if (nodeAtIndex || hasChildCheckStates) {
|
|
5612
|
+
const nodeId = nodeAtIndex?.props.nodeId ?? startNodeId;
|
|
5613
|
+
const visitedNodeIds = [startNodeId];
|
|
5614
|
+
if (nodeId !== startNodeId) visitedNodeIds.push(nodeId);
|
|
5615
|
+
const legacyMeta = sanitizeBranchMeta(
|
|
5616
|
+
{ activeNodeId: nodeId, visitedNodeIds, choiceScores: {} },
|
|
5617
|
+
nodeIndexMap,
|
|
5618
|
+
startNodeId
|
|
5619
|
+
);
|
|
5620
|
+
commitMeta(legacyMeta);
|
|
5621
|
+
syncBranchViewedRef(legacyMeta);
|
|
5622
|
+
if (!legacyResumeWarnedRef.current && isDevEnvironment() && (hasChildCheckStates || state.activePageIndex !== 0)) {
|
|
5623
|
+
legacyResumeWarnedRef.current = true;
|
|
5624
|
+
console.warn(
|
|
5625
|
+
"[lessonkit] BranchingScenario: legacy save without branch meta; restored via activePageIndex and child states"
|
|
5626
|
+
);
|
|
5627
|
+
}
|
|
5628
|
+
return;
|
|
5629
|
+
}
|
|
5630
|
+
if (!legacyResumeWarnedRef.current && isDevEnvironment() && (state.activePageIndex !== 0 || Object.keys(state.childStates).length > 0)) {
|
|
5631
|
+
legacyResumeWarnedRef.current = true;
|
|
5632
|
+
console.warn(
|
|
5633
|
+
"[lessonkit] BranchingScenario: legacy save without branch meta; starting at startNodeId"
|
|
5634
|
+
);
|
|
5635
|
+
}
|
|
5636
|
+
const fresh = sanitizeBranchMeta(createInitialBranchMeta(startNodeId), nodeIndexMap, startNodeId);
|
|
5637
|
+
commitMeta(fresh);
|
|
5638
|
+
syncBranchViewedRef(fresh);
|
|
5639
|
+
},
|
|
5640
|
+
[commitMeta, nodeIndexMap, nodes, startNodeId, syncBranchViewedRef]
|
|
5641
|
+
);
|
|
5642
|
+
const resetBranchMeta = (0, import_react51.useCallback)(() => {
|
|
5643
|
+
commitMeta(createInitialBranchMeta(startNodeId));
|
|
5644
|
+
branchViewedRef.current = /* @__PURE__ */ new Set();
|
|
5645
|
+
}, [commitMeta, startNodeId]);
|
|
5646
|
+
const transformState = (0, import_react51.useCallback)(
|
|
5647
|
+
(state) => mergeBranchMetaIntoState(state, metaRef.current),
|
|
5648
|
+
[]
|
|
5649
|
+
);
|
|
5650
|
+
const shouldIncludeChildState = (0, import_react51.useCallback)(
|
|
5651
|
+
(_checkId, pageIndex) => pageIndex !== void 0 && visitedNodeIndices.has(pageIndex),
|
|
5652
|
+
[visitedNodeIndices]
|
|
5653
|
+
);
|
|
5654
|
+
useCompoundPersistence({
|
|
5655
|
+
courseId: config.courseId,
|
|
5656
|
+
compoundId: blockId,
|
|
5657
|
+
pageCount: nodes.length,
|
|
5658
|
+
index: activeIndex,
|
|
5659
|
+
setIndex: () => {
|
|
5660
|
+
},
|
|
5661
|
+
enabled: persistEnabled,
|
|
5662
|
+
storage,
|
|
5663
|
+
transformState,
|
|
5664
|
+
onCompoundResume: applyResumeState,
|
|
5665
|
+
shouldIncludeChildState
|
|
5666
|
+
});
|
|
5667
|
+
const maxChoiceScoreOnPath = (0, import_react51.useMemo)(() => {
|
|
5668
|
+
if (!meta.choiceScores) return 0;
|
|
5669
|
+
const branchFromIds = /* @__PURE__ */ new Set();
|
|
5670
|
+
for (const key of Object.keys(meta.choiceScores)) {
|
|
5671
|
+
const fromId = key.split(":")[0];
|
|
5672
|
+
if (fromId) branchFromIds.add(fromId);
|
|
5673
|
+
}
|
|
5674
|
+
let total = 0;
|
|
5675
|
+
for (const node of nodes) {
|
|
5676
|
+
const nodeId = node.props.nodeId;
|
|
5677
|
+
if (!branchFromIds.has(nodeId)) continue;
|
|
5678
|
+
let maxWeight = 0;
|
|
5679
|
+
let found = false;
|
|
5680
|
+
for (const child of import_react51.default.Children.toArray(node.props.children)) {
|
|
5681
|
+
if (!import_react51.default.isValidElement(child)) continue;
|
|
5682
|
+
const weight = child.props.scoreWeight;
|
|
5683
|
+
if (typeof weight === "number" && Number.isFinite(weight)) {
|
|
5684
|
+
found = true;
|
|
5685
|
+
maxWeight = Math.max(maxWeight, weight);
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
if (found) total += maxWeight;
|
|
5689
|
+
}
|
|
5690
|
+
return total;
|
|
5691
|
+
}, [meta.choiceScores, nodes]);
|
|
5692
|
+
useCompoundBranchHandle(ref, {
|
|
5693
|
+
activePageIndex: activeIndex,
|
|
5694
|
+
getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
|
|
5695
|
+
visitedNodeIndices,
|
|
5696
|
+
choiceScores: meta.choiceScores ?? {},
|
|
5697
|
+
meta,
|
|
5698
|
+
maxChoiceScore: maxChoiceScoreOnPath,
|
|
5699
|
+
onResetMeta: resetBranchMeta,
|
|
5700
|
+
enableSolutionsButton: props.enableSolutionsButton
|
|
5701
|
+
});
|
|
5702
|
+
const activeNode = nodes[activeIndex];
|
|
5703
|
+
const isTerminal = Boolean(activeNode?.props.terminal) || (activeNode ? !nodeHasChoices(activeNode) && meta.activeNodeId !== startNodeId : false);
|
|
5704
|
+
const visitedLabels = (0, import_react51.useMemo)(
|
|
5705
|
+
() => meta.visitedNodeIds.map((id) => ({
|
|
5706
|
+
nodeId: id,
|
|
5707
|
+
label: nodeLabels.get(id) ?? id
|
|
5708
|
+
})),
|
|
5709
|
+
[meta.visitedNodeIds, nodeLabels]
|
|
5710
|
+
);
|
|
5711
|
+
(0, import_react51.useEffect)(() => {
|
|
5712
|
+
if (!lessonId || !activeNode) return;
|
|
5713
|
+
const dedupeKey = `${blockId}:${meta.activeNodeId}`;
|
|
5714
|
+
if (branchViewedRef.current.has(dedupeKey)) return;
|
|
5715
|
+
branchViewedRef.current.add(dedupeKey);
|
|
5716
|
+
track(
|
|
5717
|
+
"branch_node_viewed",
|
|
5718
|
+
{
|
|
5719
|
+
blockId,
|
|
5720
|
+
nodeId: meta.activeNodeId,
|
|
5721
|
+
nodeIndex: activeIndex,
|
|
5722
|
+
nodeTitle: activeNode.props.title
|
|
5723
|
+
},
|
|
5724
|
+
{ lessonId }
|
|
5725
|
+
);
|
|
5726
|
+
}, [activeIndex, activeNode, blockId, lessonId, meta.activeNodeId, track]);
|
|
5727
|
+
const navigateToNode = (0, import_react51.useCallback)(
|
|
5728
|
+
(opts) => {
|
|
5729
|
+
const toNodeId = normalizeComponentId(opts.toNodeId, "blockId");
|
|
5730
|
+
const fromNodeId = normalizeComponentId(opts.fromNodeId, "blockId");
|
|
5731
|
+
if (!nodeIndexMap.has(toNodeId)) {
|
|
5732
|
+
if (isDevEnvironment()) {
|
|
5733
|
+
console.warn(
|
|
5734
|
+
`[lessonkit] BranchingScenario: unknown targetNodeId "${toNodeId}" from "${fromNodeId}"`
|
|
5735
|
+
);
|
|
5736
|
+
}
|
|
5737
|
+
return;
|
|
5738
|
+
}
|
|
5739
|
+
const activeNodeId = metaRef.current.activeNodeId;
|
|
5740
|
+
if (fromNodeId !== activeNodeId) {
|
|
5741
|
+
if (isDevEnvironment()) {
|
|
5742
|
+
console.warn(
|
|
5743
|
+
`[lessonkit] BranchingScenario: navigateToNode from "${fromNodeId}" but active node is "${activeNodeId}"`
|
|
5744
|
+
);
|
|
5745
|
+
}
|
|
5746
|
+
return;
|
|
5747
|
+
}
|
|
5748
|
+
if (lessonId) {
|
|
5749
|
+
track(
|
|
5750
|
+
"branch_selected",
|
|
5751
|
+
{
|
|
5752
|
+
blockId,
|
|
5753
|
+
fromNodeId,
|
|
5754
|
+
toNodeId,
|
|
5755
|
+
label: opts.label,
|
|
5756
|
+
scoreWeight: opts.scoreWeight
|
|
5757
|
+
},
|
|
5758
|
+
{ lessonId }
|
|
5759
|
+
);
|
|
5760
|
+
}
|
|
5761
|
+
setMeta((prev) => {
|
|
5762
|
+
const choiceScores = applyChoiceScoreUpdate(
|
|
5763
|
+
prev.choiceScores,
|
|
5764
|
+
fromNodeId,
|
|
5765
|
+
toNodeId,
|
|
5766
|
+
opts.scoreWeight
|
|
5767
|
+
);
|
|
5768
|
+
const visited = prev.visitedNodeIds.includes(toNodeId) ? prev.visitedNodeIds : [...prev.visitedNodeIds, toNodeId];
|
|
5769
|
+
const next = sanitizeBranchMeta(
|
|
5770
|
+
{
|
|
5771
|
+
activeNodeId: toNodeId,
|
|
5772
|
+
visitedNodeIds: visited,
|
|
5773
|
+
choiceScores
|
|
5774
|
+
},
|
|
5775
|
+
nodeIndexMap,
|
|
5776
|
+
startNodeId
|
|
5777
|
+
);
|
|
5778
|
+
metaRef.current = next;
|
|
5779
|
+
return next;
|
|
5780
|
+
});
|
|
5781
|
+
},
|
|
5782
|
+
[blockId, lessonId, nodeIndexMap, startNodeId, track]
|
|
5783
|
+
);
|
|
5784
|
+
const choicesLocked = isTerminal;
|
|
5785
|
+
const contextValue = (0, import_react51.useMemo)(
|
|
5786
|
+
() => ({
|
|
5787
|
+
compoundBlockId: blockId,
|
|
5788
|
+
activeNodeId: meta.activeNodeId,
|
|
5789
|
+
visitedNodeIds: meta.visitedNodeIds,
|
|
5790
|
+
visitedLabels: visitedLabels.map((entry) => entry.label),
|
|
5791
|
+
navigateToNode,
|
|
5792
|
+
isTerminal,
|
|
5793
|
+
choicesLocked
|
|
5794
|
+
}),
|
|
5795
|
+
[blockId, choicesLocked, isTerminal, meta.activeNodeId, meta.visitedNodeIds, navigateToNode, visitedLabels]
|
|
5796
|
+
);
|
|
5797
|
+
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;
|
|
5798
|
+
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;
|
|
5799
|
+
return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(BranchingScenarioProvider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("section", { "aria-label": props.title, "data-testid": "branching-scenario", "data-lk-block-id": blockId, children: [
|
|
5800
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)("h3", { children: props.title }),
|
|
5801
|
+
props.showPathScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("p", { "data-testid": "branch-score", children: [
|
|
5802
|
+
"Score: ",
|
|
5803
|
+
pathScore,
|
|
5804
|
+
" / ",
|
|
5805
|
+
pathMaxScore
|
|
5806
|
+
] }) : null,
|
|
5807
|
+
props.showPathRecap && isTerminal && meta.visitedNodeIds.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("aside", { "data-testid": "branch-path-recap", "aria-label": "Your path", children: [
|
|
5808
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)("h4", { children: "Your path" }),
|
|
5809
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)("ol", { children: visitedLabels.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("li", { children: entry.label }, entry.nodeId)) })
|
|
5810
|
+
] }) : null,
|
|
5811
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { "data-testid": "branching-scenario-active-node", children: nodes.map((node, i) => {
|
|
5812
|
+
const content = import_react51.default.Children.map(node.props.children, (child) => {
|
|
5813
|
+
if (!import_react51.default.isValidElement(child)) return child;
|
|
5814
|
+
if (getLessonkitBlockType(child.type) !== "BranchChoice") return child;
|
|
5815
|
+
return import_react51.default.cloneElement(child, {
|
|
5816
|
+
fromNodeId: node.props.nodeId
|
|
5817
|
+
});
|
|
5818
|
+
});
|
|
5819
|
+
return import_react51.default.cloneElement(node, {
|
|
5820
|
+
key: node.key ?? node.props.nodeId,
|
|
5821
|
+
hidden: i !== activeIndex,
|
|
5822
|
+
nodeIndex: i,
|
|
5823
|
+
children: content
|
|
5824
|
+
});
|
|
5825
|
+
}) })
|
|
5826
|
+
] }) });
|
|
5827
|
+
});
|
|
5828
|
+
var BranchingScenario = (0, import_react51.forwardRef)(
|
|
5829
|
+
function BranchingScenario2(props, ref) {
|
|
5830
|
+
const nodes = import_react51.default.Children.toArray(props.children).filter(
|
|
5831
|
+
import_react51.default.isValidElement
|
|
5832
|
+
);
|
|
5833
|
+
const { config } = useLessonkit();
|
|
5834
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
5835
|
+
requireCompoundBlockIdWhenPersisting({
|
|
5836
|
+
persistEnabled,
|
|
5837
|
+
blockId: props.blockId,
|
|
5838
|
+
componentName: "BranchingScenario"
|
|
5839
|
+
});
|
|
5840
|
+
const blockId = (0, import_react51.useMemo)(
|
|
5841
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
5842
|
+
[props.blockId]
|
|
5843
|
+
);
|
|
5844
|
+
const startNodeId = (0, import_react51.useMemo)(
|
|
5845
|
+
() => normalizeComponentId(props.startNodeId, "blockId"),
|
|
5846
|
+
[props.startNodeId]
|
|
5847
|
+
);
|
|
5848
|
+
const nodeIndexMap = (0, import_react51.useMemo)(() => buildNodeIndexMap(nodes), [nodes]);
|
|
5849
|
+
const initialIndex = nodeIndexMap.get(startNodeId) ?? 0;
|
|
5850
|
+
const hydrationKey = `${config.courseId ?? "no-course"}:${blockId}`;
|
|
5851
|
+
const setIndexStable = (0, import_react51.useCallback)(() => {
|
|
5852
|
+
}, []);
|
|
5853
|
+
return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(CompoundProvider, { activePageIndex: initialIndex, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
|
|
5854
|
+
BranchingScenarioInner,
|
|
5855
|
+
{
|
|
5856
|
+
...props,
|
|
5857
|
+
startNodeId,
|
|
5858
|
+
ref,
|
|
5859
|
+
blockId,
|
|
5860
|
+
nodes,
|
|
5861
|
+
persistEnabled
|
|
5862
|
+
},
|
|
5863
|
+
hydrationKey
|
|
5864
|
+
) });
|
|
5865
|
+
}
|
|
5866
|
+
);
|
|
5867
|
+
setLessonkitBlockType(BranchingScenario, "BranchingScenario");
|
|
5868
|
+
|
|
5869
|
+
// src/blocks/BranchNode.tsx
|
|
5870
|
+
var import_react52 = require("react");
|
|
5871
|
+
var import_jsx_runtime40 = require("react/jsx-runtime");
|
|
5872
|
+
function BranchNode(props) {
|
|
5873
|
+
validateCompoundChildren("BranchNode", filterBranchNodeContent(props.children));
|
|
5874
|
+
return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
|
|
5875
|
+
"section",
|
|
5876
|
+
{
|
|
5877
|
+
"aria-label": props.title ?? props.nodeId,
|
|
5878
|
+
"data-lk-node-id": props.nodeId,
|
|
5879
|
+
"data-testid": `branch-node-${props.nodeId}`,
|
|
5880
|
+
hidden: props.hidden ? true : void 0,
|
|
5881
|
+
style: props.hidden ? { display: "none" } : void 0,
|
|
5882
|
+
children: [
|
|
5883
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("h4", { children: props.title }) : null,
|
|
5884
|
+
/* @__PURE__ */ (0, import_jsx_runtime40.jsx)(CompoundPageIndexProvider, { pageIndex: props.nodeIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { children: props.children }) })
|
|
5885
|
+
]
|
|
5886
|
+
}
|
|
5887
|
+
);
|
|
5888
|
+
}
|
|
5889
|
+
setLessonkitBlockType(BranchNode, "BranchNode");
|
|
5890
|
+
|
|
5891
|
+
// src/blocks/BranchChoice.tsx
|
|
5892
|
+
var import_react53 = require("react");
|
|
5893
|
+
var import_jsx_runtime41 = require("react/jsx-runtime");
|
|
5894
|
+
function BranchChoice(props) {
|
|
5895
|
+
const ctx = useBranchingScenarioOptional();
|
|
5896
|
+
const groupId = (0, import_react53.useId)();
|
|
5897
|
+
const fromNodeId = props.fromNodeId ?? ctx?.activeNodeId ?? "";
|
|
5898
|
+
const isActiveNode = ctx ? ctx.activeNodeId === fromNodeId : true;
|
|
5899
|
+
const locked = ctx?.choicesLocked ?? false;
|
|
5900
|
+
const onSelect = () => {
|
|
5901
|
+
if (!ctx || !fromNodeId || props.disabled || locked || !isActiveNode) return;
|
|
5902
|
+
ctx.navigateToNode({
|
|
5903
|
+
fromNodeId,
|
|
5904
|
+
toNodeId: props.targetNodeId,
|
|
5905
|
+
label: props.label,
|
|
5906
|
+
scoreWeight: props.scoreWeight
|
|
5907
|
+
});
|
|
5908
|
+
};
|
|
5909
|
+
return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
|
|
5910
|
+
"button",
|
|
5911
|
+
{
|
|
5912
|
+
type: "button",
|
|
5913
|
+
role: "radio",
|
|
5914
|
+
"aria-checked": false,
|
|
5915
|
+
"aria-labelledby": groupId,
|
|
5916
|
+
"data-testid": `branch-choice-${props.targetNodeId}`,
|
|
5917
|
+
disabled: props.disabled || locked || !isActiveNode,
|
|
5918
|
+
onClick: onSelect,
|
|
5919
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { id: groupId, children: props.label })
|
|
5920
|
+
}
|
|
5921
|
+
);
|
|
5922
|
+
}
|
|
5923
|
+
setLessonkitBlockType(BranchChoice, "BranchChoice");
|
|
5924
|
+
|
|
5925
|
+
// src/blocks/Embed.tsx
|
|
5926
|
+
var import_react54 = require("react");
|
|
5927
|
+
var import_jsx_runtime42 = require("react/jsx-runtime");
|
|
5928
|
+
function Embed(props) {
|
|
5929
|
+
const blockId = normalizeComponentId(props.blockId, "blockId");
|
|
5930
|
+
const { config, track } = useLessonkit();
|
|
5931
|
+
const lessonId = useEnclosingLessonId();
|
|
5932
|
+
const resolvedSrc = resolveEmbedSrc(props.src, {
|
|
5933
|
+
allowedHosts: config.embed?.allowedHosts
|
|
5934
|
+
});
|
|
5935
|
+
const sandbox = buildEmbedSandbox(props.allow, {
|
|
5936
|
+
restrictPopupsInProduction: config.embed?.restrictPopupsInProduction ?? true
|
|
5937
|
+
});
|
|
5938
|
+
const aspectRatio = resolveEmbedAspectRatio(props.aspectRatio);
|
|
5939
|
+
(0, import_react54.useEffect)(() => {
|
|
5940
|
+
if (!resolvedSrc) return;
|
|
5941
|
+
track(
|
|
5942
|
+
"interaction",
|
|
5943
|
+
{ kind: "embed_viewed", blockId, src: telemetryEmbedSrc(resolvedSrc) },
|
|
5944
|
+
lessonId ? { lessonId } : void 0
|
|
5945
|
+
);
|
|
5946
|
+
}, [blockId, lessonId, resolvedSrc, track]);
|
|
5947
|
+
if (!resolvedSrc) {
|
|
5948
|
+
return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("figure", { "data-lk-block-id": blockId, "data-testid": `embed-${blockId}`, children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { role: "alert", children: "This embed URL is not allowed." }) });
|
|
5949
|
+
}
|
|
5950
|
+
return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("figure", { "data-lk-block-id": blockId, "data-testid": `embed-${blockId}`, children: [
|
|
5951
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
5952
|
+
"iframe",
|
|
5953
|
+
{
|
|
5954
|
+
title: props.title,
|
|
5955
|
+
src: resolvedSrc,
|
|
5956
|
+
sandbox,
|
|
5957
|
+
referrerPolicy: "no-referrer",
|
|
5958
|
+
style: aspectRatio ? { aspectRatio, width: "100%", border: 0 } : { width: "100%", border: 0 }
|
|
5959
|
+
}
|
|
5960
|
+
),
|
|
5961
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("figcaption", { className: "lk-visually-hidden", children: props.title })
|
|
5962
|
+
] });
|
|
5963
|
+
}
|
|
5964
|
+
setLessonkitBlockType(Embed, "Embed");
|
|
5965
|
+
|
|
5966
|
+
// src/blocks/Chart.tsx
|
|
5967
|
+
var import_react55 = require("react");
|
|
5968
|
+
|
|
5969
|
+
// src/blocks/chartUtils.ts
|
|
5970
|
+
function normalizeChartType(type) {
|
|
5971
|
+
if (type === "bar" || type === "pie") return type;
|
|
5972
|
+
if (type !== void 0 && isDevEnvironment()) {
|
|
5973
|
+
console.warn(`[lessonkit] Chart: unknown type "${type}"; rendering data table only.`);
|
|
5974
|
+
}
|
|
5975
|
+
return type === void 0 ? "pie" : "table";
|
|
5976
|
+
}
|
|
5977
|
+
function normalizeChartData(data) {
|
|
5978
|
+
if (!Array.isArray(data)) return [];
|
|
5979
|
+
const rows = [];
|
|
5980
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
5981
|
+
const row = data[i];
|
|
5982
|
+
if (!row || typeof row !== "object") continue;
|
|
5983
|
+
const label = typeof row.label === "string" ? row.label : `Item ${i + 1}`;
|
|
5984
|
+
const raw = Number(row.value);
|
|
5985
|
+
const value = Number.isFinite(raw) && raw >= 0 ? raw : 0;
|
|
5986
|
+
rows.push({ label, value, key: `${label}-${i}` });
|
|
5987
|
+
}
|
|
5988
|
+
return rows;
|
|
5989
|
+
}
|
|
5990
|
+
function chartMaxValue(rows) {
|
|
5991
|
+
if (rows.length === 0) return 1;
|
|
5992
|
+
return Math.max(...rows.map((row) => row.value), 1);
|
|
5993
|
+
}
|
|
5994
|
+
|
|
5995
|
+
// src/blocks/Chart.tsx
|
|
5996
|
+
var import_jsx_runtime43 = require("react/jsx-runtime");
|
|
5997
|
+
function Chart(props) {
|
|
5998
|
+
const blockId = normalizeComponentId(props.blockId, "blockId");
|
|
5999
|
+
const { track } = useLessonkit();
|
|
6000
|
+
const lessonId = useEnclosingLessonId();
|
|
6001
|
+
const chartType = normalizeChartType(props.type);
|
|
6002
|
+
const rows = (0, import_react55.useMemo)(() => normalizeChartData(props.data), [props.data]);
|
|
6003
|
+
const max = (0, import_react55.useMemo)(() => chartMaxValue(rows), [rows]);
|
|
6004
|
+
(0, import_react55.useEffect)(() => {
|
|
6005
|
+
track(
|
|
6006
|
+
"interaction",
|
|
6007
|
+
{ kind: "chart_viewed", blockId, chartType },
|
|
6008
|
+
lessonId ? { lessonId } : void 0
|
|
6009
|
+
);
|
|
6010
|
+
}, [blockId, chartType, lessonId, track]);
|
|
6011
|
+
return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("figure", { "data-lk-block-id": blockId, "data-testid": `chart-${blockId}`, children: [
|
|
6012
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("figcaption", { children: props.title }) : null,
|
|
6013
|
+
rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { "data-testid": "chart-empty", children: "No chart data." }) : chartType === "table" ? null : chartType === "bar" ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { role: "img", "aria-label": props.title ?? "Bar chart", "aria-describedby": `${blockId}-table`, children: rows.map((datum) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
|
|
6014
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { style: { minWidth: "6rem" }, children: datum.label }),
|
|
6015
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
|
|
6016
|
+
"div",
|
|
6017
|
+
{
|
|
6018
|
+
style: {
|
|
6019
|
+
height: "1rem",
|
|
6020
|
+
width: `${datum.value / max * 100}%`,
|
|
6021
|
+
background: "var(--lk-color-primary, #2563eb)"
|
|
6022
|
+
},
|
|
6023
|
+
"aria-hidden": true
|
|
6024
|
+
}
|
|
6025
|
+
),
|
|
6026
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { children: datum.value })
|
|
6027
|
+
] }, datum.key)) }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { role: "list", "aria-label": props.title ?? "Pie chart segments", children: rows.map((datum) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("li", { children: [
|
|
6028
|
+
datum.label,
|
|
6029
|
+
": ",
|
|
6030
|
+
datum.value
|
|
6031
|
+
] }, datum.key)) }),
|
|
6032
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("table", { id: `${blockId}-table`, children: [
|
|
6033
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)("caption", { children: props.title ?? "Chart data" }),
|
|
6034
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("tr", { children: [
|
|
6035
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)("th", { scope: "col", children: "Label" }),
|
|
6036
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)("th", { scope: "col", children: "Value" })
|
|
6037
|
+
] }) }),
|
|
6038
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)("tbody", { children: rows.map((datum) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("tr", { children: [
|
|
6039
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)("th", { scope: "row", children: datum.label }),
|
|
6040
|
+
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)("td", { children: datum.value })
|
|
6041
|
+
] }, datum.key)) })
|
|
6042
|
+
] })
|
|
6043
|
+
] });
|
|
6044
|
+
}
|
|
6045
|
+
setLessonkitBlockType(Chart, "Chart");
|
|
4529
6046
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4530
6047
|
0 && (module.exports = {
|
|
4531
6048
|
Accordion,
|
|
4532
6049
|
ArithmeticQuiz,
|
|
4533
6050
|
AssessmentSequence,
|
|
6051
|
+
BranchChoice,
|
|
6052
|
+
BranchNode,
|
|
6053
|
+
BranchingScenario,
|
|
6054
|
+
Chart,
|
|
4534
6055
|
DialogCards,
|
|
4535
6056
|
DragAndDrop,
|
|
4536
6057
|
DragTheWords,
|
|
6058
|
+
Embed,
|
|
4537
6059
|
Essay,
|
|
4538
6060
|
FillInTheBlanks,
|
|
4539
6061
|
FindHotspot,
|