@mathscan/math-exercise-engine 1.0.12 → 1.0.13

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.js CHANGED
@@ -23,6 +23,8 @@ const AST_TYPES = {
23
23
  BOLD_TEXT: "BoldText",
24
24
  /** Inline input widget (string or numeric) */
25
25
  INLINE_INPUT: "InlineInput",
26
+ /** Inline fraction widget (numerator/denominator with optional inputs) */
27
+ FRACTION: "Fraction",
26
28
  // ═══════════════════════════════════════════════════════════════════════════
27
29
  // Blocks
28
30
  // ═══════════════════════════════════════════════════════════════════════════
@@ -114,7 +116,9 @@ const JSON_TYPES = {
114
116
  /** Radio button options widget */
115
117
  RADIO_OPTIONS: "radio-options",
116
118
  /** Numeric keypad widget */
117
- NUMERIC_KEYPAD: "numeric-keypad"
119
+ NUMERIC_KEYPAD: "numeric-keypad",
120
+ /** Inline fraction widget */
121
+ FRACTION: "fraction"
118
122
  };
119
123
  const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
120
124
  const VALIDATION_MODES = {
@@ -136,6 +140,17 @@ const SIDE_PAD = 7;
136
140
  const CSS_VAR_SIDE_PAD = "--nl-side-pad";
137
141
  const NUMBER_LINE_LABELS = ["all", "extremes", "min-only", "max-only", "none"];
138
142
  const NUMBER_LINE_HIGHLIGHT_COLORS = ["blue", "red", "green", "orange", "purple", "yellow"];
143
+ function getFractionKey(widgetId, part) {
144
+ return `${widgetId}__${part}`;
145
+ }
146
+ function isFractionKey(key2) {
147
+ return key2.endsWith("__num") || key2.endsWith("__den");
148
+ }
149
+ function parseFractionKey(key2) {
150
+ const match = key2.match(/^(.+)__(num|den)$/);
151
+ if (!match) return null;
152
+ return { widgetId: match[1], part: match[2] };
153
+ }
139
154
  const Newline = chevrotain.createToken({
140
155
  name: "Newline",
141
156
  pattern: /\r?\n/,
@@ -237,6 +252,10 @@ const HIGHLIGHT = chevrotain.createToken({
237
252
  name: "HIGHLIGHT",
238
253
  pattern: /HIGHLIGHT(?!_TEXT)/
239
254
  });
255
+ const FRACTION = chevrotain.createToken({
256
+ name: "FRACTION",
257
+ pattern: /FRACTION/
258
+ });
240
259
  const NumberLiteral = chevrotain.createToken({
241
260
  name: "NumberLiteral",
242
261
  pattern: /-?\d+(\.\d+)?/
@@ -346,6 +365,7 @@ const allTokens = [
346
365
  CELL,
347
366
  COUNTER,
348
367
  HIGHLIGHT,
368
+ FRACTION,
349
369
  // Literals
350
370
  NumberLiteral,
351
371
  StringLiteral,
@@ -680,6 +700,8 @@ class MathExerciseParser extends chevrotain.CstParser {
680
700
  { ALT: () => this.SUBRULE(this.colorText) },
681
701
  // BOLD_TEXT (for bold text)
682
702
  { ALT: () => this.SUBRULE(this.boldText) },
703
+ // FRACTION (inline fraction widget)
704
+ { ALT: () => this.SUBRULE(this.fraction) },
683
705
  // W_S_I (inline input widget)
684
706
  { ALT: () => this.SUBRULE(this.inlineInput) }
685
707
  ]);
@@ -705,6 +727,12 @@ class MathExerciseParser extends chevrotain.CstParser {
705
727
  this.SUBRULE(this.parameterList);
706
728
  this.CONSUME(RBracket);
707
729
  });
730
+ this.fraction = this.RULE("fraction", () => {
731
+ this.CONSUME(FRACTION);
732
+ this.CONSUME(LBracket);
733
+ this.SUBRULE(this.parameterList);
734
+ this.CONSUME(RBracket);
735
+ });
708
736
  this.inlineInput = this.RULE("inlineInput", () => {
709
737
  this.OR([
710
738
  { ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
@@ -887,13 +915,22 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
887
915
  answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
888
916
  })
889
917
  ]);
918
+ const FractionInlineSchema = zod.z.object({
919
+ type: zod.z.literal(AST_TYPES.FRACTION),
920
+ name: zod.z.string(),
921
+ numerator: zod.z.number().optional(),
922
+ denominator: zod.z.number().optional(),
923
+ answer: zod.z.string().optional(),
924
+ simplified: zod.z.boolean().default(true)
925
+ });
890
926
  const InlineNodeSchema = zod.z.union([
891
927
  FreeTextInlineSchema,
892
928
  SpaceInlineSchema,
893
929
  HighlightedTextInlineSchema,
894
930
  ColorTextInlineSchema,
895
931
  BoldTextInlineSchema,
896
- InlineInputWidgetSchema
932
+ InlineInputWidgetSchema,
933
+ FractionInlineSchema
897
934
  ]);
898
935
  function createFreeText(value) {
899
936
  return FreeTextInlineSchema.parse({
@@ -946,6 +983,13 @@ function createNumericInput(name, digits, answer) {
946
983
  answer
947
984
  });
948
985
  }
986
+ function createFraction(name, options = {}) {
987
+ return FractionInlineSchema.parse({
988
+ type: AST_TYPES.FRACTION,
989
+ name,
990
+ ...options
991
+ });
992
+ }
949
993
  function isFreeText(value) {
950
994
  return FreeTextInlineSchema.safeParse(value).success;
951
995
  }
@@ -955,6 +999,9 @@ function isSpace(value) {
955
999
  function isInlineInput(value) {
956
1000
  return InlineInputWidgetSchema.safeParse(value).success;
957
1001
  }
1002
+ function isFraction(value) {
1003
+ return FractionInlineSchema.safeParse(value).success;
1004
+ }
958
1005
  function isInlineNode(value) {
959
1006
  return InlineNodeSchema.safeParse(value).success;
960
1007
  }
@@ -1447,6 +1494,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1447
1494
  if (ctx.boldText) {
1448
1495
  return this.visit(ctx.boldText);
1449
1496
  }
1497
+ if (ctx.fraction) {
1498
+ return this.visit(ctx.fraction);
1499
+ }
1450
1500
  if (ctx.inlineInput) {
1451
1501
  return this.visit(ctx.inlineInput);
1452
1502
  }
@@ -1480,6 +1530,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1480
1530
  }
1481
1531
  return createBoldText(value, highlight);
1482
1532
  }
1533
+ fraction(ctx) {
1534
+ const params = this.visit(ctx.parameterList);
1535
+ const name = params.name;
1536
+ const numerator = params.numerator;
1537
+ const denominator = params.denominator;
1538
+ const answer = params.answer;
1539
+ const simplified = params.simplified !== void 0 ? params.simplified : void 0;
1540
+ return createFraction(name, { numerator, denominator, answer, simplified });
1541
+ }
1483
1542
  inlineInput(ctx) {
1484
1543
  const params = this.visit(ctx.parameterList);
1485
1544
  const name = params.name;
@@ -1752,11 +1811,18 @@ const NumericKeypadJSONSchema = zod.z.object({
1752
1811
  includeOperators: zod.z.boolean().optional(),
1753
1812
  includeDecimal: zod.z.boolean().optional()
1754
1813
  });
1814
+ const FractionJSONSchema = zod.z.object({
1815
+ type: zod.z.literal(JSON_TYPES.FRACTION),
1816
+ numerator: zod.z.number().nullable(),
1817
+ denominator: zod.z.number().nullable(),
1818
+ simplified: zod.z.boolean()
1819
+ });
1755
1820
  const WidgetJSONSchema = zod.z.union([
1756
1821
  InlineInputJSONSchema,
1757
1822
  DragOptionsJSONSchema,
1758
1823
  RadioOptionsJSONSchema,
1759
- NumericKeypadJSONSchema
1824
+ NumericKeypadJSONSchema,
1825
+ FractionJSONSchema
1760
1826
  ]);
1761
1827
  const ValidationJSONSchema = zod.z.object({
1762
1828
  mode: zod.z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
@@ -1841,6 +1907,9 @@ function isRadioOptionsJSON(widget) {
1841
1907
  function isNumericKeypadJSON(widget) {
1842
1908
  return widget.type === JSON_TYPES.NUMERIC_KEYPAD;
1843
1909
  }
1910
+ function isFractionJSON(widget) {
1911
+ return widget.type === JSON_TYPES.FRACTION;
1912
+ }
1844
1913
  class LayoutIdGenerator {
1845
1914
  constructor() {
1846
1915
  this.counter = 0;
@@ -1905,6 +1974,17 @@ function toJSON(ast) {
1905
1974
  answers[inline.name] = inline.answer;
1906
1975
  }
1907
1976
  }
1977
+ if (inline.type === AST_TYPES.FRACTION) {
1978
+ widgets[inline.name] = {
1979
+ type: JSON_TYPES.FRACTION,
1980
+ numerator: inline.numerator ?? null,
1981
+ denominator: inline.denominator ?? null,
1982
+ simplified: inline.simplified ?? true
1983
+ };
1984
+ if (inline.answer !== void 0) {
1985
+ answers[inline.name] = inline.answer;
1986
+ }
1987
+ }
1908
1988
  }
1909
1989
  for (const block of ast.lines) {
1910
1990
  if (block.type === AST_TYPES.RADIO_OPTIONS) {
@@ -2095,6 +2175,12 @@ function transformInline(inline, idGenerator) {
2095
2175
  type: JSON_TYPES.WIDGET_REF,
2096
2176
  widgetId: inline.name
2097
2177
  };
2178
+ case AST_TYPES.FRACTION:
2179
+ return {
2180
+ id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
2181
+ type: JSON_TYPES.WIDGET_REF,
2182
+ widgetId: inline.name
2183
+ };
2098
2184
  default:
2099
2185
  throw new Error(`Unknown inline type: ${inline.type}`);
2100
2186
  }
@@ -2148,6 +2234,7 @@ function isNLDragCorrect(encoded, expectedLabel, expectedPosition) {
2148
2234
  const position = normalize(payload.slice(atIdx + 1));
2149
2235
  return label === normalize(expectedLabel) && position === normalize(expectedPosition);
2150
2236
  }
2237
+ const math$1 = mathjs.create(mathjs.all);
2151
2238
  function collectNumberLines(items) {
2152
2239
  const result = [];
2153
2240
  for (const item of items) {
@@ -2187,7 +2274,9 @@ function validateSimple(exercise, userInputs) {
2187
2274
  for (const [widgetId, expected] of Object.entries(answers)) {
2188
2275
  totalCount++;
2189
2276
  const userValue = userInputs[widgetId];
2190
- const isCorrect = areValuesEqual(userValue, expected, positionMap[widgetId]);
2277
+ const widget = exercise.widgets[widgetId];
2278
+ const isFractionLenient = widget?.type === JSON_TYPES.FRACTION && widget.simplified === false;
2279
+ const isCorrect = isFractionLenient ? areFractionsEquivalent(userValue, expected) : areValuesEqual(userValue, expected, positionMap[widgetId]);
2191
2280
  details[widgetId] = {
2192
2281
  widgetId,
2193
2282
  userValue,
@@ -2223,6 +2312,16 @@ function areValuesEqual(userValue, expected, expectedPosition) {
2223
2312
  }
2224
2313
  return normalize(userValue) === normalize(expected);
2225
2314
  }
2315
+ function areFractionsEquivalent(user, expected) {
2316
+ if (user === void 0 || user === null) return false;
2317
+ try {
2318
+ const userFrac = math$1.fraction(String(user));
2319
+ const expectedFrac = math$1.fraction(String(expected));
2320
+ return math$1.equal(userFrac, expectedFrac);
2321
+ } catch {
2322
+ return areValuesEqual(user, expected);
2323
+ }
2324
+ }
2226
2325
  const math = mathjs.create(mathjs.all);
2227
2326
  const BLOCKED_FUNCTIONS = [
2228
2327
  "import",
@@ -2376,7 +2475,7 @@ function validateExpression(exercise, userInputs) {
2376
2475
  hasInvalidInput = true;
2377
2476
  continue;
2378
2477
  }
2379
- const numValue = typeof rawValue === "number" ? rawValue : parseFloat(String(rawValue).trim());
2478
+ const numValue = typeof rawValue === "number" ? rawValue : parseFractionOrNumber(String(rawValue).trim());
2380
2479
  if (!Number.isFinite(numValue)) {
2381
2480
  details[varName] = {
2382
2481
  widgetId: varName,
@@ -2471,6 +2570,39 @@ function findValidCombination(expression, ranges) {
2471
2570
  }
2472
2571
  return null;
2473
2572
  }
2573
+ function parseFractionOrNumber(s) {
2574
+ const parts = s.split("/");
2575
+ if (parts.length === 2) {
2576
+ const num = parseFloat(parts[0]);
2577
+ const den = parseFloat(parts[1]);
2578
+ if (Number.isFinite(num) && Number.isFinite(den) && den !== 0) {
2579
+ return num / den;
2580
+ }
2581
+ return NaN;
2582
+ }
2583
+ return parseFloat(s);
2584
+ }
2585
+ function preprocessFractionValues(exercise, userInputs) {
2586
+ const result = { ...userInputs };
2587
+ for (const [widgetId, widget] of Object.entries(exercise.widgets)) {
2588
+ if (widget.type !== JSON_TYPES.FRACTION) continue;
2589
+ const numKey = getFractionKey(widgetId, "num");
2590
+ const denKey = getFractionKey(widgetId, "den");
2591
+ const numEffective = widget.numerator !== null ? String(widget.numerator) : result[numKey] !== void 0 ? String(result[numKey]) : "";
2592
+ const denEffective = widget.denominator !== null ? String(widget.denominator) : result[denKey] !== void 0 ? String(result[denKey]) : "";
2593
+ if (numEffective !== "" && denEffective !== "") {
2594
+ result[widgetId] = `${numEffective}/${denEffective}`;
2595
+ }
2596
+ delete result[numKey];
2597
+ delete result[denKey];
2598
+ }
2599
+ for (const key2 of Object.keys(result)) {
2600
+ if (isFractionKey(key2)) {
2601
+ delete result[key2];
2602
+ }
2603
+ }
2604
+ return result;
2605
+ }
2474
2606
  function validate(exercise, userInputs) {
2475
2607
  if (!exercise.validation) {
2476
2608
  return {
@@ -2478,12 +2610,13 @@ function validate(exercise, userInputs) {
2478
2610
  details: {}
2479
2611
  };
2480
2612
  }
2613
+ const processedInputs = preprocessFractionValues(exercise, userInputs);
2481
2614
  const validatedExercise = exercise;
2482
2615
  switch (validatedExercise.validation.mode) {
2483
2616
  case VALIDATION_MODES.SIMPLE:
2484
- return validateSimple(validatedExercise, userInputs);
2617
+ return validateSimple(validatedExercise, processedInputs);
2485
2618
  case VALIDATION_MODES.EXPRESSION:
2486
- return validateExpression(validatedExercise, userInputs);
2619
+ return validateExpression(validatedExercise, processedInputs);
2487
2620
  default:
2488
2621
  return {
2489
2622
  isCorrect: false,
@@ -6553,6 +6686,7 @@ const InlineInput = ({
6553
6686
  widgetId,
6554
6687
  value,
6555
6688
  isDropped = false,
6689
+ validationStatus = null,
6556
6690
  inputType,
6557
6691
  length,
6558
6692
  digits,
@@ -6570,6 +6704,7 @@ const InlineInput = ({
6570
6704
  }, [handleChange]);
6571
6705
  const expectedLength = inputType === "numeric" ? digits : length;
6572
6706
  const dynamicWidth = `${(expectedLength || 5) * 0.8 + 1}em`;
6707
+ const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
6573
6708
  return /* @__PURE__ */ jsxRuntime.jsx(
6574
6709
  Droppable,
6575
6710
  {
@@ -6582,7 +6717,7 @@ const InlineInput = ({
6582
6717
  /* @__PURE__ */ jsxRuntime.jsx(
6583
6718
  "button",
6584
6719
  {
6585
- className: "inline-input-chip",
6720
+ className: `inline-input-chip${statusClass}`,
6586
6721
  onClick: () => handleChange("", false),
6587
6722
  title: "Click to remove",
6588
6723
  children: value
@@ -6595,7 +6730,7 @@ const InlineInput = ({
6595
6730
  {
6596
6731
  id: widgetId + "-input",
6597
6732
  type: "text",
6598
- className: "inline-input",
6733
+ className: `inline-input${statusClass}`,
6599
6734
  value,
6600
6735
  onChange: (e) => handleChange(e.target.value, false),
6601
6736
  onFocus: () => onFocus?.(widgetId),
@@ -6672,18 +6807,83 @@ const NumericKeypad = ({
6672
6807
  includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
6673
6808
  ] });
6674
6809
  };
6810
+ const FractionInput = ({
6811
+ widgetId,
6812
+ numerator,
6813
+ denominator,
6814
+ values,
6815
+ droppedInputs,
6816
+ validationStatus = null,
6817
+ onChange,
6818
+ onFocus
6819
+ }) => {
6820
+ const numKey = getFractionKey(widgetId, "num");
6821
+ const denKey = getFractionKey(widgetId, "den");
6822
+ const numValue = values[numKey] ?? "";
6823
+ const denValue = values[denKey] ?? "";
6824
+ const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
6825
+ const handleDrop = React.useCallback((subKey) => (item) => {
6826
+ if (item.type === "drag-option") {
6827
+ const data = item.data;
6828
+ onChange(subKey, data.value, true, String(item.id));
6829
+ }
6830
+ }, [onChange]);
6831
+ const renderSlot = (subKey, fixedValue, currentValue, label) => {
6832
+ if (fixedValue !== null) {
6833
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fraction-fixed", children: fixedValue });
6834
+ }
6835
+ const isDroppedHere = droppedInputs.has(subKey);
6836
+ return /* @__PURE__ */ jsxRuntime.jsx(
6837
+ Droppable,
6838
+ {
6839
+ id: subKey,
6840
+ accepts: ["drag-option"],
6841
+ onDrop: handleDrop(subKey),
6842
+ className: "fraction-slot-droppable",
6843
+ children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
6844
+ "button",
6845
+ {
6846
+ className: `inline-input-chip fraction-chip${statusClass}`,
6847
+ onClick: () => onChange(subKey, "", false),
6848
+ title: "Click to remove",
6849
+ children: currentValue
6850
+ }
6851
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
6852
+ "input",
6853
+ {
6854
+ id: `${subKey}-input`,
6855
+ type: "text",
6856
+ className: `inline-input fraction-input${statusClass}`,
6857
+ value: currentValue,
6858
+ onChange: (e) => onChange(subKey, e.target.value, false),
6859
+ onFocus: () => onFocus?.(subKey),
6860
+ maxLength: 4,
6861
+ "aria-label": label
6862
+ }
6863
+ )
6864
+ }
6865
+ );
6866
+ };
6867
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fraction", role: "math", "aria-label": `fraction ${widgetId}`, children: [
6868
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fraction-numerator", children: renderSlot(numKey, numerator, numValue, "numerator") }),
6869
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fraction-bar", "aria-hidden": "true" }),
6870
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fraction-denominator", children: renderSlot(denKey, denominator, denValue, "denominator") })
6871
+ ] });
6872
+ };
6675
6873
  const RadioOptions = ({
6676
6874
  name,
6677
6875
  options,
6678
6876
  value,
6877
+ validationStatus = null,
6679
6878
  onChange
6680
6879
  }) => {
6681
6880
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "radio-options", children: options.map((option, idx) => {
6682
6881
  const isSelected = value === option;
6882
+ const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
6683
6883
  return /* @__PURE__ */ jsxRuntime.jsx(
6684
6884
  "button",
6685
6885
  {
6686
- className: `radio-option${isSelected ? " selected" : ""}`,
6886
+ className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
6687
6887
  onClick: () => onChange(name, option),
6688
6888
  type: "button",
6689
6889
  children: option
@@ -6704,11 +6904,17 @@ function isRadioOptionsWidget(widget) {
6704
6904
  function isNumericKeypadWidget(widget) {
6705
6905
  return widget.type === "numeric-keypad";
6706
6906
  }
6907
+ function isFractionWidget(widget) {
6908
+ return widget.type === "fraction";
6909
+ }
6707
6910
  const WidgetRenderer = ({
6708
6911
  widget,
6709
6912
  widgetId,
6710
6913
  value,
6914
+ values = {},
6711
6915
  isDropped = false,
6916
+ validationStatus = null,
6917
+ droppedInputs = /* @__PURE__ */ new Set(),
6712
6918
  onChange,
6713
6919
  onInputFocus,
6714
6920
  onKeypadChange
@@ -6720,6 +6926,7 @@ const WidgetRenderer = ({
6720
6926
  widgetId,
6721
6927
  value,
6722
6928
  isDropped,
6929
+ validationStatus,
6723
6930
  inputType: widget.inputType,
6724
6931
  length: widget.inputType === "string" ? widget.length : void 0,
6725
6932
  digits: widget.inputType === "numeric" ? widget.digits : void 0,
@@ -6745,6 +6952,7 @@ const WidgetRenderer = ({
6745
6952
  name: widgetId,
6746
6953
  options: widget.options,
6747
6954
  value,
6955
+ validationStatus,
6748
6956
  onChange
6749
6957
  }
6750
6958
  );
@@ -6761,6 +6969,21 @@ const WidgetRenderer = ({
6761
6969
  }
6762
6970
  );
6763
6971
  }
6972
+ if (isFractionWidget(widget)) {
6973
+ return /* @__PURE__ */ jsxRuntime.jsx(
6974
+ FractionInput,
6975
+ {
6976
+ widgetId,
6977
+ numerator: widget.numerator,
6978
+ denominator: widget.denominator,
6979
+ values,
6980
+ droppedInputs,
6981
+ validationStatus,
6982
+ onChange,
6983
+ onFocus: onInputFocus
6984
+ }
6985
+ );
6986
+ }
6764
6987
  return null;
6765
6988
  };
6766
6989
  const EPSILON = 10 ** -INTERNAL_PRECISION;
@@ -7268,6 +7491,7 @@ const NumberLineRenderer = ({
7268
7491
  widgets,
7269
7492
  values,
7270
7493
  droppedInputs,
7494
+ validationDetails,
7271
7495
  onChange,
7272
7496
  onInputFocus
7273
7497
  }) => {
@@ -7335,6 +7559,7 @@ const NumberLineRenderer = ({
7335
7559
  widgets,
7336
7560
  values,
7337
7561
  droppedInputs,
7562
+ validationDetails,
7338
7563
  onChange,
7339
7564
  onInputFocus
7340
7565
  },
@@ -7459,6 +7684,7 @@ const LayoutRenderer = ({
7459
7684
  widgets,
7460
7685
  values,
7461
7686
  droppedInputs,
7687
+ validationDetails,
7462
7688
  onChange,
7463
7689
  onInputFocus
7464
7690
  }) => {
@@ -7511,13 +7737,18 @@ const LayoutRenderer = ({
7511
7737
  case JSON_TYPES.WIDGET_REF: {
7512
7738
  const widget = widgets[item.widgetId];
7513
7739
  if (!widget) return null;
7740
+ const isCorrect = validationDetails?.[item.widgetId];
7741
+ const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
7514
7742
  return /* @__PURE__ */ jsxRuntime.jsx(
7515
7743
  WidgetRenderer,
7516
7744
  {
7517
7745
  widget,
7518
7746
  widgetId: item.widgetId,
7519
7747
  value: values[item.widgetId] || "",
7748
+ values,
7520
7749
  isDropped: droppedInputs.has(item.widgetId),
7750
+ validationStatus,
7751
+ droppedInputs,
7521
7752
  onChange,
7522
7753
  onInputFocus
7523
7754
  }
@@ -7537,6 +7768,7 @@ const LayoutRenderer = ({
7537
7768
  widgets,
7538
7769
  values,
7539
7770
  droppedInputs,
7771
+ validationDetails,
7540
7772
  onChange,
7541
7773
  onInputFocus
7542
7774
  },
@@ -7555,6 +7787,7 @@ const LayoutRenderer = ({
7555
7787
  widgets,
7556
7788
  values,
7557
7789
  droppedInputs,
7790
+ validationDetails,
7558
7791
  onChange,
7559
7792
  onInputFocus
7560
7793
  },
@@ -7571,6 +7804,7 @@ const LayoutRenderer = ({
7571
7804
  widgets,
7572
7805
  values,
7573
7806
  droppedInputs,
7807
+ validationDetails,
7574
7808
  onChange,
7575
7809
  onInputFocus
7576
7810
  }
@@ -7598,9 +7832,15 @@ const ExerciseRenderer = React.forwardRef(({
7598
7832
  const [values, setValues] = React.useState({});
7599
7833
  const [droppedSources, setDroppedSources] = React.useState(/* @__PURE__ */ new Map());
7600
7834
  const [focusedInputId, setFocusedInputId] = React.useState(null);
7835
+ const [validationDetails, setValidationDetails] = React.useState(null);
7601
7836
  React.useImperativeHandle(ref, () => ({
7602
7837
  submit: () => {
7603
7838
  const result = validate(json, values);
7839
+ const details = {};
7840
+ for (const [id, v] of Object.entries(result.details)) {
7841
+ details[id] = v.isCorrect;
7842
+ }
7843
+ setValidationDetails(details);
7604
7844
  onSubmit?.(values);
7605
7845
  return result;
7606
7846
  },
@@ -7609,6 +7849,7 @@ const ExerciseRenderer = React.forwardRef(({
7609
7849
  setValues({});
7610
7850
  setDroppedSources(/* @__PURE__ */ new Map());
7611
7851
  setFocusedInputId(null);
7852
+ setValidationDetails(null);
7612
7853
  },
7613
7854
  focusFirstInput: () => {
7614
7855
  const container = document.querySelector(".math-exercise-renderer");
@@ -7722,6 +7963,7 @@ const ExerciseRenderer = React.forwardRef(({
7722
7963
  widgets: json.widgets,
7723
7964
  values,
7724
7965
  droppedInputs,
7966
+ validationDetails,
7725
7967
  onChange: handleChange,
7726
7968
  onInputFocus: setFocusedInputId
7727
7969
  },
@@ -7736,6 +7978,7 @@ const ExerciseRenderer = React.forwardRef(({
7736
7978
  widgets: json.widgets,
7737
7979
  values,
7738
7980
  droppedInputs,
7981
+ validationDetails,
7739
7982
  onChange: handleChange,
7740
7983
  onInputFocus: setFocusedInputId
7741
7984
  },
@@ -7751,6 +7994,7 @@ const ExerciseRenderer = React.forwardRef(({
7751
7994
  widgets: json.widgets,
7752
7995
  values,
7753
7996
  droppedInputs,
7997
+ validationDetails,
7754
7998
  onChange: handleChange,
7755
7999
  onInputFocus: setFocusedInputId
7756
8000
  },
@@ -7839,6 +8083,8 @@ exports.ExerciseJSONSchema = ExerciseJSONSchema;
7839
8083
  exports.ExerciseMetaSchema = ExerciseMetaSchema;
7840
8084
  exports.ExerciseRenderer = ExerciseRenderer;
7841
8085
  exports.ExpressionValidationSchema = ExpressionValidationSchema;
8086
+ exports.FractionInlineSchema = FractionInlineSchema;
8087
+ exports.FractionJSONSchema = FractionJSONSchema;
7842
8088
  exports.FreeTextInlineSchema = FreeTextInlineSchema;
7843
8089
  exports.GroupItemSchema = GroupItemSchema;
7844
8090
  exports.HighlightedTextInlineSchema = HighlightedTextInlineSchema;
@@ -7886,6 +8132,7 @@ exports.createColorText = createColorText;
7886
8132
  exports.createDragOptions = createDragOptions;
7887
8133
  exports.createExerciseDocument = createExerciseDocument;
7888
8134
  exports.createExpressionValidation = createExpressionValidation;
8135
+ exports.createFraction = createFraction;
7889
8136
  exports.createFreeText = createFreeText;
7890
8137
  exports.createHighlightedText = createHighlightedText;
7891
8138
  exports.createLineBlock = createLineBlock;
@@ -7898,6 +8145,7 @@ exports.createTextBlock = createTextBlock;
7898
8145
  exports.evaluate = evaluate;
7899
8146
  exports.extractExpressionVariables = extractVariables;
7900
8147
  exports.findValidCombination = findValidCombination;
8148
+ exports.getFractionKey = getFractionKey;
7901
8149
  exports.getMissingInputs = getMissingInputs;
7902
8150
  exports.isBoldText = isBoldText;
7903
8151
  exports.isColorText = isColorText;
@@ -7905,6 +8153,9 @@ exports.isDragOptions = isDragOptions;
7905
8153
  exports.isDragOptionsJSON = isDragOptionsJSON;
7906
8154
  exports.isExerciseDocument = isExerciseDocument;
7907
8155
  exports.isExpressionValidation = isExpressionValidation;
8156
+ exports.isFraction = isFraction;
8157
+ exports.isFractionJSON = isFractionJSON;
8158
+ exports.isFractionKey = isFractionKey;
7908
8159
  exports.isFreeText = isFreeText;
7909
8160
  exports.isGroupItem = isGroupItem;
7910
8161
  exports.isHighlightedText = isHighlightedText;
@@ -7931,6 +8182,7 @@ exports.isValidExpression = isValidExpression;
7931
8182
  exports.isValidationNode = isValidationNode;
7932
8183
  exports.isWidgetRef = isWidgetRef;
7933
8184
  exports.parse = parse;
8185
+ exports.parseFractionKey = parseFractionKey;
7934
8186
  exports.parseSafe = parseSafe;
7935
8187
  exports.renderExercise = renderExercise;
7936
8188
  exports.toAST = toAST;