QuestionEditor
question-editorStructured editor for one math question: seven types, figures, options, blanks, rubric, and a live QuestionCard preview.
Usage
Basic usage
Single choice: add, remove, and reorder options, and the correct answer follows the content; the preview on the right is QuestionCard.
Switching the type clears options and answer
Preview (same typesetting as the question display)
In , and . The value of is ( )
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
For fill-in-the-blank, mark each blank with ____ in the stem
2 to 8 options, lettered in order
Pick one
Preview (same typesetting as the question display)
From we get .
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
1 star easiest, 5 stars hardest
Preview (the same card used for display)
<QuestionEditor value={question} onChange={setQuestion} />Fill in the blanks
The number of blanks follows ____ in the stem, with a one-click align when they differ; a blank may accept several equivalent forms.
Switching the type clears options and answer
Preview (same typesetting as the question display)
Write as a decimal: , and as a percentage: .
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
For fill-in-the-blank, mark each blank with ____ in the stem
Wrap formulas in $…$, e.g. $x^{2}$
Wrap formulas in $…$, e.g. $x^{2}$
Wrap formulas in $…$, e.g. $x^{2}$
One row per blank, in stem order; a blank may accept several equivalent forms
Wrap formulas in $…$, e.g. $x^{2}$
1 star easiest, 5 stars hardest
Preview (the same card used for display)
<QuestionEditor value={question} onChange={setQuestion} />Rubric
Calculation and extended-response questions can switch to a rubric; the rubric total sits next to the question score.
Switching the type clears options and answer
Preview (same typesetting as the question display)
Evaluate
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
For fill-in-the-blank, mark each blank with ____ in the stem
Preview (same typesetting as the question display)
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
Optional; show the key steps
Wrap formulas in $…$, e.g. $x^{2}$
Wrap formulas in $…$, e.g. $x^{2}$
Rubric total 8, question score 8
Score point by point; the total should equal the question score
Wrap formulas in $…$, e.g. $x^{2}$
1 star easiest, 5 stars hardest
Preview (the same card used for display)
<QuestionEditor value={question} onChange={setQuestion} />Figures
 in the stem is resolved by resolveFigure; the Insert image button appears only with onUploadFigure.
Switching the type clears options and answer
Preview (same typesetting as the question display)
As shown, in with , the relation between and is ( )
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
For fill-in-the-blank, mark each blank with ____ in the stem
Wrap formulas in $…$, e.g. $x^{2}$
Wrap formulas in $…$, e.g. $x^{2}$
2 to 8 options, lettered in order
Pick one
Wrap formulas in $…$, e.g. $x^{2}$
1 star easiest, 5 stars hardest
Preview (the same card used for display)
<QuestionEditor
value={question}
onChange={setQuestion}
resolveFigure={(key) => fileUrl(key)}
onUploadFigure={async (file) => (await upload(file)).key}
/>Review bar and private fields
issues are listed at the top with a Resolved button each; extra holds the consumer's own fields.
- An option may be missing
- Answer in doubt
Switching the type clears options and answer
Preview (same typesetting as the question display)
In , and . The value of is ( )
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
For fill-in-the-blank, mark each blank with ____ in the stem
2 to 8 options, lettered in order
Pick one
Preview (same typesetting as the question display)
From we get .
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
1 star easiest, 5 stars hardest
Preview (the same card used for display)
<QuestionEditor
value={question}
onChange={setQuestion}
issues={[{ label: "An option may be missing" }]}
onResolveIssue={(label) => resolve(label)}
extra={<PrivateFields />}
/>Validate everything on submit
By default only edited fields turn red; showAllIssues attaches every validateQuestion issue at once.
Switching the type clears options and answer
Wrap formulas in $…$, e.g. $x^{2}$
For fill-in-the-blank, mark each blank with ____ in the stem
Wrap formulas in $…$, e.g. $x^{2}$
Wrap formulas in $…$, e.g. $x^{2}$
2 to 8 options, lettered in order
Pick at least two
Wrap formulas in $…$, e.g. $x^{2}$
1 star easiest, 5 stars hardest
Preview (the same card used for display)
Preview appears once the stem has text
<QuestionEditor value={question} onChange={setQuestion} showAllIssues />Read only
disabled: the read-only state after review.
Switching the type clears options and answer
Preview (same typesetting as the question display)
In , and . The value of is ( )
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
For fill-in-the-blank, mark each blank with ____ in the stem
2 to 8 options, lettered in order
Pick one
Preview (same typesetting as the question display)
From we get .
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
1 star easiest, 5 stars hardest
Preview (the same card used for display)
<QuestionEditor value={question} onChange={setQuestion} disabled />When to use
Question-bank entry pages and the calibration page after AI or Word import: anywhere "edit one question" happens, instead of a 1200-line form per page. It owns the question itself (the Question canonical shape). Subject, textbook section, knowledge points, general-scope notes, and upload endpoints are consumer-private: put them in the extra slot and upload through onUploadFigure.
Display only: QuestionCard. Student answering: QuestionAnswer. A single formula-capable input: MathTextarea (what this component is built from).
Import
import { QuestionEditor, emptyQuestion, validateQuestion, toWireAnswer, fromWire } from "@hulianui/ui/math"It lives in @hulianui/ui/math rather than the root package: the stem, options, and preview are all Formula, so KaTeX comes along; consumers that never typeset math should not pay for it.
Props
| Name | Type | Default | Description |
|---|---|---|---|
| value | Question | - | Controlled value in the canonical shape (Question from @hulianui/ui/math). Blank answers are always arrays inside the editor (one entry even for a single blank); flatten on the way out with toWireAnswer |
| onChange | (next: Question) => void | - | Called with the whole canonical shape on every edit |
| disabled | boolean | false | Read-only |
| resolveFigure | (key: string) => string | - | Maps  in the stem to a displayable URL. Both the thumbnail strip and the preview use it; when the stem has figures and it is missing, thumbnails show only the key and a dev warning fires |
| onUploadFigure | (file: File) => Promise<string> | - | Uploads one figure and resolves to its storage key. The Insert image button appears only when provided; on success  is appended to the stem, on failure the reason shows in the strip |
| extra | ReactNode | - | Consumer-private fields, rendered after the type and before the stem |
| issues | { label, tone? }[] | - | Review bar listed at the top, one Resolved button per item |
| onResolveIssue | (label: string) => void | - | Called from the Resolved button; the button is omitted when this is missing |
| defaultScoreByType | Partial<Record<QuestionType, number>> | - | Overrides the per-type default score. On a type switch the score changes only if it still equals the old type's default |
| templates | readonly FormulaTemplateGroup[] | - | Passed to every MathTextarea |
| visualEditor | ComponentType<MathFieldLikeProps> | - | Passed to every MathTextarea (MathField satisfies the contract) |
| macros | Record<string, string> | - | KaTeX macros passed to every MathTextarea and the preview |
| preview | boolean | true | Live QuestionCard preview on the right (below on narrow screens), with answer and explanation |
| showAllIssues | boolean | false | Show every validateQuestion issue at once. By default only fields the user has edited show issues; set it after the user presses submit |
| className | string | - | Root node (a two-column grid) |
Events
| Name | Arguments | Description |
|---|---|---|
| onChange | (next: Question) | Any field changed. A type switch resets options and answer together and remaps score through the default table |
| onUploadFigure | (file: File) | The user picked an image. The resolved key is written into the latest stem (text typed during the upload is kept) |
| onResolveIssue | (label: string) | Resolved pressed on the review bar |
Slots
| Name | Description |
|---|---|
| extra | Private-field area after the type and before the stem, for subject / section / knowledge-point fields that are not part of "one question" |
Localization
All copy comes from the components.questionEditor locale (QuestionEditorLocale, source of truth in question-editor.locale.ts; zhCN and enUS are wired). Type names and the True / False labels come from components.question (shared with QuestionCard). The validateQuestion code table is validation, keyed by QuestionValidationCode.
Companion pure functions
All exported from @hulianui/ui/math:
questionFormulaIssues(q)returns{ field, key?, issue }[]: runsvalidateFormulaSyntaxfield by field (stem, each option, each form of each blank, reference answer and rubric points, explanation). Every input already shows the same issue inline; this one is for the submit button. Compose the message from themathTextarealocale:position(line, column) + syntax[code].shapeIsDirty(q)returnsboolean: whether switching type would lose content (an option has text, or the answer differs from the type's empty shape). Reuse it for a "clear form" confirmation.switchType(q, type, defaults?)returnsQuestion: resets options and answer together and remaps the score.optionCaption(key, text)returnsstring: the label on the correct-answer control (Aplus the first 20 characters of the option as plain text).stemBody(stem)/joinStemFigures(body, keys): split and join the stem body and the figure block with the editor's own rule.
Pitfalls
- `value` must be canonical. Legacy variants such as
answer: "A,C",options: ["A. x"], oranswer: "true"go throughfromWirefirst; fed directly they render as "no answer selected" andvalidateQuestionreportsanswer_out_of_range. - Flatten blank answers on the way out. Inside the editor a single blank is
["90"]; if the backend wants a plain string for single blanks, calltoWireAnswer(question)before submitting. - Switching type clears options and answer (with a confirmation when there is content). Keeping the old shape would produce values such as a true-false question with options, which the backend rejects.
- Figures live in the stem, not in another field. The input hides
butvalue.stemcontains it; if figures lived elsewhere, paper preview, the student view, and export would get none of them. WithoutresolveFigurethe preview drops figures and thumbnails show only the key. - Upload appears only with `onUploadFigure`. Enforce type and size limits inside the callback; reject with
throw new Error("Max 5MB per image")and the message shows verbatim in the strip. - Validation shows only edited fields by default. Set
showAllIssuesto flag everything at submit time; do not draw errors again outside. - No submit button. Submitting, required private fields (such as subject), and an upper bound for
estimatedMinutesbelong to the page. - JSX attribute strings do not process `\\`. Put formulas with backslashes in TS strings (
stem: "$\\frac{1}{2}$"), not in JSX attributes.
Related
- QuestionCard: the preview is this component (
showAnswer+resolveFigure) - MathTextarea: every formula-capable input
- Formula: the question-domain pure functions in
@hulianui/ui/math(validateQuestion/defaultShape/toWireAnswer/fromWire/splitStemFigures) - Field: how errors are attached
Playground
Switching the type clears options and answer
Preview (same typesetting as the question display)
In , and . The value of is ( )
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
For fill-in-the-blank, mark each blank with ____ in the stem
2 to 8 options, lettered in order
Pick one
Preview (same typesetting as the question display)
From we get .
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
1 star easiest, 5 stars hardest
Preview (the same card used for display)
<QuestionEditor value={question} onChange={setQuestion} />