Commit Graph
17 Commits
Author SHA1 Message Date
54f959bb45 fix: render whiteboard math via KaTeX instead of raw LaTeX text (#938)
* fix: render whiteboard math via KaTeX instead of raw LaTeX text

* fix: recognize single-command LaTeX with a braced argument

The fallback missed valid single-command expressions (\vec{v}, \hat{x},
\overline{x}) whose command is not in COMMON_LATEX_COMMAND, because
commands.length < 2 returned null and they rendered as raw text. Now a
single command counts as math when it carries a braced argument, while
path-like prose (C:\temp) still has no braced argument and stays text.
Add regression tests for both cases.

* fix: exclude Windows drive paths before the common-LaTeX-command check

COMMON_LATEX_COMMAND.test() ran before any path-like check, so C:\alpha,
C:\theta and C:\pi were classified as LaTeX and rendered through KaTeX.

Return early for text starting with a drive letter, and add C:\alpha to the
Windows-path regression cases so a path whose segment matches
COMMON_LATEX_COMMAND is covered.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-08-04 00:04:36 -04:00
bb8d4f30e7 feat(chat): add experimental Pi classroom runtime behind a flag (#914)
* feat(chat): add experimental Pi classroom runtime

* fix(chat): harden Pi whiteboard action sequencing

* style(chat): format session retirement flow

* style(playback): format async scene transition

* fix(chat): retire actions on session errors

* fix(chat): close Pi runtime lifecycle gaps

* fix(chat): avoid replaying historical whiteboard actions

* fix(chat): prevent Pi structured-output residue from leaking and inflating turns

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

* fix(chat): expose whiteboard element/line ids and bound code-line context

Surface each element's id (and code line ids) in the Pi whiteboard context so
later child agents can target wb_delete / wb_edit_code, and cap the rendered
code lines with a shared character budget so a code-heavy board cannot grow the
prompt without bound. Return only the current turn's whiteboard ledger from the
Pi director loop instead of carrying history forward across requests.

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

* fix(chat): bound Pi whiteboard code context

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

* fix(chat): prioritize newer persisted whiteboard code in budget

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

* fix(chat): enforce whiteboard boundaries across Pi sessions

* feat(chat): refine soft-closing discussion controls

* fix(chat): make whiteboard clearing model-directed

---------

Co-authored-by: apple <apple@GigideMacBook-Air.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: LING-6150 <duan.lin@northeastern.edu>
2026-07-17 11:22:02 +08:00
7fffec78f6 feat(choreography): shared orchestration spec in lib/choreography (#863) (#890)
* feat(choreography): shared orchestration spec in lib/choreography (#863)

Introduce lib/choreography/ as the single source of truth for the
orchestration semantics a faithful classroom-video exporter needs from
playback, so the app runtime and the exporter interpret one spec instead
of each re-implementing (and silently drifting from) the other.

Kept in lib/ rather than a package: these semantics co-evolve with the
playback engine, and the exporter will also live in the app, so both
consumers share them via ordinary imports. Purity is machine-enforced by
an eslint boundary on lib/choreography/** (blocks @/ host-app paths +
react/react-dom/gsap/framer-motion/motion), so the exporter can interpret
the spec in a pure Node environment.

- timing.ts   — timing constants + the deterministic no-audio speech
                estimate, moved verbatim from the engines.
- cursor.ts   — resolvePlaybackCursor + EMPTY_SCENE_DWELL, moved from
                lib/playback/engine-cursor.ts (typed on dsl SceneCore).
- timeline.ts — new pure resolveActionTimeline: index-domain -> time-domain
                expansion (blocking cursor-advance vs fire-and-forget visual
                duration), keyed off the DSL fire-and-forget partition.
- descriptors/— versioned, zod-schema-validated animation descriptors
                spotlight.v1 + laser.v1 (declarative: property/from/to/
                duration/easing; no implementation), pinned to the current
                overlay components.

Behavior-neutral engine refactor: lib/action/engine.ts and
lib/playback/engine.ts import from lib/choreography and the local literals
are deleted, so the timing dimension now has exactly one copy.

The spotlight/laser overlay components still hardcode their animation
values (they do not yet READ the descriptors) — tracked in #889.

Closes #863

* fix(choreography): address cross-review findings on resolveActionTimeline + descriptors

- P1: model implicit whiteboard auto-open — a wb_* mutation on a closed board
  now prepends a synthetic IMPLICIT_WB_OPEN (WB_OPEN_MS) beat, mirroring the
  engine's ensureWhiteboardOpen; open state carries across scenes and toggles
  on wb_open/wb_close (new `whiteboardOpen` option to seed it).
- P2: scale real speech audio duration by playbackSpeed too (live path sets
  AudioPlayer.setPlaybackRate), keeping it in lockstep with the estimate path.
- P2: express the spotlight mask relationship in the descriptor model —
  LayerSchema gains `role` ('content'|'mask') + `maskedBy` (subtract|intersect);
  spotlight.v1's cutout is now a mask layer the dim layer subtracts, so a
  non-React consumer reconstructs the cutout instead of a black rect.
- P3: wb_clear on an empty board is 0ms (engine early-returns), not wbClearMs(0).

Tests: tests/lib/choreography 45 pass (+6); engine regression 28 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): address final-audit findings on effect lifetime + spotlight descriptor

Second cross-review round (fresh-session codex final audit) surfaced three
deeper mismatches with live playback:

- P1: fire-and-forget effect lifetime is not a flat EFFECT_AUTO_CLEAR_MS. The
  engine's processNext clears effects at every scene boundary and on completion,
  and scheduleEffectClear uses one shared timer each new effect resets. Added
  clampFireAndForgetLifetimes: an effect's visual durationMs is now min(next
  scene boundary / completion, shared-timer deadline chained through later
  effects in the same scene). advancesCursorMs (0) is untouched.
- P2: spotlight dimness default is 0.5 (executeSpotlight: dimOpacity ?? 0.5; DSL
  documents 0.5), not the component's unreachable ?? 0.7 fallback. Fixed the
  descriptor param + test.
- P2: model the spotlight wrapper's enter/exit opacity fade (motion.div, no
  explicit duration → engine default). TrackSchema.durationMs is now optional to
  express "use the consumer's engine default"; dim layer carries the fade tracks.

Tests: tests/lib/choreography 50 pass (+5, incl. boundary-cut / completion-cut /
full-lifetime / shared-timer-extension / wrapper-fade); engine regression 28 pass;
tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): pin spotlight fade duration + laser dot geometry in descriptors

Third cross-review round (codex) flagged two descriptor-completeness gaps that
would make a non-Motion consumer (the exporter) diverge from the app:

- Spotlight wrapper fade: the enter/exit opacity tracks left durationMs implicit
  (Motion default). A literal consumer treats a missing duration as instant, so
  the spotlight would pop on/off. Pinned to Motion's default 300ms tween.
- Laser dot geometry: the descriptor captured only tracks, not the dot group's
  center anchor (translate -50%,-50%) or the rounded-full ring/core. A literal
  renderer would draw an offset 10px square. Added the static geometry (anchor,
  borderRadius 9999, ring inset/position) so the shape/position match the app.

Also refined the effect-lifetime docstring to cite the app's per-scene engine
teardown/completion (the actual clearEffects path) rather than an intra-engine
boundary gate that is dead in the single-scene-per-engine configuration.

The empty-scene "speech dwell → blank chat bubble" observation is pre-existing
behavior: EMPTY_SCENE_DWELL is a verbatim move from lib/playback/engine-cursor.ts
(unchanged from origin/main), out of scope for this move-only PR; tracked separately.

Tests: tests/lib/choreography 54 pass; engine regression 28 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): zero-duration for engine-skipped/no-op actions in timeline

Fourth cross-review round (codex) flagged two remaining timeline divergences:

- Skipped discussions: the engine skips a discussion outright (no timer) when
  it's already consumed or its agent isn't selected, but the timeline always
  charged DISCUSSION_TRIGGER_DELAY_MS. Added `isDiscussionSkipped` resolver
  (runtime-state-dependent, like getVideoDurationMs) → 0ms when skipped.
- No-op whiteboard draws: executeWbDrawText (empty content) and
  executeWbDrawTable (no rows/cols) return before any delay. The timeline now
  charges 0ms for these determinable-from-the-action no-ops instead of
  WB_DRAW_MS. (KaTeX-failure / missing-edit-target no-ops depend on runtime
  state and remain out of scope, consistent with the resolver pattern.)

Tests: tests/lib/choreography 56 pass; engine regression 28 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): exact-5s effect chain break + spotlight dim full-screen geometry

Fifth cross-review round (codex), both P2:

- Effect chain break at an EXACT 5s boundary: the earlier effect's clear timer
  is queued before the reading timer that triggers the later effect (same 5000ms
  delay), so it fires first — the predecessor is cleared at exactly deadlineMs,
  not extended. Changed the chain guard from `> deadlineMs` to `>= deadlineMs`.
- Spotlight dim layer full-screen geometry: the descriptor recorded only fill +
  mask relation, leaving a literal consumer no way to know the dim rect spans the
  0..100 viewport. Added explicit x/y/width/height (100×100 at origin) so the
  descriptor is self-contained.

Tests: tests/lib/choreography 57 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): no-op wb_edit_code resolver in timeline

Sixth cross-review round (codex), one P2: executeWbEditCode returns before its
delay when the edit can't apply (missing/non-code target, stale line refs). The
timeline always charged WB_EDIT_MS. Added `isEditCodeNoop` resolver (runtime-
state-dependent, same pattern as getClearElementCount / isDiscussionSkipped) →
0ms when the caller flags a no-op.

Tests: tests/lib/choreography 58 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): descriptor layer inheritance for nested effect layers

Seventh cross-review round (codex), two P2 with one root cause: the flat
layers[] model couldn't express a child layer riding a parent's animation
(the source nests some layers inside an animated wrapper). Added an
`inheritsFrom: {parentId, props}` relation to LayerSchema:

- Laser ring + core inheritsFrom the animated `dot` (left/top/opacity), so a
  literal consumer flies them in/out with the dot instead of leaving them at a
  static origin while only the dot moves.
- Spotlight border inheritsFrom `dim` (opacity), so the outline fades out with
  the wrapper instead of lingering after the dimming layer disappears.

Tests: tests/lib/choreography 60 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(choreography): address review — discussion auto-skip, explicit video policy, import allowlist

Human review (wyuc) raised two blocking timing issues + one non-blocking guard:

- Discussion dwell (blocking): a non-skipped discussion in unattended
  playback/export blocks for the trigger delay AND the ProactiveCard's own
  auto-skip countdown, not just DISCUSSION_TRIGGER_DELAY_MS. Added
  DISCUSSION_AUTO_SKIP_MS (5000) to the timing spec and charge
  DISCUSSION_TRIGGER_DELAY_MS + DISCUSSION_AUTO_SKIP_MS. ProactiveCard now reads
  the same constant (was a hardcoded 5000), so card countdown and timeline can't
  drift. A `spotlight -> discussion -> speech` timeline now extends the spotlight
  across the full discussion interval.
- play_video (blocking): an unresolved duration no longer silently becomes a
  0ms segment (which shifted every later action early). New
  `onUnresolvedVideoDuration` policy defaults to 'throw' (fail loudly); 'cap'
  assumes MAX_VIDEO_WAIT_MS, 'zero' opts back into no-dwell explicitly.
- Purity guard (non-blocking): turned the lib/choreography boundary into a true
  import allowlist. Beyond the existing @/… + render-package blocks, it now
  rejects parent-escape (../…) imports/re-exports, any bare package other than
  @openmaic/dsl / zod, and dynamic import()/require(). Negative-tested: ../store,
  a stray bare package, export * from ../playback, and import('react') all fail.

Tests: tests/lib/choreography 60 pass; engine regression 28 pass; tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-07-11 13:57:06 +08:00
danishsshaikh d00795094c feat(playback): add action-level navigation (#843)
Squashed from #843. Adds Notes click-to-jump action navigation and action-level playback resume/restore, with visible toolbar sentence controls removed after review.
2026-07-08 00:02:39 -04:00
cec3c6aaea Refactor widget actions into the scene action pipeline (#796)
* refactor: align widget actions with scene pipeline

* feat(edit): migrate legacy teacherActions off interactive content on load

The widget-actions pipeline no longer persists a teacherActions authoring
layer; playback reads only scene.actions. Add migrateInteractiveContent to
strip the dead field from existing documents as they load through migrateScene
(wired into all store/classroom load paths). Satisfies the #794 forward-migration
acceptance item.

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

* fix(generation): default widget_setState.state to {} when the LLM omits it

The removed TeacherAction->Action converter always defaulted state to {}.
Restore that guard in the structured-output parser so an action missing
state can't forward state: undefined into the widget iframe, where
SET_WIDGET_STATE handlers dereference it.

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

* fix(edit): also strip nested widgetConfig.teacherActions in migration

Legacy WidgetConfig variants each declared their own teacherActions, so existing
documents can carry the dead field nested inside content.widgetConfig as well as
at the top level. Extend migrateInteractiveContent to drop both, preserving all
other widgetConfig fields.

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

* feat(prompts): document stable widget-action selectors for diagram/game/viz3d

The interactive-actions prompt documented stable target selectors only for
procedural-skill and simulation widgets. Add the canonical selectors the
diagram / game / visualization3d content prompts actually emit (node IDs via
revealOrder; #zoom-in-btn / #canvas-container / #speed-slider / ...; #start-btn),
plus a rule to use widget_setState or a speech-only beat instead of guessing a
target when no stable selector is known (e.g. free-form code widgets). Hardens
against widget actions silently no-oping on non-existent selectors.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-06-28 21:13:21 +08:00
wyuc 7cb129183d chore(packages): publish the @openmaic/* SDK family to npm (#778) (#780)
* chore(packages): publish the @openmaic/* SDK family to npm (#778)

Prepares the @openmaic/{dsl,renderer,importer} family for its first npm
publish, and moves the SDK packages onto the @openmaic scope.

Why the scope move: the @maic org name is unavailable on npm (an unscoped
`maic` package already holds the name), so @maic/* is not claimable. @openmaic
matches the project name, the scope is free, and the repo already ships an
@openmaic/docs package — so the SDK family now lines up with that convention.

- rename @maic/{dsl,renderer,importer} -> @openmaic/* across packages, the
  workspace glob, the package dir, and all import sites; lockfile regenerated
- renderer: add publishConfig (public, registry.npmjs.org) — was missing, so
  a scoped publish would default to the wrong registry / restricted access
- importer: add a files allowlist (dist, README, LICENSE) and drop the
  fragile .npmignore blacklist that shipped src; add an exports map so ESM
  consumers resolve dist/index.js instead of falling back to the .cjs main
- all three: add a prepublishOnly build (+ test/typecheck) guard so a publish
  can never ship a stale or empty dist
- add a tag-triggered publish workflow with npm provenance, pinned by name to
  the three @openmaic packages so the vendored forks (mathml2omml, pptxgenjs)
  are never published

Refs #778, #720 (Phase 1).

* fix(packages): address cross-review on the @openmaic publish prep

Cross-review (Claude /code-review + codex) on this PR surfaced:

- renderer's advertised CJS entry was broken: it keeps @openmaic/dsl external
  and imports a runtime enum from it, but dsl is ESM-only (no `require`
  condition), so `require('@openmaic/renderer')` would throw
  ERR_PACKAGE_PATH_NOT_EXPORTED. Make renderer ESM-only: drop the `.cjs`
  rollup output, `main` now points at the ESM build, and the `require`
  conditions are removed from `exports`. (importer is unaffected — it bundles
  dsl, so its CJS build still works.)
- prepublishOnly re-ran the test suite during `pnpm -r publish`, so a flaky
  test after dsl had already published gave a non-atomic partial release.
  Reduce prepublishOnly to a build-only guard (never ship stale/empty dist)
  and move the real test/typecheck gate into the workflow, before any publish.
- document that an @openmaic/* tag publishes the whole family via `pnpm -r`
  (pnpm skips already-published versions); the tag is a release marker, not a
  per-package gate.

Verified: dsl + renderer + importer build; renderer emits ESM only (0 .cjs),
all exports entries resolve; `npm pack` ships dist + README + LICENSE with no
src leak; frozen-lockfile passes.

Refs #778.

* style: reflow @openmaic/dsl type imports past print-width after rename

The @maic -> @openmaic rename lengthened two single-line type imports past
prettier's 100-col width; prettier --check flagged them. Pure formatting.

Refs #778.

* docs(importer): mark @openmaic/importer browser-only (cr-loop accepted limitation)

codex cross-review flagged that the published @openmaic/importer throws
`XMLHttpRequest is not a constructor` when loaded in a pure Node process —
its rollup build is browser-targeted (`nodeResolve({browser:true})` + a
browser pdf.js build). The app only consumes it client-side ('use client'),
so this is by design. Document it as an accepted limitation: prominent
browser-only note in the README and a `browser` field in the manifest.

Refs #778.
2026-06-24 15:12:23 +08:00
wyucandClaude Opus 4.8 4dce1f7414 refactor(types): import the slide DSL directly from @maic/dsl; drop the shim (#738)
#707 left lib/types/slides.ts as a thin `export * from '@maic/dsl'` shim so the
~100 existing `@/lib/types/slides` import sites kept working unchanged. Point
them at `@maic/dsl` directly and delete the shim, so the app consumes the
package contract with no indirection and the legacy module path is gone.

Pure module-specifier migration — every symbol imported from
`@/lib/types/slides` was already a re-export of the same `@maic/dsl` symbol, so
behavior is identical. Also redirects the two `lib/types` relative importers
(`./slides`), the e2e fixture's relative import, and the dynamic
`import('../types/slides').Slide` type alias in stage-storage; merges the now
same-module import pair in slide-edit-elements.

Verified: tsc --noEmit clean, eslint clean, vitest 804/804, next build OK,
e2e (Playwright) 19/19.

Part of #720 (Phase 1).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:04:06 +08:00
wyuc 2dff6d10be feat: add generated video manifest refs (#540)
* feat: add generated video manifest refs

* style: format video manifest changes

* fix: address video manifest review feedback
2026-05-09 20:56:31 +08:00
Shangqing TuandClaude Opus 4.7 c02a607377 feat: interactive mode clean (#461)
* feat: add interactive mode

* feat: add TeacherActions for interactive mode

* fix: better prompt for ultra mode

* feat: upgrade normal mode's interactive generation with 5 widget type and actions

* feat: make interactive mode button clear

* fix: remove unused TeacherOverlay

* fix: resolve PR #458 review feedback

- Revert unrelated infra changes (Dockerfile, docker-compose.yml)
- Fix placeholder strings in generateWidgetContent
- Rename ultraMode to interactiveMode consistently across codebase
- Extract duplicated patchHtmlForIframe to shared utility
- Fix widget store race condition by keying callbacks by sceneId
- Fix teacher-action ID collisions by using descriptive IDs in prompt
- Relax outline prompt constraints for flexibility
- Optimize sidebar thumbnail with IntersectionObserver lazy loading
- Add trailing newlines to modified files

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

* fix: update generateSceneActions call signatures

The function signature changed to individual parameters instead of
options object pattern after cherry-pick.

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

* style: fix prettier formatting

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

* fix: removed extra language arg

* style: fix prettier formatting v2

* fix:  add missing i18n keys

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 21:35:30 +08:00
杨慎andClaude Opus 4.6 c75cf6e602 feat: add code element support for whiteboard (#385)
* feat: add CODE element type and wb_draw_code/wb_edit_code action types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: implement wb_draw_code and wb_edit_code in ActionEngine

* feat: add BaseCodeElement with Shiki syntax highlighting and animations

* feat: register CodeElement in renderer and add inline action tags

* feat: add wb_draw_code and wb_edit_code to agent action schemas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add code element context to director prompt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add code element guidelines and context to prompt builder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add i18n keys for code element

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: prevent whiteboard zoom from intercepting code element scroll

* chore: remove unused code element i18n keys

The code.untitled and code.lines keys were added but never referenced
by the BaseCodeElement component. Remove to reduce i18n maintenance
burden and merge conflict surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 15:57:51 +08:00
kaiyuan ZhangandClaude cce7087f67 Fix action filtering logic and add safety improvements (#163)
* fix: fix 3 bugs found during code review

1. formatQuestionsForPrompt called .join() on QuizOption objects,
   producing "[object Object]" in AI prompts instead of readable
   option text (e.g. "A. Option1, B. Option2").

2. json-repair Fix 1 regex double-escaped valid JSON escapes (\t,
   \n, \r, \b, \f) and couldn't handle strings with escaped quotes.
   Now uses a proper string-aware regex and preserves valid escapes.

3. PlaybackEngine spotlight/laser actions used synchronous recursion
   via processNext(), risking stack overflow with many consecutive
   non-speech actions. Now uses queueMicrotask() to break recursion.

https://claude.ai/code/session_01JdigqQtiAvomeifvWSQbjm

* fix: ensure allowedActions whitelist applies to non-slide scenes

The early return in Step 6 (slide-only action filter) caused Step 7
(allowedActions whitelist) to be skipped for non-slide scenes. This
meant hallucinated actions from agents in quiz/interactive/pbl scenes
were not filtered by the role-based allowedActions whitelist.

Refactored to use a mutable result variable instead of early returns
so both filters always apply.

https://claude.ai/code/session_01JdigqQtiAvomeifvWSQbjm

* fix: prevent video playback hang and division by zero in CJK detection

1. executePlayVideo could hang forever if the video element was invalid
   or the state change was missed between playVideo() and subscribe().
   Added a 5-minute safety timeout to prevent indefinite blocking.

2. CJK language detection in browser TTS divided by chunkText.length
   without checking for empty strings, producing NaN. Now guards
   against zero-length text.

https://claude.ai/code/session_01JdigqQtiAvomeifvWSQbjm

* fix: prevent ordered entry with invalid index when partial text has no remaining content

When a previously-partial text item completed but all content had
already been streamed as deltas (remaining is empty), the code still
pushed an ordered entry pointing to textChunks.length - 1, which
could be -1 or reference an unrelated chunk. This caused spurious
warnings in director-graph and potential text loss.

Now only pushes the ordered entry when there is actual remaining
content to emit.

https://claude.ai/code/session_01JdigqQtiAvomeifvWSQbjm

* fix: validate quiz points parameter and fix parse-pdf metadata spread order

1. quiz-grade: `points` from request body was used without validation,
   causing NaN in score calculation and invalid JSON responses when
   points was undefined, negative, or non-numeric. Now validates it
   is a positive finite number.

2. parse-pdf: metadata spread order put the default `pageCount` before
   `...result.metadata`, so the spread would overwrite the fallback.
   Fixed by spreading metadata first, then applying defaults after.
   Also changed `||` to `??` so pageCount=0 is preserved correctly.

https://claude.ai/code/session_01JdigqQtiAvomeifvWSQbjm

* fix: crash in moveUpElement, dead-code in video options, wrong parens in canCombine

1. use-order-element.ts: moveUpElement crashed with TypeError when a
   grouped element was already at the top of the z-order. The code
   accessed copyOfElementList[maxLevel + 1] without bounds checking,
   then dereferenced .groupId on undefined. Added early return guard
   matching the pattern used in the non-grouped branch.

2. video-providers.ts: normalizeVideoOptions had a dead-code ternary
   inside an if-block that already proved the condition was false.
   Simplified to directly use the fallback value.

3. use-canvas-operations.ts: canCombine check had incorrect
   parenthesization: (el.groupId && el.groupId) === firstGroupId
   evaluated the && first, which is equivalent to just el.groupId.
   Fixed to: el.groupId && el.groupId === firstGroupId.

https://claude.ai/code/session_01JdigqQtiAvomeifvWSQbjm

* style: fix prettier formatting in 3 files

https://claude.ai/code/session_01JdigqQtiAvomeifvWSQbjm

* fix: use valid ApiErrorCode in quiz-grade route

INVALID_PARAMETER is not a member of API_ERROR_CODES. Use
INVALID_REQUEST instead, which is the appropriate code for
invalid request parameters.

https://claude.ai/code/session_01JdigqQtiAvomeifvWSQbjm

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-04 12:46:27 +08:00
杨慎andClaude Opus 4.6 66c6229736 fix: improve whiteboard UX with bounded canvas and history fixes (#235)
* fix: improve whiteboard UX with bounded canvas, toolbar cleanup, and history fixes

- Refactor canvas to bounded viewport with visible white rectangle that moves with pan/zoom
- Add reset view button in toolbar with fade animation (replaces in-canvas overlay)
- Enable always-on pan (no toggle needed)
- Add pan boundary clamping to prevent canvas from going too far offscreen
- Separate autoFit transform from pan/zoom transform for cleaner architecture
- Move history panel to popover anchored to history button
- Save current content before restore to prevent data loss
- Fix snapshot deduplication to check all history entries (not just last)
- Remove dead code: restoredKey mechanism, zoomHint i18n keys, zoom hint overlay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: simplify whiteboard history labels to sequential numbering

Replace descriptive snapshot labels (beforeClear, beforeAIClear, beforeRestore)
with simple #1, #2, ... numbering by creation order. Remove label field from
WhiteboardSnapshot type and unused i18n keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restructure canvas layers to fix viewport clipping

The viewport was the same size as the bounded canvas with overflow-hidden,
causing panning to clip at the viewport edge instead of the workspace edge.

- Viewport now fills the workspace (w-full h-full), no overflow-hidden
- Outer container is the sole clip boundary
- Bounded canvas centered via computed position with transformOrigin: 0 0
- Pan boundaries now account for zoom level
- Zoom toward cursor instead of canvas center
- Pan drag accounts for both containerScale and viewZoom
- Removed unused isViewModified from WhiteboardCanvasHandle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: remove autoFit transform, render elements at raw coordinates

Remove the autoFitTransform that silently scaled content to fit within the
fixed canvas. Elements now render at their raw coordinates and can overflow
the white canvas background into transparent space. Users navigate with
pan/zoom instead of relying on auto-shrink.

- Remove autoFitTransform computation and all bounds calculation helpers
- Remove overflow-hidden from bounded canvas so elements can spill out
- Remove content transform layer (elements render directly)
- Clean up unused imports (getElementRange, PPTLineElement)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:06:14 +08:00
Yizuki_Ameandwyuc f3508ca15e fix: guard against undefined action.content in executeWbDrawText (#193)
Closes #192

action.content can be undefined when the AI generates a wb_draw_text
action without providing content. This caused a TypeError on
.startsWith(). Now falls back to empty string and returns early.

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-03-22 14:36:15 +08:00
fbe8399678 feat(generate): server-side media & TTS generation (#75)
* feat(generate): server-side media & TTS generation in classroom pipeline

When `enableImageGeneration`, `enableVideoGeneration`, or `enableTTS` flags
are passed to /api/generate-classroom, the server now actually generates
media files and TTS audio, persists them to disk, and replaces placeholders
in the classroom JSON with serving URLs.

- Add media serving API route (GET /api/classroom-media/[id]/[...path])
- Add server-side media generation utility (image, video, TTS)
- Wire generation phases into classroom pipeline with progress steps
- Update AudioPlayer to support URL-based playback (server-generated TTS)
- Add audioUrl field to SpeechAction type
- Extend /api/health with capabilities detection
- Pass through feature flags in generate-classroom route
- Update skill docs for optional feature configuration

Closes #49

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix prettier formatting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(generate): add agentMode support for server-side classroom generation

Allow callers to choose between built-in default agents ('default') or
LLM-generated custom agent profiles ('generate') via a new agentMode
field on GenerateClassroomInput. The resolved agents are injected into
outline, scene content, and scene action prompts via teacherContext.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(media): harden download, parallelize media gen, fix TTS concatenation

- Add timeout (2min) and max size (100MB) guard to downloadToBuffer
- Run image and video generation in parallel via Promise.all
- Only split TTS text for MP3 (frame-based); skip for WAV/OGG/AAC
  whose container headers break on naive byte concatenation
- Remove unnecessary type assertion for speechAction.audioUrl

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix prettier formatting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(media): harden media serving and TTS generation

- Add realpath check to prevent symlink-based path traversal in media route
- Stream files via createReadStream instead of readFile to avoid OOM on large videos
- Truncate TTS text for non-concatenable formats (WAV/OGG/AAC) when exceeding provider limit
- Add 'researching' step to distinguish web search from outline generation progress

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tts): split long speech actions instead of concatenating audio bytes

Adopt the same strategy as client-side: split a long speech action into
multiple shorter actions before TTS generation, so each sub-action gets
its own audio file. This avoids the broken byte-concatenation approach
that corrupted non-MP3 formats (WAV/OGG/AAC container headers).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(tts): extract shared TTS splitting logic into lib/audio/tts-utils

Both client-side (use-scene-generator) and server-side (classroom-media-generation)
had duplicated TTS_MAX_TEXT_LENGTH, text splitting, and action splitting logic.
Extract into a shared module to keep them in sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: remove unused TTS_MAX_TEXT_LENGTH import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-03-19 14:40:04 +08:00
Yizuki_Ame 43a0c8e39e feat: whiteboard history and auto-save (#40)
* feat: add whiteboard history and auto-save (closes #32)

- New whiteboard-history.tsx: browseable history panel UI
- New whiteboard-history.ts: Zustand store with snapshot stack,
  fingerprint-based deduplication, and restoredKey one-shot guard
- New element-fingerprint.ts: shared fingerprint utility (id+position+size)
- whiteboard-canvas.tsx: auto-snapshot effect that saves the current stable
  state after 2s debounce; unified fingerprint import; useMemo for elements
- index.tsx: pushSnapshot before UI clear; history panel trigger button
- stage.ts: i18n keys for history UI (zh/en)

* fix: address audit findings for whiteboard history

- engine.ts: AI wb_clear now saves snapshot before clearing (audit #3)
- page.tsx: clear whiteboard history on classroom switch (audit #4)

* fix: address review round 2 (P1/P2a/P2b/P3)

- P1: Block restore during clear animation (race condition)
- P2a: Skip no-op restores to avoid stale restoredKey
- P2b: clearHistory() now also resets restoredKey
- P3: Dedicated restoreError i18n key

* fix: fingerprint includes semantic content per element type (P2c)

* fix: address reviewer feedback (fingerprint cache, AI clear label, exhaustive check)
2026-03-18 23:56:11 +08:00
wyucandClaude Opus 4.6 843d33e5cc chore: enforce Prettier formatting and fix lint issues
- Add .prettierignore to exclude vendor packages, lock files, markdown, and YAML
- Update .prettierrc: printWidth 100, singleQuote, trailingComma "all"
- Run Prettier across all source files for consistent formatting
- Fix unused imports (UserRequirements, setTTSProvider)
- Fix eslint-disable comment placement after Prettier reformat

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:09:45 +08:00
wyucandClaude Opus 4.6 0d20abfed9 Initial commit: OpenMAIC AI classroom platform
Next.js 16 App Router application with:
- AI-powered interactive classroom generation from PDF/text requirements
- Multi-agent discussion system (teacher, students, assistant roles)
- Provider abstraction for LLM, TTS, ASR, image, video, web search
- Local-first data architecture (IndexedDB/Dexie)
- i18n support (zh-CN/en-US)
- Structured logger, SSRF guard, lint/build passing (0 errors)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:41:07 +08:00