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 Matcher #2156

Open
wants to merge 53 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7560a59
[tb/LEMS-2761/matcher-split] (wip) Add function to get public widget …
Myranae Jan 24, 2025
12bfcf1
[tb/LEMS-2761/matcher-split] Add test for the new function
Myranae Jan 24, 2025
30c9a44
[tb/LEMS-2761/matcher-split] Export function from perseus-core
Myranae Jan 24, 2025
9c3960a
[tb/LEMS-2761/matcher-split] Add function to union type for all publi…
Myranae Jan 24, 2025
844eb7f
[tb/LEMS-2761/matcher-split] Add to matcher's widget export
Myranae Jan 24, 2025
7957294
[tb/LEMS-2761/matcher-split] docs(changeset): Implement a widget expo…
Myranae Jan 24, 2025
0ef061c
[tb/LEMS-2761/matcher-split] Update test, type, and function to shuff…
Myranae Jan 24, 2025
4d36168
[tb/LEMS-2761/matcher-split] Change from a shuffle to a sort
Myranae Jan 24, 2025
bc4c368
[tb/LEMS-2761/matcher-split] Merge branch 'main' into tb/LEMS-2761/ma…
Myranae Jan 24, 2025
042be9e
[tb/LEMS-2761/matcher-split] Update test for sorted information
Myranae Jan 24, 2025
197fa17
[tb/LEMS-2761/matcher-split] Remove unneeded comments
Myranae Jan 24, 2025
95cfb9f
[tb/LEMS-2761/matcher-split] Shorten test data
Myranae Jan 28, 2025
d0cb74a
[tb/LEMS-2761/matcher-split] Merge branch 'main' into tb/LEMS-2761/ma…
Myranae Jan 28, 2025
1bd5449
[tb/LEMS-2761/matcher-split] Update types to match widget options
Myranae Jan 28, 2025
74593a1
[tb/LEMS-2761/matcher-split] Move shuffle, seededRNG, and random to p…
Myranae Jan 29, 2025
e9a035f
[tb/LEMS-2761/matcher-split] Update CoreUtil export and usage
Myranae Jan 29, 2025
7495493
[tb/LEMS-2761/matcher-split] Move matcher shuffle logic to helper
Myranae Jan 30, 2025
0723456
[tb/LEMS-2761/matcher-split] Apply matcherShuffle to public options f…
Myranae Jan 30, 2025
9d5c7be
[tb/LEMS-2761/matcher-split] Update spelling
Myranae Jan 30, 2025
8b39196
[tb/LEMS-2761/matcher-split] Merge branch 'main' into tb/LEMS-2761/ma…
Myranae Jan 30, 2025
31aa314
[tb/LEMS-2761/matcher-split] Merge branch 'main' into tb/LEMS-2761/ma…
Myranae Jan 30, 2025
9611f13
[tb/LEMS-2761/matcher-split] Merge branch 'main' into tb/LEMS-2761/ma…
Myranae Feb 3, 2025
771e1e4
[tb/LEMS-2761/matcher-split] Add note to update Matcher to not shuffl…
Myranae Feb 6, 2025
a84d9a1
[tb/LEMS-2761/matcher-split] Update TODO with ticket
Myranae Feb 6, 2025
328283e
[tb/LEMS-2761/matcher-split] Add TODOs
Myranae Feb 6, 2025
b04bf7b
[tb/LEMS-2761/matcher-split] Simplify return by spreading options
Myranae Feb 6, 2025
3022ba3
[tb/LEMS-2761/matcher-split] Add new type for new shuffling logic
Myranae Feb 6, 2025
139bd8a
[tb/LEMS-2761/matcher-split] Add open source hash function and seed f…
Myranae Feb 6, 2025
7678039
[tb/LEMS-2761/matcher-split] Add matcher shuffle that uses new seed
Myranae Feb 6, 2025
0168779
[tb/LEMS-2761/matcher-split] Update getPublicOptions with hashed shuffle
Myranae Feb 6, 2025
c4e8b82
[tb/LEMS-2761/matcher-split] Update test with new shuffle and more ex…
Myranae Feb 6, 2025
9360065
[tb/LEMS-2761/matcher-split] Merge branch 'main' into tb/LEMS-2761/ma…
Myranae Feb 6, 2025
1f1abe7
[tb/LEMS-2761/matcher-split] Update comment
Myranae Feb 6, 2025
fa1b3aa
[tb/LEMS-2761/matcher-split] Merge branch 'main' into tb/LEMS-2761/ma…
Myranae Feb 7, 2025
3185b4c
[tb/LEMS-2761/matcher-split] Merge branch 'main' into tb/LEMS-2761/ma…
Myranae Feb 7, 2025
6c336b2
[tb/LEMS-2761/matcher-split] Rename file and update comment
Myranae Feb 7, 2025
5a259bd
[tb/LEMS-2761/matcher-split] Put left info before right info in test …
Myranae Feb 7, 2025
de79348
[tb/LEMS-2761/matcher-split] Be more specific with test names
Myranae Feb 7, 2025
7f26be1
[tb/LEMS-2761/matcher-split] Rename to avoid yoda speak
Myranae Feb 7, 2025
aea4ca3
[tb/LEMS-2761/matcher-split] Update comments to jsDoc comments with r…
Myranae Feb 7, 2025
53e37e8
[tb/LEMS-2761/matcher-split] Reorder a type import
Myranae Feb 7, 2025
7ceca4b
[tb/LEMS-2761/matcher-split] Cast to any to remove ts-expect-error st…
Myranae Feb 7, 2025
bea7618
[tb/LEMS-2761/matcher-split] Put left above right again; missed a test
Myranae Feb 7, 2025
7c7c080
[tb/LEMS-2761/matcher-split] Fix format
Myranae Feb 7, 2025
46cd80b
[tb/LEMS-2761/matcher-split] Rename functions based on PR feedback
Myranae Feb 7, 2025
e67b285
[tb/LEMS-2761/matcher-split] Update to JSON.stringify to ensure diffe…
Myranae Feb 7, 2025
3cc37c3
[tb/LEMS-2761/matcher-split] Make comment even more specific
Myranae Feb 7, 2025
182e458
[tb/LEMS-2761/matcher-split] Remove negative in test name and be more…
Myranae Feb 7, 2025
8266eac
[tb/LEMS-2761/matcher-split] Merge branch 'main' into tb/LEMS-2761/ma…
Myranae Feb 7, 2025
32ae1e4
[tb/LEMS-2761/matcher-split] Fix spelling and filename in note
Myranae Feb 10, 2025
53d7fd7
[tb/LEMS-2761/matcher-split] Update shuffle to use Math.random instea…
Myranae Feb 10, 2025
682a039
[tb/LEMS-2761/matcher-split] Commit package manager update
Myranae Feb 10, 2025
3e1a5f2
[tb/LEMS-2761/matcher-split] Update order of columns in test
Myranae Feb 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tricky-hairs-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": minor
"@khanacademy/perseus-core": minor
---

Implement a widget export function to filter out rubric data from widget options for the Matcher widget
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,6 @@
"nyc": {
"report-dir": "coverage/cypress/"
},
"dependencies": {}
"dependencies": {},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
3 changes: 3 additions & 0 deletions packages/perseus-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,6 @@ export {default as getTablePublicWidgetOptions} from "./widgets/table/table-util
export {default as getIFramePublicWidgetOptions} from "./widgets/iframe/iframe-util";
export {default as getMatrixPublicWidgetOptions} from "./widgets/matrix/matrix-util";
export {default as getPlotterPublicWidgetOptions} from "./widgets/plotter/plotter-util";
export {default as getMatcherPublicWidgetOptions} from "./widgets/matcher/matcher-util";
export {shuffleMatcher} from "./widgets/matcher/matcher-util";
export {default as CoreUtil} from "./utils/random_utils";
77 changes: 77 additions & 0 deletions packages/perseus-core/src/utils/random_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* Note(tamara): Brought over from the perseus package packages/perseus/src/util.ts file.
May be useful to bring other perseus package utilities here. Contains utility functions
and types used across multiple widgets for randomization and shuffling. */

import _ from "underscore";

type RNG = () => number;

const seededRNG: (seed: number) => RNG = function (seed: number): RNG {
let randomSeed = seed;

return function () {
// Robert Jenkins' 32 bit integer hash function.
let seed = randomSeed;
seed = (seed + 0x7ed55d16 + (seed << 12)) & 0xffffffff;
seed = (seed ^ 0xc761c23c ^ (seed >>> 19)) & 0xffffffff;
seed = (seed + 0x165667b1 + (seed << 5)) & 0xffffffff;
seed = ((seed + 0xd3a2646c) ^ (seed << 9)) & 0xffffffff;
seed = (seed + 0xfd7046c5 + (seed << 3)) & 0xffffffff;
seed = (seed ^ 0xb55a4f09 ^ (seed >>> 16)) & 0xffffffff;
return (randomSeed = seed & 0xfffffff) / 0x10000000;
};
};

// Shuffle an array using a given random seed or function.
// If `ensurePermuted` is true, the input and output are guaranteed to be
// distinct permutations.
function shuffle<T>(
array: ReadonlyArray<T>,
randomSeed: number | RNG,
ensurePermuted = false,
): ReadonlyArray<T> {
// Always return a copy of the input array
const shuffled = _.clone(array);

// Handle edge cases (input array is empty or uniform)
if (
!shuffled.length ||
_.all(shuffled, function (value) {
return _.isEqual(value, shuffled[0]);
})
) {
return shuffled;
}

let random;
if (typeof randomSeed === "function") {
random = randomSeed;
} else {
random = seededRNG(randomSeed);
}

do {
// Fischer-Yates shuffle
for (let top = shuffled.length; top > 0; top--) {
const newEnd = Math.floor(random() * top);
const temp = shuffled[newEnd];

// @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
shuffled[newEnd] = shuffled[top - 1];
// @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
shuffled[top - 1] = temp;
}
} while (ensurePermuted && _.isEqual(array, shuffled));

return shuffled;
}

const random: RNG = seededRNG(new Date().getTime() & 0xffffffff);

export const CoreUtil = {
seededRNG,
shuffle,
random,
} as const;

export default CoreUtil;
41 changes: 41 additions & 0 deletions packages/perseus-core/src/widgets/matcher/matcher-util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import getMatcherPublicWidgetOptions from "./matcher-util";

import type {PerseusMatcherWidgetOptions} from "../../data-schema";

describe("getMatcherPublicWidgetOptions", () => {
it("should return a shuffled right array when order doesn't matter to remove the correct order information", () => {
// Arrange
const options: PerseusMatcherWidgetOptions = {
labels: ["**Claims**", "**Evidence**"],
padding: true,
orderMatters: false,
left: ["Fuel", "Plate", "Hydrogen", "Average", "Billion"],
right: ["Stars", "Earth", "Life", "Rapid", "Milky Way"],
};

// Act
const publicWidgetOptions = getMatcherPublicWidgetOptions(options);

// Assert
expect(publicWidgetOptions.left).toEqual(options.left);
expect(publicWidgetOptions.right).not.toEqual(options.right);
});

it("should return shuffled left and right arrays when order matters to remove the correct order information", () => {
// Arrange
const options: PerseusMatcherWidgetOptions = {
labels: ["**Claims**", "**Evidence**"],
padding: true,
orderMatters: true,
left: ["Fuel", "Plate", "Hydrogen", "Average", "Billion"],
right: ["Stars", "Earth", "Life", "Rapid", "Milky Way"],
};

// Act
const publicWidgetOptions = getMatcherPublicWidgetOptions(options);

// Assert
expect(publicWidgetOptions.left).not.toEqual(options.left);
expect(publicWidgetOptions.right).not.toEqual(options.right);
});
});
94 changes: 94 additions & 0 deletions packages/perseus-core/src/widgets/matcher/matcher-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {CoreUtil} from "@khanacademy/perseus-core";

import type {PerseusMatcherWidgetOptions} from "@khanacademy/perseus-core";

// TODO(LEMS-2841): Should be able to remove once getPublicWidgetOptions is hooked up
type MatcherInfo = {
left: ReadonlyArray<string>;
right: ReadonlyArray<string>;
orderMatters: boolean;
problemNum: number | null | undefined;
};

type MatcherShuffleInfo = {
left: ReadonlyArray<string>;
right: ReadonlyArray<string>;
orderMatters: boolean;
};

// TODO(LEMS-2841): Should be able to remove once getPublicWidgetOptions is hooked up
export const shuffleMatcher = (
props: MatcherInfo,
Myranae marked this conversation as resolved.
Show resolved Hide resolved
): {left: ReadonlyArray<string>; right: ReadonlyArray<string>} => {
// Use the same random() function to shuffle both columns sequentially
const rng = CoreUtil.seededRNG(props.problemNum as number);
Myranae marked this conversation as resolved.
Show resolved Hide resolved

let left;
if (!props.orderMatters) {
// If the order doesn't matter, don't shuffle the left column
left = props.left;
} else {
left = CoreUtil.shuffle(props.left, rng, /* ensurePermuted */ true);
}

const right = CoreUtil.shuffle(props.right, rng, /* ensurePermuted */ true);
Myranae marked this conversation as resolved.
Show resolved Hide resolved

return {left, right};
};

// TODO(LEMS-2841): Can shorten to shuffleMatcher after above function removed
function shuffleMatcherWithRandom(data: MatcherShuffleInfo): {
left: ReadonlyArray<string>;
right: ReadonlyArray<string>;
} {
// Use the same random() function to shuffle both columns sequentially
let left;
if (!data.orderMatters) {
// If the order doesn't matter, don't shuffle the left column
left = data.left;
} else {
left = CoreUtil.shuffle(
data.left,
Math.random() * 100,
/* ensurePermuted */ true,
);
}

const right = CoreUtil.shuffle(
data.right,
Math.random() * 100,
Comment on lines +52 to +59
Copy link
Contributor Author

@Myranae Myranae Feb 10, 2025

Choose a reason for hiding this comment

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

Multiplied the Math.random() here by 100 to get the shuffle to be random each time. Without multiplying, the shuffle is the same each time, though it is different than the answer. I was expecting it to be random, so updated to get that response. It might have something to do with the shuffle expecting a problemNum, which would either be undefined or a whole number.

/* ensurePermuted */ true,
);

return {left, right};
}

/**
* For details on the individual options, see the
* PerseusMatcherWidgetOptions type
*/
type MatcherPublicWidgetOptions = {
labels: PerseusMatcherWidgetOptions["labels"];
left: PerseusMatcherWidgetOptions["left"];
right: PerseusMatcherWidgetOptions["right"];
orderMatters: PerseusMatcherWidgetOptions["orderMatters"];
padding: PerseusMatcherWidgetOptions["padding"];
};

/**
* Given a PerseusMatcherWidgetOptions object, return a new object with only
* the public options that should be exposed to the client.
*/
function getMatcherPublicWidgetOptions(
options: PerseusMatcherWidgetOptions,
): MatcherPublicWidgetOptions {
const {left, right} = shuffleMatcherWithRandom(options);

return {
...options,
left: left,
right: right,
};
}

export default getMatcherPublicWidgetOptions;
2 changes: 2 additions & 0 deletions packages/perseus/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
getIFramePublicWidgetOptions,
getMatrixPublicWidgetOptions,
getPlotterPublicWidgetOptions,
getMatcherPublicWidgetOptions,
} from "@khanacademy/perseus-core";
import type {LinterContextProps} from "@khanacademy/perseus-linter";
import type {
Expand Down Expand Up @@ -496,6 +497,7 @@ export type PublicWidgetOptionsFunction =
| typeof getPlotterPublicWidgetOptions
| typeof getIFramePublicWidgetOptions
| typeof getRadioPublicWidgetOptions
| typeof getMatcherPublicWidgetOptions
| typeof getNumericInputPublicWidgetOptions
| typeof getDropdownPublicWidgetOptions
| typeof getCategorizerPublicWidgetOptions
Expand Down
67 changes: 0 additions & 67 deletions packages/perseus/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ type WordAndPosition = {
pos: WordPosition;
};

type RNG = () => number;

export type ParsedValue = {
value: number;
exact: boolean;
Expand Down Expand Up @@ -92,66 +90,6 @@ const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/;
const rWidgetParts = new RegExp(rWidgetRule.source + "$");
const snowman = "\u2603";

const seededRNG: (seed: number) => RNG = function (seed: number): RNG {
let randomSeed = seed;

return function () {
// Robert Jenkins' 32 bit integer hash function.
let seed = randomSeed;
seed = (seed + 0x7ed55d16 + (seed << 12)) & 0xffffffff;
seed = (seed ^ 0xc761c23c ^ (seed >>> 19)) & 0xffffffff;
seed = (seed + 0x165667b1 + (seed << 5)) & 0xffffffff;
seed = ((seed + 0xd3a2646c) ^ (seed << 9)) & 0xffffffff;
seed = (seed + 0xfd7046c5 + (seed << 3)) & 0xffffffff;
seed = (seed ^ 0xb55a4f09 ^ (seed >>> 16)) & 0xffffffff;
return (randomSeed = seed & 0xfffffff) / 0x10000000;
};
};

// Shuffle an array using a given random seed or function.
// If `ensurePermuted` is true, the input and ouput are guaranteed to be
// distinct permutations.
function shuffle<T>(
array: ReadonlyArray<T>,
randomSeed: number | RNG,
ensurePermuted = false,
): ReadonlyArray<T> {
// Always return a copy of the input array
const shuffled = _.clone(array);

// Handle edge cases (input array is empty or uniform)
if (
!shuffled.length ||
_.all(shuffled, function (value) {
return _.isEqual(value, shuffled[0]);
})
) {
return shuffled;
}

let random;
if (typeof randomSeed === "function") {
random = randomSeed;
} else {
random = seededRNG(randomSeed);
}

do {
// Fischer-Yates shuffle
for (let top = shuffled.length; top > 0; top--) {
const newEnd = Math.floor(random() * top);
const temp = shuffled[newEnd];

// @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
shuffled[newEnd] = shuffled[top - 1];
// @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
shuffled[top - 1] = temp;
}
} while (ensurePermuted && _.isEqual(array, shuffled));

return shuffled;
}

/**
* TODO(somewhatabstract, FEI-3463):
* Drop this custom split thing.
Expand Down Expand Up @@ -648,17 +586,13 @@ const textarea = {
const unescapeMathMode: (label: string) => string = (label) =>
label.startsWith("$") && label.endsWith("$") ? label.slice(1, -1) : label;

const random: RNG = seededRNG(new Date().getTime() & 0xffffffff);

const Util = {
inputPathsEqual,
nestedMap,
rWidgetRule,
rTypeFromWidgetId,
rWidgetParts,
snowman,
seededRNG,
shuffle,
split,
firstNumericalParse,
stringArrayOfSize,
Expand Down Expand Up @@ -687,7 +621,6 @@ const Util = {
getDataUrl: GraphieUtil.getDataUrl,
textarea,
unescapeMathMode,
random,
} as const;

export default Util;
9 changes: 6 additions & 3 deletions packages/perseus/src/widgets/categorizer/categorizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
type PerseusCategorizerWidgetOptions,
getCategorizerPublicWidgetOptions,
CoreUtil,
} from "@khanacademy/perseus-core";
import {linterContextDefault} from "@khanacademy/perseus-linter";
import {StyleSheet, css} from "aphrodite";
Expand All @@ -16,7 +17,6 @@ import * as Changeable from "../../mixins/changeable";
import Renderer from "../../renderer";
import mediaQueries from "../../styles/media-queries";
import sharedStyles from "../../styles/shared";
import Util from "../../util";
import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/categorizer/categorizer-ai-utils";

import type {Widget, WidgetExports, WidgetProps} from "../../types";
Expand Down Expand Up @@ -92,8 +92,11 @@ export class Categorizer
const isMobile = this.props.apiOptions.isMobile;
let indexedItems = this.props.items.map((item, n) => [item, n]);
if (this.props.randomizeItems) {
// @ts-expect-error - TS4104 - The type 'readonly (string | number)[][]' is 'readonly' and cannot be assigned to the mutable type '(string | number)[][]'. | TS2345 - Argument of type 'number | null | undefined' is not assignable to parameter of type 'number | RNG'.
indexedItems = Util.shuffle(indexedItems, this.props.problemNum);
// @ts-expect-error - TS4104 - The type 'readonly (string | number)[][]' is 'readonly' and cannot be assigned to the mutable type '(string | number)[][]'.
indexedItems = CoreUtil.shuffle(
indexedItems,
this.props.problemNum as any,
);
}

const table = (
Expand Down
Loading