Skip to content

Commit f160c80

Browse files
committed
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
1 parent 639a79a commit f160c80

6 files changed

Lines changed: 425 additions & 147 deletions

File tree

components/canvas/canvas-area.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export function CanvasArea({
3737
onNextSlide,
3838
onPlayPause,
3939
onWhiteboardClose,
40+
isPresenting,
41+
onTogglePresentation,
4042
showStopDiscussion,
4143
onStopDiscussion,
4244
hideToolbar,
@@ -246,6 +248,8 @@ export function CanvasArea({
246248
onNextSlide={onNextSlide}
247249
onPlayPause={onPlayPause}
248250
onWhiteboardClose={onWhiteboardClose}
251+
isPresenting={isPresenting}
252+
onTogglePresentation={onTogglePresentation}
249253
showStopDiscussion={showStopDiscussion}
250254
onStopDiscussion={onStopDiscussion}
251255
/>

components/canvas/canvas-toolbar.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
Volume2,
1414
VolumeX,
1515
Repeat,
16+
Maximize2,
17+
Minimize2,
1618
} from 'lucide-react';
1719
import { cn } from '@/lib/utils';
1820
import { useStageStore } from '@/lib/store';
@@ -35,6 +37,8 @@ export interface CanvasToolbarProps {
3537
readonly onWhiteboardClose: () => void;
3638
readonly showStopDiscussion?: boolean;
3739
readonly onStopDiscussion?: () => void;
40+
readonly isPresenting?: boolean;
41+
readonly onTogglePresentation?: () => void;
3842
readonly className?: string;
3943
// Audio/playback controls
4044
readonly ttsEnabled?: boolean;
@@ -92,6 +96,8 @@ export function CanvasToolbar({
9296
onWhiteboardClose,
9397
showStopDiscussion,
9498
onStopDiscussion,
99+
isPresenting,
100+
onTogglePresentation,
95101
className,
96102
ttsEnabled,
97103
ttsMuted,
@@ -131,6 +137,7 @@ export function CanvasToolbar({
131137

132138
// Effective volume for display
133139
const effectiveVolume = ttsMuted ? 0 : ttsVolume;
140+
const presentationLabel = isPresenting ? t('stage.exitFullscreen') : t('stage.fullscreen');
134141

135142
return (
136143
<div className={cn('flex items-center', className)}>
@@ -382,6 +389,26 @@ export function CanvasToolbar({
382389

383390
{/* ── Right: chat toggle ── */}
384391
<div className="flex items-center justify-end gap-px shrink-0 pr-1">
392+
{onTogglePresentation && (
393+
<button
394+
onClick={onTogglePresentation}
395+
className={cn(
396+
ctrlBtn,
397+
'w-6 h-6',
398+
isPresenting
399+
? 'text-violet-600 dark:text-violet-400'
400+
: 'text-gray-600 dark:text-gray-300',
401+
)}
402+
aria-label={presentationLabel}
403+
title={presentationLabel}
404+
>
405+
{isPresenting ? (
406+
<Minimize2 className="w-3.5 h-3.5" />
407+
) : (
408+
<Maximize2 className="w-3.5 h-3.5" />
409+
)}
410+
</button>
411+
)}
385412
{onToggleChat && (
386413
<button
387414
onClick={onToggleChat}

components/roundtable/index.tsx

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ interface RoundtableProps {
7474
readonly onPrevSlide?: () => void;
7575
readonly onNextSlide?: () => void;
7676
readonly onWhiteboardClose?: () => void;
77+
readonly isPresenting?: boolean;
78+
readonly controlsVisible?: boolean;
79+
readonly onTogglePresentation?: () => void;
80+
readonly onPresentationInteractionChange?: (active: boolean) => void;
7781
}
7882

7983
const DEFAULT_TEACHER_AVATAR = '/avatars/teacher.png';
@@ -131,6 +135,10 @@ export function Roundtable({
131135
onPrevSlide,
132136
onNextSlide,
133137
onWhiteboardClose,
138+
isPresenting,
139+
controlsVisible,
140+
onTogglePresentation,
141+
onPresentationInteractionChange,
134142
}: RoundtableProps) {
135143
const { t } = useI18n();
136144
const ttsMuted = useSettingsStore((s) => s.ttsMuted);
@@ -308,6 +316,18 @@ export function Roundtable({
308316
}
309317
};
310318

319+
const isPresentationInteractionActive = isInputOpen || isVoiceOpen || isRecording || isProcessing;
320+
321+
useEffect(() => {
322+
onPresentationInteractionChange?.(isPresentationInteractionActive);
323+
324+
return () => {
325+
if (isPresentationInteractionActive) {
326+
onPresentationInteractionChange?.(false);
327+
}
328+
};
329+
}, [isPresentationInteractionActive, onPresentationInteractionChange]);
330+
311331
// Determine active speaking state and bubble ownership
312332
// Check if current speaker is a student agent (not teacher)
313333
const speakingStudent = speakingAgentId
@@ -387,46 +407,66 @@ export function Roundtable({
387407
}, [playbackSpeed, setPlaybackSpeed]);
388408

389409
return (
390-
<div className="h-[192px] w-full flex flex-col relative z-10 border-t border-gray-100 dark:border-gray-800 bg-white/60 dark:bg-gray-800/60 backdrop-blur-md">
410+
<div
411+
className={cn(
412+
'h-[192px] w-full flex flex-col relative z-10 transition-all duration-300',
413+
isPresenting && !controlsVisible
414+
? 'border-t border-transparent bg-transparent backdrop-blur-none'
415+
: 'border-t border-gray-100 dark:border-gray-800 bg-white/60 dark:bg-gray-800/60 backdrop-blur-md',
416+
)}
417+
>
391418
{/* ── Toolbar strip — merged from CanvasArea ── */}
392-
<CanvasToolbar
393-
className="shrink-0 h-8 px-3 border-b border-gray-100/40 dark:border-gray-700/30"
394-
currentSceneIndex={currentSceneIndex}
395-
scenesCount={scenesCount}
396-
engineState={
397-
engineMode === 'playing' || engineMode === 'live'
398-
? 'playing'
399-
: engineMode === 'paused'
400-
? 'paused'
401-
: 'idle'
402-
}
403-
isLiveSession={isStreaming || isTopicPending || engineMode === 'live'}
404-
whiteboardOpen={whiteboardOpen}
405-
sidebarCollapsed={sidebarCollapsed}
406-
chatCollapsed={chatCollapsed}
407-
onToggleSidebar={onToggleSidebar}
408-
onToggleChat={onToggleChat}
409-
onPrevSlide={onPrevSlide ?? (() => {})}
410-
onNextSlide={onNextSlide ?? (() => {})}
411-
onPlayPause={onPlayPause ?? (() => {})}
412-
onWhiteboardClose={onWhiteboardClose ?? (() => {})}
413-
showStopDiscussion={showStopButton}
414-
onStopDiscussion={onStopDiscussion}
415-
ttsEnabled={ttsEnabled}
416-
ttsMuted={ttsMuted}
417-
ttsVolume={ttsVolume}
418-
onToggleMute={() => ttsEnabled && setTTSMuted(!ttsMuted)}
419-
onVolumeChange={(v) => setTTSVolume(v)}
420-
autoPlayLecture={autoPlayLecture}
421-
onToggleAutoPlay={() => setAutoPlayLecture(!autoPlayLecture)}
422-
playbackSpeed={playbackSpeed}
423-
onCycleSpeed={handleCycleSpeed}
424-
/>
425-
419+
<div
420+
className={cn(
421+
'transition-opacity duration-300',
422+
isPresenting && !controlsVisible && 'opacity-0 pointer-events-none',
423+
)}
424+
>
425+
<CanvasToolbar
426+
className="shrink-0 h-8 px-3 border-b border-gray-100/40 dark:border-gray-700/30"
427+
currentSceneIndex={currentSceneIndex}
428+
scenesCount={scenesCount}
429+
engineState={
430+
engineMode === 'playing' || engineMode === 'live'
431+
? 'playing'
432+
: engineMode === 'paused'
433+
? 'paused'
434+
: 'idle'
435+
}
436+
isLiveSession={isStreaming || isTopicPending || engineMode === 'live'}
437+
whiteboardOpen={whiteboardOpen}
438+
sidebarCollapsed={sidebarCollapsed}
439+
chatCollapsed={chatCollapsed}
440+
onToggleSidebar={onToggleSidebar}
441+
onToggleChat={onToggleChat}
442+
onPrevSlide={onPrevSlide ?? (() => {})}
443+
onNextSlide={onNextSlide ?? (() => {})}
444+
onPlayPause={onPlayPause ?? (() => {})}
445+
onWhiteboardClose={onWhiteboardClose ?? (() => {})}
446+
isPresenting={isPresenting}
447+
onTogglePresentation={onTogglePresentation}
448+
showStopDiscussion={showStopButton}
449+
onStopDiscussion={onStopDiscussion}
450+
ttsEnabled={ttsEnabled}
451+
ttsMuted={ttsMuted}
452+
ttsVolume={ttsVolume}
453+
onToggleMute={() => ttsEnabled && setTTSMuted(!ttsMuted)}
454+
onVolumeChange={(v) => setTTSVolume(v)}
455+
autoPlayLecture={autoPlayLecture}
456+
onToggleAutoPlay={() => setAutoPlayLecture(!autoPlayLecture)}
457+
playbackSpeed={playbackSpeed}
458+
onCycleSpeed={handleCycleSpeed}
459+
/>
460+
</div>
426461
{/* ── Interaction area — three-column layout ── */}
427462
<div className="flex-1 flex items-stretch min-h-0">
428463
{/* Left: Teacher identity */}
429-
<div className="w-[90px] shrink-0 flex flex-col border-r border-gray-100/50 dark:border-gray-700/50 bg-white/40 dark:bg-gray-900/40 overflow-visible relative">
464+
<div
465+
className={cn(
466+
'w-[90px] shrink-0 flex flex-col border-r border-gray-100/50 dark:border-gray-700/50 bg-white/40 dark:bg-gray-900/40 overflow-visible relative transition-opacity duration-300',
467+
isPresenting && !controlsVisible && 'opacity-0 pointer-events-none',
468+
)}
469+
>
430470
{/* Decorative Element (Top) */}
431471
<div className="absolute top-0 inset-x-0 h-16 bg-gradient-to-b from-purple-50/50 dark:from-purple-900/10 to-transparent pointer-events-none" />
432472
<div className="absolute top-3 inset-x-0 flex flex-col items-center justify-center gap-1 opacity-10 pointer-events-none">
@@ -1099,7 +1139,12 @@ export function Roundtable({
10991139
</div>
11001140

11011141
{/* Right: Participants area */}
1102-
<div className="w-[140px] shrink-0 flex flex-col py-3 border-l border-gray-100/50 dark:border-gray-700/50 bg-gray-50/30 dark:bg-gray-900/30 overflow-visible">
1142+
<div
1143+
className={cn(
1144+
'w-[140px] shrink-0 flex flex-col py-3 border-l border-gray-100/50 dark:border-gray-700/50 bg-gray-50/30 dark:bg-gray-900/30 overflow-visible transition-opacity duration-300',
1145+
isPresenting && !controlsVisible && 'opacity-0 pointer-events-none',
1146+
)}
1147+
>
11031148
{/* Companion agent avatars — horizontal row, scrollable on overflow, arrows on hover */}
11041149
<div className="flex-none relative group/scroll">
11051150
{/* Left arrow */}

0 commit comments

Comments
 (0)