From ff1a4c065a46cf79580b3cc693f5de8a191615e1 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Thu, 2 Apr 2026 13:32:39 -0700 Subject: [PATCH 01/31] [benc/prompt-json-docs] Add doc comments for CategorizerPromptJSON --- .../categorizer/categorizer-ai-utils.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/categorizer/categorizer-ai-utils.ts b/packages/perseus/src/widget-ai-utils/categorizer/categorizer-ai-utils.ts index ccfa3d466f5..7fbee4e2cd0 100644 --- a/packages/perseus/src/widget-ai-utils/categorizer/categorizer-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/categorizer/categorizer-ai-utils.ts @@ -1,13 +1,44 @@ import type categorizer from "../../widgets/categorizer/categorizer"; import type React from "react"; +/** + * JSON describing a categorizer widget. Intended for consumption by AI tools. + * A categorizer displays a table where each row represents an item to be + * categorized, and each column represents a category. Each table cell contains + * a bubble that the learner can fill to indicate that that item belongs in + * that category. Only one bubble in each row can be filled. Learners fill + * bubbles by clicking on them. + */ export type CategorizerPromptJSON = { type: "categorizer"; + + /** + * The configuration of the widget, set by the content creator. + */ options: { + /** + * The items for the learner to categorize. + */ items: ReadonlyArray; + + /** + * Categories to which items can be assigned. + */ categories: ReadonlyArray; }; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** + * The category indices for each item. Elements in this array + * correspond to elements of `options.items`, and refer to indices of + * the `categories` array. For example, a value of `[2, null, 0]` means + * that the first item is in category 2, the second item has not been + * assigned to a category, and the third item is in category 0. + */ itemToCategoryMapping: ReadonlyArray; }; }; From d3d8015ad2efae2af6ae11ee216d34c1454efe76 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Thu, 2 Apr 2026 14:00:37 -0700 Subject: [PATCH 02/31] [benc/prompt-json-docs] Document UnsupportedWidgetPromptJSON --- packages/perseus/src/index.ts | 2 ++ packages/perseus/src/widget-ai-utils/unsupported-widget.ts | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index 3a171aaf473..7f6da663a6e 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -175,3 +175,5 @@ export type { RendererPromptJSON, WidgetPromptJSON, } from "./widget-ai-utils/prompt-types"; +export type {CategorizerPromptJSON} from "./widget-ai-utils/categorizer/categorizer-ai-utils"; +export type {UnsupportedWidgetPromptJSON} from "./widget-ai-utils/unsupported-widget"; diff --git a/packages/perseus/src/widget-ai-utils/unsupported-widget.ts b/packages/perseus/src/widget-ai-utils/unsupported-widget.ts index 5c25434a1e4..dd2b912d8cd 100644 --- a/packages/perseus/src/widget-ai-utils/unsupported-widget.ts +++ b/packages/perseus/src/widget-ai-utils/unsupported-widget.ts @@ -1,15 +1,20 @@ import type {UnsupportedWidget} from "./prompt-types"; +/** + * A placeholder in prompt JSON for a widget that doesn't support AI tooling. + */ export type UnsupportedWidgetPromptJSON = { type: UnsupportedWidget; + /** Always empty; not used. */ message?: string; + /** Always false. */ isSupported: boolean; }; export const getUnsupportedPromptJSON = ( widgetType: UnsupportedWidget, message: string = "", -) => { +): UnsupportedWidgetPromptJSON => { return { type: widgetType, isSupported: false, From e04f145a5a79f1d341b3bbb2b001d5350eef01f5 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Thu, 2 Apr 2026 14:05:35 -0700 Subject: [PATCH 03/31] [benc/prompt-json-docs] Document DefinitionPromptJSON --- packages/perseus/src/index.ts | 1 + .../src/widget-ai-utils/definition/definition-ai-utils.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index 7f6da663a6e..166993575bf 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -176,4 +176,5 @@ export type { WidgetPromptJSON, } from "./widget-ai-utils/prompt-types"; export type {CategorizerPromptJSON} from "./widget-ai-utils/categorizer/categorizer-ai-utils"; +export type {DefinitionPromptJSON} from "./widget-ai-utils/definition/definition-ai-utils"; export type {UnsupportedWidgetPromptJSON} from "./widget-ai-utils/unsupported-widget"; diff --git a/packages/perseus/src/widget-ai-utils/definition/definition-ai-utils.ts b/packages/perseus/src/widget-ai-utils/definition/definition-ai-utils.ts index 1b3f2599ec8..6cd34e750aa 100644 --- a/packages/perseus/src/widget-ai-utils/definition/definition-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/definition/definition-ai-utils.ts @@ -1,9 +1,15 @@ import type definition from "../../widgets/definition/definition"; import type React from "react"; +/** + * The definition widget usually appears inline in a paragraph of text. It + * renders a term and displays its definition in a popover when clicked. + */ export type DefinitionPromptJSON = { type: "definition"; + /** The definition of the term. */ definition: string; + /** The term being defined. */ togglePrompt: string; }; From 7f91ee0f541381013cfd5bd2a6bcd2a183c78a01 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Thu, 2 Apr 2026 15:55:52 -0700 Subject: [PATCH 04/31] [benc/prompt-json-docs] Document DropdownPromptJSON --- packages/perseus/src/index.ts | 1 + .../src/widget-ai-utils/dropdown/dropdown-ai-utils.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index 166993575bf..51762267635 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -177,4 +177,5 @@ export type { } from "./widget-ai-utils/prompt-types"; export type {CategorizerPromptJSON} from "./widget-ai-utils/categorizer/categorizer-ai-utils"; export type {DefinitionPromptJSON} from "./widget-ai-utils/definition/definition-ai-utils"; +export type {DropdownPromptJSON} from "./widget-ai-utils/dropdown/dropdown-ai-utils"; export type {UnsupportedWidgetPromptJSON} from "./widget-ai-utils/unsupported-widget"; diff --git a/packages/perseus/src/widget-ai-utils/dropdown/dropdown-ai-utils.ts b/packages/perseus/src/widget-ai-utils/dropdown/dropdown-ai-utils.ts index ec2f1d68006..f60e0f3e17f 100644 --- a/packages/perseus/src/widget-ai-utils/dropdown/dropdown-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/dropdown/dropdown-ai-utils.ts @@ -3,9 +3,19 @@ import type React from "react"; export type DropdownPromptJSON = { type: "dropdown"; + + /** + * The configuration of the widget, set by the content creator. + */ options: { + /** The choices present in the dropdown */ items: ReadonlyArray; }; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { selectedIndex: number; }; From 52f97d146599a497cc57025e53994372d8cb57b8 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Fri, 3 Apr 2026 08:22:40 -0700 Subject: [PATCH 05/31] [benc/prompt-json-docs] Document explanation prompt JSON --- .../definition/definition-ai-utils.ts | 2 ++ .../widget-ai-utils/dropdown/dropdown-ai-utils.ts | 4 ++++ .../explanation/explanation-ai-utils.ts | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/definition/definition-ai-utils.ts b/packages/perseus/src/widget-ai-utils/definition/definition-ai-utils.ts index 6cd34e750aa..c27b2857fe9 100644 --- a/packages/perseus/src/widget-ai-utils/definition/definition-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/definition/definition-ai-utils.ts @@ -2,8 +2,10 @@ import type definition from "../../widgets/definition/definition"; import type React from "react"; /** + * JSON describing a definition widget. Intended for consumption by AI tools. * The definition widget usually appears inline in a paragraph of text. It * renders a term and displays its definition in a popover when clicked. + * Learners' interactions with this widget are not graded. */ export type DefinitionPromptJSON = { type: "definition"; diff --git a/packages/perseus/src/widget-ai-utils/dropdown/dropdown-ai-utils.ts b/packages/perseus/src/widget-ai-utils/dropdown/dropdown-ai-utils.ts index f60e0f3e17f..1635824e889 100644 --- a/packages/perseus/src/widget-ai-utils/dropdown/dropdown-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/dropdown/dropdown-ai-utils.ts @@ -1,6 +1,10 @@ import type dropdown from "../../widgets/dropdown/dropdown"; import type React from "react"; +/** + * JSON describing a dropdown widget. Intended for consumption by AI tools. + * The dropdown widget displays a menu of options. + */ export type DropdownPromptJSON = { type: "dropdown"; diff --git a/packages/perseus/src/widget-ai-utils/explanation/explanation-ai-utils.ts b/packages/perseus/src/widget-ai-utils/explanation/explanation-ai-utils.ts index 9488dc1c9b1..070f77ba0f7 100644 --- a/packages/perseus/src/widget-ai-utils/explanation/explanation-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/explanation/explanation-ai-utils.ts @@ -1,9 +1,21 @@ import type explanation from "../../widgets/explanation/explanation"; import type React from "react"; +/** + * JSON describing an explanation widget. Intended for consumption by AI tools. + * The explanation widget displays a disclosure element that the user can + * click to see more details about a topic. Learners' interactions with this + * widget are not graded. + */ export type ExplanationPromptJSON = { type: "explanation"; + /** + * The clickable text shown when the disclosure is closed. + */ showPrompt: string; + /** + * The text shown when the disclosure is open. + */ explanation: string; }; From d164866e921212a30afec7694ca4f101b2ff999c Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Fri, 3 Apr 2026 08:28:35 -0700 Subject: [PATCH 06/31] [benc/prompt-json-docs] Document expression prompt JSON --- .../expression/expression-ai-utils.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/expression/expression-ai-utils.ts b/packages/perseus/src/widget-ai-utils/expression/expression-ai-utils.ts index baf75c63660..7dc2ebf13ba 100644 --- a/packages/perseus/src/widget-ai-utils/expression/expression-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/expression/expression-ai-utils.ts @@ -1,9 +1,22 @@ import type {PerseusExpressionUserInput} from "@khanacademy/perseus-core"; +/** + * JSON describing an expression widget. Intended for consumption by AI tools. + * The expression widget shows an input field that allows a user to input a + * math expression or equation. + */ export type ExpressionPromptJSON = { type: "expression"; + + /** The label shown on the input field */ label?: string; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** The expression or equation input by the user. Uses TeX syntax. */ value: string; }; }; From e39f46d851bdbdb66dd3dc9fe4df9cee6504923c Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Fri, 3 Apr 2026 09:10:53 -0700 Subject: [PATCH 07/31] [benc/prompt-json-docs] Add doc comments for graded group prompt JSON --- .../graded-group/graded-group-ai-utils.ts | 20 ++++++++++++++++--- .../src/widget-ai-utils/prompt-types.ts | 19 ++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/perseus/src/widget-ai-utils/graded-group/graded-group-ai-utils.ts b/packages/perseus/src/widget-ai-utils/graded-group/graded-group-ai-utils.ts index 8f9ab6a30ad..ca6c62f454e 100644 --- a/packages/perseus/src/widget-ai-utils/graded-group/graded-group-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/graded-group/graded-group-ai-utils.ts @@ -1,10 +1,24 @@ import type {RendererPromptJSON} from "../prompt-types"; -export type GradedGroupPromptJSON = RendererPromptJSON & { +/** + * JSON describing a graded group widget. Intended for consumption by AI tools. + * The graded group widget displays a self-contained question with its own + * "check answer" button. The learner's score on this question is not recorded. + * Graded groups are provided for learners to check their own understanding of + * a concept. + */ +export interface GradedGroupPromptJSON extends RendererPromptJSON { type: "graded-group"; + + /** Displayed above the graded group. */ title: string; - hint: RendererPromptJSON | string; -}; + + /** + * Explanation of the question, which the learner can show or hide by + * clicking a button. There is no penalty for looking at this hint. + */ + hint: RendererPromptJSON; +} export const getPromptJSON = ( title: string, diff --git a/packages/perseus/src/widget-ai-utils/prompt-types.ts b/packages/perseus/src/widget-ai-utils/prompt-types.ts index fef792ad425..0077b5afbf2 100644 --- a/packages/perseus/src/widget-ai-utils/prompt-types.ts +++ b/packages/perseus/src/widget-ai-utils/prompt-types.ts @@ -54,12 +54,27 @@ export type WidgetPromptJSON = | SorterPromptJSON | UnsupportedWidgetPromptJSON; -export type RendererPromptJSON = { +/** + * JSON describing a Perseus renderer. Intended for consumption by AI tools. + * A "renderer" is essentially a Markdown document with embedded interactive + * widgets. + */ +export interface RendererPromptJSON { + /** + * Markdown content of the document. Widgets are represented by + * placeholders containing a Unicode snowman symbol, e.g. + * `[[☃ radio 1]]`. May contain TeX delimited by dollar signs, e.g. + * `$\dfrac{1}{2}$`. Literal dollar signs are escaped by backslashes. + */ content: string; + + /** + * Information about the configuration and UI state of each widget. + */ widgets: { [widgetId: string]: WidgetPromptJSON; }; -}; +} export interface GetPromptJSONInterface { getPromptJSON(): RendererPromptJSON; From fc758499e08a6137475f2262140e24fd08479e4d Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Fri, 3 Apr 2026 10:40:25 -0700 Subject: [PATCH 08/31] [benc/prompt-json-docs] Document GradedGroupSet widget prompt JSON --- .../graded-group-set/graded-group-set-ai-utils.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/graded-group-set/graded-group-set-ai-utils.ts b/packages/perseus/src/widget-ai-utils/graded-group-set/graded-group-set-ai-utils.ts index 538be3a50a1..dac99b28767 100644 --- a/packages/perseus/src/widget-ai-utils/graded-group-set/graded-group-set-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/graded-group-set/graded-group-set-ai-utils.ts @@ -2,10 +2,22 @@ import type gradedGroupSet from "../../widgets/graded-group-set"; import type {GradedGroupPromptJSON} from "../graded-group/graded-group-ai-utils"; import type React from "react"; +/** + * JSON describing a graded group set widget. Intended for consumption by AI tools. + * A graded group set displays several review questions (GradedGroups) in a + * carousel-like UI. The learner's score on a graded group set is not recorded. + * Graded group sets are provided for learners to check their own understanding + * of a concept. + */ export type GradedGroupSetPromptJSON = { type: "graded-group-set"; + + /** The configuration of the widget, set by the content creator. */ options: { + /** The number of questions (`GradedGroup`s) in this widget */ groupCount: number; + + /** The currently-displayed question */ currentGroup: GradedGroupPromptJSON; }; }; From 32358a097700fc2ce8bbdbd01446bef1c98b17cf Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Fri, 3 Apr 2026 11:48:18 -0700 Subject: [PATCH 09/31] [benc/prompt-json-docs] Document grapher prompt JSON --- .../grapher/grapher-ai-utils.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/grapher/grapher-ai-utils.ts b/packages/perseus/src/widget-ai-utils/grapher/grapher-ai-utils.ts index f2f31bd7991..4f6f439875f 100644 --- a/packages/perseus/src/widget-ai-utils/grapher/grapher-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/grapher/grapher-ai-utils.ts @@ -2,17 +2,60 @@ import type grapher from "../../widgets/grapher/grapher"; import type {GrapherAnswerTypes} from "@khanacademy/perseus-core"; import type React from "react"; +/** + * JSON describing a grapher widget. Intended for consumption by AI tools. + * The grapher widget displays a Cartesian plane where the learner can plot an + * equation by clicking and dragging control points. + */ export type GrapherPromptJSON = { type: "grapher"; + + /** The configuration of the widget, set by the content creator. */ options: { + /** + * The types of equations the learner can plot. If there is more than + * one entry in this array, the widget displays a set of buttons for + * selecting the graph type. + */ availableTypes: ReadonlyArray; + + /** + * The bounds of the graph, in the format + * `[[xMin, xMax], [yMin, yMax]]`. + */ range: [x: [min: number, max: number], y: [min: number, max: number]]; + + /** + * Labels for the x and y axes of the graph, in the format `[x, y]`. + */ labels: ReadonlyArray; + + /** + * The spacing between tick markings on the graph axes, measured in + * units on the Cartesian plane. Format: `[xTickStep, yTickStep]`. + */ tickStep: [number, number]; + + /** + * The spacing between grid lines, measured in units on the Cartesian + * plane. Format: `[xGridStep, yGridStep]`. + */ gridStep?: [number, number]; + + /** + * Control points snap to coordinates that are multiples of the snap + * step. Format: `[xSnapStep, ySnapStep]`. + */ snapStep?: [number, number]; + + /** An image displayed behind the graph */ backgroundImageUrl?: string | null; }; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: GrapherAnswerTypes; }; From 6faad53f150b70365fd98b94ce9c161c09a8b70d Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Fri, 3 Apr 2026 12:01:15 -0700 Subject: [PATCH 10/31] [benc/prompt-json-docs] Document group widget prompt JSON --- .../perseus/src/widget-ai-utils/group/group-ai-utils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/group/group-ai-utils.ts b/packages/perseus/src/widget-ai-utils/group/group-ai-utils.ts index 7a269c5e98e..cd62e8ccb9d 100644 --- a/packages/perseus/src/widget-ai-utils/group/group-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/group/group-ai-utils.ts @@ -1,5 +1,11 @@ import type {RendererPromptJSON} from "../prompt-types"; +/** + * JSON describing a group widget. Intended for consumption by AI tools. + * A group widget is simply an embedded Markdown document that can contain + * other widgets. To the user, the group widget looks like part of the + * surrounding document. + */ export type GroupPromptJSON = RendererPromptJSON & { type: "group"; }; From fe381aca0db7c10f95c1d29c6c760193dfda86a7 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Fri, 3 Apr 2026 12:44:15 -0700 Subject: [PATCH 11/31] [benc/prompt-json-docs] Document image widget prompt JSON --- .../widget-ai-utils/image/image-ai-utils.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/image/image-ai-utils.ts b/packages/perseus/src/widget-ai-utils/image/image-ai-utils.ts index bbba588cccf..830ee00d571 100644 --- a/packages/perseus/src/widget-ai-utils/image/image-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/image/image-ai-utils.ts @@ -1,12 +1,32 @@ import type image from "../../widgets/image/image.class"; import type React from "react"; +/** + * JSON describing an image widget. Intended for consumption by AI tools. + */ export type ImagePromptJSON = { type: "image"; + + /** Configuration set by the content creator. */ options: { + /** + * Text displayed to screenreaders and browsers without graphical + * capabilities + */ altText: string; + + /** + * Displayed above the image. May contain Markdown formatting, and TeX + * delimited by dollar signs, e.g. `$\dfrac{1}{2}$`. + */ title: string; + + /** + * Displayed below the image. May contain Markdown formatting, and TeX + * delimited by dollar signs, e.g. `$\dfrac{1}{2}$`. + */ caption: string; + imageUrl: string | null | undefined; }; }; From 2f8fd33a4bad31099afeef045defdf35b7b6121b Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 12:35:12 -0700 Subject: [PATCH 12/31] [benc/prompt-json-docs] Document InputNumberPromptJSON --- .../input-number/input-number-ai-utils.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/input-number/input-number-ai-utils.ts b/packages/perseus/src/widget-ai-utils/input-number/input-number-ai-utils.ts index 198ab0b6e21..0bc235ae666 100644 --- a/packages/perseus/src/widget-ai-utils/input-number/input-number-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/input-number/input-number-ai-utils.ts @@ -1,13 +1,40 @@ import type inputNumber from "../../widgets/input-number/input-number"; import type React from "react"; +/** + * JSON describing an InputNumber widget. Intended for consumption by AI tools. + * An InputNumber displays a text field where users can enter numbers in a + * variety of formats: decimals, integers, fractions, mixed numbers, + * percentages, and multiples of pi. The allowed formats are configurable by + * the content creator. + */ export type InputNumberPromptJSON = { type: "input-number"; + + /** The configuration of the widget, set by the content creator. */ options: { + /** + * Indicates how answers in unsimplified form are scored. + * + * - "optional" means the answer can be unsimplified. + * - "required" means an unsimplified answer is considered invalid, + * and the learner can try again without penalty. + * - "enforced" means unsimplified answers are counted as incorrect. + */ + // TODO(benchristel): render an intelligible string for simplify; the + // current values ("optional", "required", "enforced") aren't + // self-explanatory. simplify: string; + /** The expected numeric form, e.g. "rational", "decimal" */ answerType: string; }; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** The text input by the user */ value: string; }; }; From 79ebb13419d9b92a4b85c7d38363c424fd7fa360 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 12:44:09 -0700 Subject: [PATCH 13/31] [benc/prompt-json-docs] Fix type safety issues in InteractiveGraphPromptJSON --- .../interactive-graph-ai-utils.ts | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts index 71d628fd92d..ba5d759983c 100644 --- a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts @@ -8,67 +8,76 @@ import type React from "react"; type Coord = [x: number, y: number]; type CollinearTuple = readonly [Coord, Coord]; -type BaseGraphOptions = { - type: string; -}; - -type AngleGraphOptions = BaseGraphOptions & { - angleOffsetDegrees: number; +type AngleGraphOptions = { + type: "angle"; + angleOffsetDegrees: number | undefined; startCoords?: readonly [Coord, Coord, Coord]; }; -type CircleGraphOptions = BaseGraphOptions & { +type CircleGraphOptions = { + type: "circle"; startParams: { center?: Coord; radius?: number; }; }; -type LinearGraphOptions = BaseGraphOptions & { +type LinearGraphOptions = { + type: "linear"; startCoords?: CollinearTuple; }; -type LinearSystemGraphOptions = BaseGraphOptions & { +type LinearSystemGraphOptions = { + type: "linear-system"; startCoords?: readonly CollinearTuple[]; }; -type PointGraphOptions = BaseGraphOptions & { +type PointGraphOptions = { + type: "point"; numPoints?: number | "unlimited"; startCoords?: readonly Coord[]; }; -type PolygonGraphOptions = BaseGraphOptions & { +type PolygonGraphOptions = { + type: "polygon"; match?: string; numSides?: number | "unlimited"; startCoords?: readonly Coord[]; }; -type QuadraticGraphOptions = BaseGraphOptions & { +type QuadraticGraphOptions = { + type: "quadratic"; startCoords?: readonly [Coord, Coord, Coord]; }; -type RayGraphOptions = BaseGraphOptions & { +type RayGraphOptions = { + type: "ray"; startCoords?: CollinearTuple; }; -type SegmentGraphOptions = BaseGraphOptions & { +type SegmentGraphOptions = { + type: "segment"; numSegments?: number; - startCoords?: CollinearTuple; + startCoords?: CollinearTuple[]; }; -type SinusoidGraphOptions = BaseGraphOptions & { +type SinusoidGraphOptions = { + type: "sinusoid"; startCoords?: readonly Coord[]; }; -type AbsoluteValueGraphOptions = BaseGraphOptions & { +type AbsoluteValueGraphOptions = { + type: "absolute-value"; startCoords?: readonly [Coord, Coord]; }; -type TangentGraphOptions = BaseGraphOptions & { +type TangentGraphOptions = { + type: "tangent"; startCoords?: readonly Coord[]; }; -type ExponentialGraphOptions = BaseGraphOptions & { +type ExponentialGraphOptions = { + type: "exponential"; startCoords?: {coords: readonly [Coord, Coord]; asymptote: number}; }; From f31bde4543d6e4b938ef1537a8c05f858308209c Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 12:50:25 -0700 Subject: [PATCH 14/31] [benc/prompt-json-docs] Document InteractiveGraphPromptJSON --- .../interactive-graph-ai-utils.ts | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts index ba5d759983c..8b0ac64d091 100644 --- a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts @@ -180,13 +180,40 @@ type UserInput = | TangentUserInput | LogarithmUserInput; +/** + * JSON describing an interactive graph widget. Intended for consumption by AI tools. + * An interactive graph plots equations and draws geometric figures on a + * Cartesian plane. The user can move and reshape these elements by dragging + * control points. + */ export type InteractiveGraphPromptJSON = { type: "interactive-graph"; + + /** + * The configuration of the widget, set by the content creator. + */ options: { + /** + * Configuration of the plotted equation or geometric figure. + */ graph: GraphOptions; + + /** + * The bounds of the graph. Format: `[[xMin, xMax], [yMin, yMax]]` + */ + range: [ + x: [min: number, max: number], + y: [min: number, max: number], + ]; + + /** + * Labels on the graph axes. Format: `[xLabel, yLabel]`. + */ + labels: string[]; + backgroundImageUrl: string | null | undefined; - range: [min: number, max: number][]; - labels: ReadonlyArray; + + // TODO(benchristel): add locked figures to the prompt JSON }; userInput: UserInput; }; From 050303d8bcdb0559a2f9bab54eb0ea67d5e70a6b Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 13:51:00 -0700 Subject: [PATCH 15/31] [benc/prompt-json-docs] Link to Jira ticket in TODOs --- .../src/widget-ai-utils/input-number/input-number-ai-utils.ts | 2 +- .../interactive-graph/interactive-graph-ai-utils.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/perseus/src/widget-ai-utils/input-number/input-number-ai-utils.ts b/packages/perseus/src/widget-ai-utils/input-number/input-number-ai-utils.ts index 0bc235ae666..4577b28846a 100644 --- a/packages/perseus/src/widget-ai-utils/input-number/input-number-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/input-number/input-number-ai-utils.ts @@ -21,7 +21,7 @@ export type InputNumberPromptJSON = { * and the learner can try again without penalty. * - "enforced" means unsimplified answers are counted as incorrect. */ - // TODO(benchristel): render an intelligible string for simplify; the + // TODO(LEMS-4033): render an intelligible string for simplify; the // current values ("optional", "required", "enforced") aren't // self-explanatory. simplify: string; diff --git a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts index 8b0ac64d091..255df4f54cb 100644 --- a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts @@ -10,6 +10,8 @@ type CollinearTuple = readonly [Coord, Coord]; type AngleGraphOptions = { type: "angle"; + // TODO(LEMS-4033): angleOffsetDegrees should never be undefined. + // Communicate the default value explicitly here. angleOffsetDegrees: number | undefined; startCoords?: readonly [Coord, Coord, Coord]; }; From 65a448aa8474a5b0282f7419ca04920800bb5572 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 13:57:44 -0700 Subject: [PATCH 16/31] [benc/prompt-json-docs] Add TODOs --- .../interactive-graph/interactive-graph-ai-utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts index 255df4f54cb..7bb0e8a890b 100644 --- a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts @@ -5,7 +5,11 @@ import type {UnsupportedWidgetPromptJSON} from "../unsupported-widget"; import type {PerseusGraphType} from "@khanacademy/perseus-core"; import type React from "react"; +// TODO(LEMS-4033): use more self-explanatory types, e.g. +// `{x: number, y: number}`. type Coord = [x: number, y: number]; +// TODO(LEMS-4033): use more self-explanatory types, e.g. +// `{point1: {x, y}, point2: {x, y}}`. type CollinearTuple = readonly [Coord, Coord]; type AngleGraphOptions = { From 0cff7d0acf3790438932e58166c05e500a3394b5 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 13:59:58 -0700 Subject: [PATCH 17/31] [benc/prompt-json-docs] Fix lint --- .../interactive-graph/interactive-graph-ai-utils.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts index 7bb0e8a890b..6201cf2cab8 100644 --- a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts @@ -207,10 +207,7 @@ export type InteractiveGraphPromptJSON = { /** * The bounds of the graph. Format: `[[xMin, xMax], [yMin, yMax]]` */ - range: [ - x: [min: number, max: number], - y: [min: number, max: number], - ]; + range: [x: [min: number, max: number], y: [min: number, max: number]]; /** * Labels on the graph axes. Format: `[xLabel, yLabel]`. From 8ee9ee95542e28f89e8a8d0127c0b32c6e7f9e96 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 14:00:20 -0700 Subject: [PATCH 18/31] [benc/prompt-json-docs] Link to Jira ticket in TODO --- .../interactive-graph/interactive-graph-ai-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts index 6201cf2cab8..c803478002f 100644 --- a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts @@ -216,7 +216,7 @@ export type InteractiveGraphPromptJSON = { backgroundImageUrl: string | null | undefined; - // TODO(benchristel): add locked figures to the prompt JSON + // TODO(LEMS-4033): add locked figures to the prompt JSON }; userInput: UserInput; }; From a741037e1fb976622318c67d3c617f071ce15ec2 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 14:10:46 -0700 Subject: [PATCH 19/31] [benc/prompt-json-docs] Add TODO --- .../interactive-graph/interactive-graph-ai-utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts index c803478002f..e8f52416e37 100644 --- a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts @@ -167,6 +167,10 @@ type LogarithmUserInput = { }; type TangentUserInput = { + // TODO(LEMS-4033): change to a more self-explanatory format. These points + // are special (one is at the midline of the graph, the other determines + // the period and vertical scaling) but I am not sure of their exact + // mathematical significance. coords?: readonly Coord[] | null; }; From 92a39f452e552ad8bd7057d3dee7b86678f27f23 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 15:09:53 -0700 Subject: [PATCH 20/31] [benc/prompt-json-docs] Document LabelImagePromptJSON --- .../label-image/label-image-ai-utils.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/label-image/label-image-ai-utils.ts b/packages/perseus/src/widget-ai-utils/label-image/label-image-ai-utils.ts index 5112dd46c54..101e4f57bce 100644 --- a/packages/perseus/src/widget-ai-utils/label-image/label-image-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/label-image/label-image-ai-utils.ts @@ -1,25 +1,105 @@ import type labelImage from "../../widgets/label-image/label-image"; import type React from "react"; +// TODO(LEMS-4033): These types are weird. We should have separate types for +// the `Marker` used in widget options and the `UserInputMarker` used in user +// input, but the runtime values do not correspond directly to either of these +// types. + +/** + * A marker placed on the image, representing a location the learner must + * label. Used in the `options` section to describe the marker's identity and + * correct answers. + */ type BaseMarker = { + /** + * The text label identifying this marker's position on the image. Not + * shown directly to the learner; used to correlate options markers with + * user input markers. + */ label: string; }; +/** + * A marker as it appears in the user input section. Extends the base marker + * with optional correct answers (when present) and the learner's current + * selections. + */ type UserInputMarker = { + /** + * The text label identifying this marker's position on the image. + * Corresponds to the matching marker in `options.markers`. + */ label: string; + + /** + * The set of correct answer labels for this marker, as defined by the + * content creator. Only present when the marker has designated correct + * answers. A marker without answers is used purely for labeling purposes + * and is not scored. + */ + // TODO(LEMS-4033): `answers` is not part of the user input. It should be + // moved to `options`. answers?: string[]; + + /** + * The answer labels the learner has selected for this marker. Each entry + * is one of the strings from `options.choices`. May be absent or empty if + * the learner has not yet made a selection for this marker. + */ selected?: ReadonlyArray; }; +/** + * JSON describing a label-image widget. Intended for consumption by AI tools. + * A label-image widget displays an image with interactive markers placed at + * specific locations. The learner selects one or more answer labels from a + * shared list of choices for each marker. + */ export type LabelImagePromptJSON = { type: "label-image"; + + /** + * The configuration of the widget, set by the content creator. + */ options: { + /** + * The list of answer choices available to the learner. Each marker's + * selection must come from this list. + */ choices: ReadonlyArray; + + /** + * The URL of the image on which markers are placed. + */ imageUrl: string; + + /** + * Accessible alternative text for the image, used in the `alt` + * attribute. + */ imageAlt: string; + + /** + * The markers placed on the image by the content creator, each + * identifying a location the learner must label. + */ markers: BaseMarker[]; + + // TODO(LEMS-4033): surface multipleAnswers here; that seems like + // important context. }; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** + * The learner's selections for each marker, parallel to + * `options.markers`. Each element corresponds to the marker at the + * same index. + */ markers: UserInputMarker[]; }; }; From b79936f920e406c68b6b445b91186c5e658cf375 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 15:46:19 -0700 Subject: [PATCH 21/31] [benc/prompt-json-docs] Document MatcherPromptJSON --- .../matcher/matcher-ai-utils.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/matcher/matcher-ai-utils.ts b/packages/perseus/src/widget-ai-utils/matcher/matcher-ai-utils.ts index 9070b157adf..ae0a5b87043 100644 --- a/packages/perseus/src/widget-ai-utils/matcher/matcher-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/matcher/matcher-ai-utils.ts @@ -1,16 +1,62 @@ import type matcher from "../../widgets/matcher/matcher"; import type React from "react"; +/** + * JSON describing a matcher widget. Intended for consumption by AI tools. + * A matcher displays two columns of items that the learner must match by + * dragging cards in the right column to align with their corresponding items + * in the left column. + */ export type MatcherPromptJSON = { type: "matcher"; + + /** + * The configuration of the widget, set by the content creator. + */ options: { + /** + * Labels for the column headings. Contains exactly 2 values: + * the left column label and the right column label. + * e.g. `["Concepts", "Things"]` + */ labels: ReadonlyArray; + + /** + * The items shown in the left column that the learner must match + * against. e.g. `["Fruit", "Color", "Clothes"]` + */ left: ReadonlyArray; + + /** + * The draggable cards in the right column that the learner arranges + * to match the left column items. e.g. `["Red", "Shirt", "Banana"]` + */ right: ReadonlyArray; + + /** + * When true, the items in the left column are draggable and the + * learner must put them into the correct order. When false, the left + * column is static. + */ orderMatters: boolean; }; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** + * The current order of items in the left column. When `orderMatters` + * is false, the left column is fixed and only right can be reordered. + */ left: ReadonlyArray; + + /** + * The current order of cards in the right column, as arranged by the + * learner. Each element corresponds to the left column item at the + * same index. + */ right: ReadonlyArray; }; }; From 8986174676c6baa24c673aa932b23a1b17fd53cf Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 15:48:36 -0700 Subject: [PATCH 22/31] [benc/prompt-json-docs] Document MatrixPromptJSON --- .../widget-ai-utils/matrix/matrix-ai-utils.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/matrix/matrix-ai-utils.ts b/packages/perseus/src/widget-ai-utils/matrix/matrix-ai-utils.ts index cc50e57ce2b..7e4d9f77de5 100644 --- a/packages/perseus/src/widget-ai-utils/matrix/matrix-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/matrix/matrix-ai-utils.ts @@ -1,13 +1,39 @@ import type matrix from "../../widgets/matrix/matrix"; import type React from "react"; +/** + * JSON describing a matrix widget. Intended for consumption by AI tools. + * A matrix widget displays a grid of numeric cells that the learner fills in. + */ export type MatrixPromptJSON = { type: "matrix"; + + /** + * The configuration of the widget, set by the content creator. + */ options: { + /** + * The number of rows in the matrix grid. + */ height: number; + + /** + * The number of columns in the matrix grid. + */ width: number; }; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** + * The current values entered by the learner. Each element of the outer + * array represents a row; each element of the inner arrays represents a + * cell value within that row. Cells that have not been filled in are + * represented as empty strings. + */ answerRows: ReadonlyArray>; }; }; From 1bbb17f4b17acb80de5e6e12e905276c3fa4ef28 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 15:59:26 -0700 Subject: [PATCH 23/31] [benc/prompt-json-docs] Document NumberLinePromptJSON --- .../number-line/number-line-ai-utils.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/number-line/number-line-ai-utils.ts b/packages/perseus/src/widget-ai-utils/number-line/number-line-ai-utils.ts index 943f28ca070..7a388347965 100644 --- a/packages/perseus/src/widget-ai-utils/number-line/number-line-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/number-line/number-line-ai-utils.ts @@ -1,15 +1,72 @@ import type numberLine from "../../widgets/number-line/number-line"; import type React from "react"; +/** + * JSON describing a number-line widget. Intended for consumption by AI tools. + * A number-line widget displays a horizontal number line with a draggable + * point. The learner positions the point to answer a question, optionally + * selecting an inequality relationship (e.g. ≤, ≥) when the widget is in + * inequality mode. + */ export type NumberLinePromptJSON = { type: "number-line"; + + /** + * The configuration of the widget, set by the content creator. + */ options: { + /** + * The numeric values of the left and right endpoints of the number + * line, e.g. `[-5, 5]`. These bounds also constrain where the learner + * can place the point. + */ range: ReadonlyArray; + + /** + * The number of sub-intervals between adjacent tick marks into which + * the point can snap. Higher values allow finer-grained placement. + * For example, a value of `4` means the point snaps to quarter-tick + * increments. + */ snapDivisions: number; }; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** + * The numeric axis value where the learner has placed the point, + * e.g. `3.5` on a `[-5, 5]` number line. Clamped to `options.range` + * and snapped to the nearest tick increment. + */ numLinePosition: number; + + /** + * The number of tick-mark divisions currently shown on the number + * line. When the widget's `isTickCtrl` option is enabled, the learner + * can adjust this value; otherwise it is set by the content author. + */ numDivisions: number; + + /** + * The number line widget can represent a set of real numbers that is + * greater than, greater than or equal to, less than, or less than or + * equal to some number. Visually, the portion of the number line + * included in this set is shaded. The `rel` property describes the + * inequality relation the learner has selected. + * + * Possible values: + * - `"eq"` – equals (standard point, no inequality shading) + * - `"lt"` – less than + * - `"gt"` – greater than + * - `"le"` – less than or equal to + * - `"ge"` – greater than or equal to + * + * Only meaningful when the widget is configured for inequality mode; + * otherwise always `"eq"`. + */ rel: string; }; }; From 968951255923eda10f433f95dfcfae3e6ecc574f Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 16:05:46 -0700 Subject: [PATCH 24/31] [benc/prompt-json-docs] Document NumericInputPromptJSON --- .../numeric-input/prompt-utils.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/numeric-input/prompt-utils.ts b/packages/perseus/src/widget-ai-utils/numeric-input/prompt-utils.ts index fe2eb04f876..66105b79109 100644 --- a/packages/perseus/src/widget-ai-utils/numeric-input/prompt-utils.ts +++ b/packages/perseus/src/widget-ai-utils/numeric-input/prompt-utils.ts @@ -1,10 +1,29 @@ import type numericInput from "../../widgets/numeric-input/numeric-input.class"; import type React from "react"; +/** + * JSON describing a numeric-input widget. Intended for consumption by AI tools. + * A numeric-input widget displays a single text field where the learner types + * a numeric answer (integer, decimal, fraction, etc.). + */ export type NumericInputPromptJSON = { type: "numeric-input"; + + /** + * Accessible label for the input field, set by the content creator. + * Shown to learners using screen readers to describe what value should + * be entered. + */ label: string; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** + * The text currently entered in the input field by the learner. + */ value: string; }; }; From 5bbaf925c616bbef8b33d2f9b3ca18413d329992 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 16:10:51 -0700 Subject: [PATCH 25/31] [benc/prompt-json-docs] Document NOrdererPromptJSON --- .../orderer/orderer-ai-utils.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/orderer/orderer-ai-utils.ts b/packages/perseus/src/widget-ai-utils/orderer/orderer-ai-utils.ts index b745767e47c..e31150d96b3 100644 --- a/packages/perseus/src/widget-ai-utils/orderer/orderer-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/orderer/orderer-ai-utils.ts @@ -1,12 +1,37 @@ import type orderer from "../../widgets/orderer/orderer"; import type React from "react"; +/** + * JSON describing an orderer widget. Intended for consumption by AI tools. + * An orderer presents a set of cards that the learner must arrange into the + * correct sequence. Cards can be dragged and dropped to reorder them. The + * same card may appear multiple times in the correct answer but is displayed + * only once in the card bank. + */ export type OrdererPromptJSON = { type: "orderer"; + + /** + * The configuration of the widget, set by the content creator. + */ options: { + /** + * All of the cards available to the learner. Each string is the + * rendered content of one card (may include TeX or Markdown). + */ options: ReadonlyArray; }; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** + * The cards in their current order as arranged by the learner. Each + * string is the content of a card. An empty array means the learner + * has not yet placed any cards. + */ values: ReadonlyArray; }; }; From e694f6b0559d48e036345d0a75826b1ca49c0f63 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 16:20:01 -0700 Subject: [PATCH 26/31] [benc/prompt-json-docs] Document RadioPromptJSON --- .../widget-ai-utils/radio/radio-ai-utils.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/radio/radio-ai-utils.ts b/packages/perseus/src/widget-ai-utils/radio/radio-ai-utils.ts index be0cceb536b..dea213963ea 100644 --- a/packages/perseus/src/widget-ai-utils/radio/radio-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/radio/radio-ai-utils.ts @@ -5,17 +5,60 @@ import type { } from "@khanacademy/perseus-core"; import type React from "react"; +/** + * A single answer choice in a radio widget. + */ type BasicOption = { + /** + * The label displayed for this choice. + */ value: string; + + /** + * An opaque string that uniquely identifies this choice within the radio + * widget. The format is subject to change. + */ id: string; + + /** + * Rationale for why this answer is correct or incorrect. Shown to the + * learner when they select an incorrect answer. Only present when the + * content creator supplied one. + */ rationale?: string; }; +/** + * JSON describing a radio (multiple-choice) widget. Intended for consumption + * by AI tools. A radio widget presents a list of answer choices and asks the + * learner to select one (or, when multiple-select is enabled, one or more). + */ export type RadioPromptJSON = { type: "radio"; + + /** + * Whether the widget includes a "None of the above" option. When true, + * the last entry in `options` represents that special choice. + */ + // TODO(LEMS-4033): Don't expose this; instead, communicate on the option + // object whether each option is "none of the above". hasNoneOfTheAbove: boolean; + + /** + * The answer choices presented to the learner. + */ options: BasicOption[]; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** + * The IDs of the choices the learner has selected. Each entry + * corresponds to a choice's `id` field in `options`. Order is + * insignificant — scoring uses set membership, not position. + */ selectedOptions: ReadonlyArray; }; }; From ccb23bb3f9d98eac2b7db974908287ba729bee53 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Mon, 6 Apr 2026 16:23:01 -0700 Subject: [PATCH 27/31] [benc/prompt-json-docs] Document SorterPromptJSON --- .../widget-ai-utils/sorter/sorter-ai-utils.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/perseus/src/widget-ai-utils/sorter/sorter-ai-utils.ts b/packages/perseus/src/widget-ai-utils/sorter/sorter-ai-utils.ts index 940e58db715..8fbd08a65f2 100644 --- a/packages/perseus/src/widget-ai-utils/sorter/sorter-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/sorter/sorter-ai-utils.ts @@ -1,9 +1,30 @@ import type {PerseusSorterUserInput} from "@khanacademy/perseus-core"; +/** + * JSON describing a sorter widget. Intended for consumption by AI tools. + * A sorter presents a list of cards that the learner must arrange into the + * correct order by dragging them. The cards are initially displayed in a + * randomized order. + */ export type SorterPromptJSON = { type: "sorter"; + + /** + * The current state of the widget user interface. Usually represents a + * learner's attempt to answer a question. + */ userInput: { + /** + * The content strings of the sortable cards in the learner's current + * order. + */ values: ReadonlyArray; + + /** + * Whether the learner has moved any cards from their initial + * randomized order. The widget is considered empty (invalid) until + * this is true. + */ changed: boolean; }; }; From 6d168493ad09de3fd62773f8c31651a13e887834 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Tue, 7 Apr 2026 12:34:34 -0700 Subject: [PATCH 28/31] [benc/prompt-json-docs] Export PromptJSON types --- packages/perseus/src/index.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index 51762267635..0cd2a0ae71a 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -178,4 +178,21 @@ export type { export type {CategorizerPromptJSON} from "./widget-ai-utils/categorizer/categorizer-ai-utils"; export type {DefinitionPromptJSON} from "./widget-ai-utils/definition/definition-ai-utils"; export type {DropdownPromptJSON} from "./widget-ai-utils/dropdown/dropdown-ai-utils"; +export type {ExplanationPromptJSON} from "./widget-ai-utils/explanation/explanation-ai-utils"; +export type {ExpressionPromptJSON} from "./widget-ai-utils/expression/expression-ai-utils"; +export type {GradedGroupPromptJSON} from "./widget-ai-utils/graded-group/graded-group-ai-utils"; +export type {GradedGroupSetPromptJSON} from "./widget-ai-utils/graded-group-set/graded-group-set-ai-utils"; +export type {GrapherPromptJSON} from "./widget-ai-utils/grapher/grapher-ai-utils"; +export type {GroupPromptJSON} from "./widget-ai-utils/group/group-ai-utils"; +export type {ImagePromptJSON} from "./widget-ai-utils/image/image-ai-utils"; +export type {InputNumberPromptJSON} from "./widget-ai-utils/input-number/input-number-ai-utils"; +export type {InteractiveGraphPromptJSON} from "./widget-ai-utils/interactive-graph/interactive-graph-ai-utils"; +export type {LabelImagePromptJSON} from "./widget-ai-utils/label-image/label-image-ai-utils"; +export type {MatcherPromptJSON} from "./widget-ai-utils/matcher/matcher-ai-utils"; +export type {MatrixPromptJSON} from "./widget-ai-utils/matrix/matrix-ai-utils"; +export type {NumberLinePromptJSON} from "./widget-ai-utils/number-line/number-line-ai-utils"; +export type {NumericInputPromptJSON} from "./widget-ai-utils/numeric-input/prompt-utils"; +export type {OrdererPromptJSON} from "./widget-ai-utils/orderer/orderer-ai-utils"; +export type {RadioPromptJSON} from "./widget-ai-utils/radio/radio-ai-utils"; +export type {SorterPromptJSON} from "./widget-ai-utils/sorter/sorter-ai-utils"; export type {UnsupportedWidgetPromptJSON} from "./widget-ai-utils/unsupported-widget"; From 90af87daa69ea1f93a451f66b5de10c814c0d0c4 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Wed, 8 Apr 2026 10:44:57 -0700 Subject: [PATCH 29/31] [benc/prompt-json-docs] Clarify doc comments --- .../src/widget-ai-utils/categorizer/categorizer-ai-utils.ts | 5 +++-- packages/perseus/src/widget-ai-utils/radio/radio-ai-utils.ts | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/perseus/src/widget-ai-utils/categorizer/categorizer-ai-utils.ts b/packages/perseus/src/widget-ai-utils/categorizer/categorizer-ai-utils.ts index 7fbee4e2cd0..c584041b102 100644 --- a/packages/perseus/src/widget-ai-utils/categorizer/categorizer-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/categorizer/categorizer-ai-utils.ts @@ -36,8 +36,9 @@ export type CategorizerPromptJSON = { * The category indices for each item. Elements in this array * correspond to elements of `options.items`, and refer to indices of * the `categories` array. For example, a value of `[2, null, 0]` means - * that the first item is in category 2, the second item has not been - * assigned to a category, and the third item is in category 0. + * that the first item is in the category at index 2, the second item + * has not been assigned to a category, and the third item is in the + * category at index 0. */ itemToCategoryMapping: ReadonlyArray; }; diff --git a/packages/perseus/src/widget-ai-utils/radio/radio-ai-utils.ts b/packages/perseus/src/widget-ai-utils/radio/radio-ai-utils.ts index dea213963ea..03d049a9dc3 100644 --- a/packages/perseus/src/widget-ai-utils/radio/radio-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/radio/radio-ai-utils.ts @@ -45,7 +45,9 @@ export type RadioPromptJSON = { hasNoneOfTheAbove: boolean; /** - * The answer choices presented to the learner. + * The answer choices presented to the learner, in the order they appear + * on screen. The first choice is labeled "A", the second is "B", and so + * on. */ options: BasicOption[]; From 504567b0dede5fc707edea9d706a50e7a12b7930 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Wed, 8 Apr 2026 11:11:02 -0700 Subject: [PATCH 30/31] [benc/prompt-json-docs] docs(changeset): Document the `*PromptJSON` types. --- .changeset/hungry-eagles-call.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hungry-eagles-call.md diff --git a/.changeset/hungry-eagles-call.md b/.changeset/hungry-eagles-call.md new file mode 100644 index 00000000000..3f5ec80e0e9 --- /dev/null +++ b/.changeset/hungry-eagles-call.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Document the `*PromptJSON` types. From c827f810be46ff882b3d21bb49e1da99a0cfa190 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Wed, 8 Apr 2026 12:22:15 -0700 Subject: [PATCH 31/31] [benc/prompt-json-docs] Fix types after rebasing --- .../interactive-graph/interactive-graph-ai-utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts index e8f52416e37..b7d32c2f089 100644 --- a/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts +++ b/packages/perseus/src/widget-ai-utils/interactive-graph/interactive-graph-ai-utils.ts @@ -87,7 +87,8 @@ type ExponentialGraphOptions = { startCoords?: {coords: readonly [Coord, Coord]; asymptote: number}; }; -type LogarithmGraphOptions = BaseGraphOptions & { +type LogarithmGraphOptions = { + type: "logarithm"; startCoords?: {coords: readonly [Coord, Coord]; asymptote: number}; };