@mathscan/math-exercise-engine 1.1.11 → 1.1.12
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/dist/index.d.ts +2 -0
- package/dist/index.js +93 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +93 -16
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +22 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -15765,6 +15765,8 @@ export declare interface ExerciseRendererRef {
|
|
|
15765
15765
|
getCompletion: () => InputCompletion;
|
|
15766
15766
|
reset: () => void;
|
|
15767
15767
|
focusFirstInput: () => void;
|
|
15768
|
+
/** Fill every widget with its correct answer. No-op for display-only exercises. */
|
|
15769
|
+
revealAnswers: () => void;
|
|
15768
15770
|
}
|
|
15769
15771
|
|
|
15770
15772
|
export declare type ExpressionValidation = z.infer<typeof ExpressionValidationSchema>;
|
package/dist/index.js
CHANGED
|
@@ -17676,26 +17676,42 @@ const NumberLineRenderer = ({
|
|
|
17676
17676
|
positions,
|
|
17677
17677
|
cursors
|
|
17678
17678
|
} = item;
|
|
17679
|
-
const [counterCount, setCounterCount] = React.useState(
|
|
17680
|
-
counter
|
|
17681
|
-
|
|
17679
|
+
const [counterCount, setCounterCount] = React.useState(() => {
|
|
17680
|
+
if (counter === void 0) return 0;
|
|
17681
|
+
const raw = values[counter.id];
|
|
17682
|
+
const n = typeof raw === "string" && raw !== "" ? Number(raw) : NaN;
|
|
17683
|
+
return Number.isFinite(n) ? Math.min(counter.max, Math.max(counter.min, n)) : counter.default ?? counter.min;
|
|
17684
|
+
});
|
|
17682
17685
|
const defaultCounterCount = counter !== void 0 ? counter.default ?? counter.min : 0;
|
|
17686
|
+
const prevCounterKey = React.useRef(null);
|
|
17683
17687
|
React.useEffect(() => {
|
|
17684
|
-
|
|
17685
|
-
|
|
17688
|
+
const key2 = counter ? `${counter.id}:${defaultCounterCount}` : "";
|
|
17689
|
+
const changed = prevCounterKey.current !== null && prevCounterKey.current !== key2;
|
|
17690
|
+
prevCounterKey.current = key2;
|
|
17691
|
+
if (changed) setCounterCount(defaultCounterCount);
|
|
17692
|
+
}, [counter, defaultCounterCount]);
|
|
17686
17693
|
const onChangeRef = React.useRef(onChange);
|
|
17687
17694
|
onChangeRef.current = onChange;
|
|
17695
|
+
const valuesRef = React.useRef(values);
|
|
17696
|
+
valuesRef.current = values;
|
|
17688
17697
|
React.useEffect(() => {
|
|
17689
|
-
if (counter
|
|
17690
|
-
|
|
17698
|
+
if (counter === void 0) return;
|
|
17699
|
+
const next = String(counterCount);
|
|
17700
|
+
if (valuesRef.current[counter.id] !== next) {
|
|
17701
|
+
onChangeRef.current(counter.id, next);
|
|
17691
17702
|
}
|
|
17692
17703
|
}, [counterCount, counter?.id]);
|
|
17693
17704
|
const cursorKeys = React.useMemo(
|
|
17694
17705
|
() => (cursors ?? []).map((c) => `${c.id}:${c.value}`).join(","),
|
|
17695
17706
|
[cursors]
|
|
17696
17707
|
);
|
|
17708
|
+
const prevCursorKeys = React.useRef(null);
|
|
17697
17709
|
React.useEffect(() => {
|
|
17710
|
+
const isDefinitionChange = prevCursorKeys.current !== null && prevCursorKeys.current !== cursorKeys;
|
|
17711
|
+
prevCursorKeys.current = cursorKeys;
|
|
17698
17712
|
for (const cursor of cursors ?? []) {
|
|
17713
|
+
const existing = valuesRef.current[cursor.id];
|
|
17714
|
+
if (!isDefinitionChange && existing !== void 0 && existing !== "") continue;
|
|
17699
17715
|
onChangeRef.current(cursor.id, String(cursor.value));
|
|
17700
17716
|
}
|
|
17701
17717
|
}, [cursorKeys]);
|
|
@@ -18447,6 +18463,44 @@ function buildInitialValues(json) {
|
|
|
18447
18463
|
}
|
|
18448
18464
|
return initial;
|
|
18449
18465
|
}
|
|
18466
|
+
function buildRevealedValues(json) {
|
|
18467
|
+
const revealed = {};
|
|
18468
|
+
const validation = json.validation;
|
|
18469
|
+
if (!validation) return revealed;
|
|
18470
|
+
const answers = validation.answers ?? {};
|
|
18471
|
+
for (const [widgetId, expected] of Object.entries(answers)) {
|
|
18472
|
+
const widget = json.widgets[widgetId];
|
|
18473
|
+
if (widget?.type === JSON_TYPES.FRACTION && typeof expected === "string") {
|
|
18474
|
+
const [num, den] = expected.split("/", 2);
|
|
18475
|
+
if (widget.numerator === null && num !== void 0) revealed[getFractionKey(widgetId, "num")] = num;
|
|
18476
|
+
if (widget.denominator === null && den !== void 0) revealed[getFractionKey(widgetId, "den")] = den;
|
|
18477
|
+
continue;
|
|
18478
|
+
}
|
|
18479
|
+
revealed[widgetId] = typeof expected === "number" ? String(expected) : expected;
|
|
18480
|
+
}
|
|
18481
|
+
if ((validation.mode === VALIDATION_MODES.EXPRESSION || validation.mode === VALIDATION_MODES.MIXED) && validation.expression) {
|
|
18482
|
+
const vars = validation.variables ?? [];
|
|
18483
|
+
const freeVars = vars.filter((v) => !(v in answers));
|
|
18484
|
+
if (freeVars.length > 0) {
|
|
18485
|
+
const ranges = {};
|
|
18486
|
+
for (const v of vars) {
|
|
18487
|
+
if (v in answers) {
|
|
18488
|
+
const pinned = Number(answers[v]);
|
|
18489
|
+
if (Number.isFinite(pinned)) ranges[v] = { min: pinned, max: pinned, step: 1 };
|
|
18490
|
+
} else {
|
|
18491
|
+
ranges[v] = { min: 1, max: 100, step: 1 };
|
|
18492
|
+
}
|
|
18493
|
+
}
|
|
18494
|
+
const combo = findValidCombination(validation.expression, ranges);
|
|
18495
|
+
if (combo) {
|
|
18496
|
+
for (const v of freeVars) {
|
|
18497
|
+
if (combo[v] !== void 0) revealed[v] = String(combo[v]);
|
|
18498
|
+
}
|
|
18499
|
+
}
|
|
18500
|
+
}
|
|
18501
|
+
}
|
|
18502
|
+
return revealed;
|
|
18503
|
+
}
|
|
18450
18504
|
const ExerciseRenderer = React.forwardRef(({
|
|
18451
18505
|
json,
|
|
18452
18506
|
onSubmit,
|
|
@@ -18457,10 +18511,13 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
18457
18511
|
sounds
|
|
18458
18512
|
}, ref) => {
|
|
18459
18513
|
const [values, setValues] = React.useState(() => buildInitialValues(json));
|
|
18514
|
+
const valuesRef = React.useRef(values);
|
|
18515
|
+
valuesRef.current = values;
|
|
18460
18516
|
const [droppedSources, setDroppedSources] = React.useState(/* @__PURE__ */ new Map());
|
|
18461
18517
|
const [focusedInputId, setFocusedInputId] = React.useState(null);
|
|
18462
18518
|
const [validationDetails, setValidationDetails] = React.useState(null);
|
|
18463
18519
|
const [resetKey, setResetKey] = React.useState(0);
|
|
18520
|
+
const [revealed, setRevealed] = React.useState(false);
|
|
18464
18521
|
const playSound = useSoundPlayer(soundEnabled, sounds);
|
|
18465
18522
|
React.useImperativeHandle(ref, () => ({
|
|
18466
18523
|
submit: () => {
|
|
@@ -18494,7 +18551,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
18494
18551
|
getValues: () => values,
|
|
18495
18552
|
getCompletion: () => {
|
|
18496
18553
|
const answerable = getAnswerableInputs(json);
|
|
18497
|
-
const missing = getMissingInputs(json,
|
|
18554
|
+
const missing = getMissingInputs(json, valuesRef.current);
|
|
18498
18555
|
const missingSet = new Set(missing);
|
|
18499
18556
|
const filledIds = answerable.filter((id) => !missingSet.has(id));
|
|
18500
18557
|
return {
|
|
@@ -18507,11 +18564,28 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
18507
18564
|
};
|
|
18508
18565
|
},
|
|
18509
18566
|
reset: () => {
|
|
18510
|
-
|
|
18567
|
+
const initial = buildInitialValues(json);
|
|
18568
|
+
valuesRef.current = initial;
|
|
18569
|
+
setValues(initial);
|
|
18511
18570
|
setDroppedSources(/* @__PURE__ */ new Map());
|
|
18512
18571
|
setFocusedInputId(null);
|
|
18513
18572
|
setValidationDetails(null);
|
|
18514
18573
|
setResetKey((k) => k + 1);
|
|
18574
|
+
setRevealed(false);
|
|
18575
|
+
},
|
|
18576
|
+
revealAnswers: () => {
|
|
18577
|
+
const revealedValues = buildRevealedValues(json);
|
|
18578
|
+
const details = {};
|
|
18579
|
+
for (const [widgetId, userValue] of Object.entries(revealedValues)) {
|
|
18580
|
+
details[widgetId] = { widgetId, userValue, isCorrect: true };
|
|
18581
|
+
}
|
|
18582
|
+
const next = { ...valuesRef.current, ...revealedValues };
|
|
18583
|
+
valuesRef.current = next;
|
|
18584
|
+
setValues(next);
|
|
18585
|
+
setDroppedSources(/* @__PURE__ */ new Map());
|
|
18586
|
+
setValidationDetails(details);
|
|
18587
|
+
setRevealed(true);
|
|
18588
|
+
setResetKey((k) => k + 1);
|
|
18515
18589
|
},
|
|
18516
18590
|
focusFirstInput: () => {
|
|
18517
18591
|
const container = document.querySelector(".math-exercise-renderer");
|
|
@@ -18522,17 +18596,20 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
18522
18596
|
}
|
|
18523
18597
|
}), [json, values, onSubmit, playSound]);
|
|
18524
18598
|
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
|
|
18525
|
-
|
|
18599
|
+
setRevealed(false);
|
|
18600
|
+
const next = { ...valuesRef.current, [widgetId]: value };
|
|
18601
|
+
valuesRef.current = next;
|
|
18602
|
+
setValues(next);
|
|
18526
18603
|
setDroppedSources((prev) => {
|
|
18527
|
-
const
|
|
18604
|
+
const next2 = new Map(prev);
|
|
18528
18605
|
if (fromDrop && typeof value === "string" && value !== "" && sourceOptionId) {
|
|
18529
|
-
|
|
18606
|
+
next2.set(widgetId, sourceOptionId);
|
|
18530
18607
|
} else {
|
|
18531
|
-
|
|
18608
|
+
next2.delete(widgetId);
|
|
18532
18609
|
}
|
|
18533
|
-
return
|
|
18610
|
+
return next2;
|
|
18534
18611
|
});
|
|
18535
|
-
onChange?.(
|
|
18612
|
+
onChange?.(next);
|
|
18536
18613
|
};
|
|
18537
18614
|
const handleSubmit = () => {
|
|
18538
18615
|
onSubmit?.(values);
|
|
@@ -18650,7 +18727,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
18650
18727
|
if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
|
|
18651
18728
|
return { tools: list, toolsMinHeight: maxH };
|
|
18652
18729
|
}, [rulerWidget, protractorWidget, setSquareWidget]);
|
|
18653
|
-
return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `math-exercise-renderer ${className}`, children: [
|
|
18730
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${className}`, children: [
|
|
18654
18731
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
18655
18732
|
"div",
|
|
18656
18733
|
{
|