UNPKG

embedia

Version:

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

511 lines (416 loc) 16.5 kB
/** * React Component Integrator - Phase 2 Implementation * * ARCHITECTURAL PRINCIPLE: Framework-Specific Integration Patterns * * This integrator handles React component integration for Next.js and React projects. * It adapts integration strategy based on the specific framework and router type * while respecting the generated code as authoritative. */ const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const logger = require('../utils/logger'); class ReactComponentIntegrator { 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 React components based on framework and router type * @param {Array} generatedFiles - Generated files from server * @returns {Promise<Object>} Integration results */ async integrate(generatedFiles) { console.log(chalk.cyan(`🔧 Integrating React components for ${this.framework}`)); console.log(chalk.gray(` Router: ${this.routerType}, TypeScript: ${this.isTypeScript}`)); const results = { success: false, strategy: 'react-component', framework: this.framework, router: this.routerType, files: [], integrationFiles: [], errors: [], instructions: [] }; try { // 1. Create component files await this.createComponentFiles(generatedFiles, results); // 2. Framework-specific integration if (this.framework === 'nextjs') { await this.integrateNextJS(generatedFiles, results); } else if (this.framework === 'react') { await this.integrateReact(generatedFiles, results); } else { await this.integrateGeneric(generatedFiles, results); } // 3. Create API routes await this.createAPIRoutes(generatedFiles, results); results.success = results.errors.length === 0; if (results.success) { console.log(chalk.green(`✅ React component integration completed successfully`)); } else { console.log(chalk.yellow(`⚠️ React 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(`❌ React component integration failed: ${error.message}`)); } return results; } /** * Create component files in the appropriate directory */ async createComponentFiles(generatedFiles, results) { const componentDir = path.join(this.projectPath, 'components', 'generated', 'embedia-chat'); await fs.ensureDir(componentDir); // Filter component files const componentFiles = generatedFiles.filter(file => file.path.includes('embedia-chat') && !file.path.includes('api/') && (file.path.endsWith('.js') || file.path.endsWith('.jsx') || file.path.endsWith('.ts') || file.path.endsWith('.tsx') || file.path.endsWith('.json') || file.path.endsWith('.md')) ); for (const file of componentFiles) { const fileName = path.basename(file.path); const filePath = path.join(componentDir, fileName); // Adjust file extension based on TypeScript setting let finalPath = filePath; if (this.isTypeScript) { finalPath = filePath.replace(/\.jsx?$/, '.tsx').replace(/\.js$/, '.ts'); } await fs.writeFile(finalPath, file.content); results.files.push(path.relative(this.projectPath, finalPath)); } console.log(chalk.gray(` ✓ Created ${componentFiles.length} component files`)); } /** * Integrate with Next.js based on router type */ async integrateNextJS(generatedFiles, results) { if (this.routerType === 'app') { await this.integrateNextJSAppRouter(results); } else if (this.routerType === 'pages') { await this.integrateNextJSPagesRouter(results); } else { // Hybrid or unknown - prefer app router await this.integrateNextJSAppRouter(results); } } /** * Integrate with Next.js App Router */ async integrateNextJSAppRouter(results) { const layoutPaths = [ path.join(this.projectPath, 'app', `layout.${this.isTypeScript ? 'tsx' : 'jsx'}`), path.join(this.projectPath, 'src', 'app', `layout.${this.isTypeScript ? 'tsx' : 'jsx'}`) ]; let layoutPath = null; for (const lPath of layoutPaths) { if (await fs.pathExists(lPath)) { layoutPath = lPath; break; } } if (layoutPath) { await this.modifyAppRouterLayout(layoutPath, results); } else { await this.createAppRouterLayout(results); } results.instructions.push( 'React component integrated with Next.js App Router', 'The chat widget will appear on all pages', 'Start your dev server to see the chatbot' ); } /** * Integrate with Next.js Pages Router */ async integrateNextJSPagesRouter(results) { const appPaths = [ path.join(this.projectPath, 'pages', `_app.${this.isTypeScript ? 'tsx' : 'jsx'}`), path.join(this.projectPath, 'src', 'pages', `_app.${this.isTypeScript ? 'tsx' : 'jsx'}`) ]; let appPath = null; for (const aPath of appPaths) { if (await fs.pathExists(aPath)) { appPath = aPath; break; } } if (appPath) { await this.modifyPagesRouterApp(appPath, results); } else { await this.createPagesRouterApp(results); } results.instructions.push( 'React component integrated with Next.js Pages Router', 'The chat widget will appear on all pages', 'Start your dev server to see the chatbot' ); } /** * Integrate with standalone React project */ async integrateReact(generatedFiles, results) { // Create a wrapper component for easier integration await this.createReactWrapper(results); results.instructions.push( 'React component created successfully', 'Import and use EmbediaChatWrapper in your React app:', " import EmbediaChatWrapper from './components/EmbediaChatWrapper'", ' // Add <EmbediaChatWrapper /> to your main component', 'The chat widget will appear where you place the component' ); } /** * Generic integration for unknown frameworks */ async integrateGeneric(generatedFiles, results) { await this.createReactWrapper(results); results.instructions.push( 'Generic React component integration completed', 'Use the generated components in your framework of choice', 'See README.md for integration instructions' ); } /** * Modify existing App Router layout */ async modifyAppRouterLayout(layoutPath, results) { try { const content = await fs.readFile(layoutPath, 'utf8'); // Add dynamic import for the chat component const modifiedContent = this.addChatComponentToLayout(content, 'app'); // 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(` ✓ Modified App Router layout: ${path.relative(this.projectPath, layoutPath)}`)); } catch (error) { results.errors.push({ type: 'layout_modification', message: `Failed to modify layout: ${error.message}`, solution: 'Manually add the chat component to your layout' }); } } /** * Create new App Router layout */ async createAppRouterLayout(results) { const layoutDir = this.contract.structure.srcDirectory ? path.join(this.projectPath, 'src', 'app') : path.join(this.projectPath, 'app'); await fs.ensureDir(layoutDir); const ext = this.isTypeScript ? 'tsx' : 'jsx'; const layoutPath = path.join(layoutDir, `layout.${ext}`); const layoutContent = this.generateAppRouterLayout(); await fs.writeFile(layoutPath, layoutContent); results.integrationFiles.push(path.relative(this.projectPath, layoutPath)); console.log(chalk.gray(` ✓ Created App Router layout: ${path.relative(this.projectPath, layoutPath)}`)); } /** * Modify existing Pages Router _app */ async modifyPagesRouterApp(appPath, results) { try { const content = await fs.readFile(appPath, 'utf8'); const modifiedContent = this.addChatComponentToApp(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(` ✓ Modified Pages Router _app: ${path.relative(this.projectPath, appPath)}`)); } catch (error) { results.errors.push({ type: 'app_modification', message: `Failed to modify _app: ${error.message}`, solution: 'Manually add the chat component to your _app file' }); } } /** * Create new Pages Router _app */ async createPagesRouterApp(results) { const pagesDir = this.contract.structure.srcDirectory ? path.join(this.projectPath, 'src', 'pages') : path.join(this.projectPath, 'pages'); await fs.ensureDir(pagesDir); const ext = this.isTypeScript ? 'tsx' : 'jsx'; const appPath = path.join(pagesDir, `_app.${ext}`); const appContent = this.generatePagesRouterApp(); await fs.writeFile(appPath, appContent); results.integrationFiles.push(path.relative(this.projectPath, appPath)); console.log(chalk.gray(` ✓ Created Pages Router _app: ${path.relative(this.projectPath, appPath)}`)); } /** * Create React wrapper component */ async createReactWrapper(results) { const wrapperDir = path.join(this.projectPath, 'components'); await fs.ensureDir(wrapperDir); const ext = this.isTypeScript ? 'tsx' : 'jsx'; const wrapperPath = path.join(wrapperDir, `EmbediaChatWrapper.${ext}`); const wrapperContent = this.generateReactWrapper(); await fs.writeFile(wrapperPath, wrapperContent); results.integrationFiles.push(path.relative(this.projectPath, wrapperPath)); console.log(chalk.gray(` ✓ Created React wrapper: ${path.relative(this.projectPath, wrapperPath)}`)); } /** * Create API routes based on router type */ 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 (const apiFile of apiFiles) { if (this.routerType === 'app') { await this.createAppRouterAPI(apiFile, results); } else { await this.createPagesRouterAPI(apiFile, results); } } } /** * Create App Router API route */ 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 route */ 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)}`)); } // Template generation methods... generateAppRouterLayout() { const imports = this.isTypeScript ? "import type { Metadata } from 'next'\n" : ''; const metadata = this.isTypeScript ? ` export const metadata: Metadata = { title: 'Your App', description: 'Enhanced with Embedia Chat', } ` : ''; return `${imports}import './globals.css' import { Inter } from 'next/font/google' import EmbediaChatLoader from '../components/EmbediaChatLoader' const inter = Inter({ subsets: ['latin'] }) ${metadata} export default function RootLayout({ children, }${this.isTypeScript ? ': {\n children: React.ReactNode\n}' : ''}) { return ( <html lang="en"> <body className={inter.className}> {children} <EmbediaChatLoader /> </body> </html> ) }`; } generatePagesRouterApp() { const imports = this.isTypeScript ? "import type { AppProps } from 'next/app'\n" : ''; const props = this.isTypeScript ? ': AppProps' : ''; return `${imports}import '../styles/globals.css' import EmbediaChatWrapper from '../components/EmbediaChatWrapper' function MyApp({ Component, pageProps }${props}) { return ( <> <Component {...pageProps} /> <EmbediaChatWrapper /> </> ) } export default MyApp`; } generateReactWrapper() { return `${this.isTypeScript ? "import React, { useEffect } from 'react'" : "import { useEffect } from 'react'"} export default function EmbediaChatWrapper() { useEffect(() => { if (typeof window !== 'undefined') { import('./generated/embedia-chat/index.js').then((module) => { const EmbediaChat = module.default || module.EmbediaChat; if (EmbediaChat && !document.getElementById('embedia-chat-root')) { const container = document.createElement('div'); container.id = 'embedia-chat-root'; document.body.appendChild(container); // Simplified mounting logic } }).catch(console.error); } }, []); return null; }`; } addChatComponentToLayout(content, routerType) { // Add import const importLine = "import EmbediaChatLoader from '../components/EmbediaChatLoader'"; 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>', ' <EmbediaChatLoader />\n </body>'); return modifiedContent; } addChatComponentToApp(content) { // Add import const importLine = "import EmbediaChatWrapper from '../components/EmbediaChatWrapper'"; 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(/(<\/[^>]+>)$/, ' <EmbediaChatWrapper />\n $1'); modifiedContent = modifiedContent.replace(jsxContent, newJsx); } return modifiedContent; } } module.exports = ReactComponentIntegrator;