@guyycodes/plugin-sdk
Version:
AI-powered plugin scaffolding tool - Create full-stack applications with 7+ AI models, 50+ business integrations, and production-ready infrastructure
1,407 lines (1,251 loc) • 108 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const ora = require('ora');
async function scaffoldViteClient(projectPath, projectName, frontendType = 'typescript', integration = 'custom', backendType) {
const spinner = ora('Scaffolding Vite client...').start();
const clientPath = path.join(projectPath, 'src/client');
try {
// Create Vite project based on frontend type
const template = frontendType === 'typescript' ? 'react-ts' : 'react';
execSync(`npm create vite@latest . -- --template ${template}`, {
cwd: clientPath,
stdio: 'pipe'
});
// Update package.json
const clientPackageJson = path.join(clientPath, 'package.json');
const packageData = fs.readJsonSync(clientPackageJson);
packageData.name = `${projectName}-client`;
packageData.dependencies = {
...packageData.dependencies,
'axios': '^1.6.0',
'react-router-dom': '^6.8.0',
'@tanstack/react-query': '^5.0.0',
'@originjs/vite-plugin-federation': '^1.3.5'
};
packageData.scripts = {
...packageData.scripts,
'build': frontendType === 'typescript' ? 'tsc && vite build' : 'vite build',
'preview': 'vite preview'
};
fs.writeJsonSync(clientPackageJson, packageData, { spaces: 2 });
// Create plugin-specific files
await createPluginClientFiles(clientPath, projectName, frontendType, integration, backendType);
// Update vite.config for Module Federation
await updateViteConfig(clientPath, projectName, frontendType);
spinner.succeed('Vite client scaffolded successfully');
} catch (error) {
spinner.fail('Failed to scaffold Vite client');
throw error;
}
}
async function createPluginClientFiles(clientPath, projectName, frontendType, integration, backendType) {
// Create PluginApp file (TypeScript or JavaScript)
const fileExtension = frontendType === 'typescript' ? 'tsx' : 'jsx';
const pluginAppContent = createPluginAppContent(projectName, frontendType, integration);
fs.ensureDirSync(path.join(clientPath, 'src/pages'));
fs.writeFileSync(path.join(clientPath, `src/PluginApp.${fileExtension}`), pluginAppContent);
// Modify main.jsx to include both App and PluginApp
const mainJsxPath = path.join(clientPath, `src/main.${fileExtension}`);
const pluginAppImport = `./PluginApp.${fileExtension}`;
const mainJsxContent = `import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { PluginApp } from './PluginApp.${fileExtension}'
import './index.css'
const root = document.getElementById('root')
if (!root) throw new Error('Root element not found')
createRoot(root).render(
<StrictMode>
<PluginApp />
</StrictMode>,
)`;
fs.writeFileSync(mainJsxPath, mainJsxContent);
// Create Dashboard page
const dashboardContent = createDashboardPage(frontendType, integration);
const dashboardExtension = frontendType === 'typescript' ? 'tsx' : 'jsx';
fs.writeFileSync(path.join(clientPath, `src/pages/Dashboard.${dashboardExtension}`), dashboardContent);
// Create Settings page
const settingsContent = createSettingsPage(frontendType, projectName, backendType);
const settingsExtension = frontendType === 'typescript' ? 'tsx' : 'jsx';
fs.writeFileSync(path.join(clientPath, `src/pages/Settings.${settingsExtension}`), settingsContent);
// Create Chat page only if backend is python
const chatContent = createChatPage(frontendType, projectName, backendType);
const chatExtension = frontendType === 'typescript' ? 'tsx' : 'jsx';
fs.writeFileSync(path.join(clientPath, `src/pages/Chat.${chatExtension}`), chatContent);
// Create Auth page
const authContent = createAuthPage(frontendType, projectName, backendType);
const authExtension = frontendType === 'typescript' ? 'tsx' : 'jsx';
fs.writeFileSync(path.join(clientPath, `src/pages/Auth.${authExtension}`), authContent);
// Create components & hooks directory
fs.ensureDirSync(path.join(clientPath, 'src/components'));
fs.ensureDirSync(path.join(clientPath, 'src/hooks'));
// Create ProtectedRoute component
const protectedRouteContent = createProtectedRouteComponent(frontendType, projectName, backendType);
const protectedRouteExtension = frontendType === 'typescript' ? 'tsx' : 'jsx';
fs.writeFileSync(path.join(clientPath, `src/components/ProtectedRoute.${protectedRouteExtension}`), protectedRouteContent);
// Create Icons component
const iconsContent = createIconsComponent(frontendType);
const iconsExtension = frontendType === 'typescript' ? 'tsx' : 'jsx';
fs.writeFileSync(path.join(clientPath, `src/components/Icons.${iconsExtension}`), iconsContent);
const webSocketHook = createWebSocketHook(frontendType);
const ext = frontendType === 'typescript' ? 'tsx' : 'jsx';
fs.writeFileSync(path.join(clientPath, `src/hooks/useChatStream.${ext}`), webSocketHook);
// Create API client
fs.ensureDirSync(path.join(clientPath, 'src/lib'));
const apiContent = createApiClientContent(frontendType, projectName, backendType);
const apiExtension = frontendType === 'typescript' ? 'ts' : 'js';
fs.writeFileSync(path.join(clientPath, `src/lib/api.${apiExtension}`), apiContent);
// Create .env file for client
const clientEnvContent = `# Plugin client environment variables
VITE_PLUGIN_NAME=${projectName}
VITE_API_URL=
# Leave VITE_API_URL empty to use auto-detection based on environment
`;
fs.writeFileSync(path.join(clientPath, '.env'), clientEnvContent);
// Create CSS file
const cssContent = `.plugin-container {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.plugin-container h2 {
color: #2563eb;
margin-bottom: 20px;
}
.plugin-button {
background-color: #3b82f6;
color: white;
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.plugin-button:hover {
background-color: #2563eb;
}
.plugin-button:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
.plugin-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.plugin-status {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-connected {
background-color: #dcfce7;
color: #166534;
}
.status-disconnected {
background-color: #fee2e2;
color: #991b1b;
}
/* Chat Styles */
.plugin-container .flex-col {
display: flex;
flex-direction: column;
}
.plugin-container .h-full {
height: calc(100vh - 200px);
min-height: 500px;
}
.plugin-container textarea {
font-family: inherit;
font-size: 14px;
}
.plugin-container select {
font-size: 14px;
cursor: pointer;
}
/* Message bubbles */
.plugin-container .whitespace-pre-wrap {
white-space: pre-wrap;
word-wrap: break-word;
}
/* Attachment preview */
.plugin-container .group:hover .group-hover\:opacity-100 {
opacity: 1;
}
/* Loading animation */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.plugin-container .animate-spin {
animation: spin 1s linear infinite;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.plugin-container {
padding: 10px;
}
.plugin-container .h-full {
height: calc(100vh - 150px);
min-height: 400px;
}
}
`;
fs.writeFileSync(path.join(clientPath, 'src/PluginApp.css'), cssContent);
// Create dashboard placeholder image
await createDashboardPlaceholder(clientPath);
}
async function createDashboardPlaceholder(clientPath) {
const publicDir = path.join(clientPath, 'public');
// Ensure public directory exists
fs.ensureDirSync(publicDir);
// Create empty dashboard.png file
fs.writeFileSync(path.join(publicDir, 'dashboard.png'), '');
fs.writeFileSync(path.join(publicDir, 'reports.png'), '');
fs.writeFileSync(path.join(publicDir, 'sync-settings.png'), '');
}
function createDashboardPage(frontendType, integration) {
const integrationSpecific = {
quickbooks: `
<div className="plugin-card">
<h4>QuickBooks Integration</h4>
<p>Sync customers, invoices, and financial data with your pet care business.</p>
{data?.customers && (
<div>
<h5>Recent Customers:</h5>
<ul>
{data.customers.slice(0, 5).map((customer: any, i: number) => (
<li key={i}>{customer.name} - {customer.email}</li>
))}
</ul>
</div>
)}
</div>`,
calendar: `
<div className="plugin-card">
<h4>Calendar Integration</h4>
<p>Manage appointments and scheduling for your pet care services.</p>
{data?.events && (
<div>
<h5>Upcoming Appointments:</h5>
<ul>
{data.events.slice(0, 5).map((event: any, i: number) => (
<li key={i}>{event.summary} - {new Date(event.start).toLocaleDateString()}</li>
))}
</ul>
</div>
)}
</div>`,
mailchimp: `
<div className="plugin-card">
<h4>Mailchimp Marketing</h4>
<p>Manage email campaigns for your pet care customers.</p>
{data?.campaigns && (
<div>
<h5>Recent Campaigns:</h5>
<ul>
{data.campaigns.slice(0, 5).map((campaign: any, i: number) => (
<li key={i}>{campaign.title} - {campaign.status}</li>
))}
</ul>
</div>
)}
</div>`,
custom: `
<div className="plugin-card">
<h4>Welcome to Your Plugin</h4>
<p>This is a blank plugin template. Start building your custom integration here!</p>
<div className="getting-started">
<h5>Getting Started:</h5>
<ul>
<li>Configure your OAuth settings in the Settings page</li>
<li>Add your API integration logic</li>
<li>Customize the UI to match your needs</li>
<li>Deploy when ready!</li>
</ul>
</div>
</div>`
};
const isTypeScript = frontendType === 'typescript';
const typeImports = isTypeScript ? `
interface DashboardProps {
config?: any;
integration: string;
}` : '';
const componentType = isTypeScript ? ': React.FC<DashboardProps>' : '';
return `import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { Link, useNavigate } from 'react-router-dom';
import { getPluginData } from '../lib/api';
${typeImports}
export const Dashboard${componentType} = ({ integration }) => {
const navigate = useNavigate();
const { data, isLoading } = useQuery({
queryKey: ['plugin-data', integration],
queryFn: async () => {
const result = await getPluginData();
if (!result.success) {
throw new Error(result.message);
}
return result.data;
}
});
const handleLogout = () => {
localStorage.removeItem('api_key');
navigate('/');
};
if (isLoading) return <div>Loading...</div>;
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3>Dashboard</h3>
<div style={{ display: 'flex', gap: '10px' }}>
<Link to="/chat">
<button className="plugin-button">AI Chat</button>
</Link>
<Link to="/settings">
<button className="plugin-button">Settings</button>
</Link>
<button
className="plugin-button"
onClick={handleLogout}
style={{ backgroundColor: '#ef4444' }}
>
Logout
</button>
</div>
</div>
<div className="plugin-card">
<h4>Welcome to Your Python Agent Plugin</h4>
<p>This plugin provides access to multiple AI models with various capabilities:</p>
<div className="getting-started">
<h5>Available Features:</h5>
<ul>
<li><strong>AI Chat (nodejs - API only)</strong> - Chat with different AI models</li>
<li><strong>Text Generation</strong> - GPT-4o-mini</li>
<li><strong>Web Search</strong> - Real-time information lookup</li>
</ul>
</div>
<div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
<Link to="/chat">
<button className="plugin-button">Start Chatting</button>
</Link>
<Link to="/settings">
<button className="plugin-button" style={{ background: '#6b7280' }}>Manage Models</button>
</Link>
</div>
</div>
${integrationSpecific[integration] || integrationSpecific.custom}
<div className="plugin-card">
<h4>Integration Status</h4>
<span className={\`plugin-status \${data?.connected ? 'status-connected' : 'status-disconnected'}\`}>
{data?.connected ? 'Connected' : 'Disconnected'}
</span>
</div>
</div>
);
};`;
}
function createSettingsPage(frontendType, integration, backendType) {
const integrationLabels = {
quickbooks: 'QuickBooks',
calendar: 'Google Calendar',
mailchimp: 'Mailchimp',
custom: 'Your Service'
};
const isTypeScript = frontendType === 'typescript';
const typeImports = isTypeScript ? `
interface SettingsProps {
config?: any;
integration: string;
}` : '';
const componentType = isTypeScript ? ': React.FC<SettingsProps>' : '';
// Pre-evaluate the integration label and description
const integrationLabel = integrationLabels[integration] || 'Service';
const integrationDescription = integration === 'custom'
? 'Configure your OAuth settings and API connections for your custom integration.'
: `Connect your ${integrationLabels[integration] || 'service'} account to sync data with your pet care business.`;
return backendType.toLowerCase().includes('python') ? `import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api, testConnection, getModels, selectModel } from '../lib/api';
import { Loader } from '../components/Icons';
${typeImports}
export const Settings${componentType} = ({ config, integration }) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const [isConnecting, setIsConnecting] = useState(false);
const [connectionStatus, setConnectionStatus] = ${isTypeScript ? `useState<boolean | null>(null)` : `useState(null)`};
const [connectionMessage, setConnectionMessage] = ${isTypeScript ? `useState<string | null>(null)` : `useState('');`}
const [downloadingModel, setDownloadingModel] = useState(null);
// Test connection on component mount
useEffect(() => {
const checkConnection = async () => {
const result = await testConnection();
setConnectionStatus(result.success);
setConnectionMessage(result.message);
};
checkConnection();
}, []);
const handleConnect = async () => {
setIsConnecting(true);
try {
const { data } = await api.get('/oauth/start');
window.location.href = data.authUrl;
} catch (error) {
console.error('Failed to start OAuth:', error);
} finally {
setIsConnecting(false);
}
};
const handleDisconnect = async () => {
try {
// Note: Add a disconnect endpoint to your backend if needed
await api.post('/oauth/disconnect');
console.log('✅ Successfully disconnected from service');
window.location.reload();
} catch (error) {
console.error('❌ Failed to disconnect:', error);
// Still reload to clear any local state
window.location.reload();
}
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3>Settings</h3>
<div style={{ display: 'flex', gap: '10px' }}>
<Link to="/dashboard">
<button className="plugin-button">Back to Dashboard</button>
</Link>
<button
className="plugin-button"
onClick={() => {
localStorage.removeItem('api_key');
navigate('/');
}}
style={{ backgroundColor: '#ef4444' }}
>
Logout
</button>
</div>
</div>
<div className="plugin-card">
<h4>${integrationLabel} Connection</h4>
<p>${integrationDescription}</p>
<div style={{ marginTop: '20px' }}>
<button
className="plugin-button"
onClick={handleConnect}
disabled={isConnecting}
style={{ marginRight: '10px' }}
>
{isConnecting ? 'Connecting...' : 'Connect Account'}
</button>
<button
className="plugin-button"
onClick={handleDisconnect}
style={{ backgroundColor: '#ef4444' }}
>
Disconnect
</button>
</div>
</div>
<div className="plugin-card">
<h4>Backend Connection</h4>
<p>Open the Browser Console, test the connection.</p>
<div style={{ marginBottom: '15px' }}>
<span>Status: </span>
<span className={\`plugin-status \${connectionStatus ? 'status-connected' : 'status-disconnected'}\`}>
{connectionStatus ? 'Connected' : 'Disconnected'}
</span>
</div>
{connectionMessage && (
<p style={{ fontSize: '14px', color: '#666', marginBottom: '10px' }}>
{connectionMessage}
</p>
)}
<button
className="plugin-button"
onClick={async () => {
const result = await testConnection();
setConnectionStatus(result.success);
setConnectionMessage(result.message);
}}
style={{ fontSize: '12px', padding: '5px 10px' }}
>
Test Connection
</button>
</div>
<div className="plugin-card">
<h4>Configuration</h4>
<p>Current configuration:</p>
<pre style={{ background: '#f3f4f6', padding: '10px', borderRadius: '4px', fontSize: '12px' }}>
{JSON.stringify(config, null, 2)}
</pre>
</div>
<ModelManager
downloadingModel={downloadingModel}
setDownloadingModel={setDownloadingModel}
queryClient={queryClient}
/>
</div>
);
};
const ModelManager = (${isTypeScript ? `{ downloadingModel, setDownloadingModel, queryClient }: any` : `{ downloadingModel, setDownloadingModel, queryClient }`}) => {
const [downloadProgress, setDownloadProgress] = useState('');
// Model info
const models = {
'gpt-4o-mini': { label: 'GPT-4o Mini', size: 'Cloud API', description: 'Always available' },
'Qwen25Math': { label: 'Qwen Math', size: '~7GB', description: 'Mathematical reasoning' },
'DeepHermes3': { label: 'DeepHermes 3', size: '~16.1GB', description: 'Advanced reasoning' },
'phi4': { label: 'Phi-4 Multimodal', size: '~12.9GB', description: 'Text, image, and audio' },
'Flux': { label: 'FLUX Image Gen', size: '~54GB', description: 'Image generation' },
'FluxKontext': { label: 'FLUX Kontext', size: '~58GB', description: 'Image modification & editing' },
'Qwen25VL': { label: 'Qwen VL', size: '~15GB', description: 'Vision & Text reasoning' },
'Qwen25Code': { label: 'Qwen Coder', size: '~64GB', description: 'Code completion & generation' }
};
// Fetch model availability
const { data: modelsData, isLoading } = useQuery({
queryKey: ['models'],
queryFn: async () => {
const result = await getModels();
if (!result.success) throw new Error(result.message);
return result.data;
},
refetchInterval: 2000 // Poll every 2 seconds to check download status
});
////////////////////////////////////////////////////////////////
// Check for ongoing downloads when component mounts
useEffect(() => {
if (modelsData && !downloadingModel) {
// Find any model marked as "downloading"
const downloadingModelName = Object.entries(modelsData).find(([_, status]) => status === 'downloading')?.[0];
if (downloadingModelName) {
// Reconnect to the download (silently, no alerts)
setDownloadingModel(downloadingModelName);
setDownloadProgress('Reconnecting to download...');
selectModel(downloadingModelName, (progress) => {
setDownloadProgress(progress);
}).then(result => {
// Only update the UI, don't show alerts when reconnecting
if (result?.success) {
queryClient.invalidateQueries(['models']);
}
}).catch(() => {
// Silently handle reconnection errors
}).finally(() => {
setDownloadingModel(null);
setDownloadProgress('');
});
}
}
}, [modelsData]);
// Download handler
const handleDownload = async ${isTypeScript ? `(modelName: string)` : `(modelName)`} => {
setDownloadingModel(modelName);
setDownloadProgress('Starting download...');
try {
const result = await selectModel(modelName, (progress) => {
setDownloadProgress(progress);
});
if (result?.success) {
queryClient.invalidateQueries(['models']);
alert(\`Successfully downloaded \${models${isTypeScript ? `[modelName as keyof typeof models]` : `[modelName]`}.label}\`);
} else {
alert(\`Failed to download: \${result?.message || 'Unknown error in settings line ~183'}\`);
}
} catch (error) {
${isTypeScript ? `const errorMessage = error instanceof Error ? error.message : String(error);` : ''}
alert(\`Error: \${${isTypeScript ? 'errorMessage' : 'error.message'}}\`);
} finally {
setDownloadingModel(null);
setDownloadProgress('');
}
};
if (isLoading) return null;
return (
<div className="plugin-card">
<h4>Model Manager</h4>
<p>Download AI models for local use:</p>
<div style={{ marginTop: '15px' }}>
{Object.entries(models).map(([key, info]) => {
const modelStatus = modelsData?.[key];
const isAvailable = modelStatus === true || key === 'gpt-4o-mini';
const isDownloading = downloadingModel === key || modelStatus === 'downloading';
return (
<div key={key} style={{
padding: '10px',
border: '1px solid #e5e7eb',
borderRadius: '6px',
marginBottom: '10px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<strong>{info.label}</strong>
<span style={{ marginLeft: '10px', fontSize: '12px', color: '#6b7280' }}>
{info.size}
</span>
<div style={{ fontSize: '12px', color: '#6b7280', marginTop: '2px' }}>
{info.description}
</div>
</div>
{key !== 'gpt-4o-mini' && (
<button
onClick={() => handleDownload(key)}
disabled={isAvailable || downloadingModel !== null}
className="plugin-button"
style={{
fontSize: '12px',
padding: '5px 15px',
opacity: (isAvailable || downloadingModel !== null) ? 0.5 : 1
}}
>
{isAvailable ? '✓ Installed' : isDownloading ? 'Downloading...' : 'Download'}
</button>
)}
</div>
{isDownloading && (
<div style={{ marginTop: '10px', fontSize: '12px' }}>
<Loader className="inline w-3 h-3 animate-spin mr-2" />
{downloadProgress}
</div>
)}
</div>
);
})}
</div>
</div>
);
};
` : `import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { api, testConnection } from '../lib/api';
${typeImports}
export const Settings${componentType} = ({ config, integration }) => {
const [isConnecting, setIsConnecting] = useState(false);
const [connectionStatus, setConnectionStatus] = ${isTypeScript ? `useState<boolean | null>(null)` : `useState(null)`};
const [connectionMessage, setConnectionMessage] = ${isTypeScript ? `useState<string>('')` : `useState('')`};
// Test connection on component mount
useEffect(() => {
const checkConnection = async () => {
const result = await testConnection();
setConnectionStatus(result.success);
setConnectionMessage(result.message);
};
checkConnection();
}, []);
const handleConnect = async () => {
setIsConnecting(true);
try {
const { data } = await api.get('/oauth/start');
window.location.href = data.authUrl;
} catch (error) {
console.error('Failed to start OAuth:', error);
} finally {
setIsConnecting(false);
}
};
const handleDisconnect = async () => {
try {
// Note: Add a disconnect endpoint to your backend if needed
await api.post('/oauth/disconnect');
console.log('✅ Successfully disconnected from service');
window.location.reload();
} catch (error) {
console.error('❌ Failed to disconnect:', error);
// Still reload to clear any local state
window.location.reload();
}
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3>Settings</h3>
<Link to="/">
<button className="plugin-button">Back to Dashboard</button>
</Link>
</div>
<div className="plugin-card">
<h4>${integrationLabels[integration] || 'Service'} Connection</h4>
<p>${integration === 'custom'
? 'Configure your OAuth settings and API connections for your custom integration.'
: `Connect your ${integrationLabels[integration] || 'service'} account to sync data with your business.`}</p>
<div style={{ marginTop: '20px' }}>
<button
className="plugin-button"
onClick={handleConnect}
disabled={isConnecting}
style={{ marginRight: '10px' }}
>
{isConnecting ? 'Connecting...' : 'Connect Account'}
</button>
<button
className="plugin-button"
onClick={handleDisconnect}
style={{ backgroundColor: '#ef4444' }}
>
Disconnect
</button>
</div>
</div>
<div className="plugin-card">
<h4>Backend Connection</h4>
<p>Open the Browser Console, test the connection.</p>
<div style={{ marginBottom: '15px' }}>
<span>Status: </span>
<span className={\`plugin-status \${connectionStatus ? 'status-connected' : 'status-disconnected'}\`}>
{connectionStatus ? 'Connected' : 'Disconnected'}
</span>
</div>
{connectionMessage && (
<p style={{ fontSize: '14px', color: '#666', marginBottom: '10px' }}>
{connectionMessage}
</p>
)}
<button
className="plugin-button"
onClick={async () => {
const result = await testConnection();
setConnectionStatus(result.success);
setConnectionMessage(result.message);
}}
style={{ fontSize: '12px', padding: '5px 10px' }}
>
Test Connection
</button>
</div>
<div className="plugin-card">
<h4>Configuration</h4>
<p>Current configuration:</p>
<pre style={{ background: '#f3f4f6', padding: '10px', borderRadius: '4px', fontSize: '12px' }}>
{JSON.stringify(config, null, 2)}
</pre>
</div>
</div>
);
};`;
}
async function updateViteConfig(clientPath, projectName, frontendType) {
const fileExtension = frontendType === 'typescript' ? 'ts' : 'js';
const pluginAppPath = frontendType === 'typescript' ? './src/PluginApp.tsx' : './src/PluginApp.jsx';
const viteConfigContent = `import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: '${projectName}',
filename: 'remoteEntry.js',
exposes: {
'./PluginApp': '${pluginAppPath}'
},
shared: ['react', 'react-dom', 'react-router-dom']
})
],
build: {
modulePreload: false,
target: 'esnext',
minify: false,
cssCodeSplit: false,
outDir: '../../dist/client'
},
server: {
port: 5173,
cors: true,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
secure: false,
},
},
},
define: {
// Make plugin name available to the client
'import.meta.env.VITE_PLUGIN_NAME': JSON.stringify('${projectName}'),
}
});
`;
fs.writeFileSync(path.join(clientPath, `vite.config.${fileExtension}`), viteConfigContent);
}
// Helper functions for creating content based on frontend type and integration
function createPluginAppContent(projectName, frontendType, integration) {
const isTypeScript = frontendType === 'typescript';
const fileExt = isTypeScript ? 'tsx' : 'jsx';
const typeImports = isTypeScript ? `
export interface PluginProps {
config?: {
apiKey?: string;
userId?: string;
[key: string]: any;
};
}` : '';
const componentType = isTypeScript ? ': React.FC<PluginProps>' : '';
const configParam = isTypeScript ? '{ config }' : '{ config }';
const title = integration === 'custom'
? projectName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
: `${projectName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')} Plugin`;
return `import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Dashboard } from './pages/Dashboard';
import { Settings } from './pages/Settings';
import { Chat } from './pages/Chat';
import { Auth } from './pages/Auth';
import { ProtectedRoute } from './components/ProtectedRoute';
import './PluginApp.css';
const queryClient = new QueryClient();
${typeImports}
export const PluginApp${componentType} = (${configParam}) => {
return (
<QueryClientProvider client={queryClient}>
{/* Optional: <BrowserRouter basename={'/plugins/${projectName}'}> */}
<BrowserRouter basename={'/'}>
<div className="plugin-container">
<h2>${title}</h2>
<Routes>
<Route path="/" element={<Auth />} />
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard config={config} integration="${integration}" />
</ProtectedRoute>
} />
<Route path="/settings" element={
<ProtectedRoute>
<Settings config={config} integration="${integration}" />
</ProtectedRoute>
} />
<Route path="/chat" element={
<ProtectedRoute>
<Chat />
</ProtectedRoute>
} />
</Routes>
</div>
</BrowserRouter>
</QueryClientProvider>
);
};
// Export for Module Federation
export default PluginApp;
`;
}
// #########################################################################################
function createApiClientContent(frontendType, projectName, backendType) {
const isTypeScript = frontendType === 'typescript';
return backendType.toLowerCase().includes('python') ? `
// theses controll the api calls to the backend
// The python backend uses header based routing, so we need to set the headers manually
import axios, { AxiosError } from 'axios';
import appConfig from '../../../../app.config.json' with { type: "json" };
const hostIP = appConfig.deployment.dev.host;
const serverName = appConfig['server-name'];
const environment = appConfig.environment;
const BACKEND_SERVICE = \`\${serverName}-latest\`;
const SUPPORT_GPU = appConfig.gpu.supportGPU || false;
const stagingHost = appConfig.deployment.staging.host;
// Determine API base URL based on environment
// Development: Use /api prefix (proxied to localhost:3000)
// Production: Use /api/integrations/[plugin-name] (routed by main app)
const isDevelopment = environment === 'development';
const isStaging = environment === 'staging';
const isProduction = environment === 'production';
const pluginName = BACKEND_SERVICE.split('-')[0];
const API_BASE_URL = (isDevelopment ? \`https://\${hostIP}\` : isProduction ? \`/api/integrations/\${pluginName}\` : isStaging ? \`https://\${stagingHost}\` : \`/api\`);
export const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'X-Backend-Service': BACKEND_SERVICE, //routing header
'X-Support-GPU': SUPPORT_GPU,
},
});
// Add auth token and API key if available
api.interceptors.request.use((config) => {
const token = localStorage.getItem('plugin_token');
const apiKey = localStorage.getItem('api_key');
if (token) {
config.headers.Authorization = \`Bearer \${token}\`;
}
if (apiKey) {
config.headers['X-API-Key'] = apiKey;
}
return config;
});
// Note: X-Method-Endpoint is now set manually in each API function
// This gives us full control over the routing endpoint
// Request interceptor for debugging in development
api.interceptors.request.use((config) => {
if (isDevelopment || environment === 'staging') {
console.log(\`🔗 API Request: \${config.method?.toUpperCase()} \${config.baseURL}\${config.url}\`);
console.log(\`🎯 Routing to backend: \${config.headers['X-Backend-Service']}\`);
console.log(\`📍 Method-Endpoint: \${config.headers['X-Method-Endpoint']}\`);
}
return config;
});
/*
* API Functions following the established pattern
*
* All requests automatically include these headers:
* - Content-Type: application/json
* - X-Backend-Service: python-agent-backend-latest (for nginx routing)
* - X-Support-GPU: true/false (from config)
* - X-Method-Endpoint: METHOD:endpoint (dynamically set, e.g., "POST:/chat")
* - Authorization: Bearer [token] (if available)
*
* Response format: { success: boolean, data?: any, message?: string }
*/
// Test connection to backend
export const testConnection = async () => {
try {
const response = await api.get('/', {
headers: { 'X-Method-Endpoint': 'GET:/health' }
});
if (response.data?.status === 'ok') {
console.log('✅ Successfully connected to plugin backend:', response.data.message);
return { success: true, message: response.data.message };
}
return { success: false, message: 'Unexpected response from backend' };
} catch (error) {
${isTypeScript ? `const errorMessage = error instanceof Error ? error.message : String(error);` : ''}
console.error('❌ Failed to connect to plugin backend:', ${isTypeScript ? 'errorMessage' : 'error.message'});
return { success: false, message: ${isTypeScript ? 'errorMessage' : 'error.message'} };
}
};
// Test authentication with API key
export const testAuthentication = async () => {
try {
const response = await api.get('/', {
headers: { 'X-Method-Endpoint': 'GET:/api/data' }
});
console.log('✅ Authentication successful');
return { success: true, data: response.data };
} catch (error) {
${isTypeScript ? `const errorMessage = error instanceof Error ? error.message : String(error);` : ''}
console.error('❌ Authentication failed:', ${isTypeScript ? 'errorMessage' : 'error.message'});
// Check if it's a 401 error specifically
if (${isTypeScript ? 'error instanceof AxiosError && error.response?.status === 401' : 'error.response?.status === 401'}) {
return { success: false, message: 'Invalid API key', isAuthError: true };
}
return { success: false, message: ${isTypeScript ? 'errorMessage' : 'error.message'} };
}
};
// Send chat message to backend
export const sendChatMessage = async ${isTypeScript ? `(payload: any)` : `(payload)`} => {
try {
// Ensure payload matches server expectations
${isTypeScript ? `const requestPayload: any ={
message: payload.message,
model: payload.model,
sessionId: payload.sessionId
};` : `const requestPayload = {
message: payload.message,
model: payload.model,
sessionId: payload.sessionId
};`}
// Add optional fields if present
if (payload.images) requestPayload.images = payload.images;
if (payload.audios) requestPayload.audios = payload.audios;
const response = await api.post('/', requestPayload, {
headers: { 'X-Method-Endpoint': 'POST:/chat' }
});
return { success: true, data: response.data };
} catch (error) {
${isTypeScript ? `const errorMessage = error instanceof Error ? error.message : String(error);` : ''}
console.error('❌ Chat request failed:', ${isTypeScript ? 'errorMessage' : 'error.message'});
return {
success: false,
message: ${isTypeScript ? `error instanceof AxiosError ? error.response?.data?.message : errorMessage` : `error.response?.data?.message || error.message || 'Failed to send message'`}
};
}
};
// Get available models
export const getModels = async () => {
try {
const response = await api.get('/', {
headers: { 'X-Method-Endpoint': 'GET:/models' }
});
return { success: true, data: response.data };
} catch (error) {
${isTypeScript ? `const errorMessage = error instanceof Error ? error.message : String(error);` : ''}
console.error('❌ Failed to fetch models:', ${isTypeScript ? 'errorMessage' : 'error.message'});
return { success: false, message: ${isTypeScript ? 'errorMessage' : 'error.message'} };
}
};
// Select/activate a model (with download progress support)
export const selectModel = async ${isTypeScript ? `(model: string, onProgress: (message: string) => void)` : `(model, onProgress)`} => {
try {
// Use fetch for SSE support instead of axios
const response = await fetch(\`\${API_BASE_URL}/\`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Method-Endpoint': 'POST:/select_model',
'X-Backend-Service': BACKEND_SERVICE,
'X-Support-GPU': String(SUPPORT_GPU),
...(() => {
const token = localStorage.getItem('plugin_token');
const apiKey = localStorage.getItem('api_key');
return {
...(token ? { 'Authorization': \`Bearer \${token}\` } : {}),
...(apiKey ? { 'X-API-Key': apiKey } : {})
};
})()
},
body: JSON.stringify({ model })
});
if (!response.ok) {
throw new Error(\`HTTP error! status: \${response.status}\`);
}
// Check if it's a streaming response
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('text/event-stream')) {
// Handle SSE stream for download progress
if (!response.body) throw new Error('Response body is null');
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
try {
const parsed = JSON.parse(data);
if (parsed.type === 'progress' && onProgress) {
onProgress(parsed.message);
} else if (parsed.type === 'complete') {
return { success: true, data: parsed };
} else if (parsed.type === 'error') {
throw new Error(parsed.error);
}
} catch (e) {
// Ignore JSON parse errors for incomplete chunks
}
}
}
}
} else {
// Non-streaming response (model already available)
const data = await response.json();
return { success: true, data };
}
} catch (error) {
${isTypeScript ? `const errorMessage = error instanceof Error ? error.message : String(error);` : ''}
console.error('❌ Failed to select model:', ${isTypeScript ? 'errorMessage' : 'error.message'});
return { success: false, message: ${isTypeScript ? 'errorMessage' : 'error.message'} };
}
};
// Get environment info
export const getEnvInfo = async () => {
try {
const response = await api.get('/', {
headers: { 'X-Method-Endpoint': 'GET:/env' }
});
return { success: true, data: response.data };
} catch (error) {
${isTypeScript ? `const errorMessage = error instanceof Error ? error.message : String(error);` : ''}
console.error('❌ Failed to get environment info:', ${isTypeScript ? 'errorMessage' : 'error.message'});
return { success: false, message: ${isTypeScript ? 'errorMessage' : 'error.message'} };
}
};
// Get plugin data
export const getPluginData = async () => {
try {
const response = await api.get('/', {
headers: { 'X-Method-Endpoint': 'GET:/api/data' }
});
return { success: true, data: response.data };
} catch (error) {
${isTypeScript ? `const errorMessage = error instanceof Error ? error.message : String(error);` : ''}
console.error('❌ Failed to get plugin data:', ${isTypeScript ? 'errorMessage' : 'error.message'});
return { success: false, message: ${isTypeScript ? 'errorMessage' : 'error.message'} };
}
};` : `// The node backend uses url based routing
import axios, { AxiosError } from 'axios';
import appConfig from '../../../../app.config.json' with { type: "json" };
const hostIP = appConfig.deployment.dev.host;
const serverName = appConfig['server-name'];
const environment = appConfig.environment;
const stagingHost = appConfig.deployment.staging.host;
// Determine API base URL based on environment
// Development: Use /api prefix (proxied to localhost:3000)
// Production: Use /api/integrations/[plugin-name] (routed by main app)
const isDevelopment = environment === 'development';
const isStaging = environment === 'staging';
const isProduction = environment === 'production';
const pluginName = import.meta.env.VITE_PLUGIN_NAME;
const API_BASE_URL = import.meta.env.VITE_API_URL ||
(isDevelopment ? \`https://\${hostIP}\` : isProduction ? \`/api/integrations/\${pluginName}\` : isStaging ? \`https://\${stagingHost}\` : \`/api\`);
// Backend container name
const BACKEND_SERVICE = \`\${serverName}-latest\`;
export const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'X-Backend-Service': BACKEND_SERVICE, // routing header
},
});
// Add API key to all requests
api.interceptors.request.use((config) => {
const apiKey = localStorage.getItem('api_key');
if (apiKey) {
config.headers['X-API-Key'] = apiKey;
}
return config;
});
// Request interceptor for debugging in development
api.interceptors.request.use((config) => {
if (isDevelopment) {
console.log(\`🔗 API Request: \${config.method?.toUpperCase()} \${config.baseURL}\${config.url}\`);
console.log(\`🎯 Routing to backend: \${config.headers['X-Backend-Service']}\`);
}
return config;
});
// Test connection to backend
export const testConnection = async () => {
try {
const response = await api.get('/health');
if (response.data?.status === 'ok') {
console.log('✅ Successfully connected to plugin backend:', response.data.message);
return { success: true, message: response.data.message };
}
return { success: false, message: 'Unexpected response from backend' };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('❌ Failed to connect to plugin backend:', errorMessage);
return { success: false, message: errorMessage };
}
};
// Test auth to backend - checks if the current API key in localStorage is valid
export const testAuthentication = async () => {
try {
const response = await api.get('/data');
if (response.data?.status === 'ok') {
console.log('✅ Successfully authenticated:', response.data.message);
return { success: true, message: response.data.message };
}
return { success: false, message: 'Authentication failed' };
} catch (${isTypeScript ? `error: any` : `error`}) {
if (error.response?.status === 401) {
return { success: false, message: 'Invalid or missing API key' };
}
const errorMessage = error.response?.data?.message || error.message || 'Authentication failed';
console.error('❌ Failed to authenticate:', errorMessage);
return { success: false, message: errorMessage };
}
};
export const getPluginData = async () => {
try {
const response = await api.get('/data');
if (response.data?.status === 'ok') {
console.log('✅ Successfully connected to plugin backend:', response.data.message);
return { success: true, data: response.data, message: response.data.message };
}
return { success: false, message: 'Unexpected response from backend' };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('❌ Failed to connect to plugin backend:', errorMessage);
return { success: false, message: errorMessage };
}
};
// Validate API key
export const validateApiKey = async (${isTypeScript ? `apiKey: string` : `apiKey`}) => {
try {
const response = await api.post('/validate-key', { apiKey });
return {
success: response.data.success,
message: response.data.message
};
} catch (${isTypeScript ? `error: any` : `error`}) {
return {
success: false,
message: error.response?.data?.message || 'Invalid API key'
};
}
};
// Send chat message
export const sendChatMessage = async (${isTypeScript ? `message: string, sessionId: string` : `message, sessionId`}) => {
try {
const response = await api.post('/chat', { message, sessionId });
return {
success: true,
data: response.data
};
} catch (${isTypeScript ? `error: any` : `error`}) {
return {
success: false,
message: error.response?.data?.message || 'Failed to send message'
};
}
};
`;
}
// #########################################################################################
function createChatPage(frontendType, projectName, backendType) {
const isTypeScript = frontendType === 'typescript';
return backendType.toLowerCase().includes('python') ? `import { useState, useRef, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate, Link } from 'react-router-dom';
import { api, sendChatMessage, getModels, selectModel } from '../lib/api';
import { useChatStream } from '../hooks/useChatStream';
import {
Send,
Image,
Mic,
X,
Loader
} from '../components/Icons';
${isTypeScript ? `
// Type definitions
interface Message {
id: number;
role: 'user' | 'assistant' | 'error';
content: string;
images?: UploadedImage[];
audio?: UploadedAudio | null;
model?: string;
timestamp: string;
}
interface UploadedImage {
name: string;
data: string | ArrayBuffer | null;
type: 'image';
}
interface UploadedAudio {
name: string;
data: string | ArrayBuffer | null;
type: 'audio';
};` : ``}
export const Chat = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();
// Generate or retrieve session ID
const [sessionId] = useState(() => {
const stored = localStorage.getItem('chat_session_id');
if (stored) return stored;
const newId = \`session_\${Date.now()}_\${Math.random().toString(36).substr(2, 9)}\`;
localStorage.setItem('chat_session_id', newId);
return newId;
});
// Model definitions (static info about models)
const modelDefinitions = {
'gpt-4o-mini': { label: 'GPT-4o Mini (Default)', supports: ['text'] },
'Qwen25Math': { label: 'Qwen Math', supports: ['text'] },
'DeepHermes3': { label: 'DeepHermes 3', supports: ['text'] },
'phi4': { label: 'Phi-4 Multimodal', supports: ['text', 'image', 'audio'] },
'Flux': { label: 'FLUX Image Gen', supports: ['text'] },
'Qwen25VL': { label: 'Qwen VL', supports: ['text', 'image'] },
'Qwen25Code': { label: 'Qwen Coder', supports: ['text'] },
'FluxKontext': { label: 'FLUX Kontext', supports: ['text', 'image'] }
};
// State
const [selectedModel, setSelectedModel] = useSt