@mathscan/math-exercise-engine 1.2.13 → 1.2.15
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/README.md +6 -3
- package/dist/{GeometryWidget-CMJTNSAG.js → GeometryWidget-CTTLw871.js} +2 -2
- package/dist/{GeometryWidget-CMJTNSAG.js.map → GeometryWidget-CTTLw871.js.map} +1 -1
- package/dist/{GeometryWidget-h2yNZsas.mjs → GeometryWidget-DZWM4LcD.mjs} +2 -2
- package/dist/{GeometryWidget-h2yNZsas.mjs.map → GeometryWidget-DZWM4LcD.mjs.map} +1 -1
- package/dist/{index-CAvdtiDQ.mjs → index-BLrnKegW.mjs} +2012 -1051
- package/dist/index-BLrnKegW.mjs.map +1 -0
- package/dist/{index-CbKXPRsS.js → index-C9OHpy2h.js} +1747 -786
- package/dist/index-C9OHpy2h.js.map +1 -0
- package/dist/index.d.ts +7574 -1766
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +282 -266
- package/dist/style.css +200 -23
- package/package.json +1 -1
- package/dist/index-CAvdtiDQ.mjs.map +0 -1
- package/dist/index-CbKXPRsS.js.map +0 -1
package/README.md
CHANGED
|
@@ -119,9 +119,12 @@ exerciseRef.current?.focusFirstInput(); // Focus first editable input field
|
|
|
119
119
|
|
|
120
120
|
- **`submit()`**: Performs validation, returns `ValidationResult`, triggers `onSubmit`, and **locks the exercise** (see below). Calling it again while locked is a no-op that replays the same `ValidationResult` — no sound, no `onSubmit`
|
|
121
121
|
- **`getValues()`**: Returns `Record<string, UserAnswerValue>` of all current input values
|
|
122
|
-
- **`getCompletion()`**: Returns `{ total, filled, missing, filledIds, isComplete, isAnyFilled }` — use
|
|
123
|
-
-
|
|
124
|
-
-
|
|
122
|
+
- **`getCompletion()`**: Returns `{ total, filled, missing, filledIds, isComplete, isSubmitGuideReady, isAnyFilled, isAnswerLengthMatched, answerLengthMatchedIds }` — use `isComplete` to allow submitting an attempt and `isSubmitGuideReady` for submit guidance
|
|
123
|
+
- `isSubmitGuideReady` is a hint signal for submit guidance (such as a hand animation), not a submit-button gate. It combines `isComplete` with known character/selection counts for typed inputs, editable fractions, and matchers. Typed counts use the same normalization as grading, so `5.0` and `05` have the same normalized length as `5`, and surrounding spaces are trimmed. Fields without stored answers (including expression-only variables) and other widget types use normal completion; display-only exercises return false. Wrong answers with matching counts can trigger guidance. A different-length attempt can suppress guidance but must still be submittable via `isComplete`. The signal does not check correctness or lock state.
|
|
124
|
+
- `isAnswerLengthMatched` is true when every answerable text/number input and editable fraction slot has exactly as many characters as its stored expected answer, and every matcher has the required selection count. This checks length only: `456` matches the length of `123`. It uses the answer, not the box's `digits`/`length`, and updates immediately without affecting `isComplete`.
|
|
125
|
+
- For `MATCHER`, the selected item count must equal the total number of items in its stored answer groups. Group membership and correctness are ignored. For `PAIR_MATCHER`, the number of completed pairs must equal the number of stored answer pairs; selecting one endpoint alone does not count. Extra unselected items are allowed, and removing a selection/pair updates the flag immediately.
|
|
126
|
+
- `answerLengthMatchedIds` lists matching input and matcher widget IDs; fractions use slot IDs such as `f__num` and `f__den`. Disabled inputs and fixed fraction slots are excluded. Untouched defaults do not match. Other widget types retain their existing completion rules; use `isSubmitGuideReady` to combine those rules with character/selection counts.
|
|
127
|
+
- The flag is false when there are no eligible fields or any eligible field has no stored answer (for example, an expression-only variable). The schema currently supports one stored answer per input, not alternative answer lists. Length counts the stored/entered characters literally, including signs, decimal points, spaces, and leading zeros; it does not normalize the values.
|
|
125
128
|
- **`revealAnswers()`**: Fills every widget with its correct answer and **locks the exercise**. No-op for display-only exercises
|
|
126
129
|
- **`reset()`**: Clears all input fields and **unlocks** the exercise
|
|
127
130
|
- **`focusFirstInput()`**: Focuses the first editable input of *this* renderer. Skips read-only/disabled fields, so it does nothing while the exercise is locked
|
|
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
4
4
|
const React = require("react");
|
|
5
5
|
const react = require("@mathscan/geometry-engine/react");
|
|
6
6
|
const validation = require("@mathscan/geometry-engine/validation");
|
|
7
|
-
const index = require("./index-
|
|
7
|
+
const index = require("./index-C9OHpy2h.js");
|
|
8
8
|
const GeometryWidget = ({
|
|
9
9
|
widgetId,
|
|
10
10
|
widget,
|
|
@@ -47,4 +47,4 @@ const GeometryWidget = ({
|
|
|
47
47
|
) });
|
|
48
48
|
};
|
|
49
49
|
exports.GeometryWidget = GeometryWidget;
|
|
50
|
-
//# sourceMappingURL=GeometryWidget-
|
|
50
|
+
//# sourceMappingURL=GeometryWidget-CTTLw871.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GeometryWidget-
|
|
1
|
+
{"version":3,"file":"GeometryWidget-CTTLw871.js","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["useExerciseLocked","useMemo","readGeometryAttempt","useEffect","logRejectedGeometryAttempt","jsx","StudentGeometryExercise","serializeGeometryState","issues"],"mappings":";;;;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiBA,MAAAA,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAWC,MAAAA,QAAQ,MAAMC,MAAAA,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7EC,QAAAA,UAAU,MAAM;AACd,QAAI,OAAQC,OAAAA,2BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACEC,2BAAAA,IAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAAA,2BAAAA;AAAAA,IAACC,MAAAA;AAAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAUC,WAAAA,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACC,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;;"}
|
|
@@ -2,7 +2,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useMemo, useEffect } from "react";
|
|
3
3
|
import { StudentGeometryExercise } from "@mathscan/geometry-engine/react";
|
|
4
4
|
import { serializeGeometryState } from "@mathscan/geometry-engine/validation";
|
|
5
|
-
import { u as useExerciseLocked, r as readGeometryAttempt, l as logRejectedGeometryAttempt } from "./index-
|
|
5
|
+
import { u as useExerciseLocked, r as readGeometryAttempt, l as logRejectedGeometryAttempt } from "./index-BLrnKegW.mjs";
|
|
6
6
|
const GeometryWidget = ({
|
|
7
7
|
widgetId,
|
|
8
8
|
widget,
|
|
@@ -47,4 +47,4 @@ const GeometryWidget = ({
|
|
|
47
47
|
export {
|
|
48
48
|
GeometryWidget
|
|
49
49
|
};
|
|
50
|
-
//# sourceMappingURL=GeometryWidget-
|
|
50
|
+
//# sourceMappingURL=GeometryWidget-DZWM4LcD.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GeometryWidget-
|
|
1
|
+
{"version":3,"file":"GeometryWidget-DZWM4LcD.mjs","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["issues"],"mappings":";;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiB,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAW,QAAQ,MAAM,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7E,YAAU,MAAM;AACd,QAAI,OAAQ,4BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACE,oBAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAU,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACA,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;"}
|