@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 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 !== void 0 ? counter.default ?? counter.min : 0
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
- setCounterCount(defaultCounterCount);
17685
- }, [counter]);
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 !== void 0) {
17690
- onChangeRef.current(counter.id, String(counterCount));
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, values);
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
- setValues(buildInitialValues(json));
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
- setValues((prev) => ({ ...prev, [widgetId]: value }));
18599
+ setRevealed(false);
18600
+ const next = { ...valuesRef.current, [widgetId]: value };
18601
+ valuesRef.current = next;
18602
+ setValues(next);
18526
18603
  setDroppedSources((prev) => {
18527
- const next = new Map(prev);
18604
+ const next2 = new Map(prev);
18528
18605
  if (fromDrop && typeof value === "string" && value !== "" && sourceOptionId) {
18529
- next.set(widgetId, sourceOptionId);
18606
+ next2.set(widgetId, sourceOptionId);
18530
18607
  } else {
18531
- next.delete(widgetId);
18608
+ next2.delete(widgetId);
18532
18609
  }
18533
- return next;
18610
+ return next2;
18534
18611
  });
18535
- onChange?.({ ...values, [widgetId]: value });
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
  {