UNPKG

@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
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