@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.d.ts +920 -13
- package/dist/index.js +262 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +262 -10
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +102 -1
- package/package.json +1 -1
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
|
|
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 :
|
|
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,
|
|
2617
|
+
return validateSimple(validatedExercise, processedInputs);
|
|
2485
2618
|
case VALIDATION_MODES.EXPRESSION:
|
|
2486
|
-
return validateExpression(validatedExercise,
|
|
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:
|
|
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:
|
|
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;
|