mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-18 05:40:25 +08:00
@openmaic/generation@0.1.0
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
40ff80ab63 |
feat(editor): add draggable insert toolbar (#912)
* feat(editor): add draggable insert toolbar * fix(editor): improve toolbar accessibility * fix(quiz): update empty-state guidance * fix(settings): sanitize retired toolbar state * fix(editor): keep hints from blocking controls * fix(editor): refine toolbar and hint interactions * fix(editor): preserve toolbar position across scenes --------- Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com> |
||
|
|
6b93980f82 |
feat(editor): redesign the narration timeline (action picker + inline insert) and enable it for interactive/PBL scenes (#834)
* feat(editor): enable narration timeline for interactive/pbl scenes * docs(editor): fix stale timeline-gating comment in EditChromeRoot * i18n(editor): add action-picker + addAction strings * feat(editor): pure pickerOptions filter for the action picker * feat(editor): ActionPicker popover (shared by header pill + inline +) * feat(editor): header add-action pill opens ActionPicker; drop drag-chips * feat(editor): inline + insert between cells opens ActionPicker * style(editor): reskin SpeechClip + TTS footer to Pro Mode v2 * style(editor): align discussion dash tint to neutral primary token Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style(editor): reskin CueMarker (bound label) + DiscussionClip + NodeDot * fix(editor): complete picker i18n locales, label inline insert button, use primary token - Add edit.timeline.addAction and edit.picker.{title,speechDesc,spotlightDesc,laserDesc,discussionDesc} to zh-TW, ja-JP, ko-KR, ru-RU, pt-BR, ar-SA locale files, mirroring the existing en-US/zh-CN structure. - Give the inline "+" insert button in ActionsBar's DropZone an accessible name (aria-label + title) by threading an insertLabel prop from the root component, which already has the translator. - Replace the raw bg-violet-500 active-divider glow in DropZone with the bg-primary token. * chore(editor): drop dead drag-to-add path and orphaned i18n keys, explain disabled discussion option * style: format picker-options with prettier --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6538e718ff |
feat(editor): in-editor authoring of classroom agents (Stage-level roster) (#816)
* feat(editor): agent roster edit operations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(editor): dedupe agent.delete lookup + cover history cap/color/teacherCount Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(editor): materialize agent roster from presets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(editor): cycle prepended-teacher index + document branch-1 as-is Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(editor): stage store viewMode + agent roster persistence Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(editor): useAgentRoster controller hook Implements the AgentRosterController hook that bridges the store, history stack, and agent-ops layer for the agents authoring view. - Materializes roster from stage on mount via materializeRoster + resolvePreset - Holds AgentRosterHistory in React state; exposes add/update/remove/reorder - Guards LAST_TEACHER errors (caught, no-op) in update + remove - Exposes SurfaceHistory-shaped history object for undo/redo wiring - Syncs history.present to setStageAgents via useEffect on every change Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(editor): Agents view UI components Adds AgentsView, AgentRosterGrid, AgentCard, AgentInspector, AvatarPicker. Layout: full-height panel with a scrollable grid of agent cards on the left and a fixed inspector panel on the right (opens when a card is selected). - AgentCard: avatar + name + role badge + persona excerpt; ◀/▶ reorder buttons; delete button disabled + titled when canRemove=false (last teacher) - AgentRosterGrid: responsive grid with trailing [+ 添加角色] add tile - AgentInspector: name input, role select (teacher/assistant/student), AvatarPicker over AGENT_DEFAULT_AVATARS, persona textarea capped 2000 chars - AvatarPicker: grid of AGENT_DEFAULT_AVATARS with violet selection ring - AgentsView: composes the hook + grid + inspector; renders its own CommandBar (title="Agents", history=hook.history) with leading/trailing slots Styling follows quiz surface conventions (rounded-2xl cards, zinc palette, violet accent on selection, same FOCUS ring treatment for inputs). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(editor): [Slides]/[Agents] view toggle wired into Pro-mode chrome - CommandBar: add optional `leading?: ReactNode` center slot for the toggle - EditShell/Frame: thread new `commandLeading` prop down to CommandBar - EditChromeRoot: read viewMode/setViewMode from useStageStore; build a ViewModeToggle ([幻灯片] / [角色] segmented control) passed as commandLeading to both EditShell (slides mode) and AgentsView (agents mode) - When viewMode==='agents', render AgentsView instead of EditShell; leftRail and bottomRail are naturally absent since AgentsView owns its own layout - When viewMode==='slides', existing EditShell path is unchanged - CommandBar history in agents mode binds to useAgentRoster().history; title = "Agents" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editor): functional state updates in useAgentRoster + last-teacher role feedback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editor): break infinite render loop in Agents view persistence effect Remove `stage` from the useEffect dep array in useAgentRoster — setStageAgents mutates stage (new object ref), which was triggering re-render → effect → loop (React error #185). setStageAgents already no-ops when stage is null (lib/store/stage.ts:287), so the `if (stage)` guard is also removed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editor): persist agent roster edits to registry + stage snapshot - Add generatedAgentConfigs to StageRecord (database.ts) so Dexie can round-trip it through db.stages. - Include generatedAgentConfigs in saveStageData whitelist (stage-storage.ts) so editor reloads restore the user's roster edits. - In saveToStorage (stage.ts), call saveGeneratedAgents(stageId, configs) after the stage snapshot write; this is already debounced (500 ms) via debouncedSave, so db.generatedAgents + the in-memory registry are synced without writing on every keystroke. saveGeneratedAgents clears-then- bulk-inserts, so deleted agents are fully removed from the registry. - Add 3 timer-driven tests that verify saveStageData carries generatedAgentConfigs and saveGeneratedAgents is called with the correct payload after the debounce fires. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(editor): scope agent-registry sync to real roster edits (cross-review) - FIX A (P1): remove saveGeneratedAgents from shared saveToStorage; add dedicated debouncedSaveAgents called only from setStageAgents — scene advances (setCurrentSceneId etc.) no longer churn db.generatedAgents - FIX B (P2.1): add isDirtyRef guard in useAgentRoster so opening the Agents tab without editing does not persist/rewrite the roster; ref is set in applyOp, add, undo, redo before any setHistState mutation - FIX C (P2.2): coalesce consecutive agent.update ops on the same agent+fields into one history entry so undo steps over a whole edit rather than each keystroke - Tests: add regression guards asserting saveGeneratedAgents is NOT called on setCurrentSceneId or plain saveToStorage, IS called after setStageAgents (new dedicated debounce path) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(editor): clone preset agents to fresh ids on materialization (cross-review P1) * refactor(editor): move agent roster into right-rail tab beside Edit with AI The [幻灯片][角色] top toggle + full-canvas AgentsView swap is replaced by a tabbed right rail (RightRailTabs) with "Edit with AI" and "角色" tabs. The slide canvas stays visible in all modes; agents are edited in the 角色 tab of the right rail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(editor): restyle agent panel to Design B accordion (课堂阵容) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(editor): cross-review — persona draft sync, scoped preset re-id, AI-tab gating, dead-code cleanup Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(editor): avatar click expands card, editable-name affordance, add-button copy * fix(editor): cross-review — drop blur-level undo coalescing, sync playback selection to edited roster Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style(editor): prettier format agent panel files (CI fix) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
7c19ab8f1e |
feat(dsl): JSON Schema artifacts + pure validators (#787 Part B-1) (#817)
* feat(dsl): add ACTION_TYPES set + isActionType guard A frozen set of every valid ActionType plus a pure membership guard, mirroring the PPT_ELEMENT_TYPES / isPPTElementType idiom in guards.ts. Used by the new validators; useful standalone for narrowing untrusted action.type values. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dsl): build-time JSON Schema codegen for stage/scene/action TS types stay the single source of truth; ts-json-schema-generator (a devDependency, build-only) emits dist/schema/{stage,scene,action}.schema.json, shipped under the new ./schema/* export. Serves non-TS / bring-your-own-validator consumers without adding a runtime dependency. JSON Schema is not generic, so schema-roots.ts provides a concrete Scene<Action, SceneContent> entry point (kept internal, not re-exported). schema.test.ts compiles the generated schema with ajv (devDep) to prove the artifact is real and self-contained on a clean checkout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dsl): add pure structural validators (stage/scene/action) validateStage / validateScene / validateAction: hand-written, zero-dependency, fail-loud structural checks layered on the existing guards. They verify discriminants, required fields, and known nested discriminants — a gate for untrusted input (LLM/agent output) and persistence boundaries. Exhaustive per-field validation is delegated to the shipped JSON Schema. Error-collecting ValidationResult reports every issue with a JSON-pointer-ish path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(dsl): document the runtime schema + validator layer Add a Runtime layer section covering the two complementary consumption modes (schema-as-data via @openmaic/dsl/schema/* and the zero-dep validate* gate), the validate.ts module row, the build:schema step, and tick the JSON Schema roadmap box. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dsl): harden validators per cross-review (exhaustiveness, discriminant agreement) Addresses cross-review findings on the validator layer: - Add compile-time exhaustiveness assertions for ACTION_TYPES and SCENE_TYPES. `satisfies` only proved each entry is valid, not that the tuple covers the whole union — a newly-added Action/Scene variant could silently be rejected by the validators. The assertion now fails the build on drift. - validateScene cross-checks that scene.type agrees with content.type; a slide-typed scene carrying quiz content (or vice versa) is now rejected. - Move SCENE_TYPES + add an isSceneType guard to stage.ts, next to SceneType, matching the PPT_ELEMENT_TYPES / isPPTElementType idiom in guards.ts; validate.ts reuses them instead of re-encoding the union locally. - Make the validators' scope honest in the doc comment + README: they check the structural envelope, known discriminants, and scene/content agreement; they do NOT exhaustively check per-variant fields, and app-side interactive/pbl content is validated only at the envelope level (exhaustive checks → the JSON Schema). - Note the contract-owned content scope in schema-roots.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(dsl): reuse one schema generator + harden direct-invocation guard Addresses cross-review findings on the codegen path: - gen-schema.mjs built a fresh ts-json-schema-generator (full TS program parse) per root type; build one generator over the tsconfig program and reuse it for every createSchema call — parses once, identical per-type output. - Harden the `invoked directly` check to compare realpaths, so a symlinked invocation (bin shim / pnpm link) still runs main() instead of silently emitting no schema. - schema.test.ts generates all roots once at module scope, then only compiles per case — no repeated heavy codegen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dsl): bind scene schema discriminants + pin node-20-compatible generator Addresses round-2 cross-review (codex) findings: - scene.schema.json now ties scene `type` to the matching `content.type`. SerializedScene became a discriminated union (SlideScene | QuizScene via `Omit<Scene<…>, 'type'> & { type }`), so the generator emits a const-narrowed discriminant per branch. Previously the schema accepted e.g. { type: 'quiz', content: { type: 'slide', … } } even though validateScene rejects it — the schema is now consistent with the validator. Locked with a test. - Pin ts-json-schema-generator to ~2.4.0. ^2.3.0 resolved to 2.9.0, which declares `node >=22`, conflicting with the repo's `engines.node >=20.9.0`; 2.4.x supports node >=18, keeping the build runnable on every supported Node. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(dsl): prettier formatting + drop unused ts-expect-error for green CI - Apply repo Prettier (`pnpm check`) formatting to the new/edited files. - Remove the `@ts-expect-error` on the gen-schema.mjs import: the root `tsc --noEmit` (which compiles test files; allowJs infers the .mjs exports) flagged it as an unused directive (TS2578). Plain comment retained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(dsl): align validators + schema to the public Scene type (review) Addresses cosarah's review: the validator, the JSON Schema, and the public TS type had drifted into three different notions of a valid scene. Realign them so the TS types stay the single source of truth, and position the two runtime layers honestly. - schema-roots.ts: revert SerializedScene to the plain default Scene<Action, SceneContent>. The schema no longer encodes a stricter type/content binding that the public Scene type doesn't express (the constraint had effectively moved into a hidden internal type). - validate.ts: drop the scene.type<->content.type agreement check (the public Scene does not bind them, so neither does the validator); restrict content to the contract-owned kinds (slide/quiz), matching SceneContent and the schema, so validator and schema no longer disagree on interactive/pbl content. - Reposition docs (validate.ts header + README): the JSON Schema is the authoritative, exhaustive per-field validator (it checks variant fields like an action's elementId) for trust boundaries; validate* are a cheap, zero-dep structural pre-check and a strict subset of the schema — not a separate or stricter gate. - Tests updated to lock the realigned behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(dsl): bind scene type<->content in the public Scene contract (review) cosarah's review showed the scene type<->content agreement is a real, load-bearing invariant: consumers branch on `scene.type` and then read `scene.content` as the matching shape (e.g. `stage-api-canvas` casts `content as SlideContent` after a `scene.type === 'slide'` check; `complete-summary` counts by `scene.type` and reads `content as QuizContent`). A validator/schema that blesses mismatched pairs is unsafe. So tighten the *exported* contract rather than loosening the runtime checks to match a hole — and make the pure validators the authoritative, relied-upon boundary. DSL contract: - `Scene` becomes a distributive discriminated type binding `type` to `content['type']` (`SceneCore<TAction> & { type: TContent['type']; content: TContent }` distributed over TContent). TS itself now rejects mismatched scenes; generic widening still works. Extract `SceneCore` for the kind-independent fields. - `validateScene` re-establishes the type<->content agreement check; scene `type` is the contract's slide/quiz; `scene.schema.json` (from the spelled-out discriminated `SerializedScene`) binds `type` to content per branch. Type, validator, and schema now describe the same thing. - `validateAction` checks each variant's required fields (e.g. spotlight.elementId, discussion.topic) — closes the false-positive gap. A test pins the hand map to the generated schema (the TS-derived source of truth) so it cannot drift. - Reposition docs: `validate*` is the zero-dep in-process boundary producers rely on; the JSON Schema is the cross-language mirror + exhaustive value checker. App consumers (the sites the discriminated Scene exposes): - Add `ScenePatch` (non-distributive partial) + `makeScene(core, content)` helper to lib/types/stage.ts — one isolated cast, type derived from content. Patch-typed signatures (`updateScene`, `applyScenePatchInSync`, regenerate plan) and scene rebuilds (`stage-api-scene`, store `updateScene`, `migrateScene`, stage-storage deserialize) route through these. Closes two latent desync foot-guns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dsl): validate action field types + migrate scene.update; bidirectional drift-guard (review) Round-2 review of the contract-tightening change (codex + Claude): - validateAction now checks each variant-required field's TYPE, not just presence (`{ type:'spotlight', elementId: 123 }` is rejected — elementId must be a string). ACTION_REQUIRED_FIELDS carries a runtime kind per field. - The schema lockstep test is now bidirectional and type-aware: it asserts a fully-typed action is accepted (catches the map OVER-listing an optional field), every required field flags when missing (under-listing), and flags when present but mis-typed — all keyed off the generated schema's field names + types. - Migrate the public `stageApi.scene.update()` to ScenePatch + makeScene (the create() path was done last commit; update() still did a raw spread that could desync type<->content — masked from tsc only because StageStore.setState is typed `any`). Now both public scene-construction paths rebind type to content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): keep scene.create's params.type authoritative over content.type (review) cosarah's review: `CreateSceneParams` exposes an independent `type` plus `content?: Partial<SceneContent>`, and after switching create() to makeScene() (which derives the scene kind from content.type), a call like `create({ type: 'slide', content: { type: 'interactive', ... } })` would silently become an interactive scene — params.type ignored — and mix slide defaults into it. Reject a `content.type` that disagrees with `params.type`, and pin the merged content's `type` to `params.type` so a partial content override can't flip the scene's discriminant. params.type stays authoritative. Adds a test both ways. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5daf3e5a6e |
fix(editor): keep emptied/zero-action scenes playable, bind outline by stable id, surface incomplete content (#814)
Editor empty-state robustness: dwell on zero-action scenes (engine + PlaybackChromeRoot) so emptied/blank slides stay playable; bind a scene's outline by stable outlineId (reorder/insert/duplicate-safe, allOutlines in scene order); clear the cue preview on glyph unmount; surface incomplete content calmly (amber dashed frame on clips, amber dot on blank outline titles + rail, neutral N/M-ready generate gate). Cross-reviewed (Codex + Claude), 5 rounds to a clean pass. |
||
|
|
a93f875044 | fix(agent): keep edit runtime alive across read-only scenes | ||
|
|
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> |
||
|
|
a88ee3d119 |
feat(editor): add discussion authoring to the script timeline (#798)
Add an append-only 'Add discussion' control + inline editor (topic/prompt/initiating agent) to the script timeline, respecting the action-parser invariant that a discussion is the scene's last action and at most one per scene. Yellow terminal-anchor styling; agent picker (with avatars) sourced from the selected agents the playback engine gates on. Runtime unchanged. |
||
|
|
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. |
||
|
|
dcf8a63333 |
fix(generation): make outline type changes take effect (interactive/PBL no longer downgraded to slide) (#772)
* feat(generation): changeOutlineType total constructor for valid type switches Switching a scene outline's type in the editor only flipped `type` and left the new type without its required config, so applyOutlineFallbacks silently downgraded interactive/pbl back to slide. changeOutlineType returns a new outline that strips foreign per-type config and seeds the target type's required config from the shared fields, valid by construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n(generation): keys for interactive widget kind and PBL project config Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(editor): make outline type changes take effect with config forms TypePill now builds a valid outline via changeOutlineType (replace, not partial-merge) so a type switch drops stale config and seeds the new type's required config — no longer silently downgraded to slide by applyOutlineFallbacks. Adds InteractiveConfigDisclosure (widget kind + concept) and PblConfigDisclosure (topic / description / target skills), mirroring QuizConfigDisclosure, so users can configure interactive and PBL scenes that previously had no input UI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(editor): move type-specific config under the type pill The quiz/interactive/PBL config disclosure rendered as a detached chip at the bottom-left of the card, reading like a stray key point. Pin it to the bottom-right of the key-points row instead, so it sits directly under the TypePill it configures (type top-right -> its config bottom-right, same edge), with key points flowing on the left. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(editor): cascade type config as a connected segment left of the type pill Replace the standalone config chip with a cascading control in the header: the type-specific config (quiz count / interactive widget kind / PBL project) renders as a themed segment joined to the left of the TypePill, the two clipped into one rounded pill split by a hairline divider. Reads as a single 'config › type' cascade and groups the config with the selector it refines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(editor): address cross-review — preserve widget config, drop stale fields - changeOutlineType: preserve an existing procedural-skill widget config instead of replacing it with a seeded simulation (codex P2 — task-engine fields were silently dropped on Interactive re-select). - InteractiveConfigDisclosure.setWidgetType: reset widgetOutline to the shared concept only, so switching widget kind drops the previous kind's fields (claude P2 — language/gameType/etc. leaked across kinds). - i18n locale-coverage test now also checks ko-KR and pt-BR. - Drop a redundant WidgetType cast; add tests for procedural-skill preservation and pbl targetSkills dedupe/cap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(editor): round 2 cross-review nits — procedural-skill label + skill a11y - InteractiveConfigDisclosure: when an outline carries a preserved procedural-skill widget, surface it as the current (selected) kind with its own label instead of mislabeling it 'Simulation', and make a same-kind re-select a no-op so its task-engine fields are never silently clobbered. - PBL target-skill remove button uses a dedicated removeSkill aria-label. - Add widgetProceduralSkill / removeSkill i18n keys (8 locales) + test coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(generation): make re-selecting the current outline type a no-op changeOutlineType rebuilt the outline on every type-menu click, so re-selecting the already-current type dropped fields it doesn't re-seed — legacy interactiveConfig, or a partial pblConfig whose projectTopic is empty — turning a harmless click into data loss. Short-circuit same-type selections to return the outline untouched (codex P2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(format): prettier --write files left unformatted on main These 14 agent/edit files (not part of this PR's feature) fail the repo-wide `prettier . --check` in the required Lint job — they landed on main unformatted, turning main's CI (and therefore this PR after a branch update) red. Formatting them here unblocks the required check; pure formatting, no logic changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lint): suppress intentional set-state-in-effect in reasoning timer The reasoning timer does a deliberate one-shot setState in its effect to show the right elapsed value before the first 1s interval tick. main's CI fails the required Lint job on the new react-hooks/set-state-in-effect rule here; suppress it with a justification (no behavior change). Unblocks the required check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1d1ce80e04 |
feat(maic-agent): editor-agent line (v0) — Pro-mode "Edit with AI" (#777)
Promote the editor-agent line to main: read-then-act editor agent (read_scene_content, regenerate_scene, regenerate_scene_actions, edit_interactive_html), a routable maic-agent model stage, the agent sidebar UI, interactive-scene runtime-error capture, and the timeline/script editor. Cross-reviewed and smoke-verified; see #777. |
||
|
|
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> |
||
|
|
47d28145b4 |
feat(maic-editor): slide surface — MAIC Editor v0 (epic #562) (#615)
* feat(maic-editor): framework primitives + edit StageMode (#564)
* feat(maic-editor): framework primitives + edit StageMode
Phase 1 framework foundation for the MAIC Editor (RFC #547,
tracking #560). Plumbing only — no UI consumers ship in this
sub-PR; the EditShell chrome and slide surface registration
land in follow-ups.
- StageMode gains 'edit' alongside 'autonomous' | 'playback';
setMode resets canvas selection when leaving 'edit'.
- <Stage> auto-exits 'edit' whenever the current scene becomes
uneditable (no scenes / pending generation / no current
scene) so a follow-up Pro toggle can never strand the user
in an empty edit shell.
- SceneEditorSurface contract + tiny registry under lib/edit/
so each SceneType plugs in a surface without the shell
importing surfaces directly. Surfaces declare CanvasComponent,
useSurfaceState(), insert palette items, floating actions,
commands, and (reserved for AI) inline coach hints.
- Slide kernel (lib/edit/slide-ops.ts): immutable, history-aware
operations covering slide-update, element add / update /
updateMany / delete / deleteMany / reorder / duplicate / align /
removeProps, and text content edit.
- Slide element factories (lib/edit/slide-edit-elements.ts) for
default text / shape / image elements + HTML <-> plain-text
helpers.
- i18n: stage.editCourse + stage.doneEditing across all 6
locales, consumed by the header toggle in the next sub-PR.
- Vitest coverage for the slide kernel (operations + history)
and the edit-mode store transition (entry + canvas reset on
exit).
PBLRenderer's mode prop is widened from the literal pair to
StageMode so <SceneRenderer> (which already passes StageMode)
type-checks. The prop is unused inside the renderer.
* style(maic-editor): apply prettier to lib/edit + tests/edit
CI runs on PRs to main only, so the prettier check did not fire
for this PR's target branch — applying formatting locally before
the merge train reaches main avoids a follow-up style commit.
* ci: also run on PRs targeting feat/maic-editor-v0
The MAIC Editor lands as a series of stacked sub-PRs against
the long-lived feat/maic-editor-v0 branch. Without this entry,
none of those sub-PRs get a CI gate — style/lint/type/test
regressions only surface when feat/maic-editor-v0 finally
merges back to main, at which point fixing them is a lot more
disruptive than catching them per sub-PR.
Push trigger is intentionally left main-only: nobody pushes
directly to feat/maic-editor-v0, every change arrives through
a PR that now runs the gate.
* fix(maic-editor): address kernel review on #564
Addresses cosarah's review against #561 scope:
Important:
- element.align now uses the canonical lib/utils/element.ts geometry
helper instead of a forked copy. The local fork ignored
PPTLineElement start/end and rotation, so bounds were wrong for
lines and rotated elements.
- Cap slide-edit history at MAX_HISTORY = 50; drop oldest on overflow.
- Narrow slide.update patch to Partial<Omit<Slide, 'elements' |
'animations'>> via a new SlideMetaPatch alias, so element /
animation collections can only be mutated through their dedicated
ops.
- element.add throws on id collision; element.duplicate throws when
idMap is missing entries or when new ids would collide with
existing elements.
- scene-editor-registry dev-warns on overwriting a *different*
surface for the same SceneType (HMR re-register of the same
instance stays silent); add unregister() for HMR cleanup and tests.
Minor:
- Unify on structuredClone over JSON.parse(JSON.stringify(...));
inside immer's produce, un-proxy with current() first.
- Skip history push when produce returns the same content reference
(true no-op detection). element.delete / deleteMany pre-check
membership so their unconditional filter assignments don't break
the ref-equality signal.
- Drop redundant cloneSlideContent calls in undo/redo/push paths;
immer's structural sharing already guarantees immutability of the
produced output. createSlideEditHistory keeps its defensive clone
since the initial value comes from outside immer.
Coverage:
- Extract auto-exit predicate into lib/edit/stage-mode.ts so the
policy can be unit-tested without rendering <Stage>.
- New tests: every align direction, line / rotated element align,
no-op paths for update/delete/reorder/removeProps/text/align,
element.add index clamping + id collision, element.duplicate
default offset + contract errors, history future cleared after
branching, history capped, registry register/unregister/HMR-safe
re-register, and the auto-exit predicate.
371 vitest tests pass (was 335). tsc/lint/prettier/i18n/build all
green locally.
* fix(maic-editor): close kernel escape hatches (subagent CR follow-up)
Two defense-in-depth fixes flagged by independent review after the
prior commit:
- element.duplicate now deep-clones the source via
structuredClone(current(element)). The previous shallow spread
shared nested mutable references (start/end tuples, outline,
points) with the source; immer's COW would have handled most
mutations but ops that operate on nested arrays in place
(sort/reverse/splice) would silently leak between source and
duplicate. The deep clone keeps the kernel's invariants
independent of how downstream op consumers write their recipes.
- slide.update gains a runtime guard that throws when patch
contains elements / animations. The type-level SlideMetaPatch
narrowing already forbids these keys, but the runtime guard
closes the `as any` escape hatch for callers that might bypass
the type system.
New tests cover both paths: meta-only slide.update succeeds, an
elements-containing patch throws, and a duplicated line element's
start/end/points tuples are independent from the source.
* feat(maic-editor): EditShell chrome and Pro mode toggle (#565)
* feat(maic-editor): EditShell chrome and Pro mode toggle
Adds the scene-type-agnostic editor chrome (EditShell + CommandBar +
FloatingToolbar + HintRail), an edit-mode sidebar, and the header Pro
toggle that flips into the 'edit' StageMode from #561.
No scene editor surfaces are registered yet — the next sub-PR wires up
the slide surface. In this PR every scene type falls through to the
i18n unsupportedScene placeholder, which is the verifiable visible
behavior.
- canEdit gating reuses the canonical isCurrentSceneEditable predicate
shipped in #561 so the toggle and the auto-exit effect are in
lock-step.
- handleToggleEditMode tears down live session / engine / TTS before
entering edit mode.
- ChatArea slides out in edit mode for a full-width canvas.
- reorderScene extracted from EditModeSidebar with unit tests; the
positional-order preservation is the part worth a guard test.
- i18n scoped to keys this PR's components actually reference;
surface-specific keys deferred to the slide-surface PR.
* test(reorder-scenes): single-element + reference-inequality cases; zh-CN newSlide distinct from addSlide
CR follow-ups:
- reorderScene tests now cover a 1-element array (both directions
return null) and explicitly assert the returned array is a new
reference, not the input.
- zh-CN edit.sidebar.newSlide was duplicating the addSlide label
("新建幻灯片" both); using "未命名幻灯片" for the default new-slide
title to match the English Add slide / New slide distinction.
* refactor(maic-editor): drop EditModeSidebar; clean Pro mode chrome (#568)
Course-correct on #565. EditModeSidebar was rejected by the design
owner as inappropriate for Pro mode (#560 wording is "minimal top
bar + slide thumbnail rail", not a file-list panel). #565 also left
the playback chrome wrapped around the editor — Header / sidebar /
Roundtable / ChatArea all stayed mounted with only the sidebar
swapped, and EditShell's CommandBar/FloatingToolbar/HintRail were
never visible since no surface registers yet.
Drop EditModeSidebar + reorder-scenes helper + tests + the edit.sidebar
i18n block (8 keys x 6 locales) + the CommandBar sidebar-toggle.
Stage keeps Header mounted in both modes — it owns the global Pro
toggle Switch, which is the entry AND exit affordance (closing the
Switch exits; no separate Done-editing button). In edit mode:
SceneSidebar / Roundtable / ChatArea are not mounted, and the canvas
slot renders <EditShell scene> instead of <CanvasArea>. EditShell
internally resolves the surface via sceneEditorRegistry; when none
is registered it falls through to edit.unsupportedScene. With no
surfaces registered, every scene type lands on that placeholder —
the visible v0 behavior.
New optional EditShell.leftRail slot reserves the spot for a
redesigned slide-navigation surface; v0 ships with the slot empty.
SceneRenderer is now playback-only — the mode === 'edit' branch and
its sidebarCollapsed / onToggleSidebar props moved up to EditShell /
Stage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): enablement infrastructure (pre-slide-surface) (#571)
* feat(maic-editor): enablement infrastructure (pre-slide-surface)
Pre-requisite for the slide surface (#562). Ships the safety
infrastructure so each subsequent surface PR is small and
recoverable:
1. Feature flag NEXT_PUBLIC_MAIC_EDITOR_ENABLED, default OFF —
gates the Pro toggle in Header. StageMode unchanged.
2. SlideContent.schemaVersion + pure idempotent migrateSlideContent
/ migrateScene; setScenes / addScene funnel legacy data through
the migrate at the store boundary.
3. tests/edit/round-trip/ harness: apply ops -> buildPptxBlob ->
JSZip parse -> assert content survived. No PPTX -> Slide reimport
exists in the codebase, so the full reimport-diff shape isn't
doable; per-op assertions extend the harness in #562.
buildPptxBlob is now exported (hook is still the only runtime
caller).
4. Per-scene slide-history persistence helpers (persist / load /
has / clear, keyed maic-editor:slide-history:${sceneId}, swallow
storage failures) + standalone SlideHistoryRestorePrompt dialog
+ 4 new i18n strings x 6 locales. Stage wiring deferred to #562.
5. Concurrency guards: isSceneEditLocked predicate (defensive; no
current call path structurally hits it); localStorage-backed
multi-tab edit lock with tryAcquire / refresh / release / heldByOther,
stale-lock takeover after 3x heartbeat; standalone
MultiTabEditConflictPrompt + 3 new i18n strings x 6 locales.
Stage wiring deferred to #562.
The slide-surface PR owns the edit-entry effect machinery (where
the history-state lifecycle and per-tab tabId ref naturally live),
so shipping half-wired dialogs here would speculatively build Stage
state we know we'll restructure on contact with the surface.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): migrateSlideContent forward-compat — no silent downgrade
CR follow-up: previously, content with schemaVersion newer than
CURRENT (e.g. v2 written by a future client) was silently truncated
back to the current version. Now: if schemaVersion >= CURRENT, return
the content untouched. The slide may not render correctly on an older
client, but its on-disk shape stays intact for the next compatible
client to read.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): slide surface skeleton + #571 wiring + geometry (#562) (#579)
PR1 of the slide-surface work (infra-first slice). Registers the slide
SceneEditorSurface so EditShell lights up Pro mode for slide scenes.
- SceneEditorSurface impl + sceneEditorRegistry registration; the surface
owns a SlideEditHistory via the #564 kernel.
- Reuse the unmodified slide renderer Canvas through a surface-owned
scene context; geometry drag/resize/rotate commits funnel into
element.update ops (scene-edit bridge), one gesture = one undo step.
- Geometry numeric x/y/w/h/rotate popover as the precise fallback; gated
off for line elements (PPTLineElement omits height/rotate).
- Wire #571 infra: cross-tab edit lock + conflict prompt, slide-history
persistence + restore prompt, regen-lock guard.
- Renderer-commit classification: a real geometry gesture commits
synchronously inside a pointer interaction; the renderer's
ResizeObserver text-normalization commits with none, so it is folded
into the baseline (no undo step / no persist / no spurious restore
prompt on entry) instead of being staged as a user edit.
- Per-op round-trip test for element.update geometry; bridge + session
unit tests; edit.geometry i18n across all 6 locales.
Upstream-shared changes are kept minimal and additive: an optional
`controller` prop on SceneProvider (uncontrolled/playback path
unchanged) so staged edits don't write through to the live stage store,
and a FloatingToolbar trigger-nesting fix (it wrapped PopoverTrigger
around <Tooltip>, a provider, so no popoverContent action could open).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* test(maic-editor): lock data-URL image PPTX round-trip (PR2 R1 gate)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): insert palette — text box + image (data-URL/URL)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(maic-editor): address Task 1 review — spy cleanup, popover-only comment, ImagePicker error log
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(maic-editor): drop PR1 debug geometry toolbar; element-aware floating bar
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(maic-editor): drop redundant PPTTextElement cast (Task 2 review)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): additive ProseMirror command bridge for the property bar (C1)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(maic-editor): exhaustiveness guard + tidy C1 adapter (Task 3 review)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): refresh property-bar attrs on caret/keyboard selection (C2)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(maic-editor): satisfy no-explicit-any in PR2 test stubs (Task 1+4 review)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): compact text property bar in the reused floating slot
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(maic-editor): Task 5 review — uniform selection-guard, Lucide icons, JSX, memo
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(maic-editor): edit.text.* + edit.insert.* across 6 locales
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(maic-editor): round-trip gate for formatted text + inserts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(maic-editor): clarify remote-URL image round-trip scope (Task 7 review)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(maic-editor): drop stale scaffolding comment + orphaned geometry i18n keys
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* style(maic-editor): prettier --write PR2 files (pre-push check)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(maic-editor): make no-explicit-any suppression prettier-robust
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): CommandBar insert popover never opened (PopoverTrigger wrapped a Tooltip provider)
Insert→Image was unreachable: InsertButton wrapped <PopoverTrigger asChild>
around <Tooltip> (a context provider, no DOM node), so Radix's Slot bound
no element. Chain both triggers onto the real <button>, exactly mirroring
the PR1 fix already in FloatingToolbar's ActionButton. PR2's insert-image
is the first popoverContent InsertButton consumer to exercise this path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): text property bar no longer clips/overflows in the floating popover
The ~450px single-row bar was jammed into FloatingToolbar's fixed w-72
(288px) PopoverContent and clipped. Let the popover size to content
(w-auto, max-w-[92vw], Radix handles edge collision) and harden the bar
row (w-max + no child shrink, fixed-width font select) so it renders as
one clean line. Chrome/surface layout only — no renderer change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): property bar stays open across consecutive formatting steps
execCommand refocuses the editor after every command; the uncontrolled
Radix popover treated that focus-shift as focus-outside and dismissed,
forcing a re-open of the Text bar for each format action. Prevent
onOpenAutoFocus (don't steal the canvas selection on open) and
onFocusOutside (editor refocus must not dismiss); Escape and pointer-down
truly outside still close it. Chrome-only, no renderer change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): editor canvas now resolves gen_img_* media placeholders
The editor's interactive ImageElement rendered elementInfo.src raw,
so entering Pro mode on any slide whose image was a generation
placeholder showed a broken-image icon (while playback's read-only
BaseImageElement correctly resolved the placeholder to the generated
objectUrl). Extract the resolution into a shared useResolvedImageSrc
hook so both variants stay aligned. Strictly additive: for any
non-placeholder src (legacy / direct URL / data URL) resolvedSrc ===
elementInfo.src and the media store is not subscribed to. Pre-existing
upstream gap surfaced by PR2 as the first real-user editor consumer —
same shape as the CommandBar popover-trigger fix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(maic-editor): unit-test gen_img placeholder resolution (9 cases)
Splits useResolvedImageSrc into a pure resolveImageSrc function (no
hooks) wrapped by the hook, so the resolution logic can be unit-tested
in vitest's plain node environment (no jsdom/RTL needed in this repo).
Covers: done→objectUrl; no task→raw; pending/generating/failed→raw;
done with no objectUrl→raw; cross-stage isolation; no-stageId path;
non-placeholder src passes through (the additive contract).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(maic-editor): auto-save edits to stage store, drop staging UX
Reverses PR1's "staged edits don't write through to the live lesson"
design. The slide-edit-session now writes through every history move
(applyOp / user commit / ResizeObserver normalization / undo / redo)
to useStageStore.updateScene as the canonical source of truth, which
Dexie already auto-persists. The renderer reads from the stage store
via the controller's getSnapshot.
Removes the entire staging surface that has no place in a modern
editor (Figma/Notion/Google Docs have no "unsaved changes" concept):
- DEL lib/edit/slide-history-persistence.ts (localStorage layer)
- DEL tests/edit/slide-history-persistence.test.ts
- DEL components/edit/SlideHistoryRestorePrompt.tsx (restore dialog)
- DROP pendingRestore field + restore() action from slide-edit-session
- DROP restorePrompt branch + handlers from useSlideCanvasController
- DROP edit.history.restore.* keys across all 6 locales
Edits now flow: user input → renderer onUpdate → controller.updateSceneData
→ slide-edit-session.commitContent → writeThrough(useStageStore.updateScene)
→ Dexie. There is nothing "unsaved" to restore, by design.
The session retains its in-memory undo/redo history (per Pro session)
and the user-vs-ResizeObserver gesture classification (so reflow
doesn't push undo steps).
Test suite rewritten to assert write-through on every history move and
no write-through on seed (the stage already has that content).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): address PR review — element delete affordance + cross-platform fonts
Two issues from review on #586:
1. A selected image/text element couldn't be deleted — the renderer's
delete lives only in a right-click menu, undiscoverable in Pro mode.
Add a Delete button to the FloatingToolbar for any single selected
element (text or image), dispatching the existing element.delete op.
Button-only, consistent with #560's keyboard-shortcuts deferral.
2. Switching fonts had no effect on macOS Chrome — the property bar's
font list was a hardcoded SimSun/SimHei set (Windows-only system
fonts the renderer never loads). Use OpenMAIC's canonical FONTS
registry (configs/font.ts) — the web fonts the renderer actually
loads, so a pick renders identically on every platform.
Adds edit.delete × 6 locales + the parity-test key; floating-actions
unit tests for the delete action.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): selection-anchored text editing for the slide surface (#590)
* feat(maic-editor): add resolveEditingElementId text-editing policy
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): drop text-format floating action (moves to anchored bar)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): surface hooks to derive and sync editingElementId
Add useResolvedSlideContent / useEditingTextElementId / useSyncEditingElementId.
Realign the PR2 buildFloatingActions tests with the new behavior (text
formatting moved off the FloatingToolbar) and co-locate the editing-state test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(ui): export PopoverAnchor from the popover wrapper
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): add useTrackedRect for element screen-rect tracking
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): add AnchoredTextBar selection-anchored format bar
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): wire anchored text bar + editing flag into SlideCanvas
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): draw a clean solid frame for the text element being edited
Gated on the canvas store's editingElementId (default ""), so the dashed
select frame is unchanged for multi-select and for any consumer that never
sets the flag. Editor-path only; playback never renders Operate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): drop the editor focus ring so text editing shows one frame
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* style(maic-editor): prettier-format the editing-state test import
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): anchor the bar to the text element node, not the wrapper
Code review caught that #editable-element-{id} is a zero-size absolute
wrapper — measuring it would pin the bar to the canvas origin. Measure the
.editable-element-text child, which carries the real geometry. Also correct
the dismiss-behavior comment: the bar is purely selection-driven.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): modernize the text format bar UI
Replace the native <select> font picker with the design-system Select,
rebuild the size control as one cohesive stepper pill, swap the color "A"
for a swatch chip, and unify every control to a single height and hover/
active language (violet accent, matching the editor's Pro-mode accent).
Behavior and the text commands are unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): curate the font picker to fonts the app actually loads
configs/font.ts listed 29 fonts but the app only ever loads Inter (via
next/font); the other 28 had no @font-face or bundled file, so picking them
silently fell back with no visible effect — and nothing but the format bar
even imports the registry. Trim it to what genuinely renders; the file's
comment records how to restore the rest (wire up font loading first).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): load the picker fonts via @fontsource
The font registry listed 29 fonts the app never loaded. Wire up a curated
set that genuinely renders — 思源黑/宋, 霞鹜文楷, 站酷快乐体, and 9 Latin
families — via @fontsource packages (npm-managed, no font binaries in the
repo; CJK faces are unicode-range-subsetted so they download lazily per
glyph range). app/editor-fonts.ts registers the @font-face CSS from the
root layout; configs/font.ts is now the real, honest 14-entry list.
The ~14 commercial decorative Chinese fonts are intentionally left out —
they need self-hosting + subsetting + a licensing review, separate work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): quote font-family names so spaced/numeric ones work
Picking a font whose family name has spaces or a trailing digit (e.g.
"Source Sans 3") threw `Failed to execute 'check' on 'FontFaceSet'` —
`document.fonts.check(\`16px ${name}\`)` needs the family quoted — and the
fontname mark's toDOM emitted an invalid unquoted `font-family`, so the
font silently never applied. Quote the family in both spots; the mark's
parseDOM already strips quotes, so the attr still round-trips clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): make the editing frame pointer-events-none
The clean editing frame is a purely visual full-size overlay, but it was
pointer-events: auto — so it masked the text element's own move cursor,
text cursor, click-to-place-caret and drag-to-move; only a thin uncovered
sliver at the edges still triggered them. The dashed BorderLines it
replaced are thin edge lines, so they never had this problem. Mark the
frame pointer-events-none; the resize/rotate handles are separate and
keep their own pointer events.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(maic-editor): drop the "font is loading" toast
With @fontsource fonts and font-display: swap, a picked font swaps in
smoothly on its own — the "Font is loading, please wait..." toast was
noise (and fired on most CJK picks while a unicode-range chunk loaded).
Remove it along with the now-unused document.fonts.check and toast import.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): move the delete action onto the anchored text bar
A text element's contextual actions now sit together on the anchored bar —
format controls + delete, hugging the element — instead of delete sitting
alone in the top-center FloatingToolbar. buildFloatingActions returns
nothing for text (its FloatingToolbar then renders null); non-text
elements still get their delete there. Delete logic is shared via a new
deleteSlideElement helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): anchor the delete action for image elements
A selected image element now gets a selection-anchored bar hugging it —
just a delete button (image replace/crop/flip stay in a later sub-PR) —
the same way text elements do. The anchoring shell is extracted out of
AnchoredTextBar into a reusable AnchoredBar, and the delete button into a
shared DeleteButton; AnchoredTextBar and the new AnchoredImageBar are thin
wrappers. useTrackedRect now measures .editable-element-text or
.editable-element-image. buildFloatingActions returns nothing for image
elements too (other element types still get their delete there).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* style(maic-editor): tighten the anchored bar padding (p-2 → p-1)
p-2 left a chunky white margin around the content — most visible on the
image bar, a lone delete button in an oversized box. p-1 (4px, the value
the FloatingToolbar used) makes both bars sit snug to their controls.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): anchor the delete bar for every element type
The selection-anchored delete bar now covers all non-text element types
(shape, line, table, chart, …), not just image — so every element's
editing chrome is anchored uniformly. AnchoredImageBar becomes the
type-agnostic AnchoredDeleteBar; useTrackedRect matches any
.editable-element-{type} content root; buildFloatingActions is dropped —
the surface no longer contributes top-center FloatingToolbar actions,
everything is on an anchored bar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): show legacy font names in the picker trigger
When a text element's `fontname` was a value not in the curated FONTS
registry (e.g. `Microsoft YaHei`, `PingFang SC`, theme defaults), the
Select couldn't match it and `<SelectValue/>` rendered a blank trigger —
both reviewers (cosarah Important, xuyuanwei678 #1) caught this. Add a
placeholder fallback so the raw family name surfaces in the trigger.
Also clean up the dead `'默认字体'` label that `text-format-bar.tsx`
overrode unconditionally: introduce an optional `labelKey` field on
`FontEntry`, use it for the default entry, and let the picker prefer
the i18n key when present — no more by-value special case.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(maic-editor): address cr minors
- `marks.ts` fontname `toDOM` rejects `"` or `\` instead of interpolating
them: a hand-crafted mark with `fontname: 'X"; background:url(...);'`
could otherwise close the quoted string and inject arbitrary CSS.
- `AnchoredBar` gains `onOpenChange` (clears the canvas selection on
Radix-initiated dismiss): silences the controlled-without-handler dev
warning, and brings back Esc / SR dismissal that our focus-outside
hardening had cut off.
- `useSyncEditingElementId` folds two `useLayoutEffect`s into one with
a cleanup; the previous unmount-only effect was structural noise.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: pin body padding-right so popovers don't reflow the page
Radix Select / Popover wrap with `react-remove-scroll`, which adds a
compensation `padding-right` to <body> when they open. Our <html>
already reserves the scrollbar gutter (`scrollbar-gutter: stable` +
`overflow-y: scroll`), so the compensation added a visible ~15px shift
on every dropdown open. Pin body's padding-right with `!important` so
the page stays still. (xuyuanwei678 review #2.)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): surface legacy font names via SelectValue children
The earlier placeholder approach didn't work — Radix's `placeholder` only
fires for an empty `value`, not for an unmatched non-empty one. So an
element with a legacy fontname (e.g. `Microsoft YaHei`, `PingFang SC`,
theme defaults) outside the curated FONTS registry still rendered a blank
trigger. Render the trigger text via `SelectValue` children instead — the
new `currentFontLabel` helper covers all three cases: matched → entry's
i18n / fallback label, unmatched non-empty → the raw family name, empty
→ the default-font label. Unit tests cover each case.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): preventDefault on pointer-down-outside so drag/resize work
The onOpenChange handler added to silence the Radix dev warning + restore
Esc dismissal also fired on pointer-down-outside — i.e. on every mousedown
on the selected element to drag it or grab a resize handle. That cleared
the selection before the drag could start, so nothing on the canvas could
be moved or resized. preventDefault on `onPointerDownOutside` (matching
the existing `onFocusOutside` hardening) keeps the bar selection-driven
while leaving Esc as the legitimate onOpenChange path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): arm-and-place insertion for text boxes
Replaces the "auto-insert at a hidden default position" UX. Click
`Text box` → arms text-insertion: the button takes the violet active
style, and the renderer's existing ElementCreateSelection overlay turns
the canvas cursor into a crosshair. On the canvas:
- click → 300×60 box at the click point
- drag → a box at the dragged rect
Either way the new box is auto-selected (addElement defaults that on),
and the surface's existing useEditingTextElementId picks it up so the
AnchoredTextBar opens on it. Esc disarms; clicking the armed button
again disarms (toggle).
Completes the text branch in the renderer's `useInsertFromCreateSelection`
(pptist scaffolding left it TODO) and bypasses the 200² square fallback
in `ElementCreateSelection` for the text type (a square wouldn't suit a
text box). `InsertPaletteItem` gains an `active?` field so `CommandBar`
can render the armed style.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): render list bullets in slide text
Tailwind's preflight resets `list-style` to none, so the format bar's
`bulletList` toggle wrapped selected text in `<ul><li>` but no marker
ever appeared — the button looked inert. Scope a list-style restoration
to `.editable-element-text ul/ol/li` so bullets / numbers render in the
slide text without leaking into the rest of the app.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): editable font-size input in the text format bar
The size was a read-only `<span>` between the −/+ steppers. Replace with
an `<input type=text>` that mirrors `attrs.fontsize` locally, commits on
Enter / blur (clamped to [8, 96]; non-numeric reverts), and reverts on
Escape. Adds the `edit.text.fontSize` aria-label key in all 6 locales.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): force list markers visible (defeat preflight specificity)
The earlier list CSS didn't survive Tailwind's preflight (which also
resets `padding: 0` on `<ul>`/`<ol>`, so with `list-style-position: outside`
the markers had no room to render). Add `!important` on `list-style` and
`padding-inline-start`, and broaden to also match `.prosemirror-editor ul`/
`ol`/`li` in case the markup ever nests differently than expected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): reset richTextAttrs when the editing element changes
`richTextAttrs` is a single shared store updated by whichever ProseMirror
was last focused. Switching from one text element to another visibly
carried the previous element's toggle states (bold / italic / alignment /
list) on the format bar for a moment — until the new element's
ProseMirror took focus and repopulated the attrs. `useSyncEditingElementId`
now resets the attrs to defaults whenever the editing id changes, so the
bar shows a neutral state during the transition instead of stale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): replace OS color dialog with a curated palette popover
Clicking the text-color swatch opened the browser's native `<input type=color>`
dialog — off-brand and inconsistent across platforms. Swap it for a
`ColorPicker` popover: a 12-swatch grid covering the common slide-text needs
(4 neutrals + warm + cool) plus a hex input for anything else. Closes on
pick. Selected swatch gets the violet outline; hex input commits on Enter /
blur (reverts if invalid).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(maic-editor): replace flat swatch popover with a real color picker
The previous popover was a chunky 12-swatch grid plus a hex input nobody
types into. Rebuild on `react-colorful` (3KB, well-tested):
- SV pad + hue slider for free-form picking, with scoped CSS overrides to
keep the picker tight (128px pad height) and rounded — not stock.
- OS eyedropper via the EyeDropper API, feature-detected (Chrome / Edge;
hidden on Safari / Firefox).
- Row of 10 small (18px) common colors at the foot for one-click reach.
- Current-color preview + read-only hex display.
- Hex input dropped entirely — picking is meant to be tactile.
Live preview while dragging; the popover closes on a swatch / eyedropper
commit (not on drag).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): keep the color popover open while dragging the picker
Each SV-pad / hue-slider drag tick fires onChange → dispatches the color
command → `editorView.focus()` pulls focus out of the popover into
ProseMirror. Radix's default onFocusOutside path was treating that as a
dismiss, so the popover closed the instant a drag started — clicking
anywhere on the picker shut it. preventDefault on `onFocusOutside`
(mirrors the AnchoredBar hardening) keeps it open; the popover still
closes on swatch / eyedropper commits and on outside-click / Esc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): scope body padding override + gate ColorPicker mid-drag sync
Two follow-ups from a self-CR on the branch:
- `body { padding-right: 0 !important }` was global, overriding Radix's
`react-remove-scroll` compensation for every Dialog / Sheet / Select /
Popover across the app. Scope it to a `body[data-maic-editor='true']`
selector; `SlideCanvas` sets the attribute while mounted. Non-editor
pages get Radix's default behavior back.
- `ColorPicker`'s `useEffect(() => setColor(value), [value])` mirror
could race a stale `value` against the user's current pointer position
mid-drag — a single late round-trip would snap the picker back. Gate
the re-sync on `isDragging.current` (cleared on `pointerup`); external
commits (swatch / eyedropper) still sync immediately because they fire
while no drag is in flight.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(maic-editor): polish from self-CR
- Gate the `richTextAttrs` reset in `useSyncEditingElementId` to only
fire on element-to-element transitions (track previous editing id via
a ref). The unconditional reset on the first selection briefly flashed
neutral defaults (color #000, fontsize 16px) before the focusing
ProseMirror repopulated the real values.
- Doc-comment the text-insertion add-element asymmetry: text uses the
renderer's `addElement` (because the rect math lives there and we get
auto-select for free), image uses surface-side `applyOp` (its source
is the ImagePicker, not a canvas gesture). Both commit through the
same store, but the text lane doesn't show as a typed `element.add`
op in the session history — acceptable, now explicit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(maic-editor): listen to every gesture-end channel in ColorPicker
CR round-2 residual nit: the single `pointerup` listener that clears the
drag-gate would silently keep the gate stuck on any browser / emulator
that only emits the older mouse/touch families. Listen on all four
(`mouseup`, `touchend`, `pointerup`, `pointercancel`) — belt-and-suspenders,
no behavior change on the common path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(maic-editor): preserve image aspect ratio on insert
`createDefaultImageElement` hardcoded the new image's box to 360×220, so
anything not ~1.6:1 (which is almost everything users upload — photos,
screenshots, logos) ended up squashed or stretched the moment it landed
on the slide. Wrap the factory in `insertImageElement` that measures the
source via `new Image()`, then dispatches `element.add` with dimensions
scaled to fit MAX 600×400 while preserving the natural ratio. Load
failure falls back to the factory default so insertion always succeeds.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(maic-editor): drop the now-dead addElement helper
`addElement` was only ever used by the inline image-insert which became
`insertImageElement`; text uses `armText` (toggle). PPTElement-typed
parameter was already unused after the text refactor — removing the dead
helper resolves the lint warning.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(maic-editor): drop now-unused PPTElement import in use-slide-surface
After `addElement` was dropped (
|