/**
* 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 (
setIsDark(!isDark)}
className="fixed bottom-4 right-4 z-50 p-3 bg-white dark:bg-zinc-800 rounded-full shadow-lg border border-gray-200 dark:border-zinc-700 text-gray-600 dark:text-gray-300 hover:text-primary transition-colors"
title="Toggle Dark Mode"
>
{isDark ? 'light_mode' : 'dark_mode'}
);
};
// 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 (
{/* Header */}
school
Scaffo.ai
AI Teaching Materials
{/* Action Buttons */}
add_circle
New Chat
library_books
Template Library
{/* Recent Generations Section - Collapsible */}
setIsRecentCollapsed(!isRecentCollapsed)}
className="flex items-center gap-1 text-xs font-semibold text-gray-400 uppercase tracking-wider hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
expand_more
Recent
{recentGenerations.length > 0 && (
{recentGenerations.length}
)}
{!isRecentCollapsed && (
{recentGenerations.length === 0 ? (
history
Your generated lessons appear here
) : (
{recentGenerations.slice(0, 10).map((gen) => {
const isActive = currentGenerationId === gen.id;
return (
onSelectGeneration(gen)}
className={`group flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all border ${isActive ? 'bg-primary/10 border-primary/30 dark:bg-primary/20' : 'hover:bg-white dark:hover:bg-white/5 border-transparent hover:border-gray-200 dark:hover:border-gray-700'}`}
>
{gen.type === 'package' ? 'folder' : 'article'}
{gen.title}
{gen.version > 1 && v{gen.version} }
{formatDate(gen.created_at)}
{gen.parent_id && • Updated }
{/* Delete button - visible on hover */}
{
e.stopPropagation();
if (confirm('Delete this generation?')) {
onDeleteGeneration(gen.id);
}
}}
className="hidden lg:flex opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-500 transition-all"
title="Delete"
>
delete
);})}
)}
)}
{/* User Profile */}
{currentUser && currentUser.name ? currentUser.name.charAt(0).toUpperCase() : 'U'}
{currentUser && currentUser.name ? currentUser.name : 'User'}
@{currentUser && currentUser.id ? currentUser.id : 'user'}
logout
{/* Mobile logout button */}
logout
);
};
// 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 */}
arrow_back
library_books
Template Library
Reusable lesson frameworks
{/* Content */}
{/* Search & Filter */}
{/* 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 */}
!isGenerationContext && setShowModal(true)}
className={`w-full max-w-2xl group text-left ${isGenerationContext && !isStreaming ? 'cursor-default' : 'cursor-pointer'}`}
>
{/* Header with icon and title */}
{isReadOnly ? 'lock' : 'description'}
{blueprint.title}
{isReadOnly && (
{isChinese(blueprint.title || '') ? '历史版本 • 只读' : 'Previous version • Read only'}
)}
{!isReadOnly && (
more_horiz
)}
{/* Document Preview - Clean Structured Layout like Manus */}
{/* Title */}
{blueprint.title}
{/* Summary/Description */}
{blueprint.summary || 'An interactive lesson designed to engage students and achieve learning objectives.'}
{/* Metadata Line */}
{isChinese(blueprint.title) ? '年级' : 'Grade'}: {(blueprint.metadata && blueprint.metadata.grade_level) || 'N/A'} |
{isChinese(blueprint.title) ? '学科' : 'Subject'}: {(blueprint.metadata && blueprint.metadata.subject) || 'N/A'} |
{isChinese(blueprint.title) ? '时长' : 'Duration'}: {(blueprint.metadata && blueprint.metadata.duration_minutes) || 45} {isChinese(blueprint.title) ? '分钟' : 'minutes'}
{/* Standards Section */}
{blueprint.standards_alignment && blueprint.standards_alignment.primary_standards && blueprint.standards_alignment.primary_standards.length > 0 && (
{isChinese(blueprint.title) ? '教学标准' : 'Standards'}
{blueprint.standards_alignment.primary_standards[0]}
)}
{/* Learning Objectives Section */}
{blueprint.learning_objectives && blueprint.learning_objectives.length > 0 && (
{isChinese(blueprint.title) ? '学习目标' : 'Learning Objectives'}
{blueprint.learning_objectives.slice(0, 3).map(function(obj, idx) {
var text = obj.text || obj;
return (
{text}
);
})}
)}
{/* Lesson Flow Preview */}
{blueprint.lesson_flow && blueprint.lesson_flow.length > 0 && (
{isChinese(blueprint.title) ? '课程流程' : 'Lesson Flow'}
{blueprint.lesson_flow[0] && blueprint.lesson_flow[0].title && ('1. ' + blueprint.lesson_flow[0].title.substring(0, 50))}
{blueprint.lesson_flow[0] && blueprint.lesson_flow[0].title && blueprint.lesson_flow[0].title.length > 50 && '...'}
)}
{/* Materials to Create - Package Types */}
{blueprint.materials_to_create && blueprint.materials_to_create.length > 0 && (
{isChinese(blueprint.title) ? '📦 将生成材料' : '📦 Materials to Create'}
{blueprint.materials_to_create.map(function(mat, idx) {
var typeIcons = {
'slide': 'slideshow',
'lesson_plan': 'description',
'worksheet': 'assignment',
'quiz': 'quiz',
'exit_ticket': 'receipt_long',
'group_activity': 'groups',
'rubric': 'format_list_numbered',
'vocabulary_cards': 'style'
};
var typeLabels = {
'slide': isChinese(blueprint.title) ? '课件' : 'Slides',
'lesson_plan': isChinese(blueprint.title) ? '教案' : 'Lesson Plan',
'worksheet': isChinese(blueprint.title) ? '练习' : 'Worksheet',
'quiz': isChinese(blueprint.title) ? '测验' : 'Quiz',
'exit_ticket': isChinese(blueprint.title) ? '出门检测' : 'Exit Ticket',
'group_activity': isChinese(blueprint.title) ? '小组活动' : 'Group Activity',
'rubric': isChinese(blueprint.title) ? '评分标准' : 'Rubric',
'vocabulary_cards': isChinese(blueprint.title) ? '词汇卡' : 'Vocab Cards'
};
var icon = typeIcons[mat.type] || 'folder';
var label = typeLabels[mat.type] || mat.type;
return (
{icon}
{label}
);
})}
)}
{/* Soft Gradient Fade */}
{/* 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.'}
{isGeneratingMaterials ? (
progress_activity
{isZh ? '生成中...' : 'Generating...'}
) : (
refresh
{isZh ? '重新生成教学材料' : 'Regenerate 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 && (
restore
Restore
)}
{/* Fullscreen Toggle */}
{isFullscreen ? 'fullscreen_exit' : 'fullscreen'}
{/* Download Button with Dropdown */}
{isDownloading ? 'hourglass_empty' : 'download'}
expand_more
{showDownloadMenu && (
{downloadFormats.map(function(fmt) {
return (
{fmt.icon}
{fmt.label}
);
})}
)}
{!isReadOnly && (
{/* Save Button - Right side of header */}
{isSaving ? 'hourglass_empty' : (hasChanges ? 'save' : 'check')}
{isSaving ? 'Saving...' : (hasChanges ? 'Save' : 'Saved')}
)}
{isReadOnly && (
lock
{isChinese(blueprint.title || '') ? '只读模式' : 'Read Only'}
)}
close
{/* Modal Content - Manus Layout with Dot Outline on Right */}
{/* Restore Panel - Simple floating panel with original version only */}
{showHistoryPanel && historyCount > 0 && (
restore
Restore Original
close
{/* 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.
restore
Restore to Original
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 (
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
)
)}
)}
);
};
// 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 */}
folder_zip
{isChinese ? '下载文档' : 'Download Docs'}
expand_more
{showDownloadAllMenu && (
Select Format
{downloadFormats.map(function(fmt) {
return (
{fmt.icon}
{fmt.label}
Download all as {fmt.label} files
);
})}
)}
{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 && (
download
Download
expand_more
{showDownloadMenu && (
{downloadFormats.map(function(fmt) {
return (
{fmt.icon}
{fmt.label}
{fmt.hint && {fmt.hint} }
);
})}
)}
)}
{/* Edit Layout Mode - only for slides */}
{previewFile.type === 'slide' && isPreviewFullscreen && (
{isDragEditMode ? 'check' : 'drag_pan'}
)}
{/* Edit Document Mode - for worksheet, quiz, exit_ticket, lesson_plan */}
{editableTypes.includes(previewFile.type) && (
{isDocEditMode ? 'edit_off' : 'edit'}
{isDocEditMode ? 'Editing' : 'Edit'}
)}
{/* Save Button - only when editing and has changes */}
{isDocEditMode && docHasChanges && (
{isSavingDoc ? (
React.createElement(React.Fragment, null,
React.createElement('span', { className: 'material-symbols-outlined text-lg animate-spin' }, 'progress_activity'),
React.createElement('span', { className: 'hidden sm:inline' }, 'Saving...')
)
) : (
React.createElement(React.Fragment, null,
React.createElement('span', { className: 'material-symbols-outlined text-lg' }, 'save'),
React.createElement('span', { className: 'hidden sm:inline' }, 'Save')
)
)}
)}
{/* Slide Edit Save Button - shows when there are pending slide edits */}
{previewFile && previewFile.type === 'slide' && Object.keys(pendingSlideEdits).length > 0 && (
{isSavingSlides ? (
React.createElement(React.Fragment, null,
React.createElement('span', { className: 'material-symbols-outlined text-lg animate-spin' }, 'progress_activity'),
React.createElement('span', { className: 'hidden sm:inline' }, 'Saving...')
)
) : slidesSaveStatus === 'success' ? (
React.createElement(React.Fragment, null,
React.createElement('span', { className: 'material-symbols-outlined text-lg' }, 'check_circle'),
React.createElement('span', { className: 'hidden sm:inline' }, 'Saved!')
)
) : slidesSaveStatus === 'error' ? (
React.createElement(React.Fragment, null,
React.createElement('span', { className: 'material-symbols-outlined text-lg' }, 'error'),
React.createElement('span', { className: 'hidden sm:inline' }, 'Error')
)
) : (
React.createElement(React.Fragment, null,
React.createElement('span', { className: 'material-symbols-outlined text-lg' }, 'save'),
React.createElement('span', { className: 'hidden sm:inline' }, 'Save Changes (' + Object.keys(pendingSlideEdits).length + ')')
)
)}
)}
{/* Fullscreen Toggle */}
{isPreviewFullscreen ? 'fullscreen_exit' : 'fullscreen'}
{/* Open in new tab */}
open_in_new
0) {
if (!confirm('You have unsaved slide edits. Are you sure you want to close?')) {
return;
}
}
setPendingSlideEdits({});
setSlidesSaveStatus(null);
setPreviewFile(null);
setIsPreviewFullscreen(false);
setCurrentSlideIndex(0);
}}
className="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
>
close
{/* 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 */}
{/* Slide number below thumbnail */}
{idx + 1}
);
})}
{/* Right Side - Main slide + notes area */}
{/* Main Slide Area - fills available space with scrolling */}
{/* Speaker Notes Area - Enhanced with Regenerate */}
speaker_notes
Speaker Notes
{slideNotes[currentSlideIndex] && (
已保存
)}
Slide {currentSlideIndex + 1} / {previewFile.slide_urls.length}
{/* Regenerate Slide Button */}
0) {
confirmMsg += '\n\n⚠️ 检测到这是一个交互组件 Slide。\n前一个 Slide 可能包含相关的使用说明。';
}
if (confirm(confirmMsg)) {
// 如果是交互组件,询问是否联动更新
if (isInteractiveSlide && currentSlideIndex > 0) {
updatePreviousSlide = confirm('是否同时更新 Slide ' + currentSlideIndex + ' 的说明内容?\n\n点击 "确定" 将同时重新生成前一个 slide 的说明,\n使其与新的交互组件保持一致。\n\n点击 "取消" 仅重新生成当前交互组件。');
}
// Call regenerate API
var currentNotes = slideNotes[currentSlideIndex] || '';
fetch(API_BASE + '/api/v1/generations/' + genId + '/regenerate-slide', {
method: 'POST',
headers: Object.assign({}, getAuthHeaders(), { 'Content-Type': 'application/json' }),
body: JSON.stringify({
slide_index: currentSlideIndex + 1,
speaker_notes: currentNotes,
regenerate_image: true,
regenerate_interactive: true,
update_previous_slide: updatePreviousSlide
})
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.status === 'success') {
alert('✅ Slide ' + (currentSlideIndex + 1) + ' 重新生成成功!\n\n耗时: ' + data.duration_seconds.toFixed(1) + 's\n重新生成: ' + (data.regenerated || []).join(', ') + '\n\n页面将自动刷新以显示更新内容。');
// Force refresh all iframes with cache-busting timestamp
var timestamp = Date.now();
var allIframes = document.querySelectorAll('iframe');
allIframes.forEach(function(iframe) {
var src = iframe.src;
if (src) {
// Remove existing timestamp and add new one
var baseUrl = src.split('?')[0];
iframe.src = baseUrl + '?_nocache=' + timestamp;
}
});
// Also update previewFile slide_urls to force re-render
if (previewFile && previewFile.slide_urls) {
var updatedUrls = previewFile.slide_urls.map(function(url) {
var baseUrl = url.split('?')[0];
return baseUrl + '?_nocache=' + timestamp;
});
setPreviewFile(Object.assign({}, previewFile, { slide_urls: updatedUrls }));
}
} else {
alert('❌ 重新生成失败: ' + (data.error || data.detail || '未知错误'));
}
})
.catch(function(e) {
alert('❌ 请求失败: ' + e.message);
});
}
}}
className="flex items-center gap-1 px-2 py-1 text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 rounded-md hover:bg-amber-100 dark:hover:bg-amber-900/40 transition-colors"
title="使用当前 Speaker Notes 重新生成此 Slide"
>
refresh
重新生成
💡 编辑笔记后点击"重新生成"可根据修改内容更新 Slide
auto_mode
自动保存
) : (
/* Default iframe preview - centered document style with zoom in fullscreen */
{/* Edit mode indicator bar */}
{isDocEditMode && editableTypes.includes(previewFile.type) && (
edit_note
Edit Mode - Click directly on text to edit
{docHasChanges && (
circle
Unsaved changes
)}
)}
)}
)}
{/* Image Edit Modal */}
{editImageModal && (
🎨
Edit Image Prompt
Slide {editImageModal.slideIndex + 1}
Image Description
💡 Tip: Be specific about style, colors, and content. For educational slides, describe the concept visually.
Cancel
{isGeneratingImage ? (
React.createElement(React.Fragment, null,
React.createElement('svg', { className: 'animate-spin h-4 w-4', viewBox: '0 0 24 24' },
React.createElement('circle', { className: 'opacity-25', cx: '12', cy: '12', r: '10', stroke: 'currentColor', strokeWidth: '4', fill: 'none' }),
React.createElement('path', { className: 'opacity-75', fill: 'currentColor', d: 'M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z' })
),
' Generating...'
)
) : (
React.createElement(React.Fragment, null,
React.createElement('span', { className: 'material-symbols-outlined text-lg' }, 'auto_awesome'),
' Generate Image'
)
)}
)}
);
};
// Confirmed Lesson Summary - Read-only display for history (same style as ConfirmationCard) - with multilingual support
var ConfirmedLessonSummary = ({ lessonInfo }) => {
// Detect language from topic
var isZh = lessonInfo.topic && /[\u4e00-\u9fa5]/.test(lessonInfo.topic);
// Localized labels
var labels = {
title: isZh ? '课程详情确认' : 'Confirm Lesson Details',
subtitle: isZh ? '蓝图生成前的确认' : 'Review and edit before generating blueprint',
topic: isZh ? '主题' : 'Topic',
grade: isZh ? '年级' : 'Grade Level',
subject: isZh ? '学科' : 'Subject',
duration: isZh ? '时长(分钟)' : 'Duration (min)',
teachingStyle: isZh ? '教学风格' : 'Teaching Style',
assessment: isZh ? '评估方式' : 'Assessment',
objectives: isZh ? '学习目标' : 'Learning Objectives',
activities: isZh ? '活动' : 'Activities',
packages: isZh ? '生成材料' : 'Materials',
notSpecified: isZh ? '未指定' : 'Not specified',
confirmed: isZh ? '已确认并生成蓝图' : 'Confirmed and blueprint generated'
};
// Package type labels
var pkgLabels = {
slide: isZh ? '互动课件' : 'Slides',
lesson_plan: isZh ? '教案' : 'Lesson Plan',
worksheet: isZh ? '学生练习' : 'Worksheet',
quiz: isZh ? '测验' : 'Quiz',
exit_ticket: isZh ? '出门检测' : 'Exit Ticket',
group_activity: isZh ? '小组活动' : 'Group Activity',
rubric: isZh ? '评分标准' : 'Rubric',
vocabulary_cards: isZh ? '词汇卡片' : 'Vocabulary Cards'
};
var infoFields = [
{ key: 'topic', label: labels.topic, icon: 'topic', required: true },
{ key: 'grade', label: labels.grade, icon: 'school', required: true },
{ key: 'subject', label: labels.subject, icon: 'menu_book' },
{ key: 'duration', label: labels.duration, icon: 'schedule' },
{ key: 'teaching_style', label: labels.teachingStyle, icon: 'style' },
{ key: 'assessment_type', label: labels.assessment, icon: 'assignment' }
];
return (
checklist
{labels.title}
{labels.subtitle}
{infoFields.map(function(field) {
var value = lessonInfo[field.key];
if (!value) return null;
var displayValue = Array.isArray(value) ? value.join(', ') : value;
return (
{field.icon}
{field.label} {field.required && * }
{displayValue || {labels.notSpecified} }
);
})}
{/* Learning Objectives - same as ConfirmationCard */}
{(lessonInfo.objectives && lessonInfo.objectives.length > 0) && (
flag
{labels.objectives}
{lessonInfo.objectives.map(function(obj, idx) {
return {obj} ;
})}
)}
{/* Activities - same as ConfirmationCard */}
{(lessonInfo.activities && lessonInfo.activities.length > 0) && (
sports_esports
{labels.activities}
{lessonInfo.activities.map(function(activity, idx) {
return (
{activity}
);
})}
)}
{/* Selected Packages */}
{(lessonInfo.selectedPackages && lessonInfo.selectedPackages.length > 0) && (
folder_zip
{labels.packages}
{lessonInfo.selectedPackages.map(function(pkg, idx) {
return (
{pkgLabels[pkg] || pkg}
);
})}
)}
{/* Confirmed status footer */}
check_circle
{labels.confirmed}
);
};
// Confirmation Card for blueprint generation - with multilingual support and package selection
var ConfirmationCard = ({ extractedInfo, onConfirm, onEdit, messages, isReadOnly, isGenerating }) => {
var [editMode, setEditMode] = React.useState(false);
var [editedInfo, setEditedInfo] = React.useState(extractedInfo || {});
// If materials already generated, show read-only mode
var readOnlyMode = isReadOnly === true;
// Detect language from topic or last user message
var detectLanguage = function() {
// Check topic first
if (editedInfo.topic && /[\u4e00-\u9fa5]/.test(editedInfo.topic)) return 'zh';
// Check last user message
if (messages && messages.length > 0) {
var userMsgs = messages.filter(function(m) { return m.role === 'user'; });
if (userMsgs.length > 0) {
var lastContent = userMsgs[userMsgs.length - 1].content || '';
if (/[\u4e00-\u9fa5]/.test(lastContent)) return 'zh';
}
}
return 'en';
};
var lang = detectLanguage();
var isZh = lang === 'zh';
// Package types configuration with required/optional
var allPackageTypes = [
{ id: 'slide', label: isZh ? '互动课件' : 'Interactive Slides', icon: 'slideshow', required: true },
{ id: 'lesson_plan', label: isZh ? '教案' : 'Lesson Plan', icon: 'description', required: true },
{ id: 'worksheet', label: isZh ? '学生练习' : 'Student Worksheet', icon: 'assignment', required: false },
{ id: 'quiz', label: isZh ? '测验' : 'Assessment Quiz', icon: 'quiz', required: false },
{ id: 'exit_ticket', label: isZh ? '出门检测' : 'Exit Ticket', icon: 'receipt_long', required: false },
{ id: 'group_activity', label: isZh ? '小组活动' : 'Group Activity', icon: 'groups', required: false },
{ id: 'rubric', label: isZh ? '评分标准' : 'Grading Rubric', icon: 'format_list_numbered', required: false },
{ id: 'vocabulary_cards', label: isZh ? '词汇卡片' : 'Vocabulary Cards', icon: 'style', required: false }
];
// Initialize selected packages (required ones always selected, plus LLM suggested packages)
var getInitialPackages = function() {
var initial = {};
// Start with required packages selected
allPackageTypes.forEach(function(pkg) {
initial[pkg.id] = pkg.required;
});
// Use LLM's suggested_packages if available (intelligent detection)
var suggestedPackages = editedInfo.suggested_packages || [];
if (suggestedPackages && suggestedPackages.length > 0) {
suggestedPackages.forEach(function(pkgId) {
if (initial.hasOwnProperty(pkgId)) {
initial[pkgId] = true;
}
});
console.log('📦 LLM suggested packages:', suggestedPackages);
}
// Auto-map activities to corresponding packages
// 活动 → 生成材料的映射
var activities = editedInfo.activities || [];
var activityToPackageMap = {
'小组活动': 'group_activity',
'小组': 'group_activity',
'group work': 'group_activity',
'group activity': 'group_activity',
'collaborative': 'group_activity',
'练习册': 'worksheet',
'练习': 'worksheet',
'worksheet': 'worksheet',
'practice': 'worksheet',
'测验': 'quiz',
'quiz': 'quiz',
'词汇': 'vocabulary_cards',
'vocabulary': 'vocabulary_cards'
};
activities.forEach(function(activity) {
var activityLower = (activity || '').toLowerCase();
Object.keys(activityToPackageMap).forEach(function(key) {
if (activityLower.includes(key.toLowerCase())) {
var pkgId = activityToPackageMap[key];
if (initial.hasOwnProperty(pkgId)) {
initial[pkgId] = true;
console.log('📦 Auto-selected package from activity:', activity, '→', pkgId);
}
}
});
});
return initial;
};
var [selectedPackages, setSelectedPackages] = React.useState(getInitialPackages);
var handleFieldChange = function(field, value) {
setEditedInfo(function(prev) {
var updated = Object.assign({}, prev);
updated[field] = value;
return updated;
});
};
var handlePackageToggle = function(pkgId, isRequired) {
if (isRequired) return; // Cannot toggle required packages
setSelectedPackages(function(prev) {
var updated = Object.assign({}, prev);
updated[pkgId] = !updated[pkgId];
return updated;
});
};
var handleConfirm = function() {
// Include selected packages in the confirmed info
var confirmedData = Object.assign({}, editedInfo, {
selectedPackages: Object.keys(selectedPackages).filter(function(k) { return selectedPackages[k]; })
});
onConfirm(confirmedData);
};
// Localized labels
var labels = {
title: isZh ? '确认课程详情' : 'Confirm Lesson Details',
subtitle: isZh ? '生成前请检查并编辑' : 'Review and edit before generating blueprint',
topic: isZh ? '主题' : 'Topic',
grade: isZh ? '年级' : 'Grade Level',
subject: isZh ? '学科' : 'Subject',
duration: isZh ? '时长(分钟)' : 'Duration (min)',
teachingStyle: isZh ? '教学风格' : 'Teaching Style',
assessment: isZh ? '评估方式' : 'Assessment',
objectives: isZh ? '学习目标' : 'Learning Objectives',
activities: isZh ? '活动' : 'Activities',
packages: isZh ? '生成材料' : 'Materials to Generate',
packagesHint: isZh ? '必选项已锁定,可额外选择其他材料' : 'Required items are locked. Select additional materials as needed.',
notSpecified: isZh ? '未指定' : 'Not specified',
edit: isZh ? '编辑' : 'Edit',
preview: isZh ? '预览' : 'Preview',
generate: isZh ? '生成蓝图' : 'Generate Blueprint',
enterPlaceholder: isZh ? '请输入' : 'Enter '
};
var infoFields = [
{ key: 'topic', label: labels.topic, icon: 'topic', required: true },
{ key: 'grade', label: labels.grade, icon: 'school', required: true },
{ key: 'subject', label: labels.subject, icon: 'menu_book' },
{ key: 'duration', label: labels.duration, icon: 'schedule', type: 'number' },
{ key: 'teaching_style', label: labels.teachingStyle, icon: 'style' },
{ key: 'assessment_type', label: labels.assessment, icon: 'assignment' }
];
return (
checklist
{labels.title}
{labels.subtitle}
{infoFields.map(function(field) {
var value = editedInfo[field.key] || '';
var displayValue = Array.isArray(value) ? value.join(', ') : value;
return (
{field.icon}
{field.label} {field.required && * }
{editMode ? (
) : (
{displayValue || {labels.notSpecified} }
)}
);
})}
{/* Objectives */}
{(editedInfo.objectives && editedInfo.objectives.length > 0) && (
flag
{labels.objectives}
{editedInfo.objectives.map(function(obj, idx) {
return {obj} ;
})}
)}
{/* Activities */}
{(editedInfo.activities && editedInfo.activities.length > 0) && (
sports_esports
{labels.activities}
{editedInfo.activities.map(function(activity, idx) {
return (
{activity}
);
})}
)}
{/* Package Selection */}
folder_zip
{labels.packages}
{labels.packagesHint}
{allPackageTypes.map(function(pkg) {
var isSelected = selectedPackages[pkg.id];
var isRequired = pkg.required;
var isDisabled = isRequired || readOnlyMode;
return (
{pkg.icon}
{pkg.label}
{isRequired && (
lock
)}
{!isRequired && isSelected && (
check_circle
)}
);
})}
{readOnlyMode ? (
check_circle
{isZh ? '已确认并生成' : 'Confirmed & Generated'}
) : isGenerating ? (
{editMode ? 'visibility' : 'edit'}
{editMode ? labels.preview : labels.edit}
) : (
{editMode ? 'visibility' : 'edit'}
{editMode ? labels.preview : labels.edit}
)}
{readOnlyMode ? (
done_all
{isZh ? '蓝图已生成' : 'Blueprint Generated'}
) : isGenerating ? (
progress_activity
{isZh ? '正在生成...' : 'Generating...'}
) : (
auto_awesome
{labels.generate}
)}
);
};
// Get icon for file type (defined globally for use in ChatMessage)
// getFileIcon, isChinese, getLocalizedMsg - defined in js/utils.js
var AttachmentPreview = ({ attachment, onClose }) => {
var [zoom, setZoom] = React.useState(1);
var [position, setPosition] = React.useState({ x: 0, y: 0 });
var [isDragging, setIsDragging] = React.useState(false);
var [dragStart, setDragStart] = React.useState({ x: 0, y: 0 });
if (!attachment) return null;
var isImage = attachment.analysis_type === 'image' ||
(attachment.filename && /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(attachment.filename));
var handleZoomIn = function(e) {
e.stopPropagation();
setZoom(function(z) { return Math.min(z + 0.25, 3); });
};
var handleZoomOut = function(e) {
e.stopPropagation();
setZoom(function(z) { return Math.max(z - 0.25, 0.5); });
};
var handleReset = function(e) {
e.stopPropagation();
setZoom(1);
setPosition({ x: 0, y: 0 });
};
var handleWheel = function(e) {
e.preventDefault();
e.stopPropagation();
if (e.deltaY < 0) {
setZoom(function(z) { return Math.min(z + 0.1, 3); });
} else {
setZoom(function(z) { return Math.max(z - 0.1, 0.5); });
}
};
var handleMouseDown = function(e) {
if (zoom > 1) {
e.stopPropagation();
setIsDragging(true);
setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y });
}
};
var handleMouseMove = function(e) {
if (isDragging && zoom > 1) {
e.stopPropagation();
setPosition({
x: e.clientX - dragStart.x,
y: e.clientY - dragStart.y
});
}
};
var handleMouseUp = function() {
setIsDragging(false);
};
return (
{/* Close button */}
close
{isImage ? (
{/* Zoom controls */}
remove
{Math.round(zoom * 100)}%
add
fit_screen
{/* Image with zoom */}
1 ? (isDragging ? 'grabbing' : 'grab') : 'default',
maxWidth: '90vw',
maxHeight: '85vh',
objectFit: 'contain'
}}
onClick={function(e) { e.stopPropagation(); }}
onMouseDown={handleMouseDown}
draggable={false}
/>
{/* Filename */}
{attachment.filename}
) : (
{getFileIcon(attachment.analysis_type)}
{attachment.filename}
{attachment.analysis_type || 'File'}
open_in_new
Open File
)}
);
};
// Gemini-style "Show thinking" collapsible component
var ThinkingCollapse = ({ thoughtText, isChinese, isStreaming }) => {
// Default to expanded (open by default)
var [isExpanded, setIsExpanded] = React.useState(true);
var contentRef = React.useRef(null);
if (!thoughtText) return null;
// Auto-scroll to bottom when streaming new content
React.useEffect(function() {
if (isStreaming && isExpanded && contentRef.current) {
contentRef.current.scrollTop = contentRef.current.scrollHeight;
}
}, [thoughtText, isStreaming, isExpanded]);
// Format thought text: convert **title** to styled headers
var formatThought = function(text) {
// Normalize multiple newlines to double newlines before splitting
var normalizedText = text.replace(/\n{3,}/g, '\n\n');
var paragraphs = normalizedText.split('\n\n');
var elements = [];
for (var i = 0; i < paragraphs.length; i++) {
// Trim each paragraph to remove leading/trailing whitespace
var paragraph = paragraphs[i].trim();
if (!paragraph) continue;
var titleMatch = paragraph.match(/^\*\*(.+?)\*\*/);
if (titleMatch) {
var title = titleMatch[1];
var rest = paragraph.replace(/^\*\*.+?\*\*\s*/, '').trim();
elements.push(
React.createElement('div', { key: i, className: "mb-3" },
React.createElement('div', { className: "font-semibold text-gray-700 dark:text-gray-200 text-sm mb-1" }, title),
rest && React.createElement('div', { className: "text-gray-500 dark:text-gray-400 text-sm leading-relaxed" }, rest)
)
);
} else {
elements.push(
React.createElement('p', { key: i, className: "text-gray-500 dark:text-gray-400 text-sm leading-relaxed mb-2" }, paragraph)
);
}
}
return elements;
};
return (
React.createElement('div', { className: "mb-2" },
React.createElement('button', {
onClick: function() { setIsExpanded(!isExpanded); },
className: "flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm text-gray-600 dark:text-gray-300 font-medium"
},
React.createElement('span', { className: "text-primary" }, "✦"),
React.createElement('span', null, isExpanded ? (isChinese ? '隐藏思考' : 'Hide thinking') : (isChinese ? '显示思考' : 'Show thinking')),
isStreaming && React.createElement('span', { className: "text-xs text-gray-400" }, "..."),
React.createElement('span', {
className: 'material-symbols-outlined text-base transition-transform ' + (isExpanded ? 'rotate-180' : ''),
style: {fontSize: '18px'}
}, "expand_more")
),
isExpanded && React.createElement('div', {
ref: contentRef,
className: "mt-3 pl-4 border-l-2 border-primary/20 max-h-96 overflow-y-auto scroll-smooth"
},
formatThought(thoughtText)
)
)
);
};
// Global cache for streaming blueprint - persists across component remounts
// This prevents flickering when StreamingBlueprintCard is unmounted/remounted during rapid state updates
// streamingBlueprintCache - defined in js/utils.js
// Streaming Blueprint Card - with stable caching to prevent flicker
var StreamingBlueprintCard = ({ partialContent, isChinese }) => {
// State for preview modal
const [showPreviewModal, setShowPreviewModal] = React.useState(false);
const [isFullscreen, setIsFullscreen] = React.useState(false);
const previewContentRef = React.useRef(null);
// Use global cache instead of useRef to persist across remounts
const parsed = tryParseStreamingBlueprint(partialContent);
// Only update cache if:
// 1. Parse is successful
// 2. Content is longer than last time (streaming is progressing, not reverting)
// 3. We have meaningful data (title or other fields)
if (parsed.success && parsed.partialData && partialContent.length >= streamingBlueprintCache.lastContentLength) {
const newBp = parsed.partialData;
const oldBp = streamingBlueprintCache.bp;
// Merge new data with existing cache - never lose data
const mergedBp = { ...oldBp };
// Only update fields if new value exists and is not empty
if (newBp.title) mergedBp.title = newBp.title;
if (newBp.summary && newBp.summary.length > (mergedBp.summary || '').length) {
mergedBp.summary = newBp.summary;
}
if (newBp.metadata) {
mergedBp.metadata = { ...(mergedBp.metadata || {}), ...newBp.metadata };
}
if (newBp.standards_alignment) {
mergedBp.standards_alignment = newBp.standards_alignment;
}
if (newBp.learning_objectives && newBp.learning_objectives.length >= (mergedBp.learning_objectives || []).length) {
mergedBp.learning_objectives = newBp.learning_objectives;
}
if (newBp.lesson_flow && newBp.lesson_flow.length >= (mergedBp.lesson_flow || []).length) {
mergedBp.lesson_flow = newBp.lesson_flow;
}
if (newBp.materials_to_create && newBp.materials_to_create.length >= (mergedBp.materials_to_create || []).length) {
mergedBp.materials_to_create = newBp.materials_to_create;
}
streamingBlueprintCache.bp = mergedBp;
streamingBlueprintCache.displayTitle = mergedBp.title || streamingBlueprintCache.displayTitle;
streamingBlueprintCache.lastContentLength = partialContent.length;
}
// Get current cached values (create a copy to ensure React detects changes)
const bp = { ...streamingBlueprintCache.bp };
const displayTitle = streamingBlueprintCache.displayTitle || (isChinese ? '正在生成课程蓝图...' : 'Generating lesson blueprint...');
// Track content length with state to force re-renders during streaming
// Use a key trick: always update state to force React to re-render
const [renderTrigger, setRenderTrigger] = React.useState(0);
const lastLengthRef = React.useRef(0);
// Force re-render whenever partialContent changes length
React.useEffect(() => {
if (partialContent.length !== lastLengthRef.current) {
lastLengthRef.current = partialContent.length;
setRenderTrigger(prev => prev + 1);
}
}, [partialContent]);
// Auto-scroll to bottom when content updates in preview modal
React.useEffect(() => {
if (showPreviewModal && previewContentRef.current) {
previewContentRef.current.scrollTop = previewContentRef.current.scrollHeight;
}
}, [partialContent, showPreviewModal, renderTrigger]);
// Generate markdown from cached blueprint data for preview
// Recompute whenever renderTrigger changes (which happens on every content update)
const streamingMarkdown = React.useMemo(() => {
// renderTrigger ensures this runs on every content update
const _ = renderTrigger;
const currentBp = streamingBlueprintCache.bp;
if (!currentBp || Object.keys(currentBp).length === 0) return '';
return blueprintToMarkdown(currentBp);
}, [renderTrigger]);
return (
{/* Blueprint Generating header - matches final style */}
edit_note
{isChinese ? '正在生成 Blueprint...' : 'Generating Blueprint...'}
{partialContent.length} chars
{/* Card styled exactly like BlueprintMessage preview - CLICKABLE */}
setShowPreviewModal(true)}
>
{/* Header with icon and title */}
description
{/* Click to preview hint */}
visibility
{isChinese ? '点击预览' : 'Click to preview'}
{/* Document Preview - Same structure as final BlueprintMessage */}
{/* Title */}
{displayTitle}
{/* Summary/Description */}
{bp.summary || (isChinese ? '正在生成课程摘要...' : 'Generating lesson summary...')}
{/* Metadata Line */}
{isChinese ? '年级' : 'Grade'}: {(bp.metadata && bp.metadata.grade_level) || '...'} |
{isChinese ? '学科' : 'Subject'}: {(bp.metadata && bp.metadata.subject) || '...'} |
{isChinese ? '时长' : 'Duration'}: {(bp.metadata && bp.metadata.duration_minutes) || '...'} {isChinese ? '分钟' : 'minutes'}
{/* Standards Section */}
{bp.standards_alignment && bp.standards_alignment.primary_standards && bp.standards_alignment.primary_standards.length > 0 && (
{isChinese ? '教学标准' : 'Standards'}
{bp.standards_alignment.primary_standards[0]}
)}
{/* Learning Objectives Section */}
{bp.learning_objectives && bp.learning_objectives.length > 0 && (
{isChinese ? '学习目标' : 'Learning Objectives'}
{bp.learning_objectives.slice(0, 3).map(function(obj, idx) {
var text = obj.text || obj;
return (
{text}
);
})}
)}
{/* Lesson Flow Preview - Show all items as they stream in */}
{bp.lesson_flow && bp.lesson_flow.length > 0 && (
{isChinese ? '课程流程' : 'Lesson Flow'} ({bp.lesson_flow.length})
{bp.lesson_flow.map(function(flow, idx) {
const duration = flow.duration_minutes ? ` (${flow.duration_minutes} ${isChinese ? '分钟' : 'min'})` : '';
return (
{(idx + 1) + '. ' + (flow.title || '').substring(0, 40)}{duration}
);
})}
)}
{/* Materials to Create - Package Types (Streaming) */}
{bp.materials_to_create && bp.materials_to_create.length > 0 && (
{isChinese ? '📦 将生成材料' : '📦 Materials'}
{bp.materials_to_create.map(function(mat, idx) {
var typeIcons = {
'slide': 'slideshow',
'lesson_plan': 'description',
'worksheet': 'assignment',
'quiz': 'quiz',
'exit_ticket': 'receipt_long',
'group_activity': 'groups',
'rubric': 'format_list_numbered',
'vocabulary_cards': 'style'
};
var typeLabels = {
'slide': isChinese ? '课件' : 'Slides',
'lesson_plan': isChinese ? '教案' : 'Plan',
'worksheet': isChinese ? '练习' : 'Sheet',
'quiz': isChinese ? '测验' : 'Quiz',
'exit_ticket': isChinese ? '出门检测' : 'Exit',
'group_activity': isChinese ? '小组' : 'Group',
'rubric': isChinese ? '评分' : 'Rubric',
'vocabulary_cards': isChinese ? '词汇' : 'Vocab'
};
var icon = typeIcons[mat.type] || 'folder';
var label = typeLabels[mat.type] || mat.type;
return (
{icon}
{label}
);
})}
)}
{/* Loading indicator when no content yet */}
{!bp.learning_objectives && !bp.lesson_flow && (
progress_activity
{isChinese ? '正在生成更多内容...' : 'Generating more content...'}
)}
{/* Soft Gradient Fade */}
{/* Preview Modal - Read-only during streaming */}
{showPreviewModal && (
{ if (e.target === e.currentTarget) setShowPreviewModal(false); }}
>
{/* Modal Header */}
description
{displayTitle}
{/* Streaming indicator */}
edit_note
{isChinese ? '生成中...' : 'Generating...'}
{isChinese ? '实时预览 • 只读模式' : 'LIVE PREVIEW • READ-ONLY'}
{/* Header Actions - Only fullscreen and close during streaming */}
{/* Fullscreen toggle */}
setIsFullscreen(!isFullscreen)}
className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400 transition-colors"
title={isFullscreen ? (isChinese ? '退出全屏' : 'Exit fullscreen') : (isChinese ? '全屏' : 'Fullscreen')}
>
{isFullscreen ? 'fullscreen_exit' : 'fullscreen'}
{/* Download disabled during streaming */}
download
{/* Close button */}
setShowPreviewModal(false)}
className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400 transition-colors"
>
close
{/* Modal Content - Streaming Markdown Preview */}
{/* Streaming indicator bar */}
{/* Document Content - Same style as final BlueprintMessage */}
{streamingMarkdown ? (
) : (
progress_activity
{isChinese ? '等待内容生成...' : 'Waiting for content...'}
)}
{/* Bottom streaming indicator */}
edit_note
{isChinese ? '正在实时生成内容...' : 'Generating content in real-time...'}
{partialContent.length} chars
)}
);
};
var ChatMessage = ({ message, isUser, type, blueprint, pkg, phase, progress, detailLog, activityLogs, onBlueprintSave, onBlueprintGenerate, generationId, blueprintHistory, version, updatedAt, quickOptions, onQuickOptionClick, isLastMessage, extractedInfo, lessonInfo, onConfirmGenerate, isGeneratingMaterials, isGeneratingBlueprint, onCreateMaterials, materialsGenerated, blueprintModifiedSinceGeneration, onBlueprintModified, isGenerationContext, attachments, isReadOnly, isHistoryVersion, historyVersion, historyTimestamp, thoughtText, streamingContent, isStreaming, conversationMessages, selectedPackages, completedSteps, currentStep, slidePreview }) => {
// State for attachment preview
var [previewAttachment, setPreviewAttachment] = React.useState(null);
// Confirmed summary - read-only display for history
if (type === 'confirmed_summary' && lessonInfo) {
return (
);
}
if (type === 'confirmation' && extractedInfo) {
return (
);
}
if (type === 'blueprint' && blueprint) {
return (
);
}
// Blueprint ready message with action buttons
// Hide buttons when materials are already generated
if (type === 'blueprint_ready') {
return (
check_circle
BLUEPRINT READY
{message}
{/* Only show action buttons if materials not yet generated */}
{!materialsGenerated && (
{isGeneratingMaterials ? (
progress_activity
Generating...
) : (
auto_awesome
Create Materials Now
)}
edit
Edit Blueprint First
)}
);
}
if (type === 'generating') {
return React.createElement(GeneratingMessage, {
phase: phase,
progress: progress,
detailLog: detailLog,
activityLogs: activityLogs,
selectedPackages: selectedPackages,
completedSteps: completedSteps,
currentStep: currentStep,
slidePreview: slidePreview
});
}
if (type === 'package' && pkg) {
return ;
}
// Check if we should show quick options (only for last assistant message with options)
var showQuickOptions = !isUser && isLastMessage && quickOptions && quickOptions.length > 0;
// Simple markdown rendering for chat messages (bold, italic)
// Also removes the "📎 Attached: ..." line since we'll render attachments separately
var renderChatMarkdown = function(text, hasAttachments) {
if (!text) return '';
var processedText = text;
// Remove the "📎 Attached: ..." line if we have structured attachments to render
if (hasAttachments) {
processedText = processedText.replace(/\n\n📎 Attached:.*$/s, '');
processedText = processedText.replace(/^📎 Attached:.*$/m, '');
}
// Escape HTML first to prevent XSS
var html = processedText.replace(/&/g, '&').replace(//g, '>');
// Replace **text** with text (bold)
html = html.replace(/\*\*(.+?)\*\*/g, '$1 ');
// Replace _text_ with text (italic)
html = html.replace(/_([^_]+)_/g, '$1 ');
// Preserve newlines
html = html.replace(/\n/g, ' ');
return html.trim();
};
// Check if message has attachments
var hasAttachments = attachments && attachments.length > 0;
var renderedMessage = renderChatMarkdown(message, hasAttachments);
return (
{!isUser && (
)}
{/* Show thinking collapsible - Gemini style (before message) */}
{!isUser && thoughtText && (
)}
{/* Text content (if any) */}
{renderedMessage && (
)}
{/* Attachments rendered as clickable cards/thumbnails */}
{hasAttachments && (
{attachments.map(function(att, idx) {
var isImage = att.analysis_type === 'image' ||
(att.filename && /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(att.filename));
// For images with file_url, show thumbnail; otherwise fallback to card
if (isImage && att.file_url) {
// Image attachment - show thumbnail
return (
zoom_in
{att.filename}
);
}
// Non-image attachment OR image without file_url (legacy) - show card
return (
{isImage ? '🖼️' : getFileIcon(att.analysis_type)}
{att.filename}
{att.file_url && (
open_in_new
)}
);
})}
)}
{/* Attachment Preview Modal */}
{previewAttachment && (
)}
{showQuickOptions && (
{quickOptions.map(function(option, idx) {
return (
{option.label}
);
})}
{/* Manual input hint */}
keyboard
{/[\u4e00-\u9fa5]/.test(message || '')
? '以上选项不合适?可以直接在下方输入框输入您的答案'
: 'None of these? You can type your answer in the input box below'}
)}
);
};
// Blueprint View
var BlueprintView = ({ blueprint, onAdjust, onCreateMaterials, isGeneratingMaterials, materialsGenerated }) => {
const metadata = blueprint.metadata || {};
const lessonFlow = blueprint.lesson_flow || [];
const objectives = blueprint.learning_objectives || [];
const differentiation = blueprint.differentiation_strategies || {};
const standards = blueprint.standards_alignment || {};
// Detect language from blueprint title
const isZh = isChinese(blueprint.title || '');
return (
{/* Header */}
school
{metadata.grade_level || '?'}
science
{metadata.subject || (isZh ? '通用' : 'General')}
schedule
{metadata.duration_minutes || 45}{isZh ? '分钟' : 'm'}
history
{isZh ? '历史' : 'History'}
share
{isZh ? '分享' : 'Share'}
{blueprint.title}
{blueprint.summary}
{/* Learning Objectives */}
{isZh ? '学习目标' : 'Objectives'}
onAdjust('objectives')} className="opacity-0 group-hover:opacity-100 transition-opacity text-primary hover:bg-primary/5 px-2 py-0.5 rounded text-[10px] font-medium flex items-center gap-0.5">
edit {isZh ? '调整' : 'Adjust'}
{objectives.map((obj, i) => (
check_circle
{obj.text || obj}
))}
{/* Standards */}
{standards.primary_standards && standards.primary_standards.length > 0 && (
{isZh ? '教学标准' : 'Standards'}
{standards.primary_standards.join(', ')}
)}
{/* Lesson Flow */}
{isZh ? '课程流程' : 'Lesson Flow'}
onAdjust('flow')} className="text-gray-400 hover:text-primary transition-colors">
tune
{lessonFlow.map((phase, i) => (
{/* Timeline vertical line */}
{i !== lessonFlow.length - 1 && (
)}
{/* Time and Phase Label */}
{phase.duration_minutes || 5} min
{phase.phase || 'Activity'}
{/* Timeline dot with icon */}
{phase.activity_type === 'Discussion' ? 'forum' :
phase.activity_type === 'Lab' || phase.activity_type === 'Activity' ? 'science' :
phase.activity_type === 'Presentation' ? 'slideshow' :
phase.activity_type === 'Assessment' ? 'assignment_turned_in' :
'radio_button_checked'}
{/* Content */}
onAdjust(phase.id)} className="flex-1 pt-0.5 group-hover:bg-gray-50 dark:group-hover:bg-zinc-800 p-3 rounded-lg cursor-pointer transition-all border border-transparent group-hover:border-gray-200 dark:group-hover:border-zinc-600 group-hover:shadow-sm">
{phase.title}
{phase.description}
{phase.activity_type && (
{phase.activity_type}
)}
))}
{/* Differentiation */}
Differentiation
{differentiation.struggling_learners && (
support
Support
{differentiation.struggling_learners}
)}
{differentiation.advanced_learners && (
rocket_launch
Extension
{differentiation.advanced_learners}
)}
{/* Create Materials CTA - only show if materials not yet generated */}
{!materialsGenerated ? (
{isGeneratingMaterials ? (
progress_activity
Generating materials...
) : (
auto_awesome
Looks great! Create my materials
)}
) : (
check_circle
Materials have been created. View them in the Materials tab.
)}
);
};
// Package View
var PackageView = ({ package: pkg, onBack }) => {
const files = (pkg && pkg.files) || [];
// Find the slides file for main download
const slidesFile = files.find(f => f.type === 'slide');
const downloadUrl = slidesFile && slidesFile.download_url;
const handlePreview = (file) => {
if (file.preview_url) {
window.open(file.preview_url, '_blank');
} else {
alert('Preview not available for this item');
}
};
const handleDownloadFile = (file) => {
if (file.download_url) {
window.location.href = file.download_url;
} else {
alert('Download not available for this item');
}
};
const handleDownloadPackage = () => {
if (downloadUrl) {
window.location.href = downloadUrl;
} else {
alert('Package download not available');
}
};
return (
check_circle
Ready
{(pkg && pkg.title) || 'Teaching Package'}
folder_zip
{isChinese ? '下载文档' : 'Download Docs'}
{files.map((file, i) => {
const icons = {
slide: { icon: 'slideshow', bg: 'bg-amber-50 dark:bg-amber-900/20', color: 'text-amber-600 dark:text-amber-400' },
worksheet: { icon: 'article', bg: 'bg-blue-50 dark:bg-blue-900/20', color: 'text-blue-600 dark:text-blue-400' },
quiz: { icon: 'assignment_turned_in', bg: 'bg-emerald-50 dark:bg-emerald-900/20', color: 'text-emerald-600 dark:text-emerald-400' },
exit_ticket: { icon: 'receipt_long', bg: 'bg-teal-50 dark:bg-teal-900/20', color: 'text-teal-600 dark:text-teal-400' },
document: { icon: 'description', bg: 'bg-rose-50 dark:bg-rose-900/20', color: 'text-rose-600 dark:text-rose-400' }
};
const iconConfig = icons[file.type] || icons.document;
const hasPreview = !!file.preview_url;
const hasDownload = !!file.download_url;
return (
{iconConfig.icon}
{file.title}
{file.description}
{file.slide_count && (
{file.slide_count} slides
)}
handlePreview(file)}
disabled={!hasPreview}
className="flex-1 flex items-center justify-center gap-1 h-7 rounded text-[10px] font-semibold text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
visibility
{hasPreview ? 'Preview' : 'N/A'}
handleDownloadFile(file)}
disabled={!hasDownload}
className="flex-1 flex items-center justify-center gap-1 h-7 rounded text-[10px] font-semibold text-primary hover:bg-primary/5 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
download
{hasDownload ? 'Get' : 'N/A'}
);
})}
{/* Back button */}
arrow_back
New Lesson
);
};
// Generation View - Show full generation record
var GenerationView = ({ generation, blueprint, teachingPackage, messages, onBack, onCreateMaterials, onAdjust, isGeneratingMaterials }) => {
const [activeTab, setActiveTab] = useState(teachingPackage ? 'package' : 'blueprint');
const metadata = (blueprint && blueprint.metadata) || {};
return (
{/* Header */}
arrow_back
folder
{(blueprint && blueprint.title) || 'Teaching Package'}
{metadata.subject && {metadata.subject} }
{metadata.grade_level && • Grade {metadata.grade_level} }
{metadata.duration_minutes && • {metadata.duration_minutes} min }
{/* Tab Switcher */}
setActiveTab('conversation')}
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${activeTab === 'conversation' ? 'bg-white dark:bg-gray-700 shadow-sm text-primary' : 'text-gray-500 hover:text-gray-700'}`}
>
chat
Conversation
setActiveTab('blueprint')}
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${activeTab === 'blueprint' ? 'bg-white dark:bg-gray-700 shadow-sm text-primary' : 'text-gray-500 hover:text-gray-700'}`}
>
article
Blueprint
{teachingPackage && (
setActiveTab('package')}
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${activeTab === 'package' ? 'bg-white dark:bg-gray-700 shadow-sm text-primary' : 'text-gray-500 hover:text-gray-700'}`}
>
folder_zip
Materials
)}
{/* Content */}
{activeTab === 'conversation' && (
Conversation History
Original conversation that generated this lesson
{messages.map((msg, i) => (
))}
{messages.length === 0 && (
history
No conversation history available
)}
)}
{activeTab === 'blueprint' && blueprint && (
)}
{activeTab === 'package' && teachingPackage && (
)}
);
};
// Generating Animation with detailed progress
var GeneratingView = ({ phase, progress, progressSteps = [], stepTracking = {} }) => {
// Default steps if none provided
const defaultSteps = [
{ id: 'slide', label: 'Interactive Lesson', icon: 'slideshow', status: 'pending' },
{ id: 'lesson_plan', label: 'Lesson Plan', icon: 'description', status: 'pending' },
{ id: 'worksheet', label: 'Student Worksheet', icon: 'assignment', status: 'pending' },
{ id: 'quiz', label: 'Assessment Quiz', icon: 'quiz', status: 'pending' },
{ id: 'group_activity', label: 'Group Activity', icon: 'groups', status: 'pending' },
{ id: 'vocabulary_cards', label: 'Vocabulary Cards', icon: 'style', status: 'pending' }
];
const steps = progressSteps.length > 0 ? progressSteps : defaultSteps;
// Use explicit step tracking from SSE events
const completedStepsSet = new Set(stepTracking.completedSteps || []);
const currentStepId = stepTracking.currentStep || null;
// Determine current step based on explicit tracking + fallback to phase message
const getStepStatus = (stepId) => {
// Normalize stepId for slide/slides
const normalizedStepId = stepId === 'slides' ? 'slide' : stepId;
// PRIORITY 1: Use explicit step tracking from SSE events (most reliable)
if (completedStepsSet.has(stepId) || completedStepsSet.has(normalizedStepId)) {
return 'completed';
}
if (currentStepId === stepId || currentStepId === normalizedStepId) {
return 'current';
}
// PRIORITY 2: For parallel generation, non-slide steps are all "current"
const isSlide = stepId === 'slide' || stepId === 'slides';
const slideCompleted = completedStepsSet.has('slide') || completedStepsSet.has('slides');
if (currentStepId === 'parallel' && !completedStepsSet.has(stepId) && !completedStepsSet.has(normalizedStepId) && !isSlide) {
return 'current';
}
// PRIORITY 3: Fall back to pattern matching for compatibility
if (!phase) return 'pending';
const phaseLower = phase.toLowerCase();
// Helper function to check if any pattern matches
const matchesAny = (patterns, text) => {
if (!patterns) return false;
return patterns.some(function(p) { return text.includes(p); });
};
// Check for completion markers
const completedPatterns = {
'slide': ['interactive lesson completed', '✓ interactive lesson', 'slides saved', 'slide complete'],
'slides': ['interactive lesson completed', '✓ interactive lesson', 'slides saved', 'slide complete'],
'worksheet': ['worksheet completed', '✓ student worksheet', '✓ worksheet', 'worksheet saved'],
'quiz': ['quiz completed', '✓ assessment quiz', '✓ quiz', 'quiz saved'],
'lesson_plan': ['lesson plan completed', '✓ lesson plan', 'lesson plan saved'],
'group_activity': ['group activity completed', '✓ group activity', 'group activity saved'],
'vocabulary_cards': ['vocabulary cards completed', '✓ vocabulary', 'vocabulary saved']
};
// Check for in-progress patterns
const inProgressPatterns = {
'slide': ['generating interactive', 'interactive presentation', 'generating image', 'background image', 'parallel image'],
'slides': ['generating interactive', 'interactive presentation', 'generating image', 'background image', 'parallel image'],
'worksheet': ['generating student worksheet', 'generating worksheet'],
'quiz': ['generating assessment quiz', 'generating quiz'],
'lesson_plan': ['generating lesson plan'],
'group_activity': ['generating group activity', 'generating group'],
'vocabulary_cards': ['generating vocabulary cards', 'generating vocabulary']
};
// Check if this step is completed via pattern
if (matchesAny(completedPatterns[stepId], phaseLower) || matchesAny(completedPatterns[normalizedStepId], phaseLower)) {
return 'completed';
}
// Check if this step is in progress via pattern
if (matchesAny(inProgressPatterns[stepId], phaseLower) || matchesAny(inProgressPatterns[normalizedStepId], phaseLower)) {
return 'current';
}
return 'pending';
};
return (
Creating Teaching Materials
Your materials will be ready in ~2 minutes
{/* Progress Steps */}
{steps.map((step, idx) => {
const status = getStepStatus(step.id);
return (
{status === 'completed' ? (
check_circle
) : status === 'current' ? (
progress_activity
) : (
{step.icon}
)}
{status === 'completed' ? '✓ ' : status === 'current' ? '⏳ ' : ''}{step.label}
{status === 'current' && phase && (
{phase}
)}
);
})}
{/* Progress Bar */}
);
};
// Adjust Blueprint Modal
var AdjustModal = ({ isOpen, onClose, onSubmit, sectionId }) => {
const [instructions, setInstructions] = useState('');
if (!isOpen) return null;
return (
edit_note
Adjust Section
Refine content with AI
close
auto_awesome
Modification Instructions
Cancel
{ onSubmit(sectionId, instructions); setInstructions(''); }} className="px-6 h-11 rounded-lg bg-primary text-white font-bold text-sm shadow-md hover:bg-primary-hover transition-colors">
Update Blueprint
);
};
// ============================================================================
// Login Page Component
// ============================================================================
var LoginPage = ({ onLogin }) => {
const [isRegister, setIsRegister] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [displayName, setDisplayName] = useState('');
const [inviteCode, setInviteCode] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
if (isRegister) {
// Register
const response = await fetch('/api/v1/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username.trim(),
password,
name: displayName.trim() || username.trim(),
invite_code: inviteCode.trim()
})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.detail || 'Registration failed');
}
setAuthToken(result.token);
setStoredUser(result.user);
onLogin(result.user);
} else {
// Login
const result = await api.login(username, password);
setAuthToken(result.token);
setStoredUser(result.user);
onLogin(result.user);
}
} catch (err) {
setError(err.message || (isRegister ? 'Registration failed.' : 'Login failed. Please check your credentials.'));
} finally {
setIsLoading(false);
}
};
const toggleMode = () => {
setIsRegister(!isRegister);
setError('');
};
return (
{/* Logo & Title */}
auto_awesome
Scaffo.ai
AI Teaching Materials Generator
{/* Login/Register Card */}
{isRegister ? 'Create Account' : 'Sign In'}
{/* Form */}
{/* Toggle Login/Register */}
{isRegister ? 'Already have an account?' : "Don't have an account?"}
{isRegister ? 'Sign In' : 'Sign Up'}
);
};
// ============================================================================
// Main App
// ============================================================================