/** * Scaffo.ai - React Components * All reusable UI components. * Loaded via Babel standalone - uses var declarations for global scope. * * Dependencies: React, ReactDOM, auth.js, api.js, utils.js */ // React hooks (var for global scope in Babel standalone) var { useState, useEffect, useRef, useCallback } = React; var DarkModeToggle = () => { const [isDark, setIsDark] = useState(false); useEffect(() => { document.documentElement.classList.toggle('dark', isDark); }, [isDark]); return ( ); }; // Sidebar with embedded Recent Generations var Sidebar = ({ onNewChat, onOpenLibrary, recentGenerations, onSelectGeneration, onDeleteGeneration, currentGenerationId, currentUser, onLogout }) => { const [isRecentCollapsed, setIsRecentCollapsed] = useState(false); const formatDate = (dateStr) => { const date = new Date(dateStr); const now = new Date(); const diff = now - date; const mins = Math.floor(diff / (1000 * 60)); if (mins < 1) return 'Just now'; if (mins < 60) return mins + 'm ago'; const hours = Math.floor(mins / 60); if (hours < 24) return hours + 'h ago'; return date.toLocaleDateString(); }; return ( ); }; // Template Library View (Full Page) var TemplateLibraryView = ({ onBack, onSelectTemplate }) => { const [search, setSearch] = useState(''); const [selectedCategory, setSelectedCategory] = useState('all'); const categories = ['all', 'Science', 'Math', 'ELA', 'Social Studies', 'Custom']; const templates = [ { id: 1, title: '5E Lesson Model', subject: 'Science', category: 'Science', description: 'Engage, Explore, Explain, Elaborate, Evaluate - Perfect for inquiry-based science lessons', icon: 'science', color: 'emerald' }, { id: 2, title: 'Workshop Model', subject: 'ELA', category: 'ELA', description: 'Mini-lesson, Independent Work, Share - Great for reading and writing workshops', icon: 'menu_book', color: 'blue' }, { id: 3, title: 'Problem-Based Learning', subject: 'Math', category: 'Math', description: 'Launch, Explore, Discuss - Student-centered math problem solving', icon: 'calculate', color: 'purple' }, { id: 4, title: 'Socratic Seminar', subject: 'Social Studies', category: 'Social Studies', description: 'Question-driven discussion for critical thinking', icon: 'forum', color: 'amber' }, { id: 5, title: 'Flipped Classroom', subject: 'General', category: 'Custom', description: 'Pre-learning at home, active learning in class', icon: 'swap_horiz', color: 'rose' }, { id: 6, title: 'Station Rotation', subject: 'General', category: 'Custom', description: 'Multiple learning stations with timed rotations', icon: 'hub', color: 'cyan' }, ]; const filteredTemplates = templates.filter(t => { const matchesSearch = !search || t.title.toLowerCase().includes(search.toLowerCase()) || t.description.toLowerCase().includes(search.toLowerCase()); const matchesCategory = selectedCategory === 'all' || t.category === selectedCategory; return matchesSearch && matchesCategory; }); const colorClasses = { emerald: 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400', blue: 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400', purple: 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400', amber: 'bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400', rose: 'bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-400', cyan: 'bg-cyan-50 dark:bg-cyan-900/20 text-cyan-600 dark:text-cyan-400', }; return (
{/* Header */}
library_books

Template Library

Reusable lesson frameworks

{/* Content */}
{/* Search & Filter */}
search setSearch(e.target.value)} className="w-full pl-10 pr-4 py-3 rounded-xl border border-gray-200 dark:border-gray-600 bg-white dark:bg-surface-dark focus:outline-none focus:ring-2 focus:ring-primary/50 dark:text-white" />
{categories.map(cat => ( ))}
{/* Templates Grid */}
{filteredTemplates.map(tpl => (
onSelectTemplate(tpl)} className="group p-5 rounded-2xl bg-white dark:bg-surface-dark border border-gray-200 dark:border-gray-700 hover:border-primary/50 hover:shadow-lg transition-all cursor-pointer" >
{tpl.icon}

{tpl.title}

{tpl.subject}

{tpl.description}

Use Template arrow_forward
))}
{/* Create Custom Template */}
add_circle

Create Custom Template

Save your lesson blueprint as a reusable template

); }; // Chat Message // tryParseStreamingBlueprint - defined in js/utils.js // blueprintToMarkdown - defined in js/utils.js // ContentEditable component that doesn't cause cursor jump var ContentEditableDiv = ({ contentRef, initialHtml, onInput, isEditing, resetKey, isReadOnly }) => { var localRef = useRef(null); var hasInitialized = useRef(false); var lastInitialHtml = useRef(initialHtml); // Assign ref React.useEffect(function() { if (localRef.current && contentRef) { contentRef.current = localRef.current; } }, [contentRef]); // Set initial content only once React.useEffect(function() { if (localRef.current && !hasInitialized.current) { localRef.current.innerHTML = initialHtml; hasInitialized.current = true; lastInitialHtml.current = initialHtml; } }, []); // Reset only when initialHtml actually changes (e.g., loading history version) // Not when isEditing changes React.useEffect(function() { if (localRef.current && initialHtml !== lastInitialHtml.current) { localRef.current.innerHTML = initialHtml; lastInitialHtml.current = initialHtml; } }, [initialHtml]); // Force reset when resetKey changes (for explicit resets like loading history) React.useEffect(function() { if (localRef.current && resetKey) { localRef.current.innerHTML = initialHtml; lastInitialHtml.current = initialHtml; } }, [resetKey]); return (
); }; // Blueprint Message - Manus Style (Compact Preview + Modal) var BlueprintMessage = ({ blueprint, onSave, onGenerate, generationId, blueprintHistory, version, updatedAt, isGeneratingMaterials, materialsGenerated, blueprintModifiedSinceGeneration, onBlueprintModified, isGenerationContext, isReadOnly, isHistoryVersion, historyVersion, historyTimestamp, streamingContent, isStreaming, thoughtText }) => { // Full Screen Modal - Manus Style const [showModal, setShowModal] = useState(false); // Use raw_content if available (for persisted edits), otherwise generate from structured blueprint var initialMarkdown = blueprint.raw_content || blueprintToMarkdown(blueprint); const [markdown, setMarkdown] = useState(initialMarkdown); const [hasChanges, setHasChanges] = useState(false); const [activeOutlineIdx, setActiveOutlineIdx] = useState(0); const [showDownloadMenu, setShowDownloadMenu] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [showHistoryPanel, setShowHistoryPanel] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isEditing, setIsEditing] = useState(false); const [localHistory, setLocalHistory] = useState(blueprintHistory || []); const [contentResetKey, setContentResetKey] = useState(0); var contentRef = useRef(null); var lastSavedMarkdown = useRef(initialMarkdown); // Sync local history when prop changes React.useEffect(function() { setLocalHistory(blueprintHistory || []); }, [blueprintHistory]); // History data - use local state var historyCount = localHistory.length; var currentVersion = version || 1; // Extract outline from markdown for the right sidebar (h2, h3, h4) var extractOutline = function(md) { var items = []; var lines = md.split('\n'); for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (line.match(/^####\s+(.+)$/)) { items.push({ level: 4, text: line.replace(/^####\s+/, ''), index: items.length }); } else if (line.match(/^###\s+(.+)$/)) { items.push({ level: 3, text: line.replace(/^###\s+/, ''), index: items.length }); } else if (line.match(/^##\s+(.+)$/)) { items.push({ level: 2, text: line.replace(/^##\s+/, ''), index: items.length }); } } return items; }; // Use last saved markdown for stable outline during editing var outline = extractOutline(lastSavedMarkdown.current); // Download format options var downloadFormats = [ { id: 'pdf', label: 'PDF', icon: 'picture_as_pdf', color: 'text-red-500' }, { id: 'docx', label: 'Word (DOCX)', icon: 'description', color: 'text-blue-500' }, { id: 'md', label: 'Markdown', icon: 'code', color: 'text-gray-600' } ]; // Handle blueprint download var handleDownload = function(format) { setShowDownloadMenu(false); setIsDownloading(true); fetch('/api/v1/download-blueprint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ markdown: markdown, title: blueprint.title || 'Lesson Blueprint', format: format }) }) .then(function(response) { if (!response.ok) throw new Error('Download failed'); return response.blob(); }) .then(function(blob) { var url = window.URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'lesson-blueprint.' + format; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); a.remove(); setIsDownloading(false); }) .catch(function(err) { console.error('Download error:', err); alert('Download failed. Please try again.'); setIsDownloading(false); }); }; // Close download menu when clicking outside React.useEffect(function() { var handleClickOutside = function(e) { if (!e.target.closest('.blueprint-download-dropdown')) { setShowDownloadMenu(false); } }; document.addEventListener('click', handleClickOutside); return function() { document.removeEventListener('click', handleClickOutside); }; }, []); // Handle content edit (directly in the rendered view) var handleContentChange = function() { if (contentRef.current) { // Just mark as having changes - don't update state to avoid cursor jump if (!isEditing) { setIsEditing(true); } setHasChanges(true); } }; // Get current content from editor (call this when saving) var getCurrentContent = function() { if (contentRef.current) { return contentRef.current.innerHTML; } return markdown; }; // Convert HTML back to markdown-like text var htmlToMarkdown = function(html) { var temp = document.createElement('div'); temp.innerHTML = html; var md = ''; var processNode = function(node, inList) { if (node.nodeType === Node.TEXT_NODE) { return node.textContent; } if (node.nodeType !== Node.ELEMENT_NODE) return ''; var tag = node.tagName.toLowerCase(); var text = ''; // Process children first for (var i = 0; i < node.childNodes.length; i++) { text += processNode(node.childNodes[i], tag === 'ul' || tag === 'ol'); } switch(tag) { case 'h1': return '# ' + text.trim() + '\n\n'; case 'h2': return '## ' + text.trim() + '\n\n'; case 'h3': return '### ' + text.trim() + '\n\n'; case 'h4': return '#### ' + text.trim() + '\n\n'; case 'p': return text.trim() + '\n\n'; case 'strong': case 'b': return '**' + text + '**'; case 'em': case 'i': return '*' + text + '*'; case 'li': return '- ' + text.trim() + '\n'; case 'ul': case 'ol': return text + '\n'; case 'br': return '\n'; default: return text; } }; for (var i = 0; i < temp.childNodes.length; i++) { md += processNode(temp.childNodes[i], false); } return md.replace(/\n{3,}/g, '\n\n').trim(); }; var handleSaveChanges = async function() { // Get current content from the editor var currentHtml = getCurrentContent(); // Convert HTML to markdown for storage var editedMarkdown = htmlToMarkdown(currentHtml); // Create updated blueprint with raw_content field to persist edits var updatedBlueprint = Object.assign({}, blueprint, { raw_content: editedMarkdown, _edited_at: new Date().toISOString() }); if (!generationId) { // No generation ID, just call local save with markdown (not HTML) onSave(editedMarkdown); setHasChanges(false); setIsEditing(false); // Notify that blueprint was modified if (onBlueprintModified) onBlueprintModified(); return; } setIsSaving(true); try { // Save updated blueprint with raw_content to server await api.saveBlueprint(generationId, updatedBlueprint, 'Blueprint content updated', false); onSave(editedMarkdown); // Update lastSavedMarkdown with edited content lastSavedMarkdown.current = editedMarkdown; setMarkdown(editedMarkdown); setHasChanges(false); setIsEditing(false); // Notify that blueprint was modified (allows regenerating materials) if (onBlueprintModified) onBlueprintModified(); } catch (err) { console.error('Failed to save blueprint:', err); alert('Failed to save changes. Please try again.'); } finally { setIsSaving(false); } }; // Delete a history entry var handleDeleteHistory = async function(version) { if (!generationId) return; try { await api.deleteHistoryEntry(generationId, version); // Update local state to remove the deleted entry (no page reload) setLocalHistory(function(prev) { return prev.filter(function(entry) { return entry.version !== version; }); }); } catch (err) { console.error('Failed to delete history:', err); alert('Failed to delete history entry. Please try again.'); } }; // Scroll to section when clicking outline var scrollToSection = function(idx) { setActiveOutlineIdx(idx); if (contentRef.current) { // Get all h2, h3, h4 headers in order var headers = contentRef.current.querySelectorAll('h2, h3, h4'); if (headers[idx]) { // Add highlight effect headers[idx].style.transition = 'background-color 0.3s'; headers[idx].style.backgroundColor = 'rgba(59, 130, 246, 0.1)'; headers[idx].style.borderRadius = '4px'; headers[idx].style.padding = '4px 8px'; headers[idx].style.marginLeft = '-8px'; headers[idx].scrollIntoView({ behavior: 'smooth', block: 'center' }); // Remove highlight after animation setTimeout(function() { headers[idx].style.backgroundColor = 'transparent'; }, 1500); } } }; var renderMarkdown = function(md) { if (typeof marked !== 'undefined') { return marked.parse(md); } return md; }; // Compact Preview Card in Chat return (
{isStreaming ? 'edit_note' : isHistoryVersion ? 'history' : isGenerationContext ? 'task' : 'auto_awesome'}
{isStreaming ? (isChinese(blueprint.title || '') ? '正在生成 Blueprint...' : 'Generating Blueprint...') : isHistoryVersion ? 'Blueprint v' + historyVersion : isGenerationContext ? 'Generating from Blueprint' : 'Blueprint Generated'} {isStreaming && streamingContent && ( {streamingContent.length} chars )} {historyTimestamp && ( {historyTimestamp} )}
{/* Show thinking collapsible for blueprint */} {thoughtText && !isStreaming && ( )} {/* Preview Card with Document Content - Manus Style */} {/* Note: Create Teaching Materials button moved to blueprint_ready card to avoid duplication */} {/* Regenerate button - shown when materials exist but blueprint was modified (not for read-only versions) */} {!isReadOnly && materialsGenerated && blueprintModifiedSinceGeneration && (function() { // Detect language from blueprint title var isZh = isChinese(blueprint.title || ''); return (
edit_note {isZh ? 'Blueprint 已修改' : 'Blueprint Modified'}

{isZh ? '您的课程蓝图已更新。点击重新生成以应用更改到教学材料。' : 'Your lesson blueprint has been updated. Click regenerate to apply changes to teaching materials.'}

); })()}
{showModal && (
{/* Modal Header - Manus Style */}
description

{blueprint.title}

{historyCount > 0 && ( v{currentVersion} )}

{(function() { var isZh = isChinese(blueprint.title || ''); if (isReadOnly) { return {isZh ? '只读 • 历史版本' : 'Read Only • Previous Version'}; } else { return {isZh ? '点击文本编辑' : 'Click text to edit'} • {hasChanges ? (isZh ? '未保存' : 'Unsaved changes') : (updatedAt ? (isZh ? '更新于: ' : 'Updated: ') + new Date(updatedAt).toLocaleString() : (isZh ? '最后修改: ' : 'Last modified: ') + new Date().toLocaleDateString())}; } })()}

{!isReadOnly && hasChanges && ( Unsaved )} {/* History Button - hidden in read-only mode */} {!isReadOnly && historyCount > 0 && ( )} {/* Fullscreen Toggle */} {/* Download Button with Dropdown */}
{showDownloadMenu && (
{downloadFormats.map(function(fmt) { return ( ); })}
)}
{!isReadOnly && (
{/* Save Button - Right side of header */}
)} {isReadOnly && (
lock {isChinese(blueprint.title || '') ? '只读模式' : 'Read Only'}
)}
{/* Modal Content - Manus Layout with Dot Outline on Right */}
{/* Restore Panel - Simple floating panel with original version only */} {showHistoryPanel && historyCount > 0 && (

restore Restore Original

{/* Show original version (first in history) */} {localHistory && localHistory.length > 0 && (function() { var originalEntry = localHistory[0]; var snapshotBlueprint = originalEntry.blueprint_snapshot || originalEntry.blueprint; return (
description

Original Version

Created: {new Date(originalEntry.timestamp).toLocaleDateString()}

This will restore the blueprint to its original generated state.

After restoring, click Save to confirm changes.

); })()} {(!localHistory || localHistory.length === 0) && (

No original version available

)}
)} {/* Main Document Area - Full Width, scales up in fullscreen */}
{/* Streaming indicator when blueprint is being generated */} {isStreaming && streamingContent && (
edit_note {isChinese(blueprint.title || '') ? '正在实时编写 Blueprint...' : 'Writing Blueprint in real-time...'} {streamingContent.length} chars
)} {/* Document Content - Wider padding, larger text in fullscreen */}
{/* Show streaming content if available, otherwise show editable content */} {isStreaming && streamingContent ? ( (() => { const parsed = tryParseStreamingBlueprint(streamingContent); if (parsed.success && parsed.markdown) { // Successfully parsed - render as markdown with same style as final blueprint return (
); } else { // Failed to parse - show raw JSON with nice formatting return (
hourglass_top {isChinese(blueprint.title || '') ? '正在接收数据,等待完整内容...' : 'Receiving data, waiting for complete content...'}
{streamingContent}
); } })() ) : ( )}
{/* Status indicator at bottom - hidden in read-only mode */} {!isReadOnly && hasChanges && !isStreaming && (
edit_note You have unsaved changes - click Save in the top right to save
)}
{/* Right Dot Navigation - Manus Style */} {outline.length > 0 && (
{outline.filter(function(item) { return item.level === 2; }).map(function(item, idx) { var actualIdx = outline.findIndex(function(o) { return o === item; }); var isActive = actualIdx === activeOutlineIdx; return (
); })}
)}
)}
); }; // Generating Message - Shows progress in chat // Enhanced with multi-step tracking and slide preview var GeneratingMessage = ({ phase, progress, detailLog, activityLogs, selectedPackages, completedSteps, currentStep, slidePreview }) => { // Package ID to display config mapping const packageConfig = { 'slide': { label: 'Interactive Lesson', labelZh: '互动课件', icon: 'slideshow' }, 'slides': { label: 'Interactive Lesson', labelZh: '互动课件', icon: 'slideshow' }, 'worksheet': { label: 'Student Worksheet', labelZh: '学生练习', icon: 'assignment' }, 'quiz': { label: 'Assessment Quiz', labelZh: '测验', icon: 'quiz' }, 'lesson_plan': { label: 'Lesson Plan', labelZh: '教案', icon: 'description' }, 'exit_ticket': { label: 'Exit Ticket', labelZh: '出门检测', icon: 'receipt_long' }, 'rubric': { label: 'Grading Rubric', labelZh: '评分标准', icon: 'grading' }, 'vocabulary_cards': { label: 'Vocabulary Cards', labelZh: '词汇卡片', icon: 'style' }, 'group_activity': { label: 'Group Activity', labelZh: '小组活动', icon: 'groups' } }; // Detect if phase is Chinese for localized labels var isZhPhase = phase && /[\u4e00-\u9fff]/.test(phase); // Build steps from selected packages or use default const defaultPackages = ['slide', 'worksheet', 'quiz', 'lesson_plan', 'exit_ticket']; const packagesToShow = (selectedPackages && selectedPackages.length > 0) ? selectedPackages : defaultPackages; const steps = packagesToShow.map(function(pkgId) { var config = packageConfig[pkgId] || { label: pkgId, labelZh: pkgId, icon: 'description' }; return { id: pkgId, label: isZhPhase ? config.labelZh : config.label, icon: config.icon }; }); // Use explicit step tracking if available (more reliable than pattern matching) var completedStepsSet = new Set(completedSteps || []); var currentStepId = currentStep || null; // Helper function to check if any pattern matches var matchesAny = function(patterns, text) { if (!patterns) return false; return patterns.some(function(p) { return text.includes(p); }); }; var getStepStatus = function(stepId) { // Normalize stepId for slide/slides var normalizedStepId = stepId === 'slides' ? 'slide' : stepId; // PRIORITY 1: Use explicit step tracking from SSE events (most reliable) // Check both original and normalized IDs if (completedStepsSet.has(stepId) || completedStepsSet.has(normalizedStepId)) return 'completed'; if (currentStepId === stepId || currentStepId === normalizedStepId) return 'current'; // PRIORITY 2: For parallel generation, check if this step is in the "in_progress" set // When multiple steps run in parallel, they're all "current" var isSlide = stepId === 'slide' || stepId === 'slides'; var slideCompleted = completedStepsSet.has('slide') || completedStepsSet.has('slides'); if (currentStepId === 'parallel' && !completedStepsSet.has(stepId) && !completedStepsSet.has(normalizedStepId) && !isSlide) { // During parallel generation, all non-slide steps are "current" return 'current'; } // PRIORITY 3: Fall back to pattern matching for compatibility if (!phase) return 'pending'; var phaseLower = phase.toLowerCase(); // Patterns to detect COMPLETED steps var completedPatterns = { 'slide': ['interactive lesson completed', '✓ interactive lesson', 'slide_complete', 'slides_complete', '课件完成', 'slide complete', 'slides saved', 'interactive lesson complete', 'slides saved to'], 'slides': ['interactive lesson completed', '✓ interactive lesson', 'slide_complete', 'slides_complete', '课件完成', 'slide complete', 'slides saved', 'interactive lesson complete', 'slides saved to'], 'worksheet': ['worksheet completed', '✓ student worksheet', '✓ worksheet', 'worksheet_complete', '练习完成', 'worksheet saved'], 'quiz': ['quiz completed', '✓ assessment quiz', '✓ quiz', 'quiz_complete', '测验完成', 'quiz saved'], 'exit_ticket': ['exit ticket completed', '✓ exit ticket', 'exit_ticket_complete', '出门检测完成', 'exit ticket saved'], 'lesson_plan': ['lesson plan completed', '✓ lesson plan', 'lesson_plan_complete', '教案完成', 'lesson plan saved'], 'rubric': ['rubric completed', '✓ grading rubric', '✓ rubric', 'rubric_complete', '评分标准完成', 'rubric saved'], 'vocabulary_cards': ['vocabulary cards completed', '✓ vocabulary cards', '✓ vocabulary', 'vocabulary_cards_complete', '词汇卡片完成', 'vocabulary saved'], 'group_activity': ['group activity completed', '✓ group activity', 'group_activity_complete', '小组活动完成', 'group activity saved'] }; // Patterns to detect IN-PROGRESS steps var inProgressPatterns = { 'slide': ['generating interactive', 'interactive presentation', '生成课件', '互动课件', 'generating image', 'image 1/', 'image 2/', 'image 3/', 'image 4/', 'image 5/', 'background image', 'generating background', 'interactive for slide', 'background template', 'parallel image', 'preparing to generate'], 'slides': ['generating interactive', 'interactive presentation', '生成课件', '互动课件', 'generating image', 'image 1/', 'image 2/', 'image 3/', 'image 4/', 'image 5/', 'background image', 'generating background', 'interactive for slide', 'background template', 'parallel image', 'preparing to generate'], 'worksheet': ['generating student worksheet', 'generating worksheet', '生成练习'], 'quiz': ['generating assessment quiz', 'generating quiz', '生成测验'], 'exit_ticket': ['generating exit ticket', '生成出门检测'], 'lesson_plan': ['generating lesson plan', '生成教案'], 'rubric': ['generating grading rubric', 'generating rubric', '评分标准'], 'vocabulary_cards': ['generating vocabulary cards', 'generating vocabulary', '词汇卡片'], 'group_activity': ['generating group activity', 'generating group', '小组活动'] }; // Use the order from steps (which comes from selectedPackages) var stepOrder = steps.map(function(s) { return s.id; }); var stepIdx = stepOrder.indexOf(stepId); // Check if this step is completed via pattern if (matchesAny(completedPatterns[stepId], phaseLower)) return 'completed'; // Check if this step is in progress var isThisStepInProgress = matchesAny(inProgressPatterns[stepId], phaseLower); // Check if any LATER step is in progress or completed var laterStepActive = false; for (var i = stepIdx + 1; i < stepOrder.length; i++) { var laterStep = stepOrder[i]; if (matchesAny(completedPatterns[laterStep], phaseLower) || matchesAny(inProgressPatterns[laterStep], phaseLower)) { laterStepActive = true; break; } } if (laterStepActive) return 'completed'; if (isThisStepInProgress) return 'current'; // Check if parallel generation message indicates multiple steps running if (phaseLower.includes('parallel generation') || phaseLower.includes('materials in parallel')) { // During parallel phase, mark non-slide steps as current if slide is done if (stepId !== 'slide' && completedStepsSet.has('slide')) { return 'current'; } } return 'pending'; }; // Calculate estimated time remaining based on progress // Full generation takes ~7-8 minutes based on logs var getTimeEstimate = function() { var progressNum = progress || 5; if (progressNum >= 95) return 'Almost done...'; if (progressNum >= 90) return '~30 seconds remaining'; if (progressNum >= 85) return '~1 minute remaining'; if (progressNum >= 75) return '~1-2 minutes remaining'; if (progressNum >= 60) return '~2-3 minutes remaining'; if (progressNum >= 40) return '~3-4 minutes remaining'; if (progressNum >= 20) return '~5-6 minutes remaining'; if (progressNum >= 10) return '~6-7 minutes remaining'; return '~7-8 minutes remaining'; }; return (
sync
Tess
auto_awesome Creating Teaching Materials...
{steps.map(function(step) { var status = getStepStatus(step.id); return (
{status === 'completed' ? ( check_circle ) : status === 'current' ? ( progress_activity ) : ( {step.icon} )}
{step.label} {status === 'completed' && ' ✓'}
); })}
{/* Slide Preview - Shows real-time slide structure */} {slidePreview && slidePreview.slides_preview && slidePreview.slides_preview.length > 0 && (
slideshow Slide Preview ({slidePreview.slide_count} slides) {/* Generating hint */} 生成完成后可预览
{slidePreview.slides_preview.map(function(slide, idx) { var bgColor = slidePreview.theme && slidePreview.theme.palette && slidePreview.theme.palette.primary ? slidePreview.theme.palette.primary : '#6366f1'; return ( React.createElement('div', { key: idx, className: 'slide-preview-item aspect-video rounded-lg border border-slate-200 dark:border-slate-600 overflow-hidden relative group transition-all hover:scale-105 hover:shadow-lg cursor-pointer', style: { background: 'linear-gradient(135deg, ' + bgColor + '22, ' + bgColor + '44)' }, onClick: function(e) { // Visual feedback: pulse animation on click console.log('Slide preview clicked:', slide.index); var target = e.currentTarget; target.classList.add('ring-2', 'ring-primary', 'ring-offset-1', 'scale-110'); setTimeout(function() { target.classList.remove('ring-2', 'ring-primary', 'ring-offset-1', 'scale-110'); }, 300); }, title: (slide.title || ('Slide ' + slide.index)) + ' - 正在生成中...' }, React.createElement('div', { className: 'absolute inset-0 flex flex-col items-center justify-center p-1' }, React.createElement('span', { className: 'text-xs font-bold text-slate-700 dark:text-slate-300 truncate w-full text-center' }, slide.index ), slide.has_interactive && React.createElement('span', { className: 'material-symbols-outlined text-xs text-amber-500 mt-0.5', title: 'Interactive' }, 'touch_app'), slide.has_image && React.createElement('span', { className: 'material-symbols-outlined text-xs text-blue-500 mt-0.5', title: 'Has Image' }, 'image') ), React.createElement('div', { className: 'absolute bottom-0 left-0 right-0 bg-black/50 px-1 py-0.5 opacity-0 group-hover:opacity-100 transition-opacity' }, React.createElement('p', { className: 'text-[8px] text-white truncate text-center' }, slide.title || ('Slide ' + slide.index) ) ) ) ); })}
)} {/* Activity Log - Shows detailed progress */} {(detailLog || (activityLogs && activityLogs.length > 0)) && (
terminal Progress Log
{activityLogs && activityLogs.slice(-10).map(function(log, idx) { return ( React.createElement('p', { key: idx, className: 'text-xs text-slate-400 leading-relaxed py-0.5' }, React.createElement('span', { className: 'text-slate-600 mr-2' }, '▸'), log ) ); })} {detailLog && ( React.createElement('p', { className: 'text-xs text-emerald-400 leading-relaxed py-0.5 animate-pulse' }, React.createElement('span', { className: 'text-emerald-600 mr-2' }, '→'), detailLog ) )}
)}

{getTimeEstimate()}

); }; // Package Message - Shows completed package in chat var PackageMessage = ({ pkg, isHistoryVersion, historyVersion, historyTimestamp }) => { var files = (pkg && pkg.files) || []; var slidesFile = files.find(function(f) { return f.type === 'slide'; }); var downloadAllUrl = slidesFile && slidesFile.download_url; // Detect if package title contains Chinese characters to determine language var isChinese = pkg && pkg.title && /[\u4e00-\u9fff]/.test(pkg.title); // State for preview modal var [previewFile, setPreviewFile] = React.useState(null); var [showDownloadMenu, setShowDownloadMenu] = React.useState(false); var [showDownloadAllMenu, setShowDownloadAllMenu] = React.useState(false); var [isPreviewFullscreen, setIsPreviewFullscreen] = React.useState(false); var [currentSlideIndex, setCurrentSlideIndex] = React.useState(0); var [slideNotes, setSlideNotes] = React.useState({}); // { slideIndex: noteText } var [isDragEditMode, setIsDragEditMode] = React.useState(false); var [slideLayouts, setSlideLayouts] = React.useState({}); // { slideIndex: { elementId: {x, y, width, height} } } var [currentPreviewGenId, setCurrentPreviewGenId] = React.useState(null); var [pendingSlideEdits, setPendingSlideEdits] = React.useState({}); // { slideIndex: { field: value } } var [isSavingSlides, setIsSavingSlides] = React.useState(false); var [slidesSaveStatus, setSlidesSaveStatus] = React.useState(null); // 'success' | 'error' | null // Extract generation ID from preview URL var getGenerationIdFromPreview = function() { if (!previewFile || !previewFile.preview_url) return null; // Support both URL formats: // - /output/user/genId/v1/... (with user_id) // - /view/genId/v1/... (anonymous, no user_id) var matchOutput = previewFile.preview_url.match(/\/output\/[^\/]+\/([^\/]+)/); if (matchOutput) return matchOutput[1]; var matchView = previewFile.preview_url.match(/\/view\/([^\/]+)/); return matchView ? matchView[1] : null; }; // Load notes and layouts from server when preview file changes React.useEffect(function() { if (previewFile && previewFile.type === 'slide') { var genId = getGenerationIdFromPreview(); if (genId && genId !== currentPreviewGenId) { setCurrentPreviewGenId(genId); // Load from server (slide-metadata endpoint) fetch(API_BASE + '/api/v1/slide-metadata/' + genId, { headers: getAuthHeaders() }) .then(function(res) { return res.ok ? res.json() : { slide_notes: {}, slide_layouts: {} }; }) .then(function(data) { var notes = data.slide_notes || {}; var layouts = data.slide_layouts || {}; // If no notes found, try loading from slides_data.json if (Object.keys(notes).length === 0) { console.log('📝 No saved notes found, loading from slides_data.json...'); fetch(API_BASE + '/api/v1/generations/' + genId + '/slides-data', { headers: getAuthHeaders() }) .then(function(res) { return res.ok ? res.json() : null; }) .then(function(slidesData) { if (slidesData && slidesData.slides) { var loadedNotes = {}; slidesData.slides.forEach(function(slide) { var idx = (slide.slide_index || 1) - 1; // Convert to 0-based if (slide.speaker_notes) { loadedNotes[idx] = slide.speaker_notes; } }); if (Object.keys(loadedNotes).length > 0) { console.log('✅ Loaded ' + Object.keys(loadedNotes).length + ' speaker notes from slides_data.json'); setSlideNotes(loadedNotes); } else { setSlideNotes({}); } } else { setSlideNotes({}); } }) .catch(function() { setSlideNotes({}); }); } else { setSlideNotes(notes); } setSlideLayouts(layouts); }) .catch(function() { setSlideNotes({}); setSlideLayouts({}); }); } } }, [previewFile]); // Debounced save to server var saveTimeoutRef = React.useRef(null); var saveSlideMetadata = function(notes, layouts) { var genId = getGenerationIdFromPreview(); if (!genId) return; // Clear previous timeout if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } // Debounce: save after 1 second of no changes saveTimeoutRef.current = setTimeout(function() { fetch(API_BASE + '/api/v1/save-slide-metadata', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, getAuthHeaders()), body: JSON.stringify({ generation_id: genId, slide_notes: notes, slide_layouts: layouts }) }).catch(function(e) { console.error('Failed to save slide metadata:', e); }); }, 1000); }; // Save notes when they change React.useEffect(function() { if (currentPreviewGenId && Object.keys(slideNotes).length > 0) { saveSlideMetadata(slideNotes, slideLayouts); } }, [slideNotes]); // Save layouts when they change React.useEffect(function() { if (currentPreviewGenId && Object.keys(slideLayouts).length > 0) { saveSlideMetadata(slideNotes, slideLayouts); } }, [slideLayouts]); // State for image editing var [editImageModal, setEditImageModal] = React.useState(null); // { slideIndex, currentPrompt } var [newImagePrompt, setNewImagePrompt] = React.useState(''); var [isGeneratingImage, setIsGeneratingImage] = React.useState(false); // State for document editing (worksheet, quiz, exit_ticket, lesson_plan) var [isDocEditMode, setIsDocEditMode] = React.useState(false); var [isSavingDoc, setIsSavingDoc] = React.useState(false); var [docHasChanges, setDocHasChanges] = React.useState(false); var docIframeRef = React.useRef(null); var originalDocContent = React.useRef(''); // Editable document types (note: lesson_plan is stored as 'document' type) var editableTypes = ['worksheet', 'quiz', 'exit_ticket', 'lesson_plan', 'document']; // Enable/disable edit mode in iframe var toggleIframeEditMode = function(enable) { var iframe = docIframeRef.current; if (!iframe || !iframe.contentDocument) return; try { var doc = iframe.contentDocument; if (enable) { // Store original content originalDocContent.current = doc.body.innerHTML; // Enable design mode doc.designMode = 'on'; // Add edit mode styles var style = doc.createElement('style'); style.id = 'edit-mode-styles'; style.textContent = ` body { cursor: text !important; outline: none !important; } body:focus { outline: none !important; } *:hover { outline: 2px dashed rgba(59, 130, 246, 0.5) !important; outline-offset: 2px !important; } *:focus { outline: 2px solid rgba(59, 130, 246, 0.8) !important; outline-offset: 2px !important; } `; doc.head.appendChild(style); // Listen for changes doc.body.addEventListener('input', function() { setDocHasChanges(doc.body.innerHTML !== originalDocContent.current); }); } else { // Disable design mode doc.designMode = 'off'; // Remove edit mode styles var editStyle = doc.getElementById('edit-mode-styles'); if (editStyle) editStyle.remove(); } } catch (e) { console.error('Failed to toggle edit mode:', e); } }; // Save slide content edits var saveSlideEdits = function() { var genId = getGenerationIdFromPreview(); if (!genId || Object.keys(pendingSlideEdits).length === 0) return; setIsSavingSlides(true); setSlidesSaveStatus(null); // Collect all pending edits var editsArray = []; Object.keys(pendingSlideEdits).forEach(function(slideIdx) { var fields = pendingSlideEdits[slideIdx]; Object.keys(fields).forEach(function(field) { editsArray.push({ slide_index: parseInt(slideIdx), field: field, value: fields[field] }); }); }); // Save all edits sequentially var saveNext = function(index) { if (index >= editsArray.length) { // All saves complete setPendingSlideEdits({}); setIsSavingSlides(false); setSlidesSaveStatus('success'); console.log('✅ All slide edits saved'); // Force refresh the current slide iframe to show changes setTimeout(function() { var slideIframe = document.querySelector('iframe[title*="Slide ' + (currentSlideIndex + 1) + '"]'); if (slideIframe && slideIframe.src) { var baseUrl = slideIframe.src.split('?')[0]; slideIframe.src = baseUrl + '?t=' + Date.now(); } setSlidesSaveStatus(null); }, 2000); return; } var edit = editsArray[index]; fetch(API_BASE + '/api/v1/save-slide-content', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, getAuthHeaders()), credentials: 'include', body: JSON.stringify({ generation_id: genId, slide_index: edit.slide_index, field: edit.field, value: edit.value }) }) .then(function(res) { if (!res.ok) throw new Error('Save failed: ' + res.status); return res.json(); }) .then(function(data) { console.log('Saved slide ' + edit.slide_index + ' ' + edit.field); saveNext(index + 1); }) .catch(function(err) { console.error('Failed to save slide edit:', err); setIsSavingSlides(false); setSlidesSaveStatus('error'); }); }; saveNext(0); }; // Save edited document directly from iframe var saveDocContent = function() { var iframe = docIframeRef.current; if (!iframe || !iframe.contentDocument || !previewFile) return; setIsSavingDoc(true); var genId = getGenerationIdFromPreview(); if (!genId) { setIsSavingDoc(false); return; } var newContent = iframe.contentDocument.body.innerHTML; fetch(API_BASE + '/api/v1/save-document', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, getAuthHeaders()), body: JSON.stringify({ generation_id: genId, doc_type: previewFile.type, content: newContent }) }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.success) { originalDocContent.current = newContent; setDocHasChanges(false); // Update the preview URL with cache-busting timestamp // This ensures the saved content is loaded when reopening if (previewFile.preview_url) { var baseUrl = previewFile.preview_url.split('?')[0]; previewFile.preview_url = baseUrl + '?t=' + Date.now(); } } setIsSavingDoc(false); }) .catch(function(e) { console.error('Failed to save document:', e); setIsSavingDoc(false); }); }; // Reset edit mode when closing modal or changing file React.useEffect(function() { if (!previewFile) { setIsDocEditMode(false); setDocHasChanges(false); } }, [previewFile]); // Apply edit mode when toggled React.useEffect(function() { if (previewFile && editableTypes.includes(previewFile.type)) { // Small delay to ensure iframe is loaded var timer = setTimeout(function() { toggleIframeEditMode(isDocEditMode); }, 500); return function() { clearTimeout(timer); }; } }, [isDocEditMode, previewFile]); // Listen for messages from slide iframes (edit image, generate image) React.useEffect(function() { function handleMessage(event) { var data = event.data; if (!data || !data.type) return; if (data.type === 'EDIT_IMAGE') { // Don't open edit modal in drag mode if (isDragEditMode) return; // Get the image prompt from the slide data if available var slideIndex = data.slideIndex; var prompt = data.prompt || 'A friendly educational illustration'; setEditImageModal({ slideIndex: slideIndex, currentPrompt: prompt }); setNewImagePrompt(prompt); } else if (data.type === 'SLIDE_EDIT') { // Store edit in pending state (will be saved when user clicks Save) var slideIdx = data.slideIndex; var field = data.field; var value = data.value; if (field && value !== undefined) { setPendingSlideEdits(function(prev) { var updated = Object.assign({}, prev); if (!updated[slideIdx]) updated[slideIdx] = {}; updated[slideIdx][field] = value; return updated; }); setSlidesSaveStatus(null); // Clear any previous status console.log('Slide edit pending:', slideIdx, field, value.substring(0, 50) + '...'); } } else if (data.type === 'GENERATE_IMAGE') { // Auto-generate with existing prompt var slideIdx = data.slideIndex; alert('Image generation for slide ' + (slideIdx + 1) + ' will be added soon!'); } else if (data.type === 'ELEMENT_MOVED') { // Save element position when dragged var slideIdx = data.slideIndex; var elementId = data.elementId; var position = data.position; setSlideLayouts(function(prev) { var updated = Object.assign({}, prev); if (!updated[slideIdx]) updated[slideIdx] = {}; updated[slideIdx][elementId] = position; return updated; }); } } window.addEventListener('message', handleMessage); return function() { window.removeEventListener('message', handleMessage); }; }, [isDragEditMode, previewFile]); // Extract folder info from any file's preview_url (including version if present) // Returns { folderName, isViewFormat } where: // - folderName: user_id/gen_id/version (for /output/) or gen_id/version (for /view/) // - isViewFormat: true if URL uses /view/ format (no user_id) var getFolderInfo = function() { for (var i = 0; i < files.length; i++) { var url = files[i].preview_url || files[i].download_url; if (url) { // Try /output/ format first: /output/USER_ID/GENERATION_ID/VERSION/file.html var matchOutput = url.match(/\/output\/([^\/]+\/[^\/]+\/v\d+)\//); if (matchOutput) return { folderName: matchOutput[1], isViewFormat: false }; // Try /output/ without version var matchOutputNoVer = url.match(/\/output\/([^\/]+\/[^\/]+)\//); if (matchOutputNoVer) return { folderName: matchOutputNoVer[1], isViewFormat: false }; // Try /view/ format: /view/GENERATION_ID/VERSION/file.html var matchView = url.match(/\/view\/([^\/]+\/v\d+)\//); if (matchView) return { folderName: matchView[1], isViewFormat: true }; // Try /view/ without version var matchViewNoVer = url.match(/\/view\/([^\/]+)\//); if (matchViewNoVer) return { folderName: matchViewNoVer[1], isViewFormat: true }; } } return null; }; var folderInfo = getFolderInfo(); // Download in specified format (pdf, docx, md) var handleDownload = function(file, format) { setShowDownloadMenu(false); if (!folderInfo) { alert('Download not available'); return; } // Map file type to API endpoint type var typeMap = { 'worksheet': 'worksheet', 'quiz': 'quiz', 'exit_ticket': 'exit_ticket', 'document': 'lesson_plan', 'slide': 'presentation', 'group_activity': 'group_activity', 'rubric': 'rubric', 'vocabulary_cards': 'vocabulary_cards' }; var apiType = typeMap[file.type]; if (apiType) { // Use view-download API for /view/ format URLs, regular download for /output/ var apiPath = folderInfo.isViewFormat ? '/api/v1/view-download/' : '/api/v1/download/'; var downloadUrl = apiPath + folderInfo.folderName + '/' + apiType + '/' + format; window.open(downloadUrl, '_blank'); } }; // Download all materials in a specific format var handleDownloadAll = function(format) { setShowDownloadAllMenu(false); if (!folderInfo) { alert('Download not available'); return; } // Use view-download-all API for /view/ format URLs, regular download-all for /output/ var apiPath = folderInfo.isViewFormat ? '/api/v1/view-download-all/' : '/api/v1/download-all/'; var url = apiPath + folderInfo.folderName + '/' + format; window.open(url, '_blank'); }; // Close menus when clicking outside React.useEffect(function() { var handleClickOutside = function(e) { if (!e.target.closest('.download-dropdown')) { setShowDownloadMenu(false); } if (!e.target.closest('.download-all-dropdown')) { setShowDownloadAllMenu(false); } }; document.addEventListener('click', handleClickOutside); return function() { document.removeEventListener('click', handleClickOutside); }; }, []); // Handle browser back button to close preview modal React.useEffect(function() { if (previewFile) { // Push a state when opening preview window.history.pushState({ preview: true }, ''); var handlePopState = function() { setPreviewFile(null); setIsPreviewFullscreen(false); setCurrentSlideIndex(0); }; window.addEventListener('popstate', handlePopState); return function() { window.removeEventListener('popstate', handlePopState); }; } }, [previewFile]); // Keyboard navigation for fullscreen slide preview React.useEffect(function() { if (isPreviewFullscreen && previewFile && previewFile.type === 'slide' && previewFile.slide_urls) { var handleKeyDown = function(e) { var totalSlides = previewFile.slide_urls.length; if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { setCurrentSlideIndex(function(prev) { return Math.min(prev + 1, totalSlides - 1); }); } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { setCurrentSlideIndex(function(prev) { return Math.max(prev - 1, 0); }); } else if (e.key === 'Home') { setCurrentSlideIndex(0); } else if (e.key === 'End') { setCurrentSlideIndex(totalSlides - 1); } }; window.addEventListener('keydown', handleKeyDown); return function() { window.removeEventListener('keydown', handleKeyDown); }; } }, [isPreviewFullscreen, previewFile]); // Apply saved layout when slide changes or edit mode is toggled React.useEffect(function() { if (isPreviewFullscreen && previewFile && previewFile.type === 'slide' && isDragEditMode) { // Small delay to let iframe load var timer = setTimeout(function() { var layout = slideLayouts[currentSlideIndex] || {}; var iframes = document.querySelectorAll('iframe'); iframes.forEach(function(iframe) { try { iframe.contentWindow.postMessage({ type: 'APPLY_LAYOUT', layout: layout }, '*'); } catch(e) {} }); }, 300); return function() { clearTimeout(timer); }; } }, [currentSlideIndex, isDragEditMode, slideLayouts, isPreviewFullscreen, previewFile]); // Reset edit mode when closing preview or exiting fullscreen React.useEffect(function() { if (!isPreviewFullscreen || !previewFile) { setIsDragEditMode(false); } }, [isPreviewFullscreen, previewFile]); // Download format options var downloadFormats = [ { id: 'pdf', label: 'PDF', icon: 'picture_as_pdf', color: 'text-red-500' }, { id: 'docx', label: 'Word (DOCX)', icon: 'description', color: 'text-blue-500' }, { id: 'md', label: 'Markdown', icon: 'code', color: 'text-gray-600' } ]; // Material type configurations with labels (English and Chinese) var typeConfig = { slide: { icon: 'slideshow', label: 'Interactive Slides', labelZh: '互动课件', labelShort: 'INTERACTIVE SLIDES', labelShortZh: '互动课件', descZh: '包含互动元素的课程幻灯片', bg: 'bg-amber-50 dark:bg-amber-900/20', color: 'text-amber-600 dark:text-amber-400', hoverBorder: 'hover:border-amber-300 dark:hover:border-amber-700', canDownload: false }, worksheet: { icon: 'assignment', label: 'Student Worksheet', labelZh: '学生练习', labelShort: 'STUDENT WORKSHEET', labelShortZh: '学生练习', descZh: '配套课堂练习题', bg: 'bg-blue-50 dark:bg-blue-900/20', color: 'text-blue-600 dark:text-blue-400', hoverBorder: 'hover:border-blue-300 dark:hover:border-blue-700', canDownload: true }, quiz: { icon: 'quiz', label: 'Assessment Quiz', labelZh: '课堂测验', labelShort: 'ASSESSMENT QUIZ', labelShortZh: '课堂测验', descZh: '课堂知识检测', bg: 'bg-emerald-50 dark:bg-emerald-900/20', color: 'text-emerald-600 dark:text-emerald-400', hoverBorder: 'hover:border-emerald-300 dark:hover:border-emerald-700', canDownload: true }, exit_ticket: { icon: 'receipt_long', label: 'Exit Ticket', labelZh: '出门检测', labelShort: 'EXIT TICKET', labelShortZh: '出门检测', descZh: '课程结束快速评估', bg: 'bg-teal-50 dark:bg-teal-900/20', color: 'text-teal-600 dark:text-teal-400', hoverBorder: 'hover:border-teal-300 dark:hover:border-teal-700', canDownload: true }, lesson_plan: { icon: 'description', label: 'Lesson Plan', labelZh: '教案', labelShort: 'LESSON PLAN', labelShortZh: '教案', descZh: '包含详细教学步骤的综合教案', bg: 'bg-rose-50 dark:bg-rose-900/20', color: 'text-rose-600 dark:text-rose-400', hoverBorder: 'hover:border-rose-300 dark:hover:border-rose-700', canDownload: true }, document: { icon: 'description', label: 'Lesson Plan', labelZh: '教案', labelShort: 'LESSON PLAN', labelShortZh: '教案', descZh: '包含详细教学步骤的综合教案', bg: 'bg-rose-50 dark:bg-rose-900/20', color: 'text-rose-600 dark:text-rose-400', hoverBorder: 'hover:border-rose-300 dark:hover:border-rose-700', canDownload: true }, rubric: { icon: 'grading', label: 'Grading Rubric', labelZh: '评分标准', labelShort: 'GRADING RUBRIC', labelShortZh: '评分标准', descZh: '作业评分参考标准', bg: 'bg-purple-50 dark:bg-purple-900/20', color: 'text-purple-600 dark:text-purple-400', hoverBorder: 'hover:border-purple-300 dark:hover:border-purple-700', canDownload: true }, vocabulary_cards: { icon: 'style', label: 'Vocabulary Cards', labelZh: '词汇卡片', labelShort: 'VOCABULARY CARDS', labelShortZh: '词汇卡片', descZh: '核心词汇学习卡片', bg: 'bg-indigo-50 dark:bg-indigo-900/20', color: 'text-indigo-600 dark:text-indigo-400', hoverBorder: 'hover:border-indigo-300 dark:hover:border-indigo-700', canDownload: true }, group_activity: { icon: 'groups', label: 'Group Activity', labelZh: '小组活动', labelShort: 'GROUP ACTIVITY', labelShortZh: '小组活动', descZh: '小组协作活动指南', bg: 'bg-orange-50 dark:bg-orange-900/20', color: 'text-orange-600 dark:text-orange-400', hoverBorder: 'hover:border-orange-300 dark:hover:border-orange-700', canDownload: true } }; return (
{isHistoryVersion ? 'history' : 'check_circle'}
Tess {isHistoryVersion && ( v{historyVersion} )}
{/* Header */}
{isHistoryVersion ? 'history' : 'check_circle'} {isHistoryVersion ? (isChinese ? '历史版本' : 'Previous Version') : (isChinese ? '材料已就绪' : 'Materials Ready')} {historyTimestamp && ( {historyTimestamp} )}

{(pkg && pkg.title) || (isChinese ? '教学资源包' : 'Teaching Package')}

{isChinese ? ('已生成 ' + files.length + ' 份材料') : (files.length + ' materials generated')}

{/* Files List - Single clickable blocks */}
{files.map(function(file, i) { var config = typeConfig[file.type] || typeConfig.document; var hasPreview = !!file.preview_url; var isSlide = file.type === 'slide'; return (
{config.icon}
{isChinese ? (config.labelShortZh || config.labelShort) : config.labelShort} {isSlide && {isChinese ? '仅支持在线预览' : 'Interactive only'}}

{isChinese ? (config.labelZh || file.title) : file.title}

{file.slide_count ? (isChinese ? (file.slide_count + ' ' + (file.type === 'slide' ? '张幻灯片' : file.type === 'quiz' ? '道题目' : '个部分')) : (file.slide_count + ' ' + (file.type === 'slide' ? 'slides' : file.type === 'quiz' ? 'questions' : 'sections'))) : (isChinese ? (config.descZh || file.description || '包含详细说明的综合教学指南') : (file.description || 'Comprehensive teaching guide with detailed instructions'))}

chevron_right
); })}
{/* Download All Button with Format Selection */}
{showDownloadAllMenu && (

Select Format

{downloadFormats.map(function(fmt) { return ( ); })}
)}

{isChinese ? '下载教案、练习等文档' : 'Download lesson plan, worksheets & more'}

{/* Version Badge - show current version number */} {pkg && pkg.version !== undefined && (
verified Version {pkg.version}
)}
{/* Preview Modal - Same page iframe preview */} {previewFile && (
{/* Preview Modal Header */}
{(typeConfig[previewFile.type] || typeConfig.document).icon}

{previewFile.title}

{(typeConfig[previewFile.type] || typeConfig.document).label}

{/* Download Button with Dropdown - Only for downloadable types */} {(typeConfig[previewFile.type] || typeConfig.document).canDownload && (
{showDownloadMenu && (
{downloadFormats.map(function(fmt) { return ( ); })}
)}
)} {/* Edit Layout Mode - only for slides */} {previewFile.type === 'slide' && isPreviewFullscreen && ( )} {/* Edit Document Mode - for worksheet, quiz, exit_ticket, lesson_plan */} {editableTypes.includes(previewFile.type) && ( )} {/* Save Button - only when editing and has changes */} {isDocEditMode && docHasChanges && ( )} {/* Slide Edit Save Button - shows when there are pending slide edits */} {previewFile && previewFile.type === 'slide' && Object.keys(pendingSlideEdits).length > 0 && ( )} {/* Fullscreen Toggle */} {/* Open in new tab */}
{/* Preview Content - Iframe or Slide Layout */} {isPreviewFullscreen && previewFile.type === 'slide' && previewFile.slide_urls && previewFile.slide_urls.length > 0 ? ( /* Fullscreen Slide Layout - Manus Style */
{/* Left Sidebar - Slide Thumbnails */}
{previewFile.slide_urls.map(function(slideUrl, idx) { var isActive = idx === currentSlideIndex; return (
{/* Thumbnail with border highlight */}