@khanacademy/perseus 44.0.0 → 45.0.0
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/es/index.js +233 -189
- package/dist/es/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +232 -184
- package/dist/index.js.map +1 -1
- package/dist/renderer-util.d.ts +4 -2
- package/dist/renderer.d.ts +6 -10
- package/dist/server-item-renderer.d.ts +32 -1
- package/dist/types.d.ts +5 -0
- package/dist/widgets/expression/expression.d.ts +1 -1
- package/dist/widgets/group/group.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { default as ServerItemRenderer } from "./server-item-renderer";
|
|
|
14
14
|
export { default as HintsRenderer } from "./hints-renderer";
|
|
15
15
|
export { default as HintRenderer } from "./hint-renderer";
|
|
16
16
|
export { default as Renderer } from "./renderer";
|
|
17
|
+
export { scorePerseusItem } from "./renderer-util";
|
|
17
18
|
/**
|
|
18
19
|
* Widgets
|
|
19
20
|
*/
|
|
@@ -91,6 +92,7 @@ export type { Alignment, APIOptions, APIOptionsWithDefaults, ChangeHandler, Devi
|
|
|
91
92
|
export type { ParsedValue } from "./util";
|
|
92
93
|
export type { Hint, LegacyButtonSets, LockedFigure, LockedFigureColor, LockedFigureFillType, LockedFigureType, LockedPointType, LockedLineType, LockedVectorType, LockedEllipseType, LockedPolygonType, LockedFunctionType, LockedLabelType, LockedLineStyle, PerseusGraphType, PerseusAnswerArea, PerseusExpressionWidgetOptions, Range, Size, CollinearTuple, MathFormat, InputNumberWidget, // TODO(jeremy): remove?
|
|
93
94
|
PerseusArticle, PerseusImageBackground, PerseusInputNumberWidgetOptions, PerseusInteractiveGraphWidgetOptions, PerseusItem, PerseusPhetSimulationWidgetOptions, PerseusPlotterWidgetOptions, PerseusPythonProgramWidgetOptions, PerseusRadioWidgetOptions, PerseusRenderer, PerseusWidget, PerseusWidgetsMap, MultiItem, } from "./perseus-types";
|
|
95
|
+
export type { UserInputMap } from "./validation.types";
|
|
94
96
|
export type { Coord } from "./interactive2/types";
|
|
95
97
|
export type { MarkerType } from "./widgets/label-image/types";
|
|
96
98
|
/**
|
package/dist/index.js
CHANGED
|
@@ -14757,9 +14757,9 @@ const pluck = function (table, subKey) {
|
|
|
14757
14757
|
* });
|
|
14758
14758
|
* {a: 2, b: 3}
|
|
14759
14759
|
*/
|
|
14760
|
-
const mapObject$
|
|
14760
|
+
const mapObject$2 = function (obj, lambda) {
|
|
14761
14761
|
const result = {};
|
|
14762
|
-
|
|
14762
|
+
Object.keys(obj).forEach(key => {
|
|
14763
14763
|
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
|
|
14764
14764
|
result[key] = lambda(obj[key], key);
|
|
14765
14765
|
});
|
|
@@ -14776,14 +14776,14 @@ const mapObject$1 = function (obj, lambda) {
|
|
|
14776
14776
|
*/
|
|
14777
14777
|
const mapObjectFromArray = function (arr, lambda) {
|
|
14778
14778
|
const result = {};
|
|
14779
|
-
|
|
14779
|
+
arr.forEach(elem => {
|
|
14780
14780
|
result[elem] = lambda(elem);
|
|
14781
14781
|
});
|
|
14782
14782
|
return result;
|
|
14783
14783
|
};
|
|
14784
14784
|
var objective_ = {
|
|
14785
14785
|
pluck: pluck,
|
|
14786
|
-
mapObject: mapObject$
|
|
14786
|
+
mapObject: mapObject$2,
|
|
14787
14787
|
mapObjectFromArray: mapObjectFromArray
|
|
14788
14788
|
};
|
|
14789
14789
|
|
|
@@ -18919,6 +18919,138 @@ class QuestionParagraph extends React__namespace.Component {
|
|
|
18919
18919
|
}
|
|
18920
18920
|
}
|
|
18921
18921
|
|
|
18922
|
+
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
18923
|
+
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
18924
|
+
|
|
18925
|
+
/**
|
|
18926
|
+
* Regex for widget placeholders in a string.
|
|
18927
|
+
*
|
|
18928
|
+
* First capture group is the widget ID (ex. 'radio 1')
|
|
18929
|
+
* Second capture group is the widget type (ex. "radio)
|
|
18930
|
+
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
18931
|
+
*/
|
|
18932
|
+
function getWidgetRegex() {
|
|
18933
|
+
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
18934
|
+
}
|
|
18935
|
+
|
|
18936
|
+
/**
|
|
18937
|
+
* Add a widget placeholder using the widget ID.
|
|
18938
|
+
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
18939
|
+
*
|
|
18940
|
+
* @param {string} id
|
|
18941
|
+
* @returns {string}
|
|
18942
|
+
*/
|
|
18943
|
+
function addWidget(id) {
|
|
18944
|
+
return `[[☃ ${id}]]`;
|
|
18945
|
+
}
|
|
18946
|
+
|
|
18947
|
+
/**
|
|
18948
|
+
* Get a widget type by a widget's ID
|
|
18949
|
+
* (please don't derive type from ID)
|
|
18950
|
+
*
|
|
18951
|
+
* @param {string} widgetId the ID of the widget
|
|
18952
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18953
|
+
* @returns {string} the widget type (ie "radio")
|
|
18954
|
+
*/
|
|
18955
|
+
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
18956
|
+
const widget = widgetMap[WidgetId];
|
|
18957
|
+
return widget?.type ?? null;
|
|
18958
|
+
}
|
|
18959
|
+
|
|
18960
|
+
/**
|
|
18961
|
+
* Does the content have a specific type of widget?
|
|
18962
|
+
*
|
|
18963
|
+
* @param {string} type the type of the widget in question (ie "radio")
|
|
18964
|
+
* @param {string} content the string to search through
|
|
18965
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18966
|
+
* @returns {boolean} if the content includes the widget type
|
|
18967
|
+
*/
|
|
18968
|
+
function contentHasWidgetType(type, content, widgetMap) {
|
|
18969
|
+
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
18970
|
+
}
|
|
18971
|
+
|
|
18972
|
+
/**
|
|
18973
|
+
* Extract all widget IDs, which includes the widget type and instance number.
|
|
18974
|
+
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
18975
|
+
*
|
|
18976
|
+
* Content should contain Perseus widget placeholders,
|
|
18977
|
+
* which look like: '[[☃ radio 1]]'.
|
|
18978
|
+
*
|
|
18979
|
+
* @param {string} content
|
|
18980
|
+
* @returns {ReadonlyArray<string>} widgetIds
|
|
18981
|
+
*/
|
|
18982
|
+
function getWidgetIdsFromContent(content) {
|
|
18983
|
+
const widgets = [];
|
|
18984
|
+
const localWidgetRegex = getWidgetRegex();
|
|
18985
|
+
let match = localWidgetRegex.exec(content);
|
|
18986
|
+
while (match !== null) {
|
|
18987
|
+
widgets.push(match[1]);
|
|
18988
|
+
match = localWidgetRegex.exec(content);
|
|
18989
|
+
}
|
|
18990
|
+
return widgets;
|
|
18991
|
+
}
|
|
18992
|
+
|
|
18993
|
+
/**
|
|
18994
|
+
* Get a list of widget IDs from content,
|
|
18995
|
+
* but only for specific widget types
|
|
18996
|
+
*
|
|
18997
|
+
* @param {string} type the type of widget (ie "radio")
|
|
18998
|
+
* @param {string} content the string to parse
|
|
18999
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
19000
|
+
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
19001
|
+
*/
|
|
19002
|
+
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
19003
|
+
const rv = [];
|
|
19004
|
+
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
19005
|
+
widgetIdsInContent.forEach(widgetId => {
|
|
19006
|
+
const widget = widgetMap[widgetId];
|
|
19007
|
+
if (widget?.type === type) {
|
|
19008
|
+
rv.push(widgetId);
|
|
19009
|
+
}
|
|
19010
|
+
});
|
|
19011
|
+
return rv;
|
|
19012
|
+
}
|
|
19013
|
+
|
|
19014
|
+
/**
|
|
19015
|
+
* Pull the widget map out of ItemData
|
|
19016
|
+
*
|
|
19017
|
+
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
19018
|
+
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
19019
|
+
*/
|
|
19020
|
+
function getWidgetsMapFromItemData(itemData) {
|
|
19021
|
+
return itemData.question.widgets;
|
|
19022
|
+
}
|
|
19023
|
+
|
|
19024
|
+
/**
|
|
19025
|
+
* Get the widget information from a WidgetMap
|
|
19026
|
+
*
|
|
19027
|
+
* @param {string} widgetId the ID of the widget
|
|
19028
|
+
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
19029
|
+
*/
|
|
19030
|
+
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
19031
|
+
return widgetMap[widgetId] ?? null;
|
|
19032
|
+
}
|
|
19033
|
+
|
|
19034
|
+
/**
|
|
19035
|
+
* Get select widgets from a widget map.
|
|
19036
|
+
* Useful for multi-items that needs to split
|
|
19037
|
+
* one PerseusItem into several
|
|
19038
|
+
*
|
|
19039
|
+
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
19040
|
+
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
19041
|
+
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
19042
|
+
*/
|
|
19043
|
+
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
19044
|
+
const widgets = {};
|
|
19045
|
+
widgetIds.forEach(widgetId => {
|
|
19046
|
+
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
19047
|
+
if (widget) {
|
|
19048
|
+
widgets[widgetId] = widget;
|
|
19049
|
+
}
|
|
19050
|
+
});
|
|
19051
|
+
return widgets;
|
|
19052
|
+
}
|
|
19053
|
+
|
|
18922
19054
|
const DEFAULT_ALIGNMENT$1 = "block";
|
|
18923
19055
|
// NOTE(kevinb): "default" is not one in `validAlignments`.
|
|
18924
19056
|
const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
|
|
@@ -19349,12 +19481,38 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
|
|
|
19349
19481
|
isLintable: isLintable
|
|
19350
19482
|
});
|
|
19351
19483
|
|
|
19484
|
+
const {
|
|
19485
|
+
mapObject: mapObject$1
|
|
19486
|
+
} = objective_;
|
|
19487
|
+
function getUpgradedWidgetOptions(oldWidgetOptions) {
|
|
19488
|
+
// @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
|
|
19489
|
+
return mapObject$1(oldWidgetOptions, (widgetInfo, widgetId) => {
|
|
19490
|
+
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19491
|
+
const newValues = {};
|
|
19492
|
+
if (!widgetInfo.type) {
|
|
19493
|
+
// TODO: why does widget have no type?
|
|
19494
|
+
// We don't want to derive type from widget ID
|
|
19495
|
+
// see: LEMS-1845
|
|
19496
|
+
newValues.type = widgetId.split(" ")[0];
|
|
19497
|
+
}
|
|
19498
|
+
if (!widgetInfo.alignment) {
|
|
19499
|
+
newValues.alignment = "default";
|
|
19500
|
+
}
|
|
19501
|
+
widgetInfo = {
|
|
19502
|
+
...widgetInfo,
|
|
19503
|
+
...newValues
|
|
19504
|
+
};
|
|
19505
|
+
}
|
|
19506
|
+
return upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
19507
|
+
});
|
|
19508
|
+
}
|
|
19352
19509
|
function emptyWidgetsFunctional(widgets,
|
|
19353
19510
|
// This is a port of old code, I'm not sure why
|
|
19354
19511
|
// we need widgetIds vs the keys of the widgets object
|
|
19355
19512
|
widgetIds, userInputMap, strings, locale) {
|
|
19513
|
+
const upgradedWidgets = getUpgradedWidgetOptions(widgets);
|
|
19356
19514
|
return widgetIds.filter(id => {
|
|
19357
|
-
const widget =
|
|
19515
|
+
const widget = upgradedWidgets[id];
|
|
19358
19516
|
if (!widget || widget.static) {
|
|
19359
19517
|
// Static widgets shouldn't count as empty
|
|
19360
19518
|
return false;
|
|
@@ -19373,12 +19531,27 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19373
19531
|
}
|
|
19374
19532
|
});
|
|
19375
19533
|
}
|
|
19534
|
+
|
|
19535
|
+
// TODO: combine scorePerseusItem with scoreWidgetsFunctional
|
|
19536
|
+
// once scorePerseusItem is the only one calling scoreWidgetsFunctional
|
|
19537
|
+
function scorePerseusItem(perseusRenderData, userInputMap,
|
|
19538
|
+
// TODO(LEMS-2461,LEMS-2391): these probably
|
|
19539
|
+
// need to be removed before we move this to the server
|
|
19540
|
+
strings, locale) {
|
|
19541
|
+
// There seems to be a chance that PerseusRenderer.widgets might include
|
|
19542
|
+
// widget data for widgets that are not in PerseusRenderer.content,
|
|
19543
|
+
// so this checks that the widgets are being used before scoring them
|
|
19544
|
+
const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
|
|
19545
|
+
const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
|
|
19546
|
+
return Util.flattenScores(scores);
|
|
19547
|
+
}
|
|
19376
19548
|
function scoreWidgetsFunctional(widgets,
|
|
19377
19549
|
// This is a port of old code, I'm not sure why
|
|
19378
19550
|
// we need widgetIds vs the keys of the widgets object
|
|
19379
19551
|
widgetIds, userInputMap, strings, locale) {
|
|
19552
|
+
const upgradedWidgets = getUpgradedWidgetOptions(widgets);
|
|
19380
19553
|
const gradedWidgetIds = widgetIds.filter(id => {
|
|
19381
|
-
const props =
|
|
19554
|
+
const props = upgradedWidgets[id];
|
|
19382
19555
|
const widgetIsGraded = props?.graded == null || props.graded;
|
|
19383
19556
|
const widgetIsStatic = !!props?.static;
|
|
19384
19557
|
// Ungraded widgets or widgets set to static shouldn't be graded.
|
|
@@ -19386,14 +19559,14 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19386
19559
|
});
|
|
19387
19560
|
const widgetScores = {};
|
|
19388
19561
|
gradedWidgetIds.forEach(id => {
|
|
19389
|
-
const widget =
|
|
19562
|
+
const widget = upgradedWidgets[id];
|
|
19390
19563
|
if (!widget) {
|
|
19391
19564
|
return;
|
|
19392
19565
|
}
|
|
19393
19566
|
const userInput = userInputMap[id];
|
|
19394
19567
|
const scorer = getWidgetScorer(widget.type);
|
|
19395
19568
|
if (widget.type === "group") {
|
|
19396
|
-
const scores = scoreWidgetsFunctional(widget.options.widgets,
|
|
19569
|
+
const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
|
|
19397
19570
|
widgetScores[id] = Util.flattenScores(scores);
|
|
19398
19571
|
} else if (scorer) {
|
|
19399
19572
|
widgetScores[id] = scorer(userInput, widget.options, strings, locale);
|
|
@@ -19918,32 +20091,12 @@ class Renderer extends React__namespace.Component {
|
|
|
19918
20091
|
};
|
|
19919
20092
|
};
|
|
19920
20093
|
_getInitialWidgetState = props => {
|
|
19921
|
-
const allWidgetInfo =
|
|
20094
|
+
const allWidgetInfo = getUpgradedWidgetOptions(props.widgets);
|
|
19922
20095
|
return {
|
|
19923
20096
|
widgetInfo: allWidgetInfo,
|
|
19924
20097
|
widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props)
|
|
19925
20098
|
};
|
|
19926
20099
|
};
|
|
19927
|
-
|
|
19928
|
-
// @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
|
|
19929
|
-
_getAllWidgetsInfo = props => {
|
|
19930
|
-
return mapObject(props.widgets, (widgetInfo, widgetId) => {
|
|
19931
|
-
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19932
|
-
const newValues = {};
|
|
19933
|
-
if (!widgetInfo.type) {
|
|
19934
|
-
// TODO: why does widget have no type?
|
|
19935
|
-
// We don't want to derive type from widget ID
|
|
19936
|
-
// see: LEMS-1845
|
|
19937
|
-
newValues.type = widgetId.split(" ")[0];
|
|
19938
|
-
}
|
|
19939
|
-
if (!widgetInfo.alignment) {
|
|
19940
|
-
newValues.alignment = "default";
|
|
19941
|
-
}
|
|
19942
|
-
widgetInfo = ___default["default"].extend({}, widgetInfo, newValues);
|
|
19943
|
-
}
|
|
19944
|
-
return upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
19945
|
-
});
|
|
19946
|
-
};
|
|
19947
20100
|
_getAllWidgetsStartProps = (allWidgetInfo, props) => {
|
|
19948
20101
|
const {
|
|
19949
20102
|
apiOptions,
|
|
@@ -20442,7 +20595,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20442
20595
|
// Instead, we center them with css magic in styles.less
|
|
20443
20596
|
// /cry(aria)
|
|
20444
20597
|
this._foundTextNodes = true;
|
|
20445
|
-
if (
|
|
20598
|
+
if (this.widgetIds.includes(node.id)) {
|
|
20446
20599
|
// We don't want to render a duplicate widget key/ref,
|
|
20447
20600
|
// as this causes problems with react (for obvious
|
|
20448
20601
|
// reasons). Instead we just notify the
|
|
@@ -20767,7 +20920,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20767
20920
|
};
|
|
20768
20921
|
getInputPaths = () => {
|
|
20769
20922
|
const inputPaths = [];
|
|
20770
|
-
|
|
20923
|
+
this.widgetIds.forEach(widgetId => {
|
|
20771
20924
|
const widget = this.getWidgetInstance(widgetId);
|
|
20772
20925
|
if (widget && widget.getInputPaths) {
|
|
20773
20926
|
// Grab all input paths and add widgetID to the front
|
|
@@ -20775,7 +20928,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20775
20928
|
// Prefix paths with their widgetID and add to collective
|
|
20776
20929
|
// list of paths.
|
|
20777
20930
|
// @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
|
|
20778
|
-
|
|
20931
|
+
widgetInputPaths.forEach(inputPath => {
|
|
20779
20932
|
const relativeInputPath = [widgetId].concat(inputPath);
|
|
20780
20933
|
inputPaths.push(relativeInputPath);
|
|
20781
20934
|
});
|
|
@@ -20968,33 +21121,30 @@ class Renderer extends React__namespace.Component {
|
|
|
20968
21121
|
}
|
|
20969
21122
|
|
|
20970
21123
|
/**
|
|
20971
|
-
*
|
|
20972
|
-
*
|
|
20973
|
-
*
|
|
20974
|
-
*/
|
|
20975
|
-
scoreWidgets() {
|
|
20976
|
-
return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20977
|
-
}
|
|
20978
|
-
|
|
20979
|
-
/**
|
|
20980
|
-
* Grades the content.
|
|
21124
|
+
* Scores the content.
|
|
21125
|
+
*
|
|
21126
|
+
* @deprecated use scorePerseusItem
|
|
20981
21127
|
*/
|
|
20982
21128
|
score() {
|
|
20983
|
-
const scores = this.
|
|
21129
|
+
const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20984
21130
|
const combinedScore = Util.flattenScores(scores);
|
|
20985
21131
|
return combinedScore;
|
|
20986
21132
|
}
|
|
20987
|
-
|
|
21133
|
+
|
|
21134
|
+
/**
|
|
21135
|
+
* @deprecated use scorePerseusItem
|
|
21136
|
+
*/
|
|
21137
|
+
guessAndScore = () => {
|
|
20988
21138
|
const totalGuess = this.getUserInput();
|
|
20989
21139
|
const totalScore = this.score();
|
|
20990
21140
|
return [totalGuess, totalScore];
|
|
20991
|
-
}
|
|
21141
|
+
};
|
|
20992
21142
|
examples = () => {
|
|
20993
21143
|
const widgetIds = this.widgetIds;
|
|
20994
|
-
const examples =
|
|
21144
|
+
const examples = widgetIds.map(widgetId => {
|
|
20995
21145
|
const widget = this.getWidgetInstance(widgetId);
|
|
20996
21146
|
return widget != null && widget.examples ? widget.examples() : null;
|
|
20997
|
-
}));
|
|
21147
|
+
}).filter(Boolean);
|
|
20998
21148
|
|
|
20999
21149
|
// no widgets with examples
|
|
21000
21150
|
if (!examples.length) {
|
|
@@ -29602,6 +29752,9 @@ class Group extends React__namespace.Component {
|
|
|
29602
29752
|
this.change("widgets", this.rendererRef.props.widgets);
|
|
29603
29753
|
}
|
|
29604
29754
|
};
|
|
29755
|
+
|
|
29756
|
+
// TODO(LEMS-2391): replace this when there's a separate check
|
|
29757
|
+
// for valid/invalid state
|
|
29605
29758
|
const score = this.rendererRef?.score();
|
|
29606
29759
|
const isValid = score && score.type !== "invalid";
|
|
29607
29760
|
const isInvalid = score && score.type === "invalid";
|
|
@@ -45227,7 +45380,7 @@ const init = function () {
|
|
|
45227
45380
|
|
|
45228
45381
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45229
45382
|
const libName = "@khanacademy/perseus";
|
|
45230
|
-
const libVersion = "
|
|
45383
|
+
const libVersion = "45.0.0";
|
|
45231
45384
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45232
45385
|
|
|
45233
45386
|
const apiVersion = {
|
|
@@ -45831,6 +45984,13 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45831
45984
|
this.props.onRendered(true);
|
|
45832
45985
|
}
|
|
45833
45986
|
}
|
|
45987
|
+
if (this.props.score && this.props.score !== prevProps.score) {
|
|
45988
|
+
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
45989
|
+
this.setState({
|
|
45990
|
+
questionCompleted: this.props.score.correct,
|
|
45991
|
+
questionHighlightedWidgets: emptyQuestionAreaWidgets
|
|
45992
|
+
});
|
|
45993
|
+
}
|
|
45834
45994
|
}
|
|
45835
45995
|
componentWillUnmount() {
|
|
45836
45996
|
if (this.blurTimeoutID != null) {
|
|
@@ -45966,18 +46126,37 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45966
46126
|
return this.questionRenderer.getPromptJSON();
|
|
45967
46127
|
}
|
|
45968
46128
|
|
|
46129
|
+
/**
|
|
46130
|
+
* Returns an array of the widget `.getUserInput()` results
|
|
46131
|
+
*
|
|
46132
|
+
* TODO: can we remove this? Seems to be just for backwards
|
|
46133
|
+
* compatibility with old Perseus Chrome logging
|
|
46134
|
+
* @deprecated use getUserInput
|
|
46135
|
+
*/
|
|
46136
|
+
getUserInputLegacy() {
|
|
46137
|
+
return this.questionRenderer.getUserInput();
|
|
46138
|
+
}
|
|
46139
|
+
|
|
46140
|
+
/**
|
|
46141
|
+
* Returns an object of the widget `.getUserInput()` results
|
|
46142
|
+
*/
|
|
46143
|
+
getUserInput() {
|
|
46144
|
+
return this.questionRenderer.getUserInputMap();
|
|
46145
|
+
}
|
|
46146
|
+
|
|
45969
46147
|
/**
|
|
45970
46148
|
* Grades the item.
|
|
46149
|
+
*
|
|
46150
|
+
* @deprecated use scorePerseusItem
|
|
45971
46151
|
*/
|
|
45972
46152
|
scoreInput() {
|
|
45973
|
-
const
|
|
45974
|
-
const
|
|
45975
|
-
const score = guessAndScore[1];
|
|
46153
|
+
const guess = this.getUserInput();
|
|
46154
|
+
const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
|
|
45976
46155
|
|
|
45977
46156
|
// Continue to include an empty guess for the now defunct answer area.
|
|
45978
46157
|
// TODO(alex): Check whether we rely on the format here for
|
|
45979
46158
|
// analyzing ProblemLogs. If not, remove this layer.
|
|
45980
|
-
const maxCompatGuess = [
|
|
46159
|
+
const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
|
|
45981
46160
|
const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
|
|
45982
46161
|
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
45983
46162
|
this.setState({
|
|
@@ -47839,138 +48018,6 @@ const registerAllWidgetsForTesting = () => {
|
|
|
47839
48018
|
replaceDeprecatedWidgets();
|
|
47840
48019
|
};
|
|
47841
48020
|
|
|
47842
|
-
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
47843
|
-
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
47844
|
-
|
|
47845
|
-
/**
|
|
47846
|
-
* Regex for widget placeholders in a string.
|
|
47847
|
-
*
|
|
47848
|
-
* First capture group is the widget ID (ex. 'radio 1')
|
|
47849
|
-
* Second capture group is the widget type (ex. "radio)
|
|
47850
|
-
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
47851
|
-
*/
|
|
47852
|
-
function getWidgetRegex() {
|
|
47853
|
-
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
47854
|
-
}
|
|
47855
|
-
|
|
47856
|
-
/**
|
|
47857
|
-
* Add a widget placeholder using the widget ID.
|
|
47858
|
-
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
47859
|
-
*
|
|
47860
|
-
* @param {string} id
|
|
47861
|
-
* @returns {string}
|
|
47862
|
-
*/
|
|
47863
|
-
function addWidget(id) {
|
|
47864
|
-
return `[[☃ ${id}]]`;
|
|
47865
|
-
}
|
|
47866
|
-
|
|
47867
|
-
/**
|
|
47868
|
-
* Get a widget type by a widget's ID
|
|
47869
|
-
* (please don't derive type from ID)
|
|
47870
|
-
*
|
|
47871
|
-
* @param {string} widgetId the ID of the widget
|
|
47872
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47873
|
-
* @returns {string} the widget type (ie "radio")
|
|
47874
|
-
*/
|
|
47875
|
-
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
47876
|
-
const widget = widgetMap[WidgetId];
|
|
47877
|
-
return widget?.type ?? null;
|
|
47878
|
-
}
|
|
47879
|
-
|
|
47880
|
-
/**
|
|
47881
|
-
* Does the content have a specific type of widget?
|
|
47882
|
-
*
|
|
47883
|
-
* @param {string} type the type of the widget in question (ie "radio")
|
|
47884
|
-
* @param {string} content the string to search through
|
|
47885
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47886
|
-
* @returns {boolean} if the content includes the widget type
|
|
47887
|
-
*/
|
|
47888
|
-
function contentHasWidgetType(type, content, widgetMap) {
|
|
47889
|
-
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
47890
|
-
}
|
|
47891
|
-
|
|
47892
|
-
/**
|
|
47893
|
-
* Extract all widget IDs, which includes the widget type and instance number.
|
|
47894
|
-
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
47895
|
-
*
|
|
47896
|
-
* Content should contain Perseus widget placeholders,
|
|
47897
|
-
* which look like: '[[☃ radio 1]]'.
|
|
47898
|
-
*
|
|
47899
|
-
* @param {string} content
|
|
47900
|
-
* @returns {ReadonlyArray<string>} widgetIds
|
|
47901
|
-
*/
|
|
47902
|
-
function getWidgetIdsFromContent(content) {
|
|
47903
|
-
const widgets = [];
|
|
47904
|
-
const localWidgetRegex = getWidgetRegex();
|
|
47905
|
-
let match = localWidgetRegex.exec(content);
|
|
47906
|
-
while (match !== null) {
|
|
47907
|
-
widgets.push(match[1]);
|
|
47908
|
-
match = localWidgetRegex.exec(content);
|
|
47909
|
-
}
|
|
47910
|
-
return widgets;
|
|
47911
|
-
}
|
|
47912
|
-
|
|
47913
|
-
/**
|
|
47914
|
-
* Get a list of widget IDs from content,
|
|
47915
|
-
* but only for specific widget types
|
|
47916
|
-
*
|
|
47917
|
-
* @param {string} type the type of widget (ie "radio")
|
|
47918
|
-
* @param {string} content the string to parse
|
|
47919
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47920
|
-
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
47921
|
-
*/
|
|
47922
|
-
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
47923
|
-
const rv = [];
|
|
47924
|
-
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
47925
|
-
widgetIdsInContent.forEach(widgetId => {
|
|
47926
|
-
const widget = widgetMap[widgetId];
|
|
47927
|
-
if (widget?.type === type) {
|
|
47928
|
-
rv.push(widgetId);
|
|
47929
|
-
}
|
|
47930
|
-
});
|
|
47931
|
-
return rv;
|
|
47932
|
-
}
|
|
47933
|
-
|
|
47934
|
-
/**
|
|
47935
|
-
* Pull the widget map out of ItemData
|
|
47936
|
-
*
|
|
47937
|
-
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
47938
|
-
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
47939
|
-
*/
|
|
47940
|
-
function getWidgetsMapFromItemData(itemData) {
|
|
47941
|
-
return itemData.question.widgets;
|
|
47942
|
-
}
|
|
47943
|
-
|
|
47944
|
-
/**
|
|
47945
|
-
* Get the widget information from a WidgetMap
|
|
47946
|
-
*
|
|
47947
|
-
* @param {string} widgetId the ID of the widget
|
|
47948
|
-
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
47949
|
-
*/
|
|
47950
|
-
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
47951
|
-
return widgetMap[widgetId] ?? null;
|
|
47952
|
-
}
|
|
47953
|
-
|
|
47954
|
-
/**
|
|
47955
|
-
* Get select widgets from a widget map.
|
|
47956
|
-
* Useful for multi-items that needs to split
|
|
47957
|
-
* one PerseusItem into several
|
|
47958
|
-
*
|
|
47959
|
-
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
47960
|
-
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
47961
|
-
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
47962
|
-
*/
|
|
47963
|
-
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
47964
|
-
const widgets = {};
|
|
47965
|
-
widgetIds.forEach(widgetId => {
|
|
47966
|
-
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
47967
|
-
if (widget) {
|
|
47968
|
-
widgets[widgetId] = widget;
|
|
47969
|
-
}
|
|
47970
|
-
});
|
|
47971
|
-
return widgets;
|
|
47972
|
-
}
|
|
47973
|
-
|
|
47974
48021
|
/**
|
|
47975
48022
|
* This function extracts the answers from the widgets.
|
|
47976
48023
|
*
|
|
@@ -50140,6 +50187,7 @@ exports.parsePerseusItem = parsePerseusItem;
|
|
|
50140
50187
|
exports.plotterPlotTypes = plotterPlotTypes;
|
|
50141
50188
|
exports.preprocessTex = preprocessTex;
|
|
50142
50189
|
exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
|
|
50190
|
+
exports.scorePerseusItem = scorePerseusItem;
|
|
50143
50191
|
exports.shapes = shapes;
|
|
50144
50192
|
exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
|
|
50145
50193
|
exports.traverse = traverse;
|