superdesign_live_gallery
Launch a live gallery server to monitor and update design files in real-time, with customizable workspace paths and port settings.
Instructions
Start a live gallery server with real-time updates and file watching
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| port | No | Port for the live gallery server (default: 3000) | |
| workspace_path | No | Workspace path (defaults to current directory) |
Implementation Reference
- src/index.ts:2037-2046 (registration)Registration of the superdesign_live_gallery tool in the listTools responsename: "superdesign_live_gallery", description: "Start a live gallery server with real-time updates and file watching", inputSchema: { type: "object", properties: { workspace_path: { type: "string", description: "Workspace path (defaults to current directory)" }, port: { type: "number", description: "Port for the live gallery server (default: 3000)" } }, }, },
- src/index.ts:61-64 (schema)Zod input schema for superdesign_live_gallery toolconst LiveGallerySchema = z.object({ workspace_path: z.string().optional().describe("Workspace path (defaults to current directory)"), port: z.number().optional().describe("Port for the live gallery server (default: 3000)") });
- src/index.ts:2459-2514 (handler)Main handler logic for superdesign_live_gallery: parses input, starts HTTP server with SSE for live updates, returns server URLcase "superdesign_live_gallery": { const { workspace_path, port } = LiveGallerySchema.parse(args); try { const superdesignDir = getSuperdeignDirectory(workspace_path); const designIterationsDir = path.join(superdesignDir, 'design_iterations'); if (!existsSync(designIterationsDir)) { return { content: [{ type: "text", text: "No design iterations found. Generate some designs first using superdesign_generate." }], }; } const serverPort = port || 3000; const serverUrl = await createLiveGalleryServer(superdesignDir, serverPort); let response = `LIVE GALLERY SERVER STARTED: 🌐 Server URL: ${serverUrl} 📁 Workspace: ${superdesignDir} 🔴 Live Updates: Enabled 📡 Port: ${serverPort} === FEATURES === ✨ Real-time file watching 🔄 Auto-refresh on file changes 🌙 Dark/Light mode toggle 📱 Responsive design 🗂️ File metadata display === USAGE === 1. Open ${serverUrl} in your browser 2. The gallery will automatically update when design files change 3. Use the theme toggle to switch between light and dark modes 4. Click "View Full" to open designs in new tabs 5. Use "Copy Path" to copy file paths to clipboard === LIVE UPDATES === The gallery will automatically detect: • New design files added to design_iterations/ • Modified design files (with visual highlights) • Deleted design files (with smooth removal) Keep this server running to maintain live updates. The gallery will reconnect automatically if the connection is lost. Server is now running at: ${serverUrl}`; return { content: [{ type: "text", text: response }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error starting live gallery server: ${error.message}` }], }; } }
- src/index.ts:434-512 (helper)Core helper function that creates the HTTP server for the live gallery, serving HTML, SSE events, and static design filesfunction createLiveGalleryServer(superdesignDir: string, port: number = 3000): Promise<string> { return new Promise((resolve, reject) => { const server = http.createServer((req, res) => { const parsedUrl = url.parse(req.url || '', true); const pathname = parsedUrl.pathname; // CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } if (pathname === '/') { // Serve the gallery HTML try { const designIterationsDir = path.join(superdesignDir, 'design_iterations'); const designFiles = glob.sync('*.{html,svg}', { cwd: designIterationsDir }); const galleryHtml = generateLiveGalleryHTML(designFiles, superdesignDir); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(galleryHtml); } catch (error: any) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end(`Error generating gallery: ${error.message}`); } } else if (pathname === '/events') { // Server-Sent Events endpoint res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); const watcher = startFileWatcher(superdesignDir); watcher.clients.add(res); // Send initial connection message res.write('data: {"event": "connected", "data": {}}\n\n'); // Clean up when client disconnects req.on('close', () => { watcher.clients.delete(res); }); } else if (pathname?.startsWith('/design_iterations/')) { // Serve design files const fileName = pathname.substring('/design_iterations/'.length); const filePath = path.join(superdesignDir, 'design_iterations', fileName); if (existsSync(filePath)) { const ext = path.extname(fileName).toLowerCase(); const contentType = ext === '.html' ? 'text/html' : ext === '.svg' ? 'image/svg+xml' : 'text/plain'; res.writeHead(200, { 'Content-Type': contentType }); res.end(readFileSync(filePath)); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('File not found'); } } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not found'); } }); server.listen(port, () => { resolve(`http://localhost:${port}`); }); server.on('error', (error) => { reject(error); }); }); }
- src/index.ts:515-1060 (helper)Generates the interactive HTML page for the live gallery with embedded CSS/JS for real-time updates via SSEfunction generateLiveGalleryHTML(designFiles: string[], superdesignDir: string): string { const metadata = getDesignMetadata(superdesignDir); const metadataMap = new Map(metadata.map(m => [m.fileName, m])); const designCards = designFiles.map((file, index) => { const fileExtension = path.extname(file).toLowerCase(); const fileName = path.basename(file, fileExtension); const relativePath = `/design_iterations/${file}`; const fileMetadata = metadataMap.get(file); let previewContent = ''; if (fileExtension === '.html') { previewContent = `<iframe src="${relativePath}" loading="lazy"></iframe>`; } else if (fileExtension === '.svg') { previewContent = `<object data="${relativePath}" type="image/svg+xml" class="svg-preview"></object>`; } // Format file size const formatFileSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; // Format date const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }); }; return ` <div class="design-card" data-file="${file}" data-created="${fileMetadata?.createdAt || ''}"> <div class="design-header"> <div class="design-title"> <h3>${fileName}</h3> <span class="design-type">${fileExtension.toUpperCase()}</span> </div> <div class="design-meta"> ${fileMetadata ? ` <span class="design-size">${formatFileSize(fileMetadata.fileSize)}</span> <span class="design-date">${formatDate(fileMetadata.createdAt)}</span> ` : ''} </div> </div> <div class="design-preview"> ${previewContent} </div> <div class="design-actions"> <button onclick="openFullscreen('${relativePath}')" class="btn-primary">View Full</button> <button onclick="copyPath('${relativePath}')" class="btn-secondary">Copy Path</button> <button onclick="deleteDesign('${file}')" class="btn-danger">Delete</button> </div> </div> `; }).join(''); return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Superdesign Live Gallery</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } :root { --bg-primary: #ffffff; --bg-secondary: #f8fafc; --bg-card: #ffffff; --text-primary: #1a202c; --text-secondary: #718096; --border-color: #e2e8f0; --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); --shadow-hover: 0 10px 25px -5px rgba(0, 0, 0, 0.2); --accent-color: #4299e1; } [data-theme="dark"] { --bg-primary: #1a202c; --bg-secondary: #2d3748; --bg-card: #2d3748; --text-primary: #f7fafc; --text-secondary: #a0aec0; --border-color: #4a5568; --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3); --shadow-hover: 0 10px 25px -5px rgba(0, 0, 0, 0.4); } body { font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; padding: 20px; transition: background 0.3s ease, color 0.3s ease; } .header { margin-bottom: 40px; display: flex; justify-content: space-between; align-items: flex-start; } .header-content h1 { font-size: 2.5rem; color: var(--text-primary); margin-bottom: 8px; font-weight: 600; letter-spacing: -0.02em; } .header-content p { color: var(--text-secondary); font-size: 1rem; font-weight: 400; } .header-controls { display: flex; gap: 12px; align-items: center; } .live-indicator { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 20px; font-size: 0.875rem; color: var(--text-secondary); } .live-dot { width: 8px; height: 8px; border-radius: 50%; background: #48bb78; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .theme-toggle { background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-primary); padding: 8px 16px; border-radius: 8px; cursor: pointer; font-family: inherit; font-size: 0.875rem; transition: all 0.2s ease; display: flex; align-items: center; gap: 8px; } .theme-toggle:hover { background: var(--bg-secondary); transform: translateY(-1px); } .gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 24px; max-width: 1400px; } .design-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; } .design-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-hover); } .design-card.new-file { border-color: var(--accent-color); box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.1); } .design-header { padding: 16px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: flex-start; } .design-title { display: flex; align-items: center; gap: 8px; } .design-title h3 { color: var(--text-primary); font-size: 1rem; font-weight: 500; font-family: inherit; } .design-type { background: var(--bg-secondary); color: var(--text-secondary); padding: 4px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 400; font-family: inherit; } .design-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; } .design-size, .design-date { color: var(--text-secondary); font-size: 0.75rem; font-family: inherit; } .design-date { opacity: 0.8; } .design-preview { height: 300px; position: relative; overflow: hidden; } .design-preview iframe { width: 100%; height: 100%; border: none; transform: scale(0.5); transform-origin: top left; width: 200%; height: 200%; } .design-preview .svg-preview { width: 100%; height: 100%; object-fit: contain; background: var(--bg-primary); } .design-actions { padding: 16px; display: flex; gap: 8px; } .btn-primary { background: var(--accent-color); color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 0.875rem; font-family: inherit; cursor: pointer; transition: background 0.2s ease; } .btn-primary:hover { background: #3182ce; } .btn-secondary { background: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-color); padding: 8px 16px; border-radius: 6px; font-size: 0.875rem; font-family: inherit; cursor: pointer; transition: background 0.2s ease; } .btn-secondary:hover { background: var(--border-color); } .btn-danger { background: #e53e3e; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 0.875rem; font-family: inherit; cursor: pointer; transition: background 0.2s ease; } .btn-danger:hover { background: #c53030; } .empty-state { text-align: center; padding: 60px 20px; color: var(--text-secondary); } .empty-state h3 { font-size: 1.25rem; margin-bottom: 8px; } @media (max-width: 768px) { .gallery-grid { grid-template-columns: 1fr; } .design-preview { height: 200px; } .header { flex-direction: column; gap: 16px; align-items: flex-start; } } </style> </head> <body> <div class="header"> <div class="header-content"> <h1>superdesign.live</h1> <p id="file-count">${designFiles.length} designs found</p> </div> <div class="header-controls"> <div class="live-indicator"> <div class="live-dot"></div> <span id="connection-status">Connecting...</span> </div> <button class="theme-toggle" onclick="toggleTheme()"> <span id="theme-icon">🌙</span> <span id="theme-text">Dark</span> </button> </div> </div> <div class="gallery-grid" id="gallery-grid"> ${designCards} </div> <script> // Theme management const theme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', theme); updateThemeButton(); function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); updateThemeButton(); } function updateThemeButton() { const theme = document.documentElement.getAttribute('data-theme'); const icon = document.getElementById('theme-icon'); const text = document.getElementById('theme-text'); if (theme === 'dark') { icon.textContent = '☀️'; text.textContent = 'Light'; } else { icon.textContent = '🌙'; text.textContent = 'Dark'; } } function openFullscreen(path) { window.open(path, '_blank'); } function copyPath(path) { navigator.clipboard.writeText(path).then(() => { showToast('Path copied to clipboard!', 'success'); }); } function deleteDesign(fileName) { if (confirm(\`Are you sure you want to delete \${fileName}?\`)) { showToast('Delete functionality via MCP server!', 'info'); } } function showToast(message, type = 'info') { const colors = { success: '#48bb78', error: '#e53e3e', info: '#4299e1', warning: '#ed8936' }; const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = \` position: fixed; top: 20px; right: 20px; background: \${colors[type] || colors.info}; color: white; padding: 12px 20px; border-radius: 6px; z-index: 1000; font-size: 14px; font-family: inherit; animation: slideIn 0.3s ease; \`; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease'; setTimeout(() => toast.remove(), 300); }, 3000); } // Server-Sent Events for live updates let eventSource; function connectToEvents() { eventSource = new EventSource('/events'); eventSource.onopen = function() { document.getElementById('connection-status').textContent = 'Live'; showToast('Connected to live updates!', 'success'); }; eventSource.onmessage = function(event) { const data = JSON.parse(event.data); handleFileChange(data.event, data.data); }; eventSource.onerror = function() { document.getElementById('connection-status').textContent = 'Disconnected'; showToast('Connection lost, attempting to reconnect...', 'warning'); // Attempt to reconnect after 3 seconds setTimeout(() => { eventSource.close(); connectToEvents(); }, 3000); }; } function handleFileChange(event, data) { const { file, type } = data; if (type === 'added') { showToast(\`New design added: \${file}\`, 'success'); // Refresh the gallery to show new file setTimeout(() => location.reload(), 1000); } else if (type === 'deleted') { showToast(\`Design deleted: \${file}\`, 'info'); // Remove the card from the DOM const card = document.querySelector(\`[data-file="\${file}"]\`); if (card) { card.style.animation = 'fadeOut 0.3s ease'; setTimeout(() => card.remove(), 300); updateFileCount(); } } else if (type === 'modified') { showToast(\`Design updated: \${file}\`, 'info'); // Highlight the modified file const card = document.querySelector(\`[data-file="\${file}"]\`); if (card) { card.classList.add('new-file'); setTimeout(() => card.classList.remove('new-file'), 3000); // Refresh the iframe if it's an HTML file const iframe = card.querySelector('iframe'); if (iframe) { iframe.src = iframe.src; } } } } function updateFileCount() { const cards = document.querySelectorAll('.design-card'); document.getElementById('file-count').textContent = \`\${cards.length} designs found\`; } // Add CSS animations const style = document.createElement('style'); style.textContent = \` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } @keyframes fadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.8); } } \`; document.head.appendChild(style); // Connect to live updates connectToEvents(); </script> </body> </html>`; }