Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create helper to build public widget options for Numeric Input #2174

Merged
merged 8 commits into from
Jan 31, 2025

Conversation

Myranae
Copy link
Contributor

@Myranae Myranae commented Jan 30, 2025

Summary:

Adds a function that takes a Numeric Input widget's full widget options and removes correct answer information. It also adds the function to the widget's widget export and adds a test confirming the function works as expected.

Issue: LEMS-2767

Test plan:

  • Confirm all checks pass
  • Confirm the Numeric Input widget still acts as expected

@Myranae Myranae self-assigned this Jan 30, 2025
Copy link
Contributor

github-actions bot commented Jan 30, 2025

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (c497be3) and published it to npm. You
can install it using the tag PR2174.

Example:

yarn add @khanacademy/perseus@PR2174

If you are working in Khan Academy's webapp, you can run:

./dev/tools/bump_perseus_version.sh -t PR2174

Copy link
Contributor

github-actions bot commented Jan 30, 2025

Size Change: +62 B (0%)

Total Size: 1.48 MB

Filename Size Change
packages/perseus-core/dist/es/index.js 43.5 kB +43 B (+0.1%)
packages/perseus/dist/es/index.js 382 kB +19 B (0%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 39 kB
packages/keypad-context/dist/es/index.js 760 B
packages/kmath/dist/es/index.js 86.8 kB
packages/math-input/dist/es/index.js 77.6 kB
packages/math-input/dist/es/strings.js 1.79 kB
packages/perseus-editor/dist/es/index.js 688 kB
packages/perseus-linter/dist/es/index.js 22.2 kB
packages/perseus-score/dist/es/index.js 113 kB
packages/perseus/dist/es/strings.js 5.82 kB
packages/pure-markdown/dist/es/index.js 3.66 kB
packages/simple-markdown/dist/es/index.js 12.5 kB

compressed-size-action

@Myranae Myranae marked this pull request as ready for review January 30, 2025 16:03
@Myranae Myranae requested review from jeremywiebe, handeyeco and benchristel and removed request for jeremywiebe and handeyeco January 30, 2025 16:03
@@ -544,6 +545,7 @@ export type WidgetScorerFunction = (
* A union type of all the functions that provide public widget options.
*/
export type PublicWidgetOptionsFunction =
| typeof getNumericInputPublicWidgetOptions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file the best place for this union? It is used to type the getPublicWidgetOptionsFunction in the widgets.ts file, which was created to be the function used in the editor to access the correct getPublicWidgetOptions function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine!

Comment on lines 22 to 23
const {answers: _, answerForms: __, ...publicWidgetOptions} = options;
return publicWidgetOptions;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason this would be preferable or not preferable to building out the specific object we're looking for like I was doing previously? ex. {size: options.size, static: options.static, etc.}

I could see this being less brittle as it makes it easier to add fields to the widgetOptions and have them be automatically included. If this is preferable, should I go through and update the functions that do this the previous way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters too much one way or the other. The tests ensure that we include the correct fields either way.

I guess one way of approaching it might be to ask, "when we add fields to the widget options, are they more likely to be public or private?" If the code is written the way you have it here, we won't need to change it when adding public options, but we will need to change it when adding private options. On the other hand, if we explicitly list the public fields, we won't need to change this code when adding private options, but we will need to change it when adding public options.

Then there's the question of what's easier to read. The current approach has the advantage of being short, but it might be a bit obscure.

Ultimately, it's a judgment call, and a two-way door, so we can just pick an approach and, worst case, change it later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing all that! I didn't think about the difference between adding public versus private options and how either way may require a change. I like this way as it shows what is getting pulled out much more clearly.

Comment on lines +7 to +12
type NumericInputPublicWidgetOptions = {
labelText?: PerseusNumericInputWidgetOptions["labelText"];
size: PerseusNumericInputWidgetOptions["size"];
coefficient: PerseusNumericInputWidgetOptions["coefficient"];
rightAlign?: PerseusNumericInputWidgetOptions["rightAlign"];
static: PerseusNumericInputWidgetOptions["static"];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left out both answers and answerForms as they both appear to contain information related to the correct answer. 'answerForms` even has a note that it's supposedly used in examples, but might not even be used anymore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think answerForms should actually be considered public information. See e.g. the examples() method on NumericInput, which apparently generates learner-facing instructions based on the answerForms that are accepted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to this, I added @SonicScrewdriver as a reviewer (and pinged her) since she's just been looking at this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern was that answerForms has information that could tell the user what to do. Specifically, the answerFrom names field:

    answerForms: ReadonlyArray<{
        simplify: "required" | "correct" | "enforced" | null | undefined;
        name: "integer" | "decimal" | "proper" | "improper" | "mixed" | "pi";
    }>;

Maybe that information is supposed to be available to the user?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the answer forms are displayed to the learner. Example:

Screen Shot 2025-01-31 at 8 43 59 AM

Copy link
Contributor

@SonicScrewdriver SonicScrewdriver Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! The answer forms are displayed to the learner, and should already be specified to the user through how the question is written.

For example: If the question wants an improper fraction answer, the user will be prompted about this in a secondary fashion as well, either in the title of their course ("Improper Fractions") or directly in the question text. ("Please write your answer as an improper fraction").

The tool tips are just additional helpful reminders for users, particularly young ones. I don't think this could be something that anyone could use to cheat or get ahead on a question, as the user should never be surprised about which answer format the question requires.

As a result, I think it's safe to keep the answerForms public!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Thank you both! Just added them back in :D

Copy link
Member

@benchristel benchristel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes because I think we want to include answerForms in the public data, since it is used to display instructions to the learner. Other than that, this looks good!

Comment on lines +7 to +12
type NumericInputPublicWidgetOptions = {
labelText?: PerseusNumericInputWidgetOptions["labelText"];
size: PerseusNumericInputWidgetOptions["size"];
coefficient: PerseusNumericInputWidgetOptions["coefficient"];
rightAlign?: PerseusNumericInputWidgetOptions["rightAlign"];
static: PerseusNumericInputWidgetOptions["static"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think answerForms should actually be considered public information. See e.g. the examples() method on NumericInput, which apparently generates learner-facing instructions based on the answerForms that are accepted.

Comment on lines 22 to 23
const {answers: _, answerForms: __, ...publicWidgetOptions} = options;
return publicWidgetOptions;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters too much one way or the other. The tests ensure that we include the correct fields either way.

I guess one way of approaching it might be to ask, "when we add fields to the widget options, are they more likely to be public or private?" If the code is written the way you have it here, we won't need to change it when adding public options, but we will need to change it when adding private options. On the other hand, if we explicitly list the public fields, we won't need to change this code when adding private options, but we will need to change it when adding public options.

Then there's the question of what's easier to read. The current approach has the advantage of being short, but it might be a bit obscure.

Ultimately, it's a judgment call, and a two-way door, so we can just pick an approach and, worst case, change it later.

@@ -544,6 +545,7 @@ export type WidgetScorerFunction = (
* A union type of all the functions that provide public widget options.
*/
export type PublicWidgetOptionsFunction =
| typeof getNumericInputPublicWidgetOptions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine!

@Myranae Myranae requested a review from benchristel January 31, 2025 17:04
@SonicScrewdriver
Copy link
Contributor

Once we have answerForms, I think this looks great! Thank you very much for sorting this out. :)

I'm a little less familiar with the server-side-scoring setup for this: will this usurp the transform method?

If so, I have a bugfix on the Numeric Input branch that will need to be added to the numeric-input-util.ts file. It's a simple fix: just a line that filters for the correct answers before generating the answerForms key, as we don't want to suggest that users enter the answerForms for the wrong answers!

const propsTransform = function (
    widgetOptions: PerseusNumericInputWidgetOptions,
): RenderProps {
    const {answers: _, ...rendererProps} = {
        ...widgetOptions,
        answerForms: unionAnswerForms(
            widgetOptions.answers
                .filter((answer) => answer.status === "correct")    <--- Just for context 
                .map((answer) => {
                    return (answer.answerForms || []).map((form) => {
                        return {
                            simplify: answer.simplify,
                            name: form,
                        };
                    });
                }),
        ),
    };

    return rendererProps;
};

@Myranae
Copy link
Contributor Author

Myranae commented Jan 31, 2025

@SonicScrewdriver

I'm a little less familiar with the server-side-scoring setup for this: will this usurp the transform method?

I'm not sure if it will overwrite anything, but the transform method on the numeric-input widget export is this propsTransform function. Or are you referring to something else?

export default {
    name: "numeric-input",
    displayName: "Numeric input",
    defaultAlignment: "inline-block",
    accessible: true,
    widget: NumericInput,
    transform: propsTransform,
    ...

Also when we actually hook these functions up, we'll have to do some more work to make sure the widget is getting all the information it needs on the frontend without revealing the answer, so I think that's when this would be addressed, if I am understanding correctly.

Copy link
Member

@benchristel benchristel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@Myranae Myranae merged commit 97e07c8 into main Jan 31, 2025
7 of 8 checks passed
@Myranae Myranae deleted the tb/LEMS-2767/numeric-input-public-options branch January 31, 2025 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants