UNPKG

embedia

Version:

Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys

659 lines (582 loc) • 18.5 kB
const express = require('express'); const cors = require('cors'); const chalk = require('chalk'); const fs = require('fs-extra'); const path = require('path'); const chokidar = require('chokidar'); const WebSocket = require('ws'); const open = require('open'); class EmbediaDevServer { constructor(projectPath, config) { this.projectPath = projectPath; this.config = config; this.watchers = []; this.wsClients = new Set(); } async start(port = 3456) { console.log(chalk.cyan('šŸš€ Starting Embedia Dev Server...\n')); try { // Start configuration watcher this.watchConfig(); // Start mock API server await this.startMockAPI(port); // Start hot reload server await this.startHotReload(port + 1); // Start static file server await this.startStaticServer(port + 2); // Open dev dashboard setTimeout(() => { this.openDashboard(port); }, 1000); console.log(chalk.green(`\n✨ Embedia Dev Server running!\n`)); console.log(chalk.gray(` Dashboard: http://localhost:${port}/dashboard`)); console.log(chalk.gray(` Mock API: http://localhost:${port}/api/embedia/chat`)); console.log(chalk.gray(` Hot Reload: ws://localhost:${port + 1}`)); console.log(chalk.gray(` Static: http://localhost:${port + 2}\n`)); console.log(chalk.yellow('Press Ctrl+C to stop\n')); } catch (error) { console.error(chalk.red(`Failed to start dev server: ${error.message}`)); process.exit(1); } } watchConfig() { const configPath = path.join(this.projectPath, 'components/generated/embedia-chat/config.json'); const watcher = chokidar.watch(configPath, { persistent: true, ignoreInitial: true }); watcher.on('change', async () => { console.log(chalk.yellow('šŸ“ Configuration changed, reloading...')); try { // Read new config const newConfig = await fs.readJson(configPath); // Validate config const validation = await this.validateConfig(newConfig); if (validation.valid) { // Broadcast change to connected clients this.broadcastConfigChange(newConfig); console.log(chalk.green('āœ… Configuration updated successfully')); } else { console.log(chalk.red('āŒ Invalid configuration:'), validation.errors); } } catch (error) { console.error(chalk.red('Error reading config:'), error.message); } }); this.watchers.push(watcher); } async startMockAPI(port) { const app = express(); app.use(cors()); app.use(express.json()); // Serve dashboard app.get('/dashboard', async (req, res) => { const dashboardHTML = await this.generateDashboardHTML(port); res.send(dashboardHTML); }); // Mock chat endpoint app.post('/api/embedia/chat', async (req, res) => { const { messages } = req.body; const lastMessage = messages[messages.length - 1]; console.log(chalk.gray(`[Chat API] Received: "${lastMessage.content}"`)); // Simulate AI response with typing delay await new Promise(resolve => setTimeout(resolve, 800)); const mockResponses = [ "That's an interesting question! Let me help you with that.", "I understand what you're asking. Here's what I think...", "Great point! Have you considered this approach?", "I'm here to help. Let me explain that for you.", "Thanks for your message! Here's my response...", `You asked about "${lastMessage.content}". Here's what I found...` ]; const response = mockResponses[Math.floor(Math.random() * mockResponses.length)]; res.json({ response: response, timestamp: new Date().toISOString() }); }); // Configuration API app.get('/api/embedia/config', async (req, res) => { const configPath = path.join(this.projectPath, 'components/generated/embedia-chat/config.json'); try { const config = await fs.readJson(configPath); res.json(config); } catch (error) { res.status(404).json({ error: 'Config not found' }); } }); app.post('/api/embedia/config', async (req, res) => { const newConfig = req.body; try { // Validate config const validation = await this.validateConfig(newConfig); if (!validation.valid) { return res.status(400).json({ error: 'Invalid config', errors: validation.errors }); } // Update config file const configPath = path.join(this.projectPath, 'components/generated/embedia-chat/config.json'); await fs.writeJson(configPath, newConfig, { spaces: 2 }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Analytics endpoint app.post('/api/embedia/analytics', (req, res) => { console.log(chalk.gray('[Analytics]'), req.body); res.json({ success: true }); }); return new Promise((resolve) => { app.listen(port, () => { resolve(); }); }); } async startHotReload(port) { const wss = new WebSocket.Server({ port }); wss.on('connection', (ws) => { this.wsClients.add(ws); console.log(chalk.gray('šŸ”Œ Client connected for hot reload')); // Send initial config this.sendInitialConfig(ws); ws.on('close', () => { this.wsClients.delete(ws); console.log(chalk.gray('šŸ”Œ Client disconnected')); }); ws.on('error', (error) => { console.error(chalk.red('WebSocket error:'), error); }); }); } async startStaticServer(port) { const app = express(); // Serve component files app.use('/embedia-chat', express.static( path.join(this.projectPath, 'components/generated/embedia-chat') )); // Serve test page app.get('/', async (req, res) => { const testHTML = await this.generateTestHTML(); res.send(testHTML); }); return new Promise((resolve) => { app.listen(port, () => { resolve(); }); }); } async sendInitialConfig(ws) { try { const configPath = path.join(this.projectPath, 'components/generated/embedia-chat/config.json'); const config = await fs.readJson(configPath); ws.send(JSON.stringify({ type: 'config-init', config: config })); } catch (error) { // Ignore if config doesn't exist yet } } broadcastConfigChange(config) { const message = JSON.stringify({ type: 'config-update', config: config, timestamp: new Date().toISOString() }); for (const client of this.wsClients) { if (client.readyState === WebSocket.OPEN) { client.send(message); } } } async validateConfig(config) { const errors = []; // Required fields const required = ['chatbotName', 'themeColors', 'position']; for (const field of required) { if (!config[field]) { errors.push(`Missing required field: ${field}`); } } // Validate theme colors if (config.themeColors) { const colorFields = ['primary', 'background', 'text']; for (const field of colorFields) { if (config.themeColors[field] && !/^#[0-9A-F]{6}$/i.test(config.themeColors[field])) { errors.push(`Invalid color format for ${field}: ${config.themeColors[field]}`); } } } // Validate position const validPositions = ['bottom-right', 'bottom-left', 'top-right', 'top-left', 'custom']; if (config.position && !validPositions.includes(config.position)) { errors.push(`Invalid position: ${config.position}`); } return { valid: errors.length === 0, errors }; } async openDashboard(port) { try { await open(`http://localhost:${port}/dashboard`); } catch (error) { console.log(chalk.yellow(`\nšŸ“‹ Open http://localhost:${port}/dashboard in your browser\n`)); } } async generateDashboardHTML(port) { const configPath = path.join(this.projectPath, 'components/generated/embedia-chat/config.json'); let config = {}; try { config = await fs.readJson(configPath); } catch (error) { config = { chatbotName: 'Embedia Chat', position: 'bottom-right' }; } return `<!DOCTYPE html> <html> <head> <title>Embedia Dev Dashboard</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f3f4f6; color: #1f2937; } .header { background: white; padding: 1.5rem 2rem; border-bottom: 1px solid #e5e7eb; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .header h1 { font-size: 1.5rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem; } .container { max-width: 1400px; margin: 0 auto; padding: 2rem; } .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-bottom: 2rem; } .card { background: white; padding: 1.5rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .card h2 { font-size: 1.25rem; margin-bottom: 1rem; color: #374151; } .preview { height: 600px; position: relative; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 0.375rem; overflow: hidden; } iframe { width: 100%; height: 100%; border: none; } .status { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0.75rem; background: #10b981; color: white; border-radius: 9999px; font-size: 0.875rem; font-weight: 500; } .status-dot { width: 8px; height: 8px; background: white; border-radius: 50%; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .config-editor { display: flex; flex-direction: column; gap: 1rem; } .form-group { display: flex; flex-direction: column; gap: 0.5rem; } label { font-weight: 500; font-size: 0.875rem; color: #374151; } input, select, textarea { padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.375rem; font-size: 0.875rem; transition: border-color 0.15s; } input:focus, select:focus, textarea:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } button { padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.375rem; font-weight: 500; cursor: pointer; transition: background 0.15s; } button:hover { background: #2563eb; } .color-input { display: flex; gap: 0.5rem; align-items: center; } .color-preview { width: 40px; height: 40px; border-radius: 0.375rem; border: 1px solid #d1d5db; } .logs { background: #1f2937; color: #f3f4f6; padding: 1rem; border-radius: 0.375rem; font-family: 'Consolas', 'Monaco', monospace; font-size: 0.875rem; height: 200px; overflow-y: auto; } .log-entry { margin-bottom: 0.5rem; opacity: 0.8; } .log-entry.new { animation: fadeIn 0.3s; opacity: 1; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } </style> </head> <body> <div class="header"> <div class="container" style="padding: 0;"> <h1> šŸš€ Embedia Dev Dashboard <span class="status"> <span class="status-dot"></span> Connected </span> </h1> </div> </div> <div class="container"> <div class="grid"> <div class="card"> <h2>āš™ļø Configuration</h2> <div class="config-editor"> <div class="form-group"> <label>Chatbot Name</label> <input type="text" id="chatbotName" value="${config.chatbotName || ''}" /> </div> <div class="form-group"> <label>Position</label> <select id="position"> <option value="bottom-right" ${config.position === 'bottom-right' ? 'selected' : ''}>Bottom Right</option> <option value="bottom-left" ${config.position === 'bottom-left' ? 'selected' : ''}>Bottom Left</option> <option value="top-right" ${config.position === 'top-right' ? 'selected' : ''}>Top Right</option> <option value="top-left" ${config.position === 'top-left' ? 'selected' : ''}>Top Left</option> </select> </div> <div class="form-group"> <label>Primary Color</label> <div class="color-input"> <input type="text" id="primaryColor" value="${config.themeColors?.primary || '#3b82f6'}" /> <div class="color-preview" style="background: ${config.themeColors?.primary || '#3b82f6'}"></div> </div> </div> <div class="form-group"> <label>Welcome Message</label> <textarea id="welcomeMessage" rows="3">${config.welcomeMessage || ''}</textarea> </div> <button onclick="saveConfig()">Save Configuration</button> </div> </div> <div class="card"> <h2>šŸ‘ļø Live Preview</h2> <div class="preview"> <iframe src="http://localhost:${port + 2}" id="previewFrame"></iframe> </div> </div> </div> <div class="card"> <h2>šŸ“Š Activity Logs</h2> <div class="logs" id="logs"> <div class="log-entry">šŸš€ Dev server started</div> </div> </div> </div> <script> // WebSocket connection for hot reload const ws = new WebSocket('ws://localhost:${port + 1}'); ws.onopen = () => { addLog('šŸ”Œ Connected to hot reload server'); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'config-update') { addLog('ā™»ļø Configuration updated'); document.getElementById('previewFrame').contentWindow.location.reload(); } }; ws.onerror = (error) => { addLog('āŒ WebSocket error: ' + error); }; // Configuration management async function saveConfig() { const config = { chatbotName: document.getElementById('chatbotName').value, position: document.getElementById('position').value, themeColors: { primary: document.getElementById('primaryColor').value, background: '#ffffff', text: '#1f2937' }, welcomeMessage: document.getElementById('welcomeMessage').value }; try { const response = await fetch('http://localhost:${port}/api/embedia/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); if (response.ok) { addLog('āœ… Configuration saved'); } else { addLog('āŒ Failed to save configuration'); } } catch (error) { addLog('āŒ Error: ' + error.message); } } // Color preview update document.getElementById('primaryColor').addEventListener('input', (e) => { document.querySelector('.color-preview').style.background = e.target.value; }); // Logging function addLog(message) { const logs = document.getElementById('logs'); const entry = document.createElement('div'); entry.className = 'log-entry new'; entry.textContent = new Date().toLocaleTimeString() + ' - ' + message; logs.appendChild(entry); logs.scrollTop = logs.scrollHeight; } // Test chat setInterval(async () => { try { await fetch('http://localhost:${port}/api/embedia/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: 'Test message' }] }) }); addLog('šŸ’¬ Mock chat API called'); } catch (error) { // Ignore errors } }, 30000); </script> </body> </html>`; } async generateTestHTML() { return `<!DOCTYPE html> <html> <head> <title>Embedia Chat Test</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f9fafb; min-height: 100vh; } .container { max-width: 800px; margin: 0 auto; text-align: center; padding-top: 100px; } h1 { color: #1f2937; margin-bottom: 10px; } p { color: #6b7280; font-size: 18px; } </style> </head> <body> <div class="container"> <h1>Embedia Chat Test Page</h1> <p>The chat widget should appear in the corner of this page.</p> <p>This is a test environment for your Embedia Chat integration.</p> </div> <!-- Embedia Chat Integration --> <embedia-chatbot></embedia-chatbot> <script src="/embedia-chatbot.js"></script> </body> </html>`; } stop() { // Clean up watchers for (const watcher of this.watchers) { watcher.close(); } // Close WebSocket connections for (const client of this.wsClients) { client.close(); } console.log(chalk.yellow('\nšŸ‘‹ Embedia Dev Server stopped\n')); } } module.exports = EmbediaDevServer;