Upgrade Perseus from 22.7.0 to 75.7.1#14388
Draft
rtibbles wants to merge 9 commits intolearningequality:developfrom
Draft
Upgrade Perseus from 22.7.0 to 75.7.1#14388rtibbles wants to merge 9 commits intolearningequality:developfrom
rtibbles wants to merge 9 commits intolearningequality:developfrom
Conversation
Contributor
Build Artifacts
Smoke test screenshot |
Perseus has undergone major version bumps since our last upgrade, bringing React 18 support, new scoring/migration APIs, and updated Wonder Blocks dependencies. This updates all @khanacademy packages to their current versions, adds new required dependencies (perseus-core, perseus-score, keypad-context, wonder-blocks-announcer, etc.), removes obsolete ones (katex, create-react-class, etc.), and bumps React from 16.14 to 18.2. The @swc/helpers package is hoisted via .npmrc for Perseus's SWC-compiled output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The old Tex.js used a complex KaTeX-first, MathJax-fallback approach that loaded MathJax 2.1 via script injection and maintained a custom render queue. Perseus now ships @khanacademy/mathjax-renderer which uses MathJax 3 with CHTML output and handles all math rendering natively. This replaces the entire vendored implementation with a thin wrapper around MathJaxRenderer, and copies the required MathJax WOFF fonts to core static assets at build time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, buildPerseus.js copied CSS files from Perseus and math-input into frontend/dist/, rewriting font URLs along the way. Now we import CSS directly from the packages in PerseusRendererIndex and handle the KA CDN font URL rewriting with a webpack pre-loader (rewritePerseusUrls.js) configured in buildConfig.js. This removes the vendored frontend/dist/ directory (CSS, fonts, README) and simplifies buildPerseus.js to only copy MathJax fonts and extract translation messages. Perseus v75 and its Wonder Blocks design tokens assume a 62.5% root font-size (1rem = 10px), which is the convention on khanacademy.org. Kolibri uses the browser default (1rem = 16px), so all rem-based sizing renders 1.6x too large. A second webpack pre-loader (rewritePerseusRem.js) converts rem to px at build time using the 10px base, applied to Perseus, Wonder Blocks tokens, and math-input CSS. The full Eric Meyer CSS reset is replaced with a surgical reset of just fieldset, ol/ul, and table — the full reset was too aggressive for Perseus v75's Aphrodite-based inline-block layout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
61dcb73 to
fcc239b
Compare
rtibbles
commented
Mar 17, 2026
Member
Author
rtibbles
left a comment
There was a problem hiding this comment.
Needs some cleanup still, and I think the test coverage might be missing the mark of what is most useful.
kolibri/plugins/perseus_viewer/frontend/views/NumericKeypad.vue
Outdated
Show resolved
Hide resolved
kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue
Outdated
Show resolved
Hide resolved
kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue
Outdated
Show resolved
Hide resolved
kolibri/plugins/perseus_viewer/frontend/__tests__/fixtures/dropdown-item.json
Outdated
Show resolved
Hide resolved
kolibri/plugins/perseus_viewer/frontend/__tests__/stateSerialization.spec.js
Show resolved
Hide resolved
Perseus v75 introduces several breaking API changes that this commit
addresses:
- React 18: replace render/unmountComponentAtNode with createRoot API
- Scoring: use scorePerseusItem and emptyWidgetsFunctional from
@khanacademy/perseus-score instead of itemRenderer.scoreInput()
- Item migration: use parseAndMigratePerseusItem from perseus-core
to migrate item data before rendering
- State serialization: replace getSerializedState/restoreSerializedState
with getUserInput/handleUserInput. Answer state is now stored as
{userInput, hintsVisible} instead of {question, hints}
- Backward compatibility: use deriveUserInputFromSerializedState to
convert previously saved old-format answer state when restoring
in coach reports
- Widget solvers: update all 17 widget solvers to use the new
handleUserInput callback instead of onChange/setState/setInputValue
- Dependencies context: add required generateUrl and useVideo providers
- Keypad: import StatefulKeypadContextProvider from @khanacademy/keypad-context
- Remove CSS reset: the aggressive .perseus-root element reset
is no longer needed with properly scoped Perseus styles
- Remove constants.js (MathJax config filename no longer needed)
Tests cover scoring (radio, numeric-input, expression, dropdown,
input-number, sorter) and state serialization round-trips including
backward compatibility with old serialized state format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sync the translator message catalog with the strings extracted from Perseus 75.7.1 and math-input 26.4.6. This adds new strings (e.g. mathInputBox, characterCount, various widget labels), updates changed messages (e.g. pi error no longer uses HTML code tags), and removes strings that are no longer used by the updated Perseus version. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users typing with non-Western keyboards (Eastern Arabic, Devanagari, Bengali, Thai, etc.) can now enter answers using their native numeral characters. The input is silently transliterated to ASCII 0-9 before reaching Perseus' scoring engine, which only understands parseFloat. Supports 16 numeral systems covering Arabic, South Asian, Southeast Asian, and Tibetan scripts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the React MobileKeypad from @khanacademy/math-input with a custom Vue component using KDS (Kolibri Design System) buttons and theme tokens. This gives us full control over styling, accessibility, and numeral localization without monkey-patching React DOM. The NumericKeypad uses KButton/KIconButton for all keys, with internationalized aria-labels from the Perseus translator strings. A useKeypad composable owns the keypad state and exposes the KeypadAPI interface that Perseus widgets expect. React's KeypadContext is bridged via a Provider with a plain JS value object that routes setKeypadActive(true/false) to the Vue keypad's activate/dismiss. The keypad supports both FRACTION (4×4 grid) and EXPRESSION (6×4 grid) layouts, with digit buttons localized to the content locale's native numeral system (Arabic-Indic, Devanagari, Bengali, Thai, etc.) via Intl.NumberFormat. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When restoring saved answer state into widgets, convert ASCII digits back to the content locale's native numeral system. This ensures users who typed ٤٢ (stored as "42" by Phase 1 normalization) see ٤٢ when returning to an exercise, not the ASCII representation. Adds localizeNumerals and localizeUserInput as inverses of the existing normalize functions, with a roundtrip test confirming they are proper inverses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wonder Blocks (clickable, button, link, etc.) imports useInRouterContext and useNavigate from react-router-dom-v5-compat, which re-exports from react-router@6. The Perseus plugin resolves react-router@5 instead, where these APIs don't exist, causing a TypeError that crashes image rendering via Perseus's ErrorBoundary. Provide a minimal shim that returns useInRouterContext=false so Wonder Blocks falls back to plain <a> tags and standard navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Upgrades
@khanacademy/perseusfrom 22.7.0 to 75.7.1 (replaces #13526). Major changes:@khanacademy/mathjax-renderer(MathJax 3)1rem = 10px, Kolibri uses16px)getSerializedState/restoreSerializedStatewithgetUserInput/setInputValueMobileKeypadwith a VueNumericKeypadcomponent using auseKeypadcomposable that bridges provide/inject with React's KeypadContextreact-router-dom-v5-compat— Wonder Blocks imports fromreact-router@6APIs that don't exist under ourreact-router@5, crashing image renderingAccessibility audit: 1 pre-existing serious violation (
valid-langon.bibliotron-exercise— thelangobject prop leaks as[object Object]via Vue'sinheritAttrs). Not introduced by this PR.References
Reviewer guidance
Best reviewed commit-by-commit — the commits are ordered to tell a logical story.
Areas deserving extra attention:
PerseusRendererIndex.vue— answer state serialization (~lines 570-640): bridges old serialization format with v75'sgetUserInput/setInputValue, and handles widget version migration that Perseus v75 droppedPerseusRendererIndex.vue— CSS overrides: rem→px pre-loader,position: fixed→relativefor choice indicators, and a surgical CSS reset replacing v22's full Eric Meyer reset (which broke v75's radio layout)Visual spacing change to evaluate: v75's Wonder Blocks design system uses larger text and padding on choices than v22 (matches khanacademy.org). Evaluate whether the increased vertical space is acceptable on smaller screens.
NumericKeypad.vueanduseKeypad.js— Vue replacement for React's MobileKeypad. Composable bridges provide/inject with React's KeypadContext.numeralNormalization.js— new module. Uses Unicode\p{Nd}to normalize any decimal digit to ASCII, andIntl.NumberFormatto localize back.deepMapStringsapplies these to nested answer state.reactRouterShim.js— returnsuseInRouterContext() → falseso Wonder Blocks falls back to plain<a>tags. Without it, inline images crash via ErrorBoundary.buildConfig.js,rewritePerseusUrls.js,rewritePerseusRem.js— webpack pre-loaders for offline font URLs and rem→px conversion. Most ofbuildPerseus.jswas removed in favor of these.To test: import the QA channel and work through exercises of various types (radio, numeric-input, expression, dropdown).
AI usage
This PR was implemented collaboratively with Claude Code (Opus 4.6) across multiple sessions. Claude researched the Perseus v22→v75 API changes, wrote the implementation and tests, and performed browser verification. All code was reviewed interactively with a
/simplifycode review pass, andgit absorbwas used to maintain clean commit history. The human directed architectural decisions (scope of numeral localization, font placement, widget migration approach, rem→px webpack loader, Vue keypad replacing React's MobileKeypad) and corrected course when Claude's approach was wrong (e.g., attempting display-level digit transliteration instead of answer-state restoration, applying a full CSS reset that broke v75's layout).