Files
OpenMAIC/package.json
T
wyucandClaude Opus 5.5 08e1447ab7 feat(export): export a classroom as a single offline HTML file (#1813)
* refactor(export): share one classroom export snapshot across formats

Split buildClassroomExportZip into buildClassroomExportSnapshot, which
returns the portable manifest plus the bytes of every payload it names,
and a thin ZIP writer on top of it. The upcoming standalone HTML export
serializes the same snapshot, so both formats share one serializer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(export): standalone HTML player bundle

A small React app that plays a classroom manifest embedded in the page:
slides through @openmaic/renderer's SlideCanvas, interactive scenes in
sandboxed srcdoc iframes, client-graded quizzes, and static PBL
briefings, with prev/next, keyboard, scene list and fullscreen.

scripts/build-standalone-player.mjs compiles it at app build time (dev
and build) into public/vendor/standalone-player/: one IIFE via esbuild,
Tailwind 4 CSS covering the renderer's classes, KaTeX fonts as an
optional sheet, and ECharts as an optional script the player reads from
a global, so files without charts do not carry it. Shiki is stubbed; code
elements render unhighlighted. esbuild is added as a direct
devDependency at the version already in the lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(export): export a classroom as a single offline HTML file

Add "Export as HTML" to the export menu. The export takes the shared
classroom snapshot, embeds slide images, backgrounds and video posters as
data URIs (archived payloads first, concrete URLs fetched at export
time), patches interactive HTML for the sandboxed iframe, and inlines the
prebuilt player into <course-name>.html.

The file makes no network requests: a restrictive CSP (default-src
'none', connect-src 'none') is set before any script, the manifest and
config are embedded as JSON with <, > and & escaped, and interactive
pages run only in iframes sandboxed like the classroom's. PBL scenes link
to the online classroom only when it is published at export time. No
narration or video bytes yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(sanitize): move the scene-content sanitizer to a client-safe module

The policy depends only on sanitize-html, so it moves to
lib/sanitize/scene-content.ts unchanged; lib/server/sanitize-scene-content.ts
re-exports it for the persistence callers. Client export paths can now
apply the same policy to working state before it leaves the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(export): sanitize and trim the standalone HTML classroom

- Run slide rich text (text, shape text, table cells, LaTeX snapshots)
  through the persistence sanitizer before embedding: the renderer injects
  it into the player's own document, and the export reads working state
  that has not crossed the persistence boundary.
- Embed chart point images as data URIs (or drop and report them); rich
  text images and CSS url() values are removed by the sanitizer.
- Leave out what the player does not use: playback actions, multi-agent
  settings, the agent roster, the video manifest and PBL runtime state.
  PBL content is resolved the way the classroom resolves it, legacy
  projects upgraded, and reduced to its briefing.
- Skip narration audio, legacy audio fetches and video bytes when
  building the snapshot for HTML (captured posters are kept); the ZIP
  path is unchanged.
- Resolve each media ref once, count a video poster as unresolved only
  when no captured frame covers it, and ship KaTeX fonts only when quiz
  text or slides actually contain math.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(export): keep player scenes scrollable and contain scene failures

Navigate with Left/Right only, so Up/Down, PageUp/PageDown and Home/End
scroll long quiz and PBL scenes. Wrap each scene in an error boundary
that shows it as unavailable instead of blanking the player, and read
PBL briefings defensively now that the export resolves legacy projects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(export): note that dev builds the standalone player once

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(export): keep inline formulas and report dropped rich-text resources

The prose sanitizer cannot express the editor's inline formulas (KaTeX
spans with SVG and positioned parts carrying data-inline-math), so the
standalone export lifts each formula's LaTeX source out before
sanitizing (from data-inline-math, else the KaTeX tex annotation) and
puts back a fresh KaTeX render afterwards. The embedded markup is
generated from the source, never taken from the document.

Images and CSS url() values in rich text, which the sanitizer removes,
are inventoried first and reported as unresolved media so the
partial-export warning counts them instead of losing them silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(export): rebuild inline formulas through the DOM, not string markers

Rework the standalone export's rich-text pass so authored text is never
spliced into strings:

- Parse each prose string with the DOM. Inventory resources over the
  whole tree first, formula subtrees included: image src and srcset,
  media sources, and CSS url(), image-set() and @import references found
  by a real CSS parse (comments, quoted URLs and escapes respected).
- Replace each inline formula with an empty span whose data-inline-math
  is set through setAttribute, then run the persistence sanitizer with an
  export-only option that keeps that one attribute on span. The default
  persistence policy is unchanged.
- Parse the sanitized output and replace each such span with a fresh
  KaTeX render of the source read back with getAttribute. Generated KaTeX
  is the only markup added after sanitizing.

Tests cover replacement patterns in LaTeX, marker-like and encoded text,
resources inside formula wrappers, quoted URLs with parentheses, srcset,
escaped url() and commented-out URLs. The export tests now run under
jsdom, where the browser DOM the export relies on is available.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(export): make the rich-text resource inventory spec-correct and fail-safe

- Decode CSS escapes per CSS Syntax: zero, surrogates and values above
  U+10FFFF become U+FFFD instead of throwing, and an escaped newline is a
  line continuation.
- Never let the inventory affect the export: each element is scanned
  under its own guard and the whole pass is guarded, so a failure can
  only under-report.
- Scan and strip formulas inside template contents, nested ones
  included, which querySelectorAll does not reach.
- Document that the inventory is best-effort and only drives the
  partial-export warning; the sanitizer and the CSP remove and block
  these resources regardless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(export): bound the public-classroom lookup in the HTML export

Every HTML export awaited the stage-meta request for the optional PBL
"continue online" link, so a request that never settled left the export
stuck in its busy state with no download.

The lookup now gives up after 3000 ms: the request is aborted through
the fetch it is given, and a race returns even when the response body
never finishes. On timeout or failure the link is omitted and the export
continues. Courses without a PBL scene skip the lookup entirely.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(export): always link PBL scenes to the online classroom

A classroom is readable by anyone holding its link; the published flag
does not gate reading. Gating the PBL "Continue this project online"
link on it hid a link that works.

The export now always builds the link from the exporting origin and the
stage id, with no stage-meta request, so there is nothing to wait on.
The explicit classroomUrl option for hosts and the player's http(s)-only
guard stay. This replaces the bounded lookup and its timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(export): render standalone-player charts without entry animation

Line series animate in by growing an SVG clipPath from zero width. WebKit
does not repaint the clipped group when the clip path's geometry changes
afterwards, so in Safari the exported file showed a line chart's points
and labels without the connecting line. Re-setting the clip path (or the
clip-path attribute) makes the line appear, which confirms the stale paint.

The charts runtime the export ships now wraps ECharts init so every chart
renders without its entry animation; the clip path is created at its
final size. The shared renderer and the online classroom are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:44:24 +08:00

217 lines
7.9 KiB
JSON

{
"name": "openmaic",
"version": "1.2.0-rc.1",
"private": true,
"license": "MIT",
"engines": {
"node": ">=22.19.0"
},
"scripts": {
"postinstall": "pnpm run build:packages",
"build:packages": "cd packages/mathml2omml && npm run build && cd ../pptxgenjs && npm run build && cd ../@openmaic/dsl && pnpm run build && cd ../generation && pnpm run build && cd ../storage && pnpm run build && cd ../importer && pnpm run build && cd ../renderer && pnpm run build && cd ../editor && pnpm run build && cd ../../.. && node scripts/sync-maic-importer.mjs",
"sync:maic-importer": "node scripts/sync-maic-importer.mjs",
"build:standalone-player": "node scripts/build-standalone-player.mjs",
"gen:video-export-katex": "node scripts/generate-video-export-katex.mjs",
"gen:video-export-noto-cjk": "node scripts/generate-video-export-noto-cjk.mjs",
"gen:video-export-noto-script-fonts": "node scripts/generate-video-export-noto-script-fonts.mjs",
"dev": "node scripts/build-standalone-player.mjs && next dev",
"build": "node scripts/assert-vendor-maic-importer.mjs && node scripts/build-standalone-player.mjs && next build",
"start": "next start",
"db:up": "docker compose -p openmaic-dev-db -f docker-compose.db.yml up -d --wait postgres",
"db:down": "docker compose -p openmaic-dev-db -f docker-compose.db.yml stop postgres",
"lint": "eslint",
"check:i18n-keys": "node scripts/check-i18n-keys.mjs",
"check:node-engine": "node scripts/check-node-engine-contract.mjs",
"check:package-versions": "node scripts/check-package-version-bumps.mjs",
"check": "prettier . --check",
"format": "prettier . --write",
"test": "vitest run",
"test:package-tarballs": "node scripts/smoke-test-package-tarballs.mjs",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"eval:pbl-v2-planner": "tsx eval/pbl-v2-planner/runner.ts",
"eval:whiteboard": "tsx eval/whiteboard-layout/runner.ts",
"eval:outline-language": "tsx eval/outline-language/runner.ts",
"eval:orchestration": "tsx eval/orchestration/runner.ts",
"eval:orchestration:answering": "tsx eval/orchestration/answering-runner.ts",
"eval:orchestration:answer-content": "tsx eval/orchestration/answer-content-runner.ts"
},
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.103",
"@ai-sdk/anthropic": "^3.0.71",
"@ai-sdk/azure": "^3.0.88",
"@ai-sdk/google": "^3.0.64",
"@ai-sdk/openai": "^3.0.84",
"@ai-sdk/react": "^3.0.170",
"@alicloud/credentials": "^2.4.5",
"@alicloud/docmind-api20220711": "^1.4.15",
"@alicloud/openapi-client": "^0.4.15",
"@alicloud/tea-util": "^1.4.11",
"@aws-sdk/client-s3": "^3.1048.0",
"@aws-sdk/credential-providers": "^3.1045.0",
"@aws-sdk/s3-request-presigner": "^3.1048.0",
"@base-ui/react": "^1.1.0",
"@copilotkit/backend": "^0.37.0",
"@copilotkit/runtime": "^1.51.2",
"@earendil-works/pi-agent-core": "0.78.0",
"@earendil-works/pi-ai": "0.78.0",
"@fontsource-variable/inter": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/literata": "^5.2.8",
"@fontsource/lxgw-wenkai": "^5.2.5",
"@fontsource/merriweather": "^5.2.11",
"@fontsource/montserrat": "^5.2.8",
"@fontsource/noto-sans-kr": "^5.2.9",
"@fontsource/noto-sans-sc": "^5.2.9",
"@fontsource/noto-serif-sc": "^5.2.8",
"@fontsource/open-sans": "^5.2.7",
"@fontsource/roboto": "^5.2.10",
"@fontsource/source-sans-3": "^5.2.9",
"@fontsource/source-serif-4": "^5.2.9",
"@fontsource/zcool-kuaile": "^5.2.8",
"@joplin/turndown-plugin-gfm": "^1.0.67",
"@langchain/core": "^1.1.16",
"@langchain/langgraph": "^1.1.1",
"@modelcontextprotocol/sdk": "^1.27.1",
"@mozilla/readability": "^0.6.0",
"@napi-rs/canvas": "^0.1.88",
"@openmaic/dsl": "workspace:*",
"@openmaic/editor": "workspace:*",
"@openmaic/generation": "workspace:*",
"@openmaic/importer": "workspace:*",
"@openmaic/renderer": "workspace:*",
"@openmaic/storage": "workspace:*",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@streamdown/code": "^1.1.1",
"@streamdown/math": "^1.0.2",
"@types/js-yaml": "^4.0.9",
"@xyflow/react": "^12.10.0",
"ai": "^6.0.168",
"animate.css": "^4.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"copilotkit": "^0.0.58",
"dexie": "^4.2.1",
"docx": "9.4.1",
"echarts": "^6.0.0",
"embla-carousel-react": "^8.6.0",
"es-module-lexer": "^2.0.0",
"file-saver": "^2.0.5",
"geist": "^1.7.0",
"graphemer": "1.4.0",
"i18next": "^26.0.1",
"i18next-resources-to-backend": "^1.2.1",
"immer": "^11.1.3",
"ipaddr.js": "^2.5.0",
"js-yaml": "4.3.0",
"jsonrepair": "^3.13.2",
"jszip": "^3.10.1",
"katex": "^0.16.33",
"linkedom": "^0.18.13",
"lodash": "4.18.1",
"lucide-react": "^0.562.0",
"mathml2omml": "workspace:*",
"micromark-extension-math": "^3.1.0",
"mitt": "^3.0.1",
"motion": "^12.27.5",
"nanoid": "5.1.16",
"next": "16.3.3",
"next-themes": "^0.4.6",
"openai": "^4.104.0",
"parse-srcset": "^1.0.2",
"parse5": "^8.0.1",
"partial-json": "^0.1.7",
"pdf-lib": "^1.17.1",
"pg": "^8.16.3",
"postcss": "^8.5.15",
"postcss-value-parser": "^4.2.0",
"pptxgenjs": "workspace:*",
"pptxtojson": "^1.11.0",
"prosemirror-commands": "^1.7.1",
"prosemirror-dropcursor": "^1.8.2",
"prosemirror-gapcursor": "^1.4.0",
"prosemirror-history": "^1.5.0",
"prosemirror-inputrules": "^1.5.1",
"prosemirror-keymap": "^1.2.3",
"prosemirror-model": "^1.25.4",
"prosemirror-schema-basic": "^1.2.4",
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.4",
"prosemirror-view": "^1.41.5",
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-colorful": "^5.7.0",
"react-dom": "19.2.3",
"react-i18next": "^17.0.1",
"sanitize-html": "2.17.0",
"shadcn": "^3.6.3",
"sharp": "0.35.4",
"shiki": "^3.21.0",
"sonner": "^2.0.7",
"streamdown": "^2.5.0",
"svg-arc-to-cubic-bezier": "^3.2.0",
"svg-pathdata": "^8.0.0",
"tailwind-merge": "^3.4.0",
"temml": "^0.13.1",
"tinycolor2": "^1.6.0",
"tokenlens": "^1.3.1",
"turndown": "^7.2.4",
"tw-animate-css": "^1.4.0",
"typebox": "^1.1.39",
"undici": "7.29.0",
"unpdf": "^1.4.0",
"uqr": "^0.1.3",
"use-stick-to-bottom": "^1.1.1",
"zod": "^4.3.5",
"zustand": "^5.0.10",
"remark-cjk-friendly": "^2.3.1"
},
"devDependencies": {
"@electric-sql/pglite": "^0.3.16",
"@fontsource/noto-sans": "5.3.0",
"@fontsource/noto-sans-arabic": "5.3.0",
"@playwright/test": "^1.58.2",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^16.0.1",
"@tailwindcss/postcss": "^4",
"@types/file-saver": "^2.0.7",
"@types/katex": "^0.16.8",
"@types/lodash": "^4.17.23",
"@types/node": "^20",
"@types/pg": "^8.15.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/sanitize-html": "^2.16.1",
"@types/tinycolor2": "^1.4.6",
"esbuild": "0.27.7",
"eslint": "^9",
"eslint-config-next": "16.2.11",
"fake-indexeddb": "^6.2.5",
"fontkit": "2.0.4",
"gsap": "^3.15.0",
"hyperframes": "0.7.60",
"prettier": "3.8.1",
"rollup": "^4.35.0",
"rollup-plugin-typescript2": "^0.36.0",
"semver": "7.8.5",
"tailwindcss": "^4",
"tslib": "^2.8.0",
"tsx": "^4.21.0",
"typescript": "^5",
"vitest": "^4.1.8",
"vue-to-react": "^1.0.0"
},
"pnpm": {
"ignoredBuiltDependencies": [
"sharp",
"unrs-resolver"
]
},
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48"
}