mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-17 05:29:46 +08:00
@openmaic/renderer@0.1.2
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
84b1907255 |
feat(video-export): service-backed MP4 render + in-app one-click export (#866) (#937)
* feat(video-export): service-backed MP4 render + in-app one-click export (#866)
Adds the last mile of classroom video export: turning the self-contained
Hyperframes project ZIP (#865) into an MP4 via an isolated render service,
one-click in-app.
- render-service/: standalone Node 22 + Chromium + FFmpeg container wrapping
@hyperframes/producer's library API. Async job model (POST /render -> 202
jobId, GET poll, GET download, DELETE cancel). Swappable JobStore /
ArtifactStore seams (in-memory + local-disk now; Redis/S3 + presigned-302
download later) so it scales horizontally without changing the HTTP contract.
Concurrency + per-user guards are config knobs.
- App integration: thin Next proxy routes under app/api/export-video/* (forward
only, no rendering) + capability probe. use-render-video.ts uploads the ZIP,
polls via runPolledTask, downloads the MP4; shared buildExportZip prefix with
the existing ZIP path. Export menu gains resolution/fps/quality selectors and
a progress bar; degrades to ZIP download when RENDER_SERVICE_URL is unset.
- docker-compose: render-service under an opt-in "video-export" profile.
- Entry is main.ts (not server.ts): the producer auto-starts its own server on
:9847 when the process entry path ends with /src/server.ts.
Verified end-to-end in the container: rendered a real 640s (10.7 min) classroom
ZIP to a valid H.264 720p + AAC MP4 (duration matches source) in ~9.6 min
(~0.9x realtime, 4-worker frame capture). Degrade path, queued-cancel + cleanup,
and per-user 429 guard all exercised. pnpm check / lint / tsc / i18n pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(video-export): global render progress store, percent+ETA UI, ring on export button (#866)
Addresses two UX issues found while driving the in-app MP4 export:
1. Progress display was raw and unfriendly (showed producer's English stage
strings like "Capturing frame 5130/19220") and had no time estimate. Now the
menu shows only "<percent>% · about <remaining> left". ETA is computed from a
recent-speed estimate (percent-per-ms over the last sample), EMA-smoothed —
which tracks the render's non-uniform pace (prep -> frame capture with a
4->1 worker drop -> encode) far better than a whole-run average, and never
shows a stale/rising ETA.
2. Switching scenes mid-render unmounted the export menu and lost the progress
(and reset the local "already rendering" ref, allowing a duplicate submit).
The whole render lifecycle now lives in a global store
(lib/store/video-render.ts), so progress survives menu close / scene switch
and duplicate submits are guarded by status. A persistent CircularProgress
ring on the export button shows live progress whether or not the menu is open.
Also fixes the progress scale: the producer reports progress as 0..100, but our
HTTP contract (and success path) is 0..1 — the service now normalizes it, so the
client no longer showed "2000%".
- lib/store/video-render.ts: new global store owning submit->poll->download,
recent-speed ETA, duplicate-submit guard.
- lib/video-export-app/use-render-video.ts: thin facade over the store.
- components/ui/circular-progress.tsx: lightweight SVG progress ring.
- components/stage/{header-controls,video-export-menu}.tsx: ring on the export
button; menu shows percent + ETA, subscribes to the store.
- render-service/src/render-manager.ts: normalize producer progress 0..100 -> 0..1.
- i18n: percent/ETA strings across all 8 locales (drops the stage-based string).
- render-service/package-lock.json: complete integrity hashes (reproducible npm ci).
Verified: ETA logic checked against the real segmented render curve (worker drop
raises ETA, encode speedup drives it to ~0); progress scale fix confirmed live
against the container (0.2 -> 20%). tsc / lint / prettier / i18n pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): persist render options in the store, not the menu component
Selecting 720p/24fps/draft, switching scenes, and reopening the export menu
showed the defaults again (1080p/30/standard). The selections lived in the
VideoExportMenu component's local state, which reset when the menu unmounted on
a scene switch — the running render still used the chosen options, but the UI
misrepresented them.
Move resolution/fps/quality into the global video-render store (with a
setOptions action). The menu now reads/writes the store, so selections survive
menu close / scene switch, and while a render runs the selectors reflect the
options that render is actually using. startRender() reads options from the
store instead of taking them as an argument.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): deployment correctness + resource/isolation controls (PR #937 review)
Addresses the blocking findings from wyuc's review. Output fidelity was fine;
these harden deployment and production resource/isolation boundaries.
#1 Compose advertised MP4 but couldn't render in prod:
- Capability now probes the service's /health (checkRenderServiceHealth), so a
configured-but-absent service reports disabled and the UI degrades to ZIP
instead of 502-ing.
- RENDER_SERVICE_URL is operator-supplied trusted config, so the proxy no longer
runs it through the SSRF guard — the one-command `docker compose --profile
video-export up` now works without globally weakening SSRF via
ALLOW_LOCAL_NETWORKS. resolveRenderServiceUrl() is now synchronous.
- Client degrades to ZIP on any failed submit (not only 501).
#2 Unbounded upload/queue (ZIP-bomb / DoS):
- unzip.ts bounds the archive via fflate's filter BEFORE decompression: entry
count, per-entry and total expanded size, and compression ratio.
- Proxy rejects oversized uploads (413) by Content-Length before forwarding.
- RenderManager enforces a global queue-depth cap (RENDER_MAX_QUEUE).
- All limits are env-tunable knobs in config.ts.
#3 Per-user guard was ineffective + admission ran after extraction:
- Identity is derived server-side (client IP) and forwarded as x-openmaic-client;
the service ignores any client-supplied userId, and the proxy strips it.
- Admission is split into reserve()/submit()/release(): the slot is reserved
BEFORE extraction, so a rejected caller never triggers a decompression.
Additional risks:
- Per-job wall-clock watchdog (RENDER_JOB_DEADLINE_MS) aborts + fails a hung
render so it can't hold a slot/scratch forever.
- Download proxy bounds only the time-to-headers, not the body stream, so large
MP4s over slow links no longer truncate.
- Client cancels the server job (DELETE) when a started render fails/times out.
- Compose puts render-service on an internal:true network (no host/internet
route), sandboxing the Chromium that runs the uploaded HTML; the export ZIP is
self-contained so no outbound is needed. README documents the standalone caveat.
Not closing #866: the smoke/golden-render CI acceptance criterion remains a
follow-up (see PR description).
Verified in-container: legal render 202; ZIP-bomb (entry-count + compression-
ratio) rejected 400 before any decompression; per-identity guard 429 with a
spoofed multipart userId ignored; reserve-before-extract leaves no scratch dir
on rejection; watchdog aborts an overrunning job and frees the slot. tsc / lint /
prettier / i18n pass; render-service tsc passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): real audio durations + burned-in subtitles (PR #937 review)
Two export-fidelity issues found in wyuc's deeper E2E:
A. Narration was scheduled from estimated durations, cutting audio off mid-
sentence and advancing the timeline early. The scheduler trusted
AudioFileRecord.duration (recorded only since #861), so the many existing
classrooms without it fell back to text-length estimates — measured 4.35s
average / 10.23s max underestimate across 47 clips. timeline-deps now probes
the real duration from each narration blob via an off-document <audio>
(symmetric to the existing video probe), preferring it over the stored
duration, then the estimate only when no audio asset exists. Everything
downstream (narration starts, scene/total duration, subtitle cues) re-derives
from the corrected value in the pure compiler — no compiler change needed.
B. The final MP4 had no subtitles (only H.264+AAC), and the ZIP's SRT/VTT used
the same estimated boundaries. The emitter now renders a burned-in subtitle
overlay: one caption box + a hidden div per cue, revealed/hidden by the paused
GSAP timeline at each cue's start/end (corrected timings from A), so Chromium's
frame capture bakes them in. The producer has no subtitle track of its own, so
burn-in is the v1 approach.
Verified: emitter unit tests + snapshot updated (subtitle overlay + toggle
statements, escaped text, hidden-by-default); 82 video-export tests pass incl.
the determinism red-line proxy. Rendered a synthetic subtitle project through the
container and confirmed by pixel analysis that captions appear only within their
cue window (2429 near-white px in the caption band at t=1.5s vs 0 at t=0.05s).
tsc / lint / prettier / i18n pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): subtitle layout + upload/admission hardening (PR #937 review)
Address the three P1 blockers plus actionable P2s from the
|
||
|
|
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. |
||
|
|
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 (
|
||
|
|
38f36a6e86 |
fix(roundtable): render confirmation dialog inside fullscreen container (#304) (#305)
The scene-switch confirmation AlertDialog was portaled to document.body, making it invisible during fullscreen presentation mode. Pass the fullscreen container (stageRef) as the portal target when presenting. Closes #304 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
0533adbe82 |
feat: refine presentation mode speech bubbles, input flow, and accessibility (#195)
* feat: add presentation mode with fullscreen, idle-hide, and keyboard navigation - Fullscreen via toolbar button or F11; exit via ESC/F11/button - Header auto-hides, sidebars collapse, slide fills viewport - Idle auto-hide (3s): toolbar/avatars fade out, speech bubble stays visible - Smart suspension: idle-hide pauses during typing/recording/voice input - Keyboard navigation: Arrow keys (prev/next), Space (play/pause), ESC (exit) feat: add presentation mode with fullscreen, idle-hide, and keyboard navigation - Fullscreen via toolbar button or F11; exit via ESC/F11/button - Header auto-hides, sidebars collapse, slide fills viewport - Idle auto-hide (3s): toolbar/avatars fade out, speech bubble stays visible - Smart suspension: idle-hide pauses during typing/recording/voice input - Keyboard navigation: Arrow keys (prev/next), Space (play/pause), ESC (exit) - F11 intercepted to use Fullscreen API (ESC-friendly) instead of browser native - Whiteboard hints reposition from bottom to top corners in fullscreen - i18n: fullscreen/exitFullscreen keys (zh-CN + en-US) Closes #102 .git/COMMIT_EDITMSG [unix] (14:47 22/03/2026) 1,1 Top feat: add presentation mode with fullscreen, idle-hide, and keyboard navigation - Fullscreen via toolbar button or F11; exit via ESC/F11/button - Header auto-hides, sidebars collapse, slide fills viewport - Idle auto-hide (3s): toolbar/avatars fade out, speech bubble stays visible - Smart suspension: idle-hide pauses during typing/recording/voice input - Keyboard navigation: Arrow keys (prev/next), Space (play/pause), ESC (exit) - F11 intercepted to use Fullscreen API (ESC-friendly) instead of browser native - Whiteboard hints reposition from bottom to top corners in fullscreen - i18n: fullscreen/exitFullscreen keys (zh-CN + en-US) Closes #102 .git/COMMIT_EDITMSG [unix] (14:47 22/03/2026) 1,1 Top - F11 intercepted to use Fullscreen API (ESC-friendly) instead of browser native - Whiteboard hints reposition from bottom to top corners in fullscreen - i18n: fullscreen/exitFullscreen keys (zh-CN + en-US) Closes #102 * feat: refine presentation mode speech bubbles, input flow, and accessibility - User messages now display as right-side speech bubbles for send confirmation - 'Your turn' cue is a clickable glass button respecting ASR preference - Cue properly excludes bubbleRole/thinkingState to avoid UI overlap - Right-side dock + bubble uses flexbox for dynamic stacking - Voice-to-text switch now stops recording to prevent mic leaks - Backdrop no longer blocks toolbar (bottom-14 cutoff) - User message clears immediately when agent starts responding - Dock buttons and avatar have proper aria-labels and button semantics - Voice panel mic trigger converted from div to button with aria-label - Added 5 i18n keys (zh-CN + en-US) for new accessible labels - matchesSide typed as boolean, removed unused userAvatar from left overlay - Avatar alt text localized via i18n Refs #102 * fix: address code review audit cancel ghost sends, consolidate getAgentConfig, fix a11y * fix: guard concurrent voice recording and suppress cancel error toast * style: vertically center placeholder text in presentation input bar * style: full light-mode adaptation for presentation UI, toolbar dividers, bubble opacity 60% * chore: address #129 follow-up remove orphan i18n keys, clarify waitUntilDrained pause behavior * fix: resolve spacebar conflict and address review feedback - Stage: skip Space during active QA/discussion (let Roundtable own buffer-level pause); deterministic, no listener-order dependency - Stage: wrap handlePlayPause in useCallback([playbackCompleted, currentScene]) to avoid keyboard listener re-registration - Stage: clear presentationIdleTimerRef on unmount - PresentationBubbleCard: add aria-live='polite' for screen readers - Extract shared avatar constants to roundtable/constants.ts * style: format with prettier * fix: render ProactiveCard inside fullscreen container via portalContainer prop * feat: show requesting agent avatar in dock on discussion request --------- Co-authored-by: YizukiAme <yizukiame@github.com> |
||
|
|
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> |
||
|
|
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> |