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