embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
837 lines (708 loc) • 21.4 kB
JavaScript
const chalk = require('chalk');
/**
* Web Component Code Generator
* Generates universal web components that can be embedded anywhere
*/
class WebComponentGenerator {
constructor() {
this.version = '1.0.0';
}
/**
* Generate a complete web component bundle
* @param {Object} config - Configuration object
* @param {string} config.botId - Unique bot identifier
* @param {string} config.botName - Display name for the bot
* @param {string} config.apiProvider - AI provider (openai, gemini, etc.)
* @param {string} config.apiKey - API key for direct provider access
* @param {Object} config.persona - Bot personality configuration
* @param {Object} config.styling - Custom styling options
* @param {Object} config.projectContext - CLI detection context for endpoint calculation
* @returns {Object} Generated web component code and assets
*/
generateWebComponent(config) {
const {
botId,
botName = 'Embedia Chat',
apiProvider = 'gemini',
apiKey,
persona = {},
styling = {},
projectContext
} = config;
// CRITICAL FIX: Calculate correct API endpoint based on CLI detection
const apiEndpoint = this.calculateApiEndpoint(projectContext);
// Generate the main web component with calculated endpoint
const webComponentCode = this.generateWebComponentClass({ ...config, apiEndpoint });
// Generate supporting files
const htmlTemplate = this.generateHTMLTemplate(config);
const cssStyles = this.generateStyles(styling);
const readme = this.generateReadme(config);
const exampleUsage = this.generateExampleUsage(config);
return {
webComponent: webComponentCode,
styles: cssStyles,
html: htmlTemplate,
readme: readme,
examples: exampleUsage,
config: {
botId,
botName,
apiProvider,
version: this.version
}
};
}
/**
* Generate the main web component class
*/
generateWebComponentClass(config) {
const { botId, botName, apiProvider, apiKey, persona, apiEndpoint } = config;
return `/**
* Embedia Chatbot Web Component
* Universal chatbot component that works in any web environment
* Generated by Embedia CLI v${this.version}
*/
class EmbediaChatbot extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.isOpen = false;
this.messages = [];
this.config = {
botId: '${botId}',
botName: '${botName}',
apiProvider: '${apiProvider}',
apiKey: ${apiKey ? `'${apiKey}'` : 'null'},
persona: ${JSON.stringify(persona, null, 2)},
apiEndpoint: this.getAttribute('api-endpoint') || '${apiEndpoint || '/api/embedia/chat'}'
};
this.init();
}
static get observedAttributes() {
return ['bot-id', 'api-key', 'api-endpoint', 'theme', 'position'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.updateConfig();
}
}
updateConfig() {
this.config.botId = this.getAttribute('bot-id') || this.config.botId;
this.config.apiKey = this.getAttribute('api-key') || this.config.apiKey;
this.config.apiEndpoint = this.getAttribute('api-endpoint') || this.config.apiEndpoint;
}
init() {
this.render();
this.attachEventListeners();
}
render() {
const theme = this.getAttribute('theme') || 'default';
const position = this.getAttribute('position') || 'bottom-right';
this.shadowRoot.innerHTML = \`
<style>
\${this.getStyles(theme, position)}
</style>
<div class="embedia-chatbot">
<!-- Chat Toggle Button -->
<button class="chat-toggle" id="chatToggle">
<svg class="chat-icon" viewBox="0 0 24 24">
<path d="M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4l4 4 4-4h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>
</svg>
<svg class="close-icon" viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
<!-- Chat Window -->
<div class="chat-window" id="chatWindow">
<div class="chat-header">
<h3>\${this.config.botName}</h3>
<button class="minimize-btn" id="minimizeBtn">−</button>
</div>
<div class="chat-messages" id="chatMessages">
<div class="message bot-message">
<div class="message-content">
\${this.config.persona.greeting || 'Hello! How can I help you today?'}
</div>
</div>
</div>
<div class="chat-input-container">
<input type="text" class="chat-input" id="chatInput" placeholder="Type your message..." />
<button class="send-btn" id="sendBtn">
<svg viewBox="0 0 24 24">
<path d="M2,21L23,12L2,3V10L17,12L2,14V21Z" />
</svg>
</button>
</div>
<div class="loading-indicator" id="loadingIndicator" style="display: none;">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
\`;
}
getStyles(theme, position) {
return \`
:host {
position: fixed;
\${position.includes('bottom') ? 'bottom: 20px;' : 'top: 20px;'}
\${position.includes('right') ? 'right: 20px;' : 'left: 20px;'}
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.embedia-chatbot {
position: relative;
}
.chat-toggle {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.chat-toggle:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
}
.chat-icon, .close-icon {
width: 24px;
height: 24px;
fill: white;
transition: opacity 0.2s ease;
}
.close-icon {
position: absolute;
opacity: 0;
}
.chat-toggle.open .chat-icon {
opacity: 0;
}
.chat-toggle.open .close-icon {
opacity: 1;
}
.chat-window {
position: absolute;
bottom: 70px;
right: 0;
width: 350px;
height: 500px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
display: none;
flex-direction: column;
overflow: hidden;
}
.chat-window.open {
display: flex;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.minimize-btn {
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.chat-messages {
flex: 1;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
display: flex;
flex-direction: column;
max-width: 80%;
}
.bot-message {
align-self: flex-start;
}
.user-message {
align-self: flex-end;
}
.message-content {
background: #f1f3f4;
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
font-size: 14px;
line-height: 1.4;
}
.user-message .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.chat-input-container {
display: flex;
padding: 16px;
border-top: 1px solid #e0e0e0;
gap: 8px;
}
.chat-input {
flex: 1;
border: 1px solid #e0e0e0;
border-radius: 20px;
padding: 12px 16px;
outline: none;
font-size: 14px;
}
.chat-input:focus {
border-color: #667eea;
}
.send-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.send-btn svg {
width: 18px;
height: 18px;
fill: white;
}
.loading-indicator {
padding: 16px;
text-align: center;
}
.typing-dots {
display: inline-flex;
gap: 4px;
}
.typing-dots span {
width: 8px;
height: 8px;
background: #667eea;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
.typing-dots span:nth-child(1) { animation-delay: -0.32s; }
.typing-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
@media (max-width: 480px) {
.chat-window {
width: calc(100vw - 40px);
height: calc(100vh - 100px);
}
}
\`;
}
attachEventListeners() {
const toggle = this.shadowRoot.getElementById('chatToggle');
const window = this.shadowRoot.getElementById('chatWindow');
const minimize = this.shadowRoot.getElementById('minimizeBtn');
const input = this.shadowRoot.getElementById('chatInput');
const sendBtn = this.shadowRoot.getElementById('sendBtn');
toggle.addEventListener('click', () => this.toggleChat());
minimize.addEventListener('click', () => this.closeChat());
sendBtn.addEventListener('click', () => this.sendMessage());
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.sendMessage();
});
}
toggleChat() {
this.isOpen = !this.isOpen;
const toggle = this.shadowRoot.getElementById('chatToggle');
const window = this.shadowRoot.getElementById('chatWindow');
toggle.classList.toggle('open', this.isOpen);
window.classList.toggle('open', this.isOpen);
if (this.isOpen) {
this.shadowRoot.getElementById('chatInput').focus();
}
}
closeChat() {
this.isOpen = false;
const toggle = this.shadowRoot.getElementById('chatToggle');
const window = this.shadowRoot.getElementById('chatWindow');
toggle.classList.remove('open');
window.classList.remove('open');
}
async sendMessage() {
const input = this.shadowRoot.getElementById('chatInput');
const message = input.value.trim();
if (!message) return;
// Add user message
this.addMessage(message, 'user');
input.value = '';
// Show loading
this.showLoading(true);
try {
const response = await this.callAPI(message);
this.addMessage(response, 'bot');
} catch (error) {
console.error('Chat API Error:', error);
this.addMessage('Sorry, I encountered an error. Please try again.', 'bot');
} finally {
this.showLoading(false);
}
}
addMessage(content, sender) {
const messagesContainer = this.shadowRoot.getElementById('chatMessages');
const messageEl = document.createElement('div');
messageEl.className = \`message \${sender}-message\`;
messageEl.innerHTML = \`
<div class="message-content">\${this.escapeHtml(content)}</div>
\`;
messagesContainer.appendChild(messageEl);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
this.messages.push({ content, sender, timestamp: Date.now() });
}
showLoading(show) {
const loading = this.shadowRoot.getElementById('loadingIndicator');
loading.style.display = show ? 'block' : 'none';
}
async callAPI(message) {
const endpoint = this.config.apiEndpoint;
const payload = {
message,
botId: this.config.botId,
history: this.messages.slice(-10) // Send last 10 messages for context
};
if (this.config.apiKey) {
// Direct API call mode
return await this.callDirectAPI(message);
} else {
// Proxy mode
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('API request failed');
}
const data = await response.json();
return data.response || data.message || 'No response received';
}
}
async callDirectAPI(message) {
// Direct API implementation based on provider
if (this.config.apiProvider === 'openai') {
return await this.callOpenAI(message);
} else if (this.config.apiProvider === 'gemini') {
return await this.callGemini(message);
} else {
throw new Error('Unsupported API provider');
}
}
async callOpenAI(message) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': \`Bearer \${this.config.apiKey}\`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-3.5-turbo',
messages: [
{ role: 'system', content: this.config.persona.systemPrompt || 'You are a helpful assistant.' },
...this.messages.slice(-10).map(m => ({ role: m.sender === 'user' ? 'user' : 'assistant', content: m.content })),
{ role: 'user', content: message }
]
})
});
const data = await response.json();
return data.choices[0].message.content;
}
async callGemini(message) {
const response = await fetch(\`https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=\${this.config.apiKey}\`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
contents: [{
parts: [{ text: message }]
}]
})
});
const data = await response.json();
return data.candidates[0].content.parts[0].text;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Register the custom element
customElements.define('embedia-chatbot', EmbediaChatbot);
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = EmbediaChatbot;
}
`;
}
/**
* Generate HTML template for standalone usage
*/
generateHTMLTemplate(config) {
const { botId } = config;
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Embedia Chatbot Integration</title>
</head>
<body>
<!-- Your existing page content -->
<h1>Welcome to Your Website</h1>
<p>Your chatbot will appear in the bottom-right corner.</p>
<!-- Embedia Chatbot -->
<embedia-chatbot
bot-id="${botId}"
theme="default"
position="bottom-right">
</embedia-chatbot>
<!-- Load the chatbot script -->
<script src="./embedia-chatbot.js"></script>
</body>
</html>`;
}
/**
* Generate additional CSS styles
*/
generateStyles(styling) {
return `/* Additional custom styles for Embedia Chatbot */
.embedia-chatbot-container {
/* Add any global styles here */
}
/* Dark theme variant */
embedia-chatbot[theme="dark"] {
/* Dark theme styles will be handled by the component */
}
/* Mobile responsive adjustments */
@media (max-width: 768px) {
embedia-chatbot {
/* Mobile-specific adjustments */
}
}`;
}
/**
* Generate README documentation
*/
generateReadme(config) {
const { botId, botName } = config;
return `# ${botName} - Universal Web Component
A universal chatbot web component that can be embedded in any website or web application.
## Quick Start
### Self-Hosted Integration
\`\`\`html
<script src="./embedia-chatbot.js"></script>
<embedia-chatbot
bot-id="${botId}"
api-key="your-api-key"
theme="default"
position="bottom-right">
</embedia-chatbot>
\`\`\`
## Attributes
- \`bot-id\`: Your unique bot identifier
- \`api-key\`: Your API key (required for self-hosted mode)
- \`api-endpoint\`: Custom API endpoint URL
- \`theme\`: Visual theme (default, dark, custom)
- \`position\`: Position on screen (bottom-right, bottom-left, top-right, top-left)
## Framework Integration
### React
\`\`\`jsx
import React from 'react';
function App() {
return (
<div>
<h1>My React App</h1>
<embedia-chatbot bot-id="${botId}" />
</div>
);
}
\`\`\`
### Vue.js
\`\`\`vue
<template>
<div>
<h1>My Vue App</h1>
<embedia-chatbot bot-id="${botId}" />
</div>
</template>
\`\`\`
### Angular
\`\`\`typescript
// In your module
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule { }
\`\`\`
\`\`\`html
<!-- In your template -->
<embedia-chatbot bot-id="${botId}"></embedia-chatbot>
\`\`\`
### WordPress
Add this to your theme's footer.php or use a plugin:
\`\`\`html
<script src="./embedia-chatbot.js"></script>
<embedia-chatbot bot-id="${botId}"></embedia-chatbot>
\`\`\`
## Customization
### Custom Styling
\`\`\`css
embedia-chatbot {
--primary-color: #your-color;
--secondary-color: #your-secondary-color;
}
\`\`\`
### JavaScript API
\`\`\`javascript
const chatbot = document.querySelector('embedia-chatbot');
// Open the chat
chatbot.toggleChat();
// Send a message programmatically
chatbot.addMessage('Hello from JavaScript!', 'user');
\`\`\`
## Browser Support
- Chrome 54+
- Firefox 63+
- Safari 10.1+
- Edge 79+
## Support
For help and documentation, visit [https://docs.embedia.ai](https://docs.embedia.ai)
Generated by Embedia CLI v${this.version}
`;
}
/**
* Generate usage examples
*/
generateExampleUsage(config) {
const { botId } = config;
return {
'basic.html': this.generateHTMLTemplate(config),
'react-example.jsx': `import React from 'react';
function App() {
return (
<div className="App">
<header>
<h1>My React App with Embedia Chat</h1>
</header>
<main>
<p>Your main content here...</p>
</main>
{/* Embedia Chatbot */}
<embedia-chatbot
bot-id="${botId}"
theme="default"
position="bottom-right"
/>
</div>
);
}
export default App;`,
'vue-example.vue': `<template>
<div id="app">
<header>
<h1>My Vue App with Embedia Chat</h1>
</header>
<main>
<p>Your main content here...</p>
</main>
<!-- Embedia Chatbot -->
<embedia-chatbot
bot-id="${botId}"
theme="default"
position="bottom-right"
/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>`,
'wordpress-integration.php': `
<script src="<?php echo get_template_directory_uri(); ?>/embedia-chatbot.js"></script>
<embedia-chatbot bot-id="${botId}"></embedia-chatbot>
`
};
}
/**
* Calculate correct API endpoint based on CLI detection
* @param {Object} projectContext - CLI detection context
* @returns {string} Calculated API endpoint path
*/
calculateApiEndpoint(projectContext) {
if (!projectContext) {
return '/api/embedia/chat'; // Default fallback
}
const { routerType, framework } = projectContext;
// CRITICAL FIX: Match endpoint to actual generated API route structure
if (framework?.name === 'next') {
// Both App Router and Pages Router use the same endpoint path
// The difference is in file structure (app/api/ vs pages/api/)
// but the HTTP endpoint is the same
return '/api/embedia/chat';
}
// For other frameworks, use standard API path
return '/api/embedia/chat';
}
}
module.exports = WebComponentGenerator;