Skip to content

feat: refine presentation mode speech bubbles, input flow, and accessibility#195

Merged
wyuc merged 12 commits intoTHU-MAIC:mainfrom
YizukiAme:feat/presentation-mode-refinements
Mar 24, 2026
Merged

feat: refine presentation mode speech bubbles, input flow, and accessibility#195
wyuc merged 12 commits intoTHU-MAIC:mainfrom
YizukiAme:feat/presentation-mode-refinements

Conversation

@YizukiAme
Copy link
Copy Markdown
Contributor

@YizukiAme YizukiAme commented Mar 21, 2026

Summary

Adds transient popup speech bubbles and a redesigned user interaction overlay for presentation mode, building on the fullscreen infrastructure from #133.

In fullscreen, the fixed 192px Roundtable strip is replaced with floating UI elements that overlay the slide canvas, following the UX direction discussed in #133 (comment):

  • Bottom-left corner — teacher / current-speaker bubble with avatar, name, and role color
  • Bottom-right corner — student agent / user bubble + a floating action dock for text/voice input
  • Bubbles slide in when speech starts and transition out when speech ends; new messages from the same role replace the previous bubble in-place

(Demo video also posted on Lark/Feishu.)
For some reason I wasn't able to post the video here.

Changes

Speech Bubble Overlay

Feature Detail
New component PresentationSpeechOverlay — dual-instance overlay (left + right side) that renders role-colored frosted-glass bubble cards
Bubble model buildPresentationBubbleModel() — pure function that maps PlaybackView → bubble props (role, avatar, name, text, loading state)
Left bubble Teacher speech — positioned bottom-left, slides in from the left
Right bubble Student / user speech — positioned bottom-right above the action dock, slides in from the right
Animations AnimatePresence + motion.div with slide + fade transitions; loading state shows animated dots
Phase gating Bubbles only appear during lecturePlaying, lecturePaused, discussionActive, or discussionPaused phases

User Input Flow (Presentation Mode)

Feature Detail
Floating action dock Bottom-right pill with mic, text, and avatar buttons — auto-shows on interaction or cue, auto-hides on idle
Text input panel Frosted-glass rounded input bar (center-bottom), opens above the dock with AnimatePresence
Voice input panel Waveform-bar visualizer + mic button, replaces the standard roundtable voice UI
"Your Turn" cue Pulsing amber prompt that opens the appropriate input mode (ASR or text) on click
Click-outside dismiss Transparent backdrop dismisses input/voice panels when clicking outside
Director thinking Animated dots indicator shown center-bottom when the AI director is processing

Accessibility & Input Improvements

  • Early user message clearing — user's bubble clears as soon as the agent starts responding, preventing stale text from lingering
  • Voice→text toggle safety — toggling to text input now properly stops an active recording before closing the voice panel (prevents orphaned recording sessions)
  • Interaction state tracking — new onPresentationInteractionChange callback keeps the parent Stage informed when the user is actively typing/recording, so idle-hide correctly suspends during interaction
  • Discussion card in fullscreenProactiveCard is anchored to the floating dock instead of the standard roundtable, with agent avatar and color resolved from the agent registry

Toolbar

  • Added gap-2 spacing between toolbar items for better touch targets
  • Fullscreen toggle button (Maximize2/Minimize2) is now wired through CanvasToolbarCanvasAreaStage

i18n

New keys in lib/i18n/stage.ts (zh-CN + en-US):

  • stage.fullscreen / stage.exitFullscreen
  • roundtable.voiceInput / voiceInputDisabled / textInput / stopRecording / startRecording

Files Changed

File Change
components/roundtable/presentation-speech-overlay.tsx [NEW] Speech bubble overlay component + bubble model builder
components/roundtable/index.tsx Presentation mode branch: floating overlay layout, enriched playbackView, input/voice panels, dock, interaction state tracking
components/stage.tsx Pass isPresenting, controlsVisible, onTogglePresentation, onPresentationInteractionChange to Roundtable
components/canvas/canvas-area.tsx Pass-through presentation props
components/canvas/canvas-toolbar.tsx Fullscreen toggle button + gap-2 layout fix
lib/i18n/stage.ts New i18n strings for fullscreen and input labels

Depends On

Refs #102

Scope Notes

Per reviewer feedback on #133:

Closes #102
Closes #174

@YizukiAme YizukiAme force-pushed the feat/presentation-mode-refinements branch 2 times, most recently from 71cd3aa to ed1fdff Compare March 21, 2026 09:49
@YizukiAme YizukiAme marked this pull request as ready for review March 21, 2026 09:49
Copy link
Copy Markdown
Collaborator

@cosarah cosarah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Overall quality is solid — feature-complete, animations are polished, i18n is thorough. A few issues to address:

Issues

1. Duplicate AvatarDisplay component

  • Defined separately in both presentation-speech-overlay.tsx:33-39 and roundtable/index.tsx:88-100 with slightly different signatures (the latter accepts a className prop).
  • Suggest extracting to a shared module (e.g. components/ui/avatar-display.tsx) to avoid maintaining two copies.

2. Math.random() called during render causes animation jitter

  • In roundtable/index.tsx:619,624 (waveform bar animations), Math.random() generates new values on every re-render, making animation parameters unstable and causing visual jumps.
  • Suggest pre-generating random values with useMemo or defining a fixed array outside the component.

3. setTimeout not cleaned up on unmount

  • roundtable/index.tsx:280-282 and 299-301: setTimeout(() => setUserMessage(null), 3000) is never cleared on component unmount, which can trigger setState on an unmounted component.
  • Suggest storing the timer ID in a useRef and calling clearTimeout in a cleanup function.

4. Incomplete useEffect dependency array

  • roundtable/index.tsx:218-222: the effect reads userMessage inside its body but doesn't include it in the dependency array. Even if intentional (only fire when playbackView.sourceText or thinkingState changes), this will be flagged by react-hooks/exhaustive-deps.
  • Suggest either adding userMessage to the deps or adding an eslint-disable comment explaining the intent.

5. useAgentRegistry.getState() called during render

  • roundtable/index.tsx:439-441: Zustand's getState() is called directly in the render body. While not a Rules-of-Hooks violation, it doesn't subscribe to state changes — if the agent registry updates at runtime, this won't trigger a re-render.
  • If this is intentional for performance (agent config rarely changes), a brief comment explaining the choice would help.

6. Bubble width jitter during streaming

  • presentation-speech-overlay.tsx:127: the bubble card uses min-w-[220px] max-w-full, and the outer container caps at max-w-[min(420px,...)]. This means the bubble starts narrow and expands as text streams in, causing noticeable width fluctuation that makes the text hard to read while it's being generated.
  • The PR description only mentions slide/fade transitions — this width growth doesn't appear to be an intentional design choice.
  • Suggest changing min-w-[220px] to w-full (or setting a fixed width matching the outer max-w) so the bubble maintains a consistent width from the start.

Minor / Nits

  • roundtable/index.tsx is now 1881 lines. The presentation mode branch alone is ~360 lines. Long-term, consider extracting the presentation mode UI into its own component.
  • whiteboard-canvas.tsx fullscreen detection change (top-3 vs bottom-3) is clean and minimal.
  • canvas-toolbar.tsx gap-2 spacing and fullscreen toggle button are well done.
  • i18n keys are complete for both zh-CN and en-US — no omissions found.

Verdict

Feature implementation is complete, animation quality is high, i18n is thorough. Recommend addressing 1–6 above before merging.

@cosarah
Copy link
Copy Markdown
Collaborator

cosarah commented Mar 21, 2026

Additional feedback: Fullscreen button placement

The fullscreen toggle is currently in the right-side area next to the Chat toggle. I'd suggest moving it to the center toolbar (after the whiteboard button) instead. Here's the reasoning:

Current placement (right side, next to Chat toggle):

  • Groups it with "layout control" buttons, which makes some logical sense
  • But fullscreen is a high-frequency action that's easy to miss in the corner
  • After entering fullscreen, the exit button go to the center tool bar

Suggested placement (center toolbar, near whiteboard/auto-play):

  • The center toolbar already contains presentation-related controls (play/pause, whiteboard, speed, auto-play). Fullscreen is semantically a presentation action and belongs with them
  • Users' attention is naturally drawn to the center — better discoverability
  • Mainstream tools (Google Slides, PowerPoint, Figma) all place their fullscreen/present button in a prominent central or top-level position
  • In fullscreen mode the toolbar floats at bottom-center, so the button stays in a consistent location either way

Suggested position: right after the whiteboard button, before the right-side section.

@YizukiAme
Copy link
Copy Markdown
Contributor Author

Additional feedback: Fullscreen button placement

The fullscreen toggle is currently in the right-side area next to the Chat toggle. I'd suggest moving it to the center toolbar (after the whiteboard button) instead. Here's the reasoning:

Current placement (right side, next to Chat toggle):

  • Groups it with "layout control" buttons, which makes some logical sense
  • But fullscreen is a high-frequency action that's easy to miss in the corner
  • After entering fullscreen, the exit button go to the center tool bar

Suggested placement (center toolbar, near whiteboard/auto-play):

  • The center toolbar already contains presentation-related controls (play/pause, whiteboard, speed, auto-play). Fullscreen is semantically a presentation action and belongs with them
  • Users' attention is naturally drawn to the center — better discoverability
  • Mainstream tools (Google Slides, PowerPoint, Figma) all place their fullscreen/present button in a prominent central or top-level position
  • In fullscreen mode the toolbar floats at bottom-center, so the button stays in a consistent location either way

Suggested position: right after the whiteboard button, before the right-side section.


Thanks for the thorough review, @cosarah! All 6 issues are valid — will address them in the next commit on this PR.

Plan:

# Issue Action
1 Duplicate AvatarDisplay Extract to components/ui/avatar-display.tsx
2 Math.random() in render Pre-generate with useMemo
3 setTimeout not cleaned up Store in useRef, clear on unmount
4 Incomplete useEffect deps Add eslint-disable with intent comment
5 useAgentRegistry.getState() Add comment explaining the deliberate choice (agent config is static after generation)
6 Bubble width jitter Change min-w-[220px]w-full for consistent width

Re: fullscreen button placement — Agree with moving it to the center toolbar. The reasoning about discoverability and consistency with mainstream tools makes sense. Will update in the same commit.

Note: Since this PR already includes the base presentation mode changes from #133, I'll address all feedback here rather than splitting across PRs. Will close #133 once this is ready~

@cosarah
Copy link
Copy Markdown
Collaborator

cosarah commented Mar 21, 2026

Follow-up on review points #4 and #5

After a second look, I want to correct/soften two points from my earlier review:

#4useEffect missing userMessage in deps

My suggestion to add userMessage to the dependency array was wrong — doing so would change the semantics. The current intent is clearly "clear userMessage when the agent starts responding" (i.e. when playbackView.sourceText or thinkingState changes), not "react to every userMessage change." Adding it to deps could cause the message to be cleared immediately after being set. The omission is intentional. A brief // eslint-disable-next-line react-hooks/exhaustive-deps comment explaining the intent would be sufficient.

#5useAgentRegistry.getState() during render

This is a standard Zustand pattern for grabbing a one-time snapshot of rarely-changing data. It's fine as-is — my original comment was overly nitpicky. Feel free to ignore.

@YizukiAme
Copy link
Copy Markdown
Contributor Author

All 6 issues + the fullscreen placement suggestion have been addressed across the follow-up commits !!!!!!!!😊😊😊

Issues 1–6

# Issue Fix
1 Duplicate AvatarDisplay Extracted to components/ui/avatar-display.tsx, all 3 call sites unified
2 Math.random() during render Pre-defined VOICE_WAVE_BARS constant array + memoized component
3 setTimeout not cleared on unmount Timer stored in useRef + clearTimeout in cleanup effect
4 Incomplete useEffect deps Refactored into showLocalUserMessage() + scheduleUserMessageClear() with correct deps
5 useAgentRegistry.getState() in render Centralized to getAgentConfig() helper with comment explaining intentional non-reactivity
6 Bubble width jitter during streaming Changed to w-full + fixed-width PRESENTATION_BUBBLE_WIDTH constant

Fullscreen button

Moved from right-side (next to Chat toggle) to center toolbar, right after the whiteboard button. Consistent in both normal and fullscreen modes.

Minor / Nits

  • Component extraction: Agreed — the file is large. Opened refactor: extract presentation mode UI from roundtable/index.tsx #200 to track this as a dedicated refactor. Given the deep state sharing between presentation and normal mode, I'd like to do more research before planning the extraction to avoid silent regressions. We should really be careful with that. 🙃
  • Other nits (whiteboard detection, gap-2, i18n) — noted, thanks for the positive feedback!

Additional fixes from audit

Also addressed a few edge cases found during deep audit:

  • Ghost auto-send prevention: replaced stopRecording() with cancelRecording() in all dismiss paths (backdrop click, stage click, input toggle)
  • Voice panel error handling: close voice panel on recording failure (onError callback)
  • Concurrent recording guard: block new recording while previous transcription is still processing (isProcessing check)
  • ASR cancel error suppression: clear onerror handler in cancelRecording() to prevent spurious toast on browser abort

@YizukiAme
Copy link
Copy Markdown
Contributor Author

YizukiAme commented Mar 21, 2026

Follow-up on review points #4 and #5

After a second look, I want to correct/soften two points from my earlier review:

#4useEffect missing userMessage in deps

My suggestion to add userMessage to the dependency array was wrong — doing so would change the semantics. The current intent is clearly "clear userMessage when the agent starts responding" (i.e. when playbackView.sourceText or thinkingState changes), not "react to every userMessage change." Adding it to deps could cause the message to be cleared immediately after being set. The omission is intentional. A brief // eslint-disable-next-line react-hooks/exhaustive-deps comment explaining the intent would be sufficient.

#5useAgentRegistry.getState() during render

This is a standard Zustand pattern for grabbing a one-time snapshot of rarely-changing data. It's fine as-is — my original comment was overly nitpicky. Feel free to ignore.


For 4 and 5 — just saw your follow-up, appreciate the correction! I went ahead and addressed them anyway since the refactoring improves the code regardless:

  • 4: Refactored into showLocalUserMessage() + scheduleUserMessageClear() with correct deps, eliminating the need for an eslint-disable
  • 5: Centralized to a getAgentConfig() helper with a comment — cleaner than scattered getState() calls even if the pattern is valid

@YizukiAme YizukiAme requested a review from cosarah March 21, 2026 16:04
Copy link
Copy Markdown
Collaborator

@cosarah cosarah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! LGTM—all previous issues have been addressed.
However, given the scale of this change and the significant design implications, I need to get final confirmation from @wyuc tomorrow before we can merge.
Thanks again for your hard work and contribution!

@wyuc
Copy link
Copy Markdown
Contributor

wyuc commented Mar 22, 2026

UI Feedback

Great work on the presentation mode! Tested locally and the overall experience is solid. A few UI-level suggestions:

1. Fullscreen button placement

I see the button was moved to the center toolbar per @cosarah's earlier suggestion. I think the reasoning about discoverability makes sense, but on second thought I'd prefer keeping it on the right side — fullscreen is more of a view mode toggle than a playback action, and mainstream references (YouTube, Bilibili, Zoom) consistently place it in the bottom-right area. Having it among play/pause/speed controls feels a bit crowded. What do you both think?

2. Double-layer toolbar in light mode

In fullscreen + light mode, the outer pill container nests the inner center controls container, creating a gray-on-gray double-border effect that looks a bit heavy. Dark mode handles it better since the contrast is lower.

Suggestion: in fullscreen mode, consider removing the inner container's background/border so all buttons sit directly inside the outer pill — single visual layer.

image

3. Bubble opacity on small screens

Light-mode bubbles are quite opaque (bg-white/92, bg-violet-50/90) — on smaller screens the 420px-wide bubble could cover a significant portion of the slide. Could we try reducing light-mode opacity to ~70–75%? With backdrop-blur-xl it should still be readable. Dark mode values seem fine as-is.

4. Student agent avatar

The left side shows teacher avatar + name nicely, but when a student agent is speaking on the right side, only the user's own avatar is visible in the dock. Showing the student agent's avatar alongside the right-side bubble would make the role-dialogue feel more complete and symmetric.

@YizukiAme
Copy link
Copy Markdown
Contributor Author

All points are valid, thanks for the detailed review. Working on fixes now!

@YizukiAme YizukiAme marked this pull request as draft March 22, 2026 05:29
@YizukiAme YizukiAme marked this pull request as ready for review March 22, 2026 06:08
@YizukiAme YizukiAme marked this pull request as draft March 22, 2026 06:09
@YizukiAme YizukiAme marked this pull request as ready for review March 22, 2026 06:12
@YizukiAme
Copy link
Copy Markdown
Contributor Author

YizukiAme commented Mar 22, 2026

Hi @wyuc ! All points have been addressed:

  1. Fullscreen button → Moved to the right side alongside the chat toggle, with a divider for separation — consistent with YouTube/Bilibili/Zoom conventions
  2. Gray-on-gray double layer → Removed the inner container's background/border in fullscreen mode, now a single visual layer
  3. Bubble opacity → Reduced to ~60% in light mode so slide content stays visible. (I found 60% to be the most comfortable value after testing? But if you'd prefer 70–75%, feel free to adjust)
  4. Agent avatar → Now displayed in the right-side dock when a student agent is speaking (with a blue active ring)
  5. Full light mode adaptation → All floating elements (toolbar, dock, input/voice panels, thinking indicator, "your turn" cue, end flash) now have proper light/dark theme support. (I primarily dev in dark mode so these were initially overlooked!)

Please take another look — happy to iterate further if needed~

@YizukiAme YizukiAme force-pushed the feat/presentation-mode-refinements branch from 3272bc3 to 4f2ed80 Compare March 22, 2026 07:16
@YizukiAme YizukiAme marked this pull request as draft March 22, 2026 07:16
@YizukiAme YizukiAme force-pushed the feat/presentation-mode-refinements branch from 4f2ed80 to 5db55fd Compare March 22, 2026 07:18
@YizukiAme YizukiAme marked this pull request as ready for review March 22, 2026 07:19
@YizukiAme YizukiAme marked this pull request as draft March 22, 2026 07:25
@YizukiAme YizukiAme marked this pull request as ready for review March 22, 2026 07:28
@wyuc
Copy link
Copy Markdown
Contributor

wyuc commented Mar 22, 2026

Code Review (Round 2)

Two issues worth addressing before merge:

1. [Major] Spacebar handler conflict in presentation mode

Both Stage (line ~776-779 in stage.tsx) and Roundtable (line ~316-347 in roundtable/index.tsx) register window-level keydown handlers for Space. During presentation mode with an active discussion, pressing Space fires both — engine play/pause and discussion buffer pause/resume. The Roundtable handler calls preventDefault() but since both are window listeners, execution order depends on effect registration timing which isn't guaranteed.

Suggestion: disable Roundtable's spacebar handler when isPresenting is true, let Stage own all keyboard shortcuts in presentation mode.

2. [Major] handlePlayPause not wrapped in useCallback

In stage.tsx line ~659, handlePlayPause is a plain arrow function recreated every render. Since it's a dependency of the keyboard shortcut effect (line ~788), the keydown listener is torn down and re-attached on every render. Wrapping it in useCallback with appropriate deps would fix this.

Minor items (can be follow-up)

  • Speech bubbles missing aria-live="polite" for screen reader announcements
  • DEFAULT_TEACHER_AVATAR / DEFAULT_STUDENT_AVATAR constants duplicated across two files
  • presentationIdleTimerRef not cleared on unmount when isPresenting=false

Overall the code quality is solid — upstream coupling is low, separation is clean, and the previous round's feedback was well addressed. Just these two items to tidy up.

@YizukiAme
Copy link
Copy Markdown
Contributor Author

YizukiAme commented Mar 22, 2026

@wyuc Thanks~ Here's an update on each item:

Major #1 — Spacebar handler conflict:
Good catch on the dual window-level listeners. I agree that relying on defaultPrevented + listener registration order is fragile. However, I went with the opposite direction from your suggestion: instead of disabling Roundtable's handler in presentation mode, Stage's handler now explicitly skips Space when chatSessionType is 'qa' or 'discussion'.

The reason is that...Roundtable's buffer-level pause actually woks in presentation mode (and users expect Space to pause the discussion stream, not the engine — the engine is in live mode during discussions anyway). This makes the conflict resolution deterministic without depending on addEventListener ordering.

Major #2 — handlePlayPause useCallback: Done
Minor — aria-live="polite": Done
Minor — Avatar constants dedup: Extracted to roundtable/constants.ts
Minor — Idle timer cleanup on unmount: Done

@YizukiAme YizukiAme force-pushed the feat/presentation-mode-refinements branch from f29b1d8 to 3197794 Compare March 22, 2026 19:06
@wyuc
Copy link
Copy Markdown
Contributor

wyuc commented Mar 23, 2026

Sorry, should have caught this earlier in testing: the ProactiveCard (proactive discussion prompt) doesn't appear in presentation mode.

When a discussion trigger fires during fullscreen playback, the card that normally pops up in the roundtable area to ask "do you want to join this discussion?" is not visible. You have to exit fullscreen to see it and interact with it.

The ProactiveCard is rendered inside the dock (line ~926), and the dock visibility logic (showPresentationDock) does include !!discussionRequest, so it should theoretically show. Could be a timing issue with AnimatePresence and the anchor ref, or something else preventing the card from rendering/positioning correctly in fullscreen.

This is a functional gap since users in presentation mode would miss discussion opportunities entirely. Could you take a look?

@YizukiAme
Copy link
Copy Markdown
Contributor Author

Sorry, should have caught this earlier in testing: the ProactiveCard (proactive discussion prompt) doesn't appear in presentation mode.

When a discussion trigger fires during fullscreen playback, the card that normally pops up in the roundtable area to ask "do you want to join this discussion?" is not visible. You have to exit fullscreen to see it and interact with it.

The ProactiveCard is rendered inside the dock (line ~926), and the dock visibility logic (showPresentationDock) does include !!discussionRequest, so it should theoretically show. Could be a timing issue with AnimatePresence and the anchor ref, or something else preventing the card from rendering/positioning correctly in fullscreen.

This is a functional gap since users in presentation mode would miss discussion opportunities entirely. Could you take a look?

Brooo okay this is actually embarrassing — can't believe I missed this!🤣 The ProactiveCard is literally rendered inside the dock, the dock visibility check includes !!discussionRequest... and yet somehow I never actually tested triggering a discussion while in fullscreen. Major oversight on my part!

I'm at Starbucks right now without my machine, so I can only poke around the code on GitHub for now. From what I can see, my best guess is that it's an AnimatePresence keying issue, or the anchor ref is stale when the fullscreen layout switches...? Not quite sure about that. Will push a proper fix later once I'm back at my desk~!

@wyuc
Copy link
Copy Markdown
Contributor

wyuc commented Mar 23, 2026

Sorry, should have caught this earlier in testing: the ProactiveCard (proactive discussion prompt) doesn't appear in presentation mode.
When a discussion trigger fires during fullscreen playback, the card that normally pops up in the roundtable area to ask "do you want to join this discussion?" is not visible. You have to exit fullscreen to see it and interact with it.
The ProactiveCard is rendered inside the dock (line ~926), and the dock visibility logic (showPresentationDock) does include !!discussionRequest, so it should theoretically show. Could be a timing issue with AnimatePresence and the anchor ref, or something else preventing the card from rendering/positioning correctly in fullscreen.
This is a functional gap since users in presentation mode would miss discussion opportunities entirely. Could you take a look?

Brooo okay this is actually embarrassing — can't believe I missed this!🤣 The ProactiveCard is literally rendered inside the dock, the dock visibility check includes !!discussionRequest... and yet somehow I never actually tested triggering a discussion while in fullscreen. Major oversight on my part!

I'm at Starbucks right now without my machine, so I can only poke around the code on GitHub for now. From what I can see, my best guess is that it's an AnimatePresence keying issue, or the anchor ref is stale when the fullscreen layout switches...? Not quite sure about that. Will push a proper fix later once I'm back at my desk~!

Take your time, no worries at all~

YizukiAme and others added 10 commits March 23, 2026 21:11
…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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#102
…ibility

- 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 THU-MAIC#102
- 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
@YizukiAme YizukiAme force-pushed the feat/presentation-mode-refinements branch from 63c04ab to 13bd7fe Compare March 23, 2026 13:20
@YizukiAme
Copy link
Copy Markdown
Contributor Author

YizukiAme commented Mar 23, 2026

Sorry, should have caught this earlier in testing: the ProactiveCard (proactive discussion prompt) doesn't appear in presentation mode.
When a discussion trigger fires during fullscreen playback, the card that normally pops up in the roundtable area to ask "do you want to join this discussion?" is not visible. You have to exit fullscreen to see it and interact with it.
The ProactiveCard is rendered inside the dock (line ~926), and the dock visibility logic (showPresentationDock) does include !!discussionRequest, so it should theoretically show. Could be a timing issue with AnimatePresence and the anchor ref, or something else preventing the card from rendering/positioning correctly in fullscreen.
This is a functional gap since users in presentation mode would miss discussion opportunities entirely. Could you take a look?

Brooo okay this is actually embarrassing — can't believe I missed this!🤣 The ProactiveCard is literally rendered inside the dock, the dock visibility check includes !!discussionRequest... and yet somehow I never actually tested triggering a discussion while in fullscreen. Major oversight on my part!
I'm at Starbucks right now without my machine, so I can only poke around the code on GitHub for now. From what I can see, my best guess is that it's an AnimatePresence keying issue, or the anchor ref is stale when the fullscreen layout switches...? Not quite sure about that. Will push a proper fix later once I'm back at my desk~!

Take your time, no worries at all~

Alright, found the root cause and pushed a fix~

It wasn't an AnimatePresence or anchor ref issue. ProactiveCard uses createPortal(card, document.body), but when we enter fullscreen via stageRef.requestFullscreen(), only descendants of the fullscreen element are visible in the top layer. The card was portaling to document.body — completely outside the fullscreen context.

I Added a portalContainer prop to ProactiveCard. In presentation mode, we pass stageRef (the fullscreen container) as the portal target, so the card renders inside the top layer. Falls back to document.body for normal mode.

Changes: proactive-card.tsx, roundtable/index.tsx, stage.tsx. About 6 lines total.

Also rebased onto latest main~

@wyuc
Copy link
Copy Markdown
Contributor

wyuc commented Mar 24, 2026

ProactiveCard is showing now, nice fix! One more thing though: in presentation mode, the card pops up anchored to the dock without any indication of which agent is initiating the discussion. In normal mode this is implicit (the card appears next to the agent's avatar), but in fullscreen that context is lost.

Since the dock already has the slide-out agent avatar animation when an agent is speaking (the blue-ringed avatar that appears next to the user avatar), could we reuse that same mechanism here? When a discussion request comes in, slide out the requesting agent's avatar in the dock, so users can see who's asking before they click "Join".

The props are already being passed (agentName, agentAvatar, agentColor at line ~933-940), just need to trigger the avatar slide-out when discussionRequest is active.

@YizukiAme YizukiAme marked this pull request as draft March 24, 2026 03:32
@YizukiAme YizukiAme marked this pull request as ready for review March 24, 2026 03:53
@YizukiAme
Copy link
Copy Markdown
Contributor Author

Since the dock already has the slide-out agent avatar animation when an agent is speaking (the blue-ringed avatar that appears next to the user avatar), could we reuse that same mechanism here? When a discussion request comes in, slide out the requesting agent's avatar in the dock, so users can see who's asking before they click "Join".

The props are already being passed (agentName, agentAvatar, agentColor at line ~933-940), just need to trigger the avatar slide-out when discussionRequest is active.

Done in 7bc5a8e~ The dock now slides out the requesting agent's avatar (reusing the existing blue-ring AnimatePresence animation) when discussionRequest is active.
Minimal change: extended the condition on speakingStudent to also trigger on presentationDiscussionParticipant.

Copy link
Copy Markdown
Contributor

@wyuc wyuc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the presentation mode changes, fullscreen lifecycle, and keyboard handling. No blocking issues. Will open a follow-up issue to polish bubble interactions and ProactiveCard anchoring.

@wyuc wyuc merged commit 0533adb into THU-MAIC:main Mar 24, 2026
2 checks passed
ifishcool pushed a commit to ifishcool/Linksy that referenced this pull request Mar 24, 2026
…ibility (THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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>
ifishcool pushed a commit to ifishcool/Linksy that referenced this pull request Mar 24, 2026
…ibility (THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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>
ifishcool pushed a commit to ifishcool/Linksy that referenced this pull request Mar 24, 2026
…ibility (THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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>
jaumemir pushed a commit to jaumemir/OpenMAIC that referenced this pull request Apr 8, 2026
…ibility (THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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 THU-MAIC#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: UI界面问题 [Feature]: 希望有课程讲解的画面全屏或最大化的功能

3 participants