UNPKG

embedia

Version:

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

840 lines (696 loc) 25.6 kB
/** * Web Component Integrator - Phase 2 Implementation * * ARCHITECTURAL PRINCIPLE: Universal Compatibility Through Web Components * * This integrator handles Web Component integration for universal framework compatibility. * Web components work across all frameworks (React, Vue, Angular, vanilla JS) and provide * the highest compatibility while respecting generated code as authoritative. */ const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const logger = require('../utils/logger'); class WebComponentIntegrator { constructor(projectPath, contract) { this.projectPath = projectPath; this.contract = contract; this.framework = contract.framework.name; this.routerType = contract.framework.router; this.isTypeScript = contract.language.typescript; } /** * Integrate Web Components for universal framework compatibility * @param {Array} generatedFiles - Generated files from server * @returns {Promise<Object>} Integration results */ async integrate(generatedFiles) { console.log(chalk.cyan(`🌐 Integrating Web Components for universal compatibility`)); console.log(chalk.gray(` Framework: ${this.framework}, TypeScript: ${this.isTypeScript}`)); const results = { success: false, strategy: 'web-component', framework: this.framework, router: this.routerType, files: [], integrationFiles: [], errors: [], instructions: [] }; try { // 1. Deploy web component to public directory await this.deployWebComponent(generatedFiles, results); // 2. Create integration examples await this.createIntegrationExamples(generatedFiles, results); // 3. Framework-specific integration (if framework detected) if (this.framework === 'nextjs') { await this.integrateNextJS(results); } else if (this.framework === 'react') { await this.integrateReact(results); } else { await this.integrateGeneric(results); } // 4. Create API routes if needed await this.createAPIRoutes(generatedFiles, results); results.success = results.errors.length === 0; if (results.success) { console.log(chalk.green(`✅ Web component integration completed successfully`)); } else { console.log(chalk.yellow(`⚠️ Web component integration completed with ${results.errors.length} issues`)); } } catch (error) { results.errors.push({ type: 'integration_error', message: error.message, solution: 'Check project structure and permissions' }); console.log(chalk.red(`❌ Web component integration failed: ${error.message}`)); } return results; } /** * Deploy web component bundle to public directory */ async deployWebComponent(generatedFiles, results) { // Create public directory const publicDir = path.join(this.projectPath, 'public'); await fs.ensureDir(publicDir); // Find web component files const webComponentFiles = generatedFiles.filter(file => file.path.includes('embedia-chatbot.js') || file.path.includes('embedia-chatbot.css') || file.path.endsWith('.js') && file.content.includes('customElements.define') ); if (webComponentFiles.length === 0) { results.errors.push({ type: 'component_missing', message: 'No web component files found in generated code', solution: 'Ensure server generates web component bundle' }); return; } // Deploy main web component bundle for (const file of webComponentFiles) { let fileName = 'embedia-chatbot.js'; if (file.path.includes('.css')) { fileName = 'embedia-chatbot.css'; } const deployPath = path.join(publicDir, fileName); await fs.writeFile(deployPath, file.content); results.files.push(`public/${fileName}`); } console.log(chalk.gray(` ✓ Deployed ${webComponentFiles.length} web component files to public/`)); } /** * Create integration examples for different frameworks */ async createIntegrationExamples(generatedFiles, results) { const publicDir = path.join(this.projectPath, 'public'); // Create basic HTML example const htmlExample = this.generateHTMLExample(); const htmlPath = path.join(publicDir, 'embedia-example.html'); await fs.writeFile(htmlPath, htmlExample); results.files.push('public/embedia-example.html'); // Create React example const reactExample = this.generateReactExample(); const reactPath = path.join(publicDir, 'embedia-react-example.jsx'); await fs.writeFile(reactPath, reactExample); results.files.push('public/embedia-react-example.jsx'); // Create Vue example const vueExample = this.generateVueExample(); const vuePath = path.join(publicDir, 'embedia-vue-example.vue'); await fs.writeFile(vuePath, vueExample); results.files.push('public/embedia-vue-example.vue'); // Create integration guide const guide = this.generateIntegrationGuide(); const guidePath = path.join(publicDir, 'embedia-integration-guide.md'); await fs.writeFile(guidePath, guide); results.files.push('public/embedia-integration-guide.md'); console.log(chalk.gray(` ✓ Created 4 integration examples in public/`)); } /** * Integrate web component with Next.js */ async integrateNextJS(results) { if (this.routerType === 'app') { await this.integrateNextJSAppRouter(results); } else if (this.routerType === 'pages') { await this.integrateNextJSPagesRouter(results); } else { // Default to app router for hybrid/unknown await this.integrateNextJSAppRouter(results); } } /** * Integrate with Next.js App Router */ async integrateNextJSAppRouter(results) { // Create a web component loader for App Router const loaderContent = this.generateAppRouterWebComponentLoader(); const loaderDir = path.join(this.projectPath, 'components'); await fs.ensureDir(loaderDir); const ext = this.isTypeScript ? 'tsx' : 'jsx'; const loaderPath = path.join(loaderDir, `EmbediaWebComponentLoader.${ext}`); await fs.writeFile(loaderPath, loaderContent); results.integrationFiles.push(path.relative(this.projectPath, loaderPath)); // Try to integrate into existing layout await this.integrateIntoLayout(results); results.instructions.push( '🌐 Web Component integrated with Next.js App Router', 'The chatbot will load automatically on all pages', 'Web component provides universal compatibility', 'View public/embedia-example.html for usage examples' ); } /** * Integrate with Next.js Pages Router */ async integrateNextJSPagesRouter(results) { // Create a web component loader for Pages Router const loaderContent = this.generatePagesRouterWebComponentLoader(); const loaderDir = path.join(this.projectPath, 'components'); await fs.ensureDir(loaderDir); const ext = this.isTypeScript ? 'tsx' : 'jsx'; const loaderPath = path.join(loaderDir, `EmbediaWebComponentLoader.${ext}`); await fs.writeFile(loaderPath, loaderContent); results.integrationFiles.push(path.relative(this.projectPath, loaderPath)); // Try to integrate into existing _app await this.integrateIntoApp(results); results.instructions.push( '🌐 Web Component integrated with Next.js Pages Router', 'The chatbot will load automatically on all pages', 'Web component provides universal compatibility', 'View public/embedia-example.html for usage examples' ); } /** * Integrate with React project */ async integrateReact(results) { const wrapperContent = this.generateReactWebComponentWrapper(); const wrapperDir = path.join(this.projectPath, 'components'); await fs.ensureDir(wrapperDir); const ext = this.isTypeScript ? 'tsx' : 'jsx'; const wrapperPath = path.join(wrapperDir, `EmbediaWebComponent.${ext}`); await fs.writeFile(wrapperPath, wrapperContent); results.integrationFiles.push(path.relative(this.projectPath, wrapperPath)); results.instructions.push( '🌐 Web Component wrapper created for React', 'Import and use EmbediaWebComponent in your React app:', " import EmbediaWebComponent from './components/EmbediaWebComponent'", ' // Add <EmbediaWebComponent /> to your component', 'Web component provides framework-agnostic integration' ); } /** * Generic integration for unknown frameworks */ async integrateGeneric(results) { results.instructions.push( '🌐 Web Component deployed for universal compatibility', 'Add this script tag to your HTML:', ' <script src="/embedia-chatbot.js"></script>', 'Add this element where you want the chat:', ' <embedia-chatbot></embedia-chatbot>', 'Check public/embedia-integration-guide.md for detailed instructions' ); } /** * Create API routes if needed */ async createAPIRoutes(generatedFiles, results) { const apiFiles = generatedFiles.filter(file => file.path.includes('api/')); if (apiFiles.length === 0) { console.log(chalk.gray(` ℹ No API routes to create`)); return; } // For web components, API routes are less framework-specific for (const apiFile of apiFiles) { if (this.framework === 'nextjs') { if (this.routerType === 'app') { await this.createAppRouterAPI(apiFile, results); } else { await this.createPagesRouterAPI(apiFile, results); } } else { // Create generic API endpoint await this.createGenericAPI(apiFile, results); } } } /** * Integrate into existing layout file */ async integrateIntoLayout(results) { const layoutPaths = [ path.join(this.projectPath, 'app', `layout.${this.isTypeScript ? 'tsx' : 'jsx'}`), path.join(this.projectPath, 'src', 'app', `layout.${this.isTypeScript ? 'tsx' : 'jsx'}`) ]; for (const layoutPath of layoutPaths) { if (await fs.pathExists(layoutPath)) { try { const content = await fs.readFile(layoutPath, 'utf8'); const modifiedContent = this.addWebComponentToLayout(content); // Create backup await fs.copy(layoutPath, `${layoutPath}.backup`); // Write modified content await fs.writeFile(layoutPath, modifiedContent); results.integrationFiles.push(path.relative(this.projectPath, layoutPath)); console.log(chalk.gray(` ✓ Integrated into layout: ${path.relative(this.projectPath, layoutPath)}`)); return; } catch (error) { results.errors.push({ type: 'layout_integration', message: `Failed to integrate into layout: ${error.message}`, solution: 'Manually add EmbediaWebComponentLoader to your layout' }); } } } // If no layout found, create minimal integration instructions results.instructions.push( 'Manual integration required:', "1. Add import: import EmbediaWebComponentLoader from '../components/EmbediaWebComponentLoader'", '2. Add component before closing </body>: <EmbediaWebComponentLoader />' ); } /** * Integrate into existing _app file */ async integrateIntoApp(results) { const appPaths = [ path.join(this.projectPath, 'pages', `_app.${this.isTypeScript ? 'tsx' : 'jsx'}`), path.join(this.projectPath, 'src', 'pages', `_app.${this.isTypeScript ? 'tsx' : 'jsx'}`) ]; for (const appPath of appPaths) { if (await fs.pathExists(appPath)) { try { const content = await fs.readFile(appPath, 'utf8'); const modifiedContent = this.addWebComponentToApp(content); // Create backup await fs.copy(appPath, `${appPath}.backup`); // Write modified content await fs.writeFile(appPath, modifiedContent); results.integrationFiles.push(path.relative(this.projectPath, appPath)); console.log(chalk.gray(` ✓ Integrated into _app: ${path.relative(this.projectPath, appPath)}`)); return; } catch (error) { results.errors.push({ type: 'app_integration', message: `Failed to integrate into _app: ${error.message}`, solution: 'Manually add EmbediaWebComponentLoader to your _app' }); } } } // If no _app found, create minimal integration instructions results.instructions.push( 'Manual integration required:', "1. Add import: import EmbediaWebComponentLoader from '../components/EmbediaWebComponentLoader'", '2. Add component to your JSX: <EmbediaWebComponentLoader />' ); } /** * Create App Router API */ async createAppRouterAPI(apiFile, results) { const apiDir = this.contract.structure.srcDirectory ? path.join(this.projectPath, 'src', 'app', 'api', 'embedia', 'chat') : path.join(this.projectPath, 'app', 'api', 'embedia', 'chat'); await fs.ensureDir(apiDir); const ext = this.isTypeScript ? 'ts' : 'js'; const apiPath = path.join(apiDir, `route.${ext}`); await fs.writeFile(apiPath, apiFile.content); results.files.push(path.relative(this.projectPath, apiPath)); console.log(chalk.gray(` ✓ Created App Router API: ${path.relative(this.projectPath, apiPath)}`)); } /** * Create Pages Router API */ async createPagesRouterAPI(apiFile, results) { const apiDir = this.contract.structure.srcDirectory ? path.join(this.projectPath, 'src', 'pages', 'api', 'embedia') : path.join(this.projectPath, 'pages', 'api', 'embedia'); await fs.ensureDir(apiDir); const ext = this.isTypeScript ? 'ts' : 'js'; const apiPath = path.join(apiDir, `chat.${ext}`); await fs.writeFile(apiPath, apiFile.content); results.files.push(path.relative(this.projectPath, apiPath)); console.log(chalk.gray(` ✓ Created Pages Router API: ${path.relative(this.projectPath, apiPath)}`)); } /** * Create generic API endpoint */ async createGenericAPI(apiFile, results) { const apiDir = path.join(this.projectPath, 'api'); await fs.ensureDir(apiDir); const ext = this.isTypeScript ? 'ts' : 'js'; const apiPath = path.join(apiDir, `embedia-chat.${ext}`); await fs.writeFile(apiPath, apiFile.content); results.files.push(path.relative(this.projectPath, apiPath)); console.log(chalk.gray(` ✓ Created generic API: ${path.relative(this.projectPath, apiPath)}`)); } // Template generation methods generateAppRouterWebComponentLoader() { const useClientDirective = this.isTypeScript ? "'use client'\n\n" : "'use client'\n\n"; return `${useClientDirective}import { useEffect } from 'react' export default function EmbediaWebComponentLoader() { useEffect(() => { // Load the web component script const script = document.createElement('script'); script.src = '/embedia-chatbot.js'; script.async = true; script.onload = () => { console.log('Embedia chatbot loaded successfully'); }; script.onerror = () => { console.error('Failed to load Embedia chatbot'); }; document.head.appendChild(script); return () => { // Cleanup if component unmounts const existingScript = document.querySelector('script[src="/embedia-chatbot.js"]'); if (existingScript) { document.head.removeChild(existingScript); } }; }, []); return <embedia-chatbot />; }`; } generatePagesRouterWebComponentLoader() { return `import { useEffect } from 'react' export default function EmbediaWebComponentLoader() { useEffect(() => { // Load the web component script const script = document.createElement('script'); script.src = '/embedia-chatbot.js'; script.async = true; script.onload = () => { console.log('Embedia chatbot loaded successfully'); }; script.onerror = () => { console.error('Failed to load Embedia chatbot'); }; document.head.appendChild(script); return () => { // Cleanup if component unmounts const existingScript = document.querySelector('script[src="/embedia-chatbot.js"]'); if (existingScript) { document.head.removeChild(existingScript); } }; }, []); return <embedia-chatbot />; }`; } generateReactWebComponentWrapper() { return `import { useEffect } from 'react' export default function EmbediaWebComponent() { useEffect(() => { // Load the web component script if not already loaded if (!document.querySelector('script[src="/embedia-chatbot.js"]')) { const script = document.createElement('script'); script.src = '/embedia-chatbot.js'; script.async = true; document.head.appendChild(script); } }, []); return <embedia-chatbot />; }`; } generateHTMLExample() { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Embedia Chatbot - HTML Example</title> <style> body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; } .example-content { max-width: 800px; margin: 0 auto; } .highlight { background-color: #f4f4f4; padding: 20px; border-radius: 5px; margin: 20px 0; } </style> </head> <body> <div class="example-content"> <h1>🤖 Embedia Chatbot Integration Example</h1> <div class="highlight"> <h2>Pure HTML Integration</h2> <p>This page demonstrates how to integrate the Embedia chatbot using pure HTML and JavaScript.</p> <p>The chatbot should appear in the bottom-right corner of this page.</p> </div> <h2>How it works:</h2> <ol> <li>Load the web component script</li> <li>Add the custom element to your HTML</li> <li>The chatbot initializes automatically</li> </ol> <div class="highlight"> <h3>Integration Code:</h3> <pre><code>&lt;script src="/embedia-chatbot.js"&gt;&lt;/script&gt; &lt;embedia-chatbot&gt;&lt;/embedia-chatbot&gt;</code></pre> </div> </div> <!-- Embedia Chatbot Integration --> <script src="/embedia-chatbot.js" async></script> <embedia-chatbot></embedia-chatbot> </body> </html>`; } generateReactExample() { return `import React, { useEffect } from 'react'; function EmbediaChatbotExample() { useEffect(() => { // Load the web component script const script = document.createElement('script'); script.src = '/embedia-chatbot.js'; script.async = true; document.head.appendChild(script); return () => { // Cleanup on unmount const existingScript = document.querySelector('script[src="/embedia-chatbot.js"]'); if (existingScript) { document.head.removeChild(existingScript); } }; }, []); return ( <div> <h1>React + Embedia Chatbot Example</h1> <p>This React component demonstrates web component integration.</p> {/* The web component */} <embedia-chatbot /> </div> ); } export default EmbediaChatbotExample;`; } generateVueExample() { return `<template> <div> <h1>Vue + Embedia Chatbot Example</h1> <p>This Vue component demonstrates web component integration.</p> <!-- The web component --> <embedia-chatbot /> </div> </template> <script> export default { name: 'EmbediaChatbotExample', mounted() { // Load the web component script const script = document.createElement('script'); script.src = '/embedia-chatbot.js'; script.async = true; document.head.appendChild(script); }, beforeUnmount() { // Cleanup on unmount const existingScript = document.querySelector('script[src="/embedia-chatbot.js"]'); if (existingScript) { document.head.removeChild(existingScript); } } } </script>`; } generateIntegrationGuide() { return `# 🌐 Embedia Web Component Integration Guide ## Overview The Embedia chatbot is implemented as a Web Component, providing universal compatibility across all modern web frameworks and vanilla JavaScript. ## Quick Start ### 1. Basic HTML Integration \`\`\`html <!DOCTYPE html> <html> <head> <title>My Website</title> </head> <body> <!-- Your content here --> <!-- Embedia Chatbot --> <script src="/embedia-chatbot.js" async></script> <embedia-chatbot></embedia-chatbot> </body> </html> \`\`\` ### 2. React Integration \`\`\`jsx import { useEffect } from 'react'; function MyComponent() { useEffect(() => { const script = document.createElement('script'); script.src = '/embedia-chatbot.js'; script.async = true; document.head.appendChild(script); }, []); return ( <div> <h1>My React App</h1> <embedia-chatbot /> </div> ); } \`\`\` ### 3. Vue.js Integration \`\`\`vue <template> <div> <h1>My Vue App</h1> <embedia-chatbot /> </div> </template> <script> export default { mounted() { const script = document.createElement('script'); script.src = '/embedia-chatbot.js'; script.async = true; document.head.appendChild(script); } } </script> \`\`\` ### 4. Angular Integration \`\`\`typescript import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-my-component', template: \` <h1>My Angular App</h1> <embedia-chatbot></embedia-chatbot> \` }) export class MyComponent implements OnInit { ngOnInit() { const script = document.createElement('script'); script.src = '/embedia-chatbot.js'; script.async = true; document.head.appendChild(script); } } \`\`\` ## Advanced Configuration ### Custom Positioning \`\`\`html <embedia-chatbot position="bottom-left"></embedia-chatbot> <embedia-chatbot position="top-right"></embedia-chatbot> <embedia-chatbot position="inline"></embedia-chatbot> \`\`\` ### Styling The web component respects your site's CSS custom properties: \`\`\`css :root { --embedia-primary-color: #007bff; --embedia-background-color: #ffffff; --embedia-text-color: #333333; } \`\`\` ## Framework-Specific Notes ### Next.js - Works with both App Router and Pages Router - Use the provided loader components for optimal integration - Supports SSR and SSG ### Nuxt.js - Add to plugins directory for global availability - Use client-side rendering for the component ### Gatsby - Use the \`useEffect\` pattern shown in React example - Consider using gatsby-plugin-load-script for optimization ## Troubleshooting ### Component Not Appearing 1. Check browser console for script loading errors 2. Ensure \`/embedia-chatbot.js\` is accessible 3. Verify the custom element is properly defined ### TypeScript Errors Add to your type definitions: \`\`\`typescript declare global { namespace JSX { interface IntrinsicElements { 'embedia-chatbot': any; } } } \`\`\` ## Browser Support - Chrome 54+ - Firefox 63+ - Safari 10.1+ - Edge 79+ ## Support For integration help, check the examples in the \`public/\` directory or visit our documentation. `; } addWebComponentToLayout(content) { // Add import const importLine = "import EmbediaWebComponentLoader from '../components/EmbediaWebComponentLoader'"; let modifiedContent = content; // Add import after existing imports const lastImportMatch = content.match(/import[^;]+;(?=\s*\n\s*(?:export|const|function|class))/g); if (lastImportMatch) { const lastImport = lastImportMatch[lastImportMatch.length - 1]; modifiedContent = modifiedContent.replace(lastImport, lastImport + '\n' + importLine); } else { modifiedContent = importLine + '\n' + modifiedContent; } // Add component before closing body tag modifiedContent = modifiedContent.replace('</body>', ' <EmbediaWebComponentLoader />\n </body>'); return modifiedContent; } addWebComponentToApp(content) { // Add import const importLine = "import EmbediaWebComponentLoader from '../components/EmbediaWebComponentLoader'"; let modifiedContent = content; // Add import after existing imports const lastImportMatch = content.match(/import[^;]+;(?=\s*\n\s*(?:export|const|function|class))/g); if (lastImportMatch) { const lastImport = lastImportMatch[lastImportMatch.length - 1]; modifiedContent = modifiedContent.replace(lastImport, lastImport + '\n' + importLine); } else { modifiedContent = importLine + '\n' + modifiedContent; } // Add component to JSX const returnMatch = content.match(/return\s*\(\s*(<[^>]+>[\s\S]*?<\/[^>]+>)\s*\)/); if (returnMatch) { const jsxContent = returnMatch[1]; const newJsx = jsxContent.replace(/(<\/[^>]+>)$/, ' <EmbediaWebComponentLoader />\n $1'); modifiedContent = modifiedContent.replace(jsxContent, newJsx); } return modifiedContent; } } module.exports = WebComponentIntegrator;