/** * ClassroomPreviewer - 课堂预览器组件 * * 预览 classroom-generator Pipeline 生成的课堂内容: * - 幻灯片导航 * - 大纲视图 * - 场景切换 * - 全屏播放模式 * - AI 教师讲解展示 */ import { useState, useCallback, useEffect } from 'react'; import { ChevronLeft, ChevronRight, Play, Pause, Maximize, Minimize, List, Grid, Volume2, VolumeX, Download, Share2, } from 'lucide-react'; import { useToast } from './ui/Toast'; // === Types === export interface ClassroomScene { id: string; title: string; type: 'title' | 'content' | 'quiz' | 'summary' | 'interactive'; content: { heading?: string; bullets?: string[]; image?: string; explanation?: string; quiz?: { question: string; options: string[]; answer: number; }; }; narration?: string; duration?: number; // seconds } export interface ClassroomData { id: string; title: string; subject: string; difficulty: '初级' | '中级' | '高级'; duration: number; // minutes scenes: ClassroomScene[]; outline: { sections: { title: string; scenes: string[]; }[]; }; createdAt: string; } interface ClassroomPreviewerProps { data: ClassroomData; onClose?: () => void; onExport?: (format: 'pptx' | 'html' | 'pdf') => void; onOpenFullPlayer?: () => void; } // === Sub-Components === interface SceneRendererProps { scene: ClassroomScene; isPlaying: boolean; showNarration: boolean; } function SceneRenderer({ scene, showNarration }: SceneRendererProps) { const renderContent = () => { switch (scene.type) { case 'title': return (

{scene.content.heading || scene.title}

{scene.content.bullets && (

{scene.content.bullets[0]}

)}
); case 'content': return (

{scene.content.heading || scene.title}

{scene.content.bullets && ( )} {scene.content.image && (
{scene.title}
)}
); case 'quiz': return (

📝 小测验

{scene.content.quiz && (

{scene.content.quiz.question}

{scene.content.quiz.options.map((option, index) => ( ))}
)}
); case 'summary': return (

📋 总结

{scene.content.bullets && ( )}
); default: return (

{scene.title}

{scene.content.explanation}

); } }; return (
{/* Scene Content */}
{renderContent()}
{/* Narration Overlay */} {showNarration && scene.narration && (

{scene.narration}

)}
); } interface OutlinePanelProps { outline: ClassroomData['outline']; scenes: ClassroomScene[]; currentIndex: number; onSelectScene: (index: number) => void; } function OutlinePanel({ outline, scenes, currentIndex, onSelectScene, }: OutlinePanelProps) { return (

课程大纲

{outline.sections.map((section, sectionIndex) => (

{section.title}

{section.scenes.map((sceneId) => { const globalIndex = scenes.findIndex(s => s.id === sceneId); const isActive = globalIndex === currentIndex; const scene = scenes.find(s => s.id === sceneId); return ( ); })}
))}
); } // === Main Component === export function ClassroomPreviewer({ data, onExport, onOpenFullPlayer, }: ClassroomPreviewerProps) { const [currentSceneIndex, setCurrentSceneIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [showNarration, setShowNarration] = useState(true); const [showOutline, setShowOutline] = useState(true); const [isFullscreen, setIsFullscreen] = useState(false); const [viewMode, setViewMode] = useState<'slides' | 'grid'>('slides'); const { toast } = useToast(); const currentScene = data.scenes[currentSceneIndex]; const totalScenes = data.scenes.length; // Navigation const goToScene = useCallback((index: number) => { if (index >= 0 && index < totalScenes) { setCurrentSceneIndex(index); } }, [totalScenes]); const nextScene = useCallback(() => { goToScene(currentSceneIndex + 1); }, [currentSceneIndex, goToScene]); const prevScene = useCallback(() => { goToScene(currentSceneIndex - 1); }, [currentSceneIndex, goToScene]); // Auto-play useEffect(() => { if (!isPlaying) return; const duration = currentScene?.duration ? currentScene.duration * 1000 : 5000; const timer = setTimeout(() => { if (currentSceneIndex < totalScenes - 1) { nextScene(); } else { setIsPlaying(false); toast('课堂播放完成', 'success'); } }, duration); return () => clearTimeout(timer); }, [isPlaying, currentSceneIndex, currentScene, totalScenes, nextScene, toast]); // Keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'ArrowRight': case ' ': e.preventDefault(); nextScene(); break; case 'ArrowLeft': e.preventDefault(); prevScene(); break; case 'Escape': if (isFullscreen) { setIsFullscreen(false); } break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [nextScene, prevScene, isFullscreen]); // Fullscreen toggle const toggleFullscreen = () => { setIsFullscreen(!isFullscreen); }; // Export handler const handleExport = (format: 'pptx' | 'html' | 'pdf') => { if (onExport) { onExport(format); return; } // Default export implementation toast(`正在导出 ${format.toUpperCase()} 格式...`, 'info'); setTimeout(() => { try { if (format === 'html') { const htmlContent = generateClassroomHTML(data); downloadFile(htmlContent, `${data.title}.html`, 'text/html'); toast('HTML 导出成功', 'success'); } else if (format === 'pptx') { // Export as JSON for conversion const pptxData = JSON.stringify(data, null, 2); downloadFile(pptxData, `${data.title}.slides.json`, 'application/json'); toast('幻灯片数据已导出(JSON格式)', 'success'); } else if (format === 'pdf') { const htmlContent = generatePrintableHTML(data); const printWindow = window.open('', '_blank'); if (printWindow) { printWindow.document.write(htmlContent); printWindow.document.close(); printWindow.print(); toast('已打开打印预览', 'success'); } } } catch (err) { const errorMsg = err instanceof Error ? err.message : '导出失败'; toast(`导出失败: ${errorMsg}`, 'error'); } }, 300); }; return (
{/* Header */}

{data.title}

{data.subject} · {data.difficulty} · {data.duration} 分钟

{onOpenFullPlayer && ( )}
{/* Main Content */}
{/* Outline Panel */} {showOutline && (
)} {/* Slide Area */}
{/* Scene Renderer */}
{viewMode === 'slides' ? ( ) : (
{data.scenes.map((scene, index) => ( ))}
)}
{/* Control Bar */}
{/* Left Controls */}
{/* Center Controls */}
{currentSceneIndex + 1} / {totalScenes}
{/* Right Controls */}
); } export default ClassroomPreviewer; // === Helper Functions === function downloadFile(content: string, filename: string, mimeType: string) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } function generateClassroomHTML(data: ClassroomData): string { const scenesHTML = data.scenes.map((scene, index) => `

${scene.content.heading || scene.title}

${scene.content.bullets ? ` ` : ''} ${scene.narration ? `

${scene.narration}

` : ''}
`).join(''); return ` ${data.title}

${data.title}

${data.subject} · ${data.difficulty} · ${data.duration} 分钟

${scenesHTML}
`; } function generatePrintableHTML(data: ClassroomData): string { const scenesHTML = data.scenes.map((scene, index) => `

${scene.content.heading || scene.title}

${scene.content.bullets ? ` ` : ''} ${scene.narration ? `

${scene.narration}

` : ''}

${index + 1} / ${data.scenes.length}

`).join(''); return ` ${data.title} - 打印版

${data.title}

${data.subject} · ${data.difficulty} · ${data.duration} 分钟

${scenesHTML}
`; }