import { useState, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { PanelLeftClose, PieChart, Cpu, MousePointer2, BookOpen, Globe, AlertCircle, RefreshCw, Trophy, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { ThumbnailSlide } from '@/components/slide-renderer/components/ThumbnailSlide'; import { ThumbnailInteractive } from '@/components/slide-renderer/components/ThumbnailInteractive'; import { useStageStore, useCanvasStore } from '@/lib/store'; import { useI18n } from '@/lib/hooks/use-i18n'; import type { SceneType, SlideContent, InteractiveContent } from '@/lib/types/stage'; import { PENDING_SCENE_ID } from '@/lib/store/stage'; interface SceneSidebarProps { readonly collapsed: boolean; readonly onCollapseChange: (collapsed: boolean) => void; readonly onSceneSelect?: (sceneId: string) => void; readonly onRetryOutline?: (outlineId: string) => Promise; readonly isCourseComplete?: boolean; } const DEFAULT_WIDTH = 220; const MIN_WIDTH = 170; const MAX_WIDTH = 400; export function SceneSidebar({ collapsed, onCollapseChange, onSceneSelect, onRetryOutline, isCourseComplete, }: SceneSidebarProps) { const { t } = useI18n(); const router = useNavigate(); const { scenes, currentSceneId, setCurrentSceneId, generatingOutlines, generationStatus } = useStageStore(); const failedOutlines = useStageStore.use.failedOutlines(); const viewportSize = useCanvasStore.use.viewportSize(); const viewportRatio = useCanvasStore.use.viewportRatio(); const [retryingOutlineId, setRetryingOutlineId] = useState(null); const handleRetryOutline = async (outlineId: string) => { if (!onRetryOutline) return; setRetryingOutlineId(outlineId); try { await onRetryOutline(outlineId); } finally { setRetryingOutlineId(null); } }; const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_WIDTH); const isDraggingRef = useRef(false); const handleDragStart = useCallback( (e: React.MouseEvent) => { e.preventDefault(); isDraggingRef.current = true; const startX = e.clientX; const startWidth = sidebarWidth; const handleMouseMove = (me: MouseEvent) => { const delta = me.clientX - startX; const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + delta)); setSidebarWidth(newWidth); }; const handleMouseUp = () => { isDraggingRef.current = false; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; }; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }, [sidebarWidth], ); const getSceneTypeIcon = (type: SceneType) => { const icons = { slide: BookOpen, quiz: PieChart, interactive: MousePointer2, pbl: Cpu, }; return icons[type] || BookOpen; }; const displayWidth = collapsed ? 0 : sidebarWidth; return (
{/* Drag handle */} {!collapsed && (
)}
{/* Logo Header */}
{/* Scenes List */}
{scenes.map((scene, index) => { const isActive = currentSceneId === scene.id; const Icon = getSceneTypeIcon(scene.type); const isSlide = scene.type === 'slide'; const isInteractive = scene.type === 'interactive'; const slideContent = isSlide ? (scene.content as SlideContent) : null; const interactiveContent = isInteractive ? (scene.content as InteractiveContent) : null; return (
{ if (onSceneSelect) { onSceneSelect(scene.id); } else { setCurrentSceneId(scene.id); } }} className={cn( 'group relative rounded-lg transition-all duration-200 cursor-pointer flex flex-col gap-1 p-1.5', isActive ? 'bg-purple-50 dark:bg-purple-900/20 ring-1 ring-purple-200 dark:ring-purple-700' : 'hover:bg-gray-50/80 dark:hover:bg-gray-800/50', )} > {/* Scene Header */}
{index + 1} {scene.title}
{/* Thumbnail */}
{isSlide && slideContent ? ( ) : scene.type === 'quiz' ? ( /* Quiz: question bar + 2x2 option grid */
{[0, 1, 2, 3].map((i) => (
))}
) : scene.type === 'interactive' && interactiveContent?.html ? ( /* Interactive: live iframe preview */ ) : scene.type === 'interactive' ? ( /* Interactive: browser window with chrome + content */
{[1, 2, 3].map((i) => (
))}
) : scene.type === 'pbl' ? ( /* PBL: kanban board with 3 columns */
{[0, 1, 2].map((col) => (
{Array.from({ length: col === 0 ? 3 : col === 1 ? 2 : 1, }).map((_, i) => (
))}
))}
) : ( /* Fallback */
{scene.type}
)} {isSlide && (
)}
); })} {/* Single placeholder for the next generating page (clickable) */} {generatingOutlines.length > 0 && (() => { const outline = generatingOutlines[0]; const isFailed = failedOutlines.some((f) => f.id === outline.id); const isRetrying = retryingOutlineId === outline.id; const isPaused = generationStatus === 'paused'; const isActive = currentSceneId === PENDING_SCENE_ID; return (
{ if (isFailed) return; if (onSceneSelect) { onSceneSelect(PENDING_SCENE_ID); } else { setCurrentSceneId(PENDING_SCENE_ID); } }} className={cn( 'group relative rounded-lg flex flex-col gap-1 p-1.5 transition-all duration-200', isFailed ? 'opacity-100 cursor-default' : 'cursor-pointer hover:bg-gray-50/80 dark:hover:bg-gray-800/50', !isFailed && !isActive && 'opacity-60', isActive && !isFailed && 'bg-purple-50 dark:bg-purple-900/20 ring-1 ring-purple-200 dark:ring-purple-700 opacity-100', )} > {/* Scene Header */}
{scenes.length + 1} {outline.title}
{/* Skeleton Thumbnail */}
{isFailed ? (
{onRetryOutline ? ( ) : ( )} {isRetrying ? t('generation.retryingScene') : t('stage.generationFailed')}
) : ( <>
{isPaused ? t('stage.paused') : t('stage.generating')} )}
{!isFailed && !isPaused && (
)}
); })()} {/* Course-complete placeholder (shown when outline is exhausted) */} {isCourseComplete && generatingOutlines.length === 0 && (() => { const isActive = currentSceneId === PENDING_SCENE_ID; return (
{ if (onSceneSelect) { onSceneSelect(PENDING_SCENE_ID); } else { setCurrentSceneId(PENDING_SCENE_ID); } }} className={cn( 'group relative rounded-lg flex flex-col gap-1 p-1.5 transition-all duration-200 cursor-pointer hover:bg-amber-50/60 dark:hover:bg-amber-900/10', !isActive && 'opacity-80', isActive && 'bg-amber-50 dark:bg-amber-900/20 ring-1 ring-amber-200 dark:ring-amber-700 opacity-100', )} >
{scenes.length + 1} {t('stage.courseComplete')}
{/* soft radial glow */}
{/* sparkles (subtle) */}
); })()}
{/* Spacer to push toggle button area */}
); }