embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
511 lines (416 loc) • 16.5 kB
JavaScript
/**
* 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;