import { useState, useRef, useCallback, useEffect } from 'react'; import { ChevronLeft, ChevronRight, Play, Pause, PencilLine, LayoutList, MessageSquare, Volume1, Volume2, VolumeX, Repeat, Maximize2, Minimize2, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useStageStore } from '@/lib/store'; import { useI18n } from '@/lib/hooks/use-i18n'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; export interface CanvasToolbarProps { readonly currentSceneIndex: number; readonly scenesCount: number; readonly engineState: 'idle' | 'playing' | 'paused'; readonly isLiveSession?: boolean; readonly whiteboardOpen: boolean; readonly sidebarCollapsed?: boolean; readonly chatCollapsed?: boolean; readonly onToggleSidebar?: () => void; readonly onToggleChat?: () => void; readonly onPrevSlide: () => void; readonly onNextSlide: () => void; readonly onPlayPause: () => void; readonly onWhiteboardClose: () => void; readonly showStopDiscussion?: boolean; readonly onStopDiscussion?: () => void; readonly isPresenting?: boolean; readonly onTogglePresentation?: () => void; readonly className?: string; // Audio/playback controls readonly ttsEnabled?: boolean; readonly ttsMuted?: boolean; readonly ttsVolume?: number; readonly onToggleMute?: () => void; readonly onVolumeChange?: (volume: number) => void; readonly autoPlayLecture?: boolean; readonly onToggleAutoPlay?: () => void; readonly playbackSpeed?: number; readonly onCycleSpeed?: () => void; } /* Compact control button */ const ctrlBtn = cn( 'relative w-7 h-7 rounded-md flex items-center justify-center', 'transition-all duration-150 outline-none cursor-pointer', 'hover:bg-gray-500/[0.08] dark:hover:bg-gray-400/[0.08] active:scale-90', ); /* Subtle separator */ function CtrlDivider() { return
; } /* Volume icon based on level */ function VolumeIcon({ muted, volume, disabled, }: { muted: boolean; volume: number; disabled: boolean; }) { const cls = 'w-3.5 h-3.5'; if (disabled || muted || volume === 0) return ; if (volume < 0.5) return ; return ; } export function CanvasToolbar({ currentSceneIndex, scenesCount, engineState, isLiveSession, whiteboardOpen, sidebarCollapsed, chatCollapsed, onToggleSidebar, onToggleChat, onPrevSlide, onNextSlide, onPlayPause, onWhiteboardClose, showStopDiscussion, onStopDiscussion, isPresenting, onTogglePresentation, className, ttsEnabled, ttsMuted, ttsVolume = 1, onToggleMute, onVolumeChange, autoPlayLecture, onToggleAutoPlay, playbackSpeed = 1, onCycleSpeed, }: CanvasToolbarProps) { const { t } = useI18n(); const canGoPrev = currentSceneIndex > 0; const canGoNext = currentSceneIndex < scenesCount - 1; const showPlayPause = !isLiveSession; const whiteboardElementCount = useStageStore( (s) => s.stage?.whiteboard?.[0]?.elements?.length || 0, ); // Volume slider hover state const [volumeHover, setVolumeHover] = useState(false); const volumeTimerRef = useRef>(undefined); const volumeContainerRef = useRef(null); const handleVolumeEnter = useCallback(() => { clearTimeout(volumeTimerRef.current); setVolumeHover(true); }, []); const handleVolumeLeave = useCallback(() => { volumeTimerRef.current = setTimeout(() => setVolumeHover(false), 300); }, []); // Cleanup volume hover timer on unmount useEffect(() => () => clearTimeout(volumeTimerRef.current), []); // Effective volume for display const effectiveVolume = ttsMuted ? 0 : ttsVolume; const presentationLabel = isPresenting ? t('stage.exitFullscreen') : t('stage.fullscreen'); return (
{/* ── Left: sidebar toggle + page indicator ── */}
{onToggleSidebar && ( )} {currentSceneIndex + 1} / {scenesCount}
{/* ── Center: unified playback controls ── */}
{/* Volume with vertical popover slider */} {onToggleMute && (
{/* Vertical volume slider (pops up above) */}
{Math.round(effectiveVolume * 100)} { const v = parseFloat(e.target.value); onVolumeChange?.(v); if (v > 0 && ttsMuted) onToggleMute?.(); }} className={cn( 'appearance-none cursor-pointer', 'h-16 w-1 rounded-full', 'bg-gray-200 dark:bg-gray-600', '[writing-mode:vertical-lr] [direction:rtl]', '[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3', '[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-violet-500 [&::-webkit-slider-thumb]:dark:bg-violet-400', '[&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:cursor-pointer', '[&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3', '[&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-violet-500 [&::-moz-range-thumb]:border-0', )} />
{/* Arrow pointing down */}
)} {/* Speed */} {onCycleSpeed && ( {t('roundtable.speed')} )} {/* Prev scene */} {scenesCount > 1 && ( )} {/* Play / Pause / Stop Discussion */} {showStopDiscussion && onStopDiscussion ? ( ) : showPlayPause ? ( ) : null} {/* Next scene */} {scenesCount > 1 && ( )} {/* Auto-play */} {onToggleAutoPlay && ( {autoPlayLecture ? t('roundtable.autoPlayOff') : t('roundtable.autoPlay')} )} {/* Whiteboard */}
{/* ── Right: fullscreen + chat toggle ── */}
{onTogglePresentation && ( )} {onToggleChat && ( )}
); }