embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
659 lines (582 loc) ⢠18.5 kB
JavaScript
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 `
<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 `
<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;