From dbf34e2d635b94d7507a7c38280e64f61ee3bb35 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:14:27 -0400 Subject: [PATCH 01/14] Add v2 sandbox implementation with new API routes and sandbox library --- app/api/create-ai-sandbox-v2/route.ts | 95 ++++ app/api/install-packages-v2/route.ts | 44 ++ app/api/run-command-v2/route.ts | 46 ++ lib/sandbox/factory.ts | 42 ++ lib/sandbox/providers/e2b-provider.ts | 494 ++++++++++++++++++ lib/sandbox/providers/vercel-provider.ts | 469 +++++++++++++++++ lib/sandbox/types.ts | 64 +++ packages/create-open-lovable/index.js | 94 ++++ packages/create-open-lovable/lib/installer.js | 261 +++++++++ packages/create-open-lovable/lib/prompts.js | 190 +++++++ packages/create-open-lovable/package.json | 36 ++ .../templates/e2b/.env.example | 25 + .../templates/e2b/README.md | 38 ++ .../templates/vercel/.env.example | 28 + .../templates/vercel/README.md | 52 ++ 15 files changed, 1978 insertions(+) create mode 100644 app/api/create-ai-sandbox-v2/route.ts create mode 100644 app/api/install-packages-v2/route.ts create mode 100644 app/api/run-command-v2/route.ts create mode 100644 lib/sandbox/factory.ts create mode 100644 lib/sandbox/providers/e2b-provider.ts create mode 100644 lib/sandbox/providers/vercel-provider.ts create mode 100644 lib/sandbox/types.ts create mode 100644 packages/create-open-lovable/index.js create mode 100644 packages/create-open-lovable/lib/installer.js create mode 100644 packages/create-open-lovable/lib/prompts.js create mode 100644 packages/create-open-lovable/package.json create mode 100644 packages/create-open-lovable/templates/e2b/.env.example create mode 100644 packages/create-open-lovable/templates/e2b/README.md create mode 100644 packages/create-open-lovable/templates/vercel/.env.example create mode 100644 packages/create-open-lovable/templates/vercel/README.md diff --git a/app/api/create-ai-sandbox-v2/route.ts b/app/api/create-ai-sandbox-v2/route.ts new file mode 100644 index 0000000..93410a1 --- /dev/null +++ b/app/api/create-ai-sandbox-v2/route.ts @@ -0,0 +1,95 @@ +import { NextResponse } from 'next/server'; +import { SandboxFactory } from '@/lib/sandbox/factory'; +import { SandboxProvider } from '@/lib/sandbox/types'; +import type { SandboxState } from '@/types/sandbox'; + +// Store active sandbox globally +declare global { + var activeSandboxProvider: SandboxProvider | null; + var sandboxData: any; + var existingFiles: Set; + var sandboxState: SandboxState; +} + +export async function POST() { + try { + console.log('[create-ai-sandbox-v2] Creating sandbox...'); + + // Clean up existing sandbox if any + if (global.activeSandboxProvider) { + console.log('[create-ai-sandbox-v2] Terminating existing sandbox...'); + try { + await global.activeSandboxProvider.terminate(); + } catch (e) { + console.error('Failed to terminate existing sandbox:', e); + } + global.activeSandboxProvider = null; + } + + // Clear existing files tracking + if (global.existingFiles) { + global.existingFiles.clear(); + } else { + global.existingFiles = new Set(); + } + + // Create new sandbox using factory + const provider = SandboxFactory.create(); + const sandboxInfo = await provider.createSandbox(); + + console.log('[create-ai-sandbox-v2] Setting up Vite React app...'); + await provider.setupViteApp(); + + // Store provider globally + global.activeSandboxProvider = provider; + global.sandboxData = { + sandboxId: sandboxInfo.sandboxId, + url: sandboxInfo.url + }; + + // Initialize sandbox state + global.sandboxState = { + fileCache: { + files: {}, + lastSync: Date.now(), + sandboxId: sandboxInfo.sandboxId + }, + sandbox: provider, // Store the provider instead of raw sandbox + sandboxData: { + sandboxId: sandboxInfo.sandboxId, + url: sandboxInfo.url + } + }; + + console.log('[create-ai-sandbox-v2] Sandbox ready at:', sandboxInfo.url); + + return NextResponse.json({ + success: true, + sandboxId: sandboxInfo.sandboxId, + url: sandboxInfo.url, + provider: sandboxInfo.provider, + message: 'Sandbox created and Vite React app initialized' + }); + + } catch (error) { + console.error('[create-ai-sandbox-v2] Error:', error); + + // Clean up on error + if (global.activeSandboxProvider) { + try { + await global.activeSandboxProvider.terminate(); + } catch (e) { + console.error('Failed to terminate sandbox on error:', e); + } + global.activeSandboxProvider = null; + } + + return NextResponse.json( + { + error: error instanceof Error ? error.message : 'Failed to create sandbox', + details: error instanceof Error ? error.stack : undefined + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/install-packages-v2/route.ts b/app/api/install-packages-v2/route.ts new file mode 100644 index 0000000..646e44b --- /dev/null +++ b/app/api/install-packages-v2/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { SandboxProvider } from '@/lib/sandbox/types'; + +declare global { + var activeSandboxProvider: SandboxProvider | null; +} + +export async function POST(request: NextRequest) { + try { + const { packages } = await request.json(); + + if (!packages || !Array.isArray(packages) || packages.length === 0) { + return NextResponse.json({ + success: false, + error: 'Packages array is required' + }, { status: 400 }); + } + + if (!global.activeSandboxProvider) { + return NextResponse.json({ + success: false, + error: 'No active sandbox' + }, { status: 400 }); + } + + console.log(`[install-packages-v2] Installing: ${packages.join(', ')}`); + + const result = await global.activeSandboxProvider.installPackages(packages); + + return NextResponse.json({ + success: result.success, + output: result.stdout, + error: result.stderr, + message: result.success ? 'Packages installed successfully' : 'Package installation failed' + }); + + } catch (error) { + console.error('[install-packages-v2] Error:', error); + return NextResponse.json({ + success: false, + error: (error as Error).message + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/run-command-v2/route.ts b/app/api/run-command-v2/route.ts new file mode 100644 index 0000000..6ea58b3 --- /dev/null +++ b/app/api/run-command-v2/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { SandboxProvider } from '@/lib/sandbox/types'; + +// Get active sandbox provider from global state +declare global { + var activeSandboxProvider: SandboxProvider | null; +} + +export async function POST(request: NextRequest) { + try { + const { command } = await request.json(); + + if (!command) { + return NextResponse.json({ + success: false, + error: 'Command is required' + }, { status: 400 }); + } + + if (!global.activeSandboxProvider) { + return NextResponse.json({ + success: false, + error: 'No active sandbox' + }, { status: 400 }); + } + + console.log(`[run-command-v2] Executing: ${command}`); + + const result = await global.activeSandboxProvider.runCommand(command); + + return NextResponse.json({ + success: result.success, + output: result.stdout, + error: result.stderr, + exitCode: result.exitCode, + message: result.success ? 'Command executed successfully' : 'Command failed' + }); + + } catch (error) { + console.error('[run-command-v2] Error:', error); + return NextResponse.json({ + success: false, + error: (error as Error).message + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/lib/sandbox/factory.ts b/lib/sandbox/factory.ts new file mode 100644 index 0000000..97dd529 --- /dev/null +++ b/lib/sandbox/factory.ts @@ -0,0 +1,42 @@ +import { SandboxProvider, SandboxProviderConfig } from './types'; +import { E2BProvider } from './providers/e2b-provider'; +import { VercelProvider } from './providers/vercel-provider'; + +export class SandboxFactory { + static create(provider?: string, config?: SandboxProviderConfig): SandboxProvider { + // Use environment variable if provider not specified + const selectedProvider = provider || process.env.SANDBOX_PROVIDER || 'e2b'; + + console.log(`[SandboxFactory] Creating ${selectedProvider} provider`); + + switch (selectedProvider.toLowerCase()) { + case 'e2b': + return new E2BProvider(config || {}); + + case 'vercel': + return new VercelProvider(config || {}); + + default: + throw new Error(`Unknown sandbox provider: ${selectedProvider}. Supported providers: e2b, vercel`); + } + } + + static getAvailableProviders(): string[] { + return ['e2b', 'vercel']; + } + + static isProviderAvailable(provider: string): boolean { + switch (provider.toLowerCase()) { + case 'e2b': + return !!process.env.E2B_API_KEY; + + case 'vercel': + // Vercel can use OIDC (automatic) or PAT + return !!process.env.VERCEL_OIDC_TOKEN || + (!!process.env.VERCEL_TOKEN && !!process.env.VERCEL_TEAM_ID && !!process.env.VERCEL_PROJECT_ID); + + default: + return false; + } + } +} \ No newline at end of file diff --git a/lib/sandbox/providers/e2b-provider.ts b/lib/sandbox/providers/e2b-provider.ts new file mode 100644 index 0000000..91fe458 --- /dev/null +++ b/lib/sandbox/providers/e2b-provider.ts @@ -0,0 +1,494 @@ +import { Sandbox } from '@e2b/code-interpreter'; +import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types'; +import { appConfig } from '@/config/app.config'; + +export class E2BProvider extends SandboxProvider { + private existingFiles: Set = new Set(); + + async createSandbox(): Promise { + try { + console.log('[E2BProvider] Creating sandbox...'); + + // Kill existing sandbox if any + if (this.sandbox) { + console.log('[E2BProvider] Killing existing sandbox...'); + try { + await this.sandbox.kill(); + } catch (e) { + console.error('Failed to close existing sandbox:', e); + } + this.sandbox = null; + } + + // Clear existing files tracking + this.existingFiles.clear(); + + // Create base sandbox + console.log(`[E2BProvider] Creating E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`); + this.sandbox = await Sandbox.create({ + apiKey: this.config.e2b?.apiKey || process.env.E2B_API_KEY, + timeoutMs: this.config.e2b?.timeoutMs || appConfig.e2b.timeoutMs + }); + + const sandboxId = (this.sandbox as any).sandboxId || Date.now().toString(); + const host = (this.sandbox as any).getHost(appConfig.e2b.vitePort); + + console.log(`[E2BProvider] Sandbox created: ${sandboxId}`); + console.log(`[E2BProvider] Sandbox host: ${host}`); + + this.sandboxInfo = { + sandboxId, + url: `https://${host}`, + provider: 'e2b', + createdAt: new Date() + }; + + // Set extended timeout on the sandbox instance if method available + if (typeof this.sandbox.setTimeout === 'function') { + this.sandbox.setTimeout(appConfig.e2b.timeoutMs); + console.log(`[E2BProvider] Set sandbox timeout to ${appConfig.e2b.timeoutMinutes} minutes`); + } + + return this.sandboxInfo; + + } catch (error) { + console.error('[E2BProvider] Error creating sandbox:', error); + throw error; + } + } + + async runCommand(command: string): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + console.log(`[E2BProvider] Executing: ${command}`); + + const result = await this.sandbox.runCode(` + import subprocess + import os + + os.chdir('/home/user/app') + result = subprocess.run(${JSON.stringify(command.split(' '))}, + capture_output=True, + text=True, + shell=False) + + print("STDOUT:") + print(result.stdout) + if result.stderr: + print("\\nSTDERR:") + print(result.stderr) + print(f"\\nReturn code: {result.returncode}") + `); + + const output = result.logs.stdout.join('\n'); + const stderr = result.logs.stderr.join('\n'); + + return { + stdout: output, + stderr, + exitCode: result.error ? 1 : 0, + success: !result.error + }; + } + + async writeFile(path: string, content: string): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + const fullPath = path.startsWith('/') ? path : `/home/user/app/${path}`; + + await this.sandbox.runCode(` + import os + + # Ensure directory exists + dir_path = os.path.dirname("${fullPath}") + os.makedirs(dir_path, exist_ok=True) + + # Write file + with open("${fullPath}", 'w') as f: + f.write(${JSON.stringify(content)}) + print(f"✓ Written: ${fullPath}") + `); + + this.existingFiles.add(path); + } + + async readFile(path: string): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + const fullPath = path.startsWith('/') ? path : `/home/user/app/${path}`; + + const result = await this.sandbox.runCode(` + with open("${fullPath}", 'r') as f: + content = f.read() + print(content) + `); + + return result.logs.stdout.join('\n'); + } + + async listFiles(directory: string = '/home/user/app'): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + const result = await this.sandbox.runCode(` + import os + import json + + def list_files(path): + files = [] + for root, dirs, filenames in os.walk(path): + # Skip node_modules and .git + dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist', 'build']] + for filename in filenames: + rel_path = os.path.relpath(os.path.join(root, filename), path) + files.append(rel_path) + return files + + files = list_files("${directory}") + print(json.dumps(files)) + `); + + try { + return JSON.parse(result.logs.stdout.join('')); + } catch { + return []; + } + } + + async installPackages(packages: string[]): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + const packageList = packages.join(' '); + const flags = appConfig.packages.useLegacyPeerDeps ? '--legacy-peer-deps' : ''; + + console.log(`[E2BProvider] Installing packages: ${packageList}`); + + const result = await this.sandbox.runCode(` + import subprocess + import os + + os.chdir('/home/user/app') + + # Install packages + result = subprocess.run( + ['npm', 'install', ${flags ? `'${flags}',` : ''} ${packages.map(p => `'${p}'`).join(', ')}], + capture_output=True, + text=True + ) + + print("STDOUT:") + print(result.stdout) + if result.stderr: + print("\\nSTDERR:") + print(result.stderr) + print(f"\\nReturn code: {result.returncode}") + `); + + const output = result.logs.stdout.join('\n'); + const stderr = result.logs.stderr.join('\n'); + + // Restart Vite if configured + if (appConfig.packages.autoRestartVite && !result.error) { + await this.restartViteServer(); + } + + return { + stdout: output, + stderr, + exitCode: result.error ? 1 : 0, + success: !result.error + }; + } + + async setupViteApp(): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + console.log('[E2BProvider] Setting up Vite React app...'); + + // Write all files in a single Python script + const setupScript = ` +import os +import json + +print('Setting up React app with Vite and Tailwind...') + +# Create directory structure +os.makedirs('/home/user/app/src', exist_ok=True) + +# Package.json +package_json = { + "name": "sandbox-app", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.0.0", + "vite": "^4.3.9", + "tailwindcss": "^3.3.0", + "postcss": "^8.4.31", + "autoprefixer": "^10.4.16" + } +} + +with open('/home/user/app/package.json', 'w') as f: + json.dump(package_json, f, indent=2) +print('✓ package.json') + +# Vite config +vite_config = """import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5173, + strictPort: true, + hmr: false, + allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1'] + } +})""" + +with open('/home/user/app/vite.config.js', 'w') as f: + f.write(vite_config) +print('✓ vite.config.js') + +# Tailwind config +tailwind_config = """/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +}""" + +with open('/home/user/app/tailwind.config.js', 'w') as f: + f.write(tailwind_config) +print('✓ tailwind.config.js') + +# PostCSS config +postcss_config = """export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}""" + +with open('/home/user/app/postcss.config.js', 'w') as f: + f.write(postcss_config) +print('✓ postcss.config.js') + +# Index.html +index_html = """ + + + + + Sandbox App + + +
+ + +""" + +with open('/home/user/app/index.html', 'w') as f: + f.write(index_html) +print('✓ index.html') + +# Main.jsx +main_jsx = """import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +)""" + +with open('/home/user/app/src/main.jsx', 'w') as f: + f.write(main_jsx) +print('✓ src/main.jsx') + +# App.jsx +app_jsx = """function App() { + return ( +
+
+

+ Sandbox Ready
+ Start building your React app with Vite and Tailwind CSS! +

+
+
+ ) +} + +export default App""" + +with open('/home/user/app/src/App.jsx', 'w') as f: + f.write(app_jsx) +print('✓ src/App.jsx') + +# Index.css +index_css = """@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background-color: rgb(17 24 39); +}""" + +with open('/home/user/app/src/index.css', 'w') as f: + f.write(index_css) +print('✓ src/index.css') + +print('\\nAll files created successfully!') +`; + + await this.sandbox.runCode(setupScript); + + // Install dependencies + console.log('[E2BProvider] Installing dependencies...'); + await this.sandbox.runCode(` +import subprocess + +print('Installing npm packages...') +result = subprocess.run( + ['npm', 'install'], + cwd='/home/user/app', + capture_output=True, + text=True +) + +if result.returncode == 0: + print('✓ Dependencies installed successfully') +else: + print(f'⚠ Warning: npm install had issues: {result.stderr}') + `); + + // Start Vite dev server + console.log('[E2BProvider] Starting Vite dev server...'); + await this.sandbox.runCode(` +import subprocess +import os +import time + +os.chdir('/home/user/app') + +# Kill any existing Vite processes +subprocess.run(['pkill', '-f', 'vite'], capture_output=True) +time.sleep(1) + +# Start Vite dev server +env = os.environ.copy() +env['FORCE_COLOR'] = '0' + +process = subprocess.Popen( + ['npm', 'run', 'dev'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env +) + +print(f'✓ Vite dev server started with PID: {process.pid}') +print('Waiting for server to be ready...') + `); + + // Wait for Vite to be ready + await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay)); + + // Track initial files + this.existingFiles.add('src/App.jsx'); + this.existingFiles.add('src/main.jsx'); + this.existingFiles.add('src/index.css'); + this.existingFiles.add('index.html'); + this.existingFiles.add('package.json'); + this.existingFiles.add('vite.config.js'); + this.existingFiles.add('tailwind.config.js'); + this.existingFiles.add('postcss.config.js'); + } + + async restartViteServer(): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + console.log('[E2BProvider] Restarting Vite server...'); + + await this.sandbox.runCode(` +import subprocess +import time +import os + +os.chdir('/home/user/app') + +# Kill existing Vite process +subprocess.run(['pkill', '-f', 'vite'], capture_output=True) +time.sleep(2) + +# Start Vite dev server +env = os.environ.copy() +env['FORCE_COLOR'] = '0' + +process = subprocess.Popen( + ['npm', 'run', 'dev'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env +) + +print(f'✓ Vite restarted with PID: {process.pid}') + `); + + // Wait for Vite to be ready + await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay)); + } + + getSandboxUrl(): string | null { + return this.sandboxInfo?.url || null; + } + + async terminate(): Promise { + if (this.sandbox) { + console.log('[E2BProvider] Terminating sandbox...'); + try { + await this.sandbox.kill(); + } catch (e) { + console.error('Failed to terminate sandbox:', e); + } + this.sandbox = null; + this.sandboxInfo = null; + } + } + + isAlive(): boolean { + return !!this.sandbox; + } +} \ No newline at end of file diff --git a/lib/sandbox/providers/vercel-provider.ts b/lib/sandbox/providers/vercel-provider.ts new file mode 100644 index 0000000..ed09dea --- /dev/null +++ b/lib/sandbox/providers/vercel-provider.ts @@ -0,0 +1,469 @@ +import { Sandbox } from '@vercel/sandbox'; +import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types'; + +export class VercelProvider extends SandboxProvider { + private existingFiles: Set = new Set(); + + async createSandbox(): Promise { + try { + console.log('[VercelProvider] Creating sandbox...'); + + // Kill existing sandbox if any + if (this.sandbox) { + console.log('[VercelProvider] Stopping existing sandbox...'); + try { + await this.sandbox.stop(); + } catch (e) { + console.error('Failed to stop existing sandbox:', e); + } + this.sandbox = null; + } + + // Clear existing files tracking + this.existingFiles.clear(); + + // Create Vercel sandbox + console.log('[VercelProvider] Creating Vercel sandbox...'); + + const sandboxConfig: any = { + timeout: 300000, // 5 minutes in ms + runtime: 'node22', // Use node22 runtime for Vercel sandboxes + ports: [5173] // Vite port + }; + + // Add authentication based on environment variables + if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) { + console.log('[VercelProvider] Using personal access token authentication'); + sandboxConfig.teamId = process.env.VERCEL_TEAM_ID; + sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID; + sandboxConfig.token = process.env.VERCEL_TOKEN; + } else if (process.env.VERCEL_OIDC_TOKEN) { + console.log('[VercelProvider] Using OIDC token authentication'); + } else { + console.log('[VercelProvider] No authentication found - relying on default Vercel authentication'); + } + + this.sandbox = await Sandbox.create(sandboxConfig); + + const sandboxId = this.sandbox.sandboxId; + console.log(`[VercelProvider] Sandbox created: ${sandboxId}`); + + // Get the sandbox URL using the correct Vercel Sandbox API + const sandboxUrl = this.sandbox.domain(5173); + console.log(`[VercelProvider] Sandbox URL: ${sandboxUrl}`); + + this.sandboxInfo = { + sandboxId, + url: sandboxUrl, + provider: 'vercel', + createdAt: new Date() + }; + + return this.sandboxInfo; + + } catch (error) { + console.error('[VercelProvider] Error creating sandbox:', error); + throw error; + } + } + + async runCommand(command: string): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + console.log(`[VercelProvider] Executing: ${command}`); + + try { + // Parse command into cmd and args (matching PR syntax) + const parts = command.split(' '); + const cmd = parts[0]; + const args = parts.slice(1); + + // Vercel uses runCommand with cmd and args object (based on PR) + const result = await this.sandbox.runCommand({ + cmd: cmd, + args: args, + cwd: '/app', + env: {} + }); + + return { + stdout: result.stdout || '', + stderr: result.stderr || '', + exitCode: result.exitCode || 0, + success: result.exitCode === 0 + }; + } catch (error: any) { + return { + stdout: '', + stderr: error.message || 'Command failed', + exitCode: 1, + success: false + }; + } + } + + async writeFile(path: string, content: string): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + const fullPath = path.startsWith('/') ? path : `/app/${path}`; + + // Based on PR, Vercel SDK has a writeFiles method that takes an array + try { + await this.sandbox.writeFiles([{ + path: fullPath, + content: Buffer.from(content) + }]); + + console.log(`[VercelProvider] Written: ${fullPath}`); + this.existingFiles.add(path); + } catch (error) { + // Fallback to command-based approach if writeFiles is not available + console.log(`[VercelProvider] writeFiles failed, using command fallback`); + + // Ensure directory exists + const dir = fullPath.substring(0, fullPath.lastIndexOf('/')); + await this.sandbox.runCommand({ + cmd: 'mkdir', + args: ['-p', dir], + cwd: '/' + }); + + // Write file using echo and redirection + const escapedContent = content + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\$/g, '\\$') + .replace(/`/g, '\\`') + .replace(/\n/g, '\\n'); + + await this.sandbox.runCommand({ + cmd: 'sh', + args: ['-c', `echo "${escapedContent}" > ${fullPath}`], + cwd: '/' + }); + + console.log(`[VercelProvider] Written via command: ${fullPath}`); + this.existingFiles.add(path); + } + } + + async readFile(path: string): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + const fullPath = path.startsWith('/') ? path : `/app/${path}`; + + const result = await this.sandbox.runCommand({ + cmd: 'cat', + args: [fullPath], + cwd: '/' + }); + + if (result.exitCode !== 0) { + throw new Error(`Failed to read file: ${result.stderr}`); + } + + return result.stdout || ''; + } + + async listFiles(directory: string = '/app'): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + const result = await this.sandbox.runCommand({ + cmd: 'sh', + args: ['-c', `find ${directory} -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/.next/*" -not -path "*/dist/*" -not -path "*/build/*" | sed "s|^${directory}/||"`], + cwd: '/' + }); + + if (result.exitCode !== 0) { + return []; + } + + return (result.stdout || '').split('\n').filter((line: string) => line.trim() !== ''); + } + + async installPackages(packages: string[]): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + const flags = process.env.NPM_FLAGS || ''; + + console.log(`[VercelProvider] Installing packages: ${packages.join(' ')}`); + + // Build args array + const args = ['install']; + if (flags) { + args.push(...flags.split(' ')); + } + args.push(...packages); + + const result = await this.sandbox.runCommand({ + cmd: 'npm', + args: args, + cwd: '/app' + }); + + // Restart Vite if configured and successful + if (result.exitCode === 0 && process.env.AUTO_RESTART_VITE === 'true') { + await this.restartViteServer(); + } + + return { + stdout: result.stdout || '', + stderr: result.stderr || '', + exitCode: result.exitCode || 0, + success: result.exitCode === 0 + }; + } + + async setupViteApp(): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + console.log('[VercelProvider] Setting up Vite React app...'); + + // Create directory structure + await this.sandbox.runCommand({ + cmd: 'mkdir', + args: ['-p', '/app/src'], + cwd: '/' + }); + + // Create package.json + const packageJson = { + name: "sandbox-app", + version: "1.0.0", + type: "module", + scripts: { + dev: "vite --host", + build: "vite build", + preview: "vite preview" + }, + dependencies: { + react: "^18.2.0", + "react-dom": "^18.2.0" + }, + devDependencies: { + "@vitejs/plugin-react": "^4.0.0", + vite: "^4.3.9", + tailwindcss: "^3.3.0", + postcss: "^8.4.31", + autoprefixer: "^10.4.16" + } + }; + + await this.writeFile('package.json', JSON.stringify(packageJson, null, 2)); + + // Create vite.config.js + const viteConfig = `import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5173, + strictPort: true, + hmr: { + clientPort: 443, + protocol: 'wss' + } + } +})`; + + await this.writeFile('vite.config.js', viteConfig); + + // Create tailwind.config.js + const tailwindConfig = `/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +}`; + + await this.writeFile('tailwind.config.js', tailwindConfig); + + // Create postcss.config.js + const postcssConfig = `export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}`; + + await this.writeFile('postcss.config.js', postcssConfig); + + // Create index.html + const indexHtml = ` + + + + + Sandbox App + + +
+ + +`; + + await this.writeFile('index.html', indexHtml); + + // Create src/main.jsx + const mainJsx = `import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +)`; + + await this.writeFile('src/main.jsx', mainJsx); + + // Create src/App.jsx + const appJsx = `function App() { + return ( +
+
+

+ Vercel Sandbox Ready
+ Start building your React app with Vite and Tailwind CSS! +

+
+
+ ) +} + +export default App`; + + await this.writeFile('src/App.jsx', appJsx); + + // Create src/index.css + const indexCss = `@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background-color: rgb(17 24 39); +}`; + + await this.writeFile('src/index.css', indexCss); + + console.log('[VercelProvider] All files created successfully!'); + + // Install dependencies + console.log('[VercelProvider] Installing dependencies...'); + const installResult = await this.sandbox.runCommand({ + cmd: 'npm', + args: ['install'], + cwd: '/app' + }); + + if (installResult.exitCode === 0) { + console.log('[VercelProvider] Dependencies installed successfully'); + } else { + console.warn('[VercelProvider] npm install had issues:', installResult.stderr); + } + + // Start Vite dev server + console.log('[VercelProvider] Starting Vite dev server...'); + + // Kill any existing Vite processes + await this.sandbox.runCommand({ + cmd: 'sh', + args: ['-c', 'pkill -f vite || true'], + cwd: '/' + }); + + // Start Vite in background + await this.sandbox.runCommand({ + cmd: 'sh', + args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'], + cwd: '/app' + }); + + console.log('[VercelProvider] Vite dev server started'); + + // Wait for Vite to be ready + await new Promise(resolve => setTimeout(resolve, 7000)); + + // Track initial files + this.existingFiles.add('src/App.jsx'); + this.existingFiles.add('src/main.jsx'); + this.existingFiles.add('src/index.css'); + this.existingFiles.add('index.html'); + this.existingFiles.add('package.json'); + this.existingFiles.add('vite.config.js'); + this.existingFiles.add('tailwind.config.js'); + this.existingFiles.add('postcss.config.js'); + } + + async restartViteServer(): Promise { + if (!this.sandbox) { + throw new Error('No active sandbox'); + } + + console.log('[VercelProvider] Restarting Vite server...'); + + // Kill existing Vite process + await this.sandbox.runCommand({ + cmd: 'sh', + args: ['-c', 'pkill -f vite || true'], + cwd: '/' + }); + + // Wait a moment + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Start Vite in background + await this.sandbox.runCommand({ + cmd: 'sh', + args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'], + cwd: '/app' + }); + + console.log('[VercelProvider] Vite restarted'); + + // Wait for Vite to be ready + await new Promise(resolve => setTimeout(resolve, 7000)); + } + + getSandboxUrl(): string | null { + return this.sandboxInfo?.url || null; + } + + async terminate(): Promise { + if (this.sandbox) { + console.log('[VercelProvider] Terminating sandbox...'); + try { + await this.sandbox.stop(); + } catch (e) { + console.error('Failed to terminate sandbox:', e); + } + this.sandbox = null; + this.sandboxInfo = null; + } + } + + isAlive(): boolean { + return !!this.sandbox; + } +} \ No newline at end of file diff --git a/lib/sandbox/types.ts b/lib/sandbox/types.ts new file mode 100644 index 0000000..9ab7a3c --- /dev/null +++ b/lib/sandbox/types.ts @@ -0,0 +1,64 @@ +export interface SandboxFile { + path: string; + content: string; + lastModified?: number; +} + +export interface SandboxInfo { + sandboxId: string; + url: string; + provider: 'e2b' | 'vercel'; + createdAt: Date; +} + +export interface CommandResult { + stdout: string; + stderr: string; + exitCode: number; + success: boolean; +} + +export interface SandboxProviderConfig { + e2b?: { + apiKey: string; + timeoutMs?: number; + template?: string; + }; + vercel?: { + teamId?: string; + projectId?: string; + token?: string; + authMethod?: 'oidc' | 'pat'; + }; +} + +export abstract class SandboxProvider { + protected config: SandboxProviderConfig; + protected sandbox: any; + protected sandboxInfo: SandboxInfo | null = null; + + constructor(config: SandboxProviderConfig) { + this.config = config; + } + + abstract createSandbox(): Promise; + abstract runCommand(command: string): Promise; + abstract writeFile(path: string, content: string): Promise; + abstract readFile(path: string): Promise; + abstract listFiles(directory?: string): Promise; + abstract installPackages(packages: string[]): Promise; + abstract getSandboxUrl(): string | null; + abstract terminate(): Promise; + abstract isAlive(): boolean; + + // Optional methods that providers can override + async setupViteApp(): Promise { + // Default implementation for setting up a Vite React app + throw new Error('setupViteApp not implemented for this provider'); + } + + async restartViteServer(): Promise { + // Default implementation for restarting Vite + throw new Error('restartViteServer not implemented for this provider'); + } +} \ No newline at end of file diff --git a/packages/create-open-lovable/index.js b/packages/create-open-lovable/index.js new file mode 100644 index 0000000..1b5495f --- /dev/null +++ b/packages/create-open-lovable/index.js @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import ora from 'ora'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { installer } from './lib/installer.js'; +import { getPrompts } from './lib/prompts.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const program = new Command(); + +program + .name('create-open-lovable') + .description('Create a new Open Lovable project with your choice of sandbox provider') + .version('1.0.0') + .option('-s, --sandbox ', 'Sandbox provider (e2b or vercel)') + .option('-n, --name ', 'Project name') + .option('-p, --path ', 'Installation path (defaults to current directory)') + .option('--skip-install', 'Skip npm install') + .option('--dry-run', 'Run without making changes') + .parse(process.argv); + +const options = program.opts(); + +async function main() { + console.log(chalk.cyan('\n🚀 Welcome to Open Lovable Setup!\n')); + + let config = { + sandbox: options.sandbox, + name: options.name || 'my-open-lovable', + path: options.path || process.cwd(), + skipInstall: options.skipInstall || false, + dryRun: options.dryRun || false + }; + + // Interactive mode if sandbox not specified + if (!config.sandbox) { + const prompts = getPrompts(config); + const answers = await inquirer.prompt(prompts); + config = { ...config, ...answers }; + } + + // Validate sandbox provider + if (!['e2b', 'vercel'].includes(config.sandbox)) { + console.error(chalk.red(`\n❌ Invalid sandbox provider: ${config.sandbox}`)); + console.log(chalk.yellow('Valid options: e2b, vercel\n')); + process.exit(1); + } + + console.log(chalk.gray('\nConfiguration:')); + console.log(chalk.gray(` Project: ${config.name}`)); + console.log(chalk.gray(` Sandbox: ${config.sandbox}`)); + console.log(chalk.gray(` Path: ${path.resolve(config.path, config.name)}\n`)); + + if (config.dryRun) { + console.log(chalk.yellow('🔍 Dry run mode - no files will be created\n')); + } + + const spinner = ora('Creating project...').start(); + + try { + await installer({ + ...config, + templatesDir: path.join(__dirname, 'templates') + }); + + spinner.succeed('Project created successfully!'); + + console.log(chalk.green('\n✅ Setup complete!\n')); + console.log(chalk.white('Next steps:')); + console.log(chalk.gray(` 1. cd ${config.name}`)); + console.log(chalk.gray(` 2. Copy .env.example to .env and add your API keys`)); + console.log(chalk.gray(` 3. npm run dev`)); + console.log(chalk.gray('\nHappy coding! 🎉\n')); + + } catch (error) { + spinner.fail('Setup failed'); + console.error(chalk.red('\n❌ Error:'), error.message); + if (error.stack && process.env.DEBUG) { + console.error(chalk.gray(error.stack)); + } + process.exit(1); + } +} + +main().catch(error => { + console.error(chalk.red('Unexpected error:'), error); + process.exit(1); +}); \ No newline at end of file diff --git a/packages/create-open-lovable/lib/installer.js b/packages/create-open-lovable/lib/installer.js new file mode 100644 index 0000000..dcb7448 --- /dev/null +++ b/packages/create-open-lovable/lib/installer.js @@ -0,0 +1,261 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { execSync } from 'child_process'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { getEnvPrompts } from './prompts.js'; + +export async function installer(config) { + const { name, sandbox, path: installPath, skipInstall, dryRun, templatesDir } = config; + const projectPath = path.join(installPath, name); + + if (dryRun) { + console.log(chalk.blue('\n📋 Dry run - would perform these actions:')); + console.log(chalk.gray(` - Create directory: ${projectPath}`)); + console.log(chalk.gray(` - Copy base template files`)); + console.log(chalk.gray(` - Copy ${sandbox}-specific files`)); + console.log(chalk.gray(` - Create .env file`)); + if (!skipInstall) { + console.log(chalk.gray(` - Run npm install`)); + } + return; + } + + // Check if directory exists + if (await fs.pathExists(projectPath)) { + const { overwrite } = await inquirer.prompt([{ + type: 'confirm', + name: 'overwrite', + message: `Directory ${name} already exists. Overwrite?`, + default: false + }]); + + if (!overwrite) { + throw new Error('Installation cancelled'); + } + await fs.remove(projectPath); + } + + // Create project directory + await fs.ensureDir(projectPath); + + // Copy base template (shared files) + const baseTemplatePath = path.join(templatesDir, 'base'); + if (await fs.pathExists(baseTemplatePath)) { + await copyTemplate(baseTemplatePath, projectPath); + } else { + // If no base template exists yet, copy from the main project + await copyMainProject(path.dirname(templatesDir), projectPath, sandbox); + } + + // Copy provider-specific template + const providerTemplatePath = path.join(templatesDir, sandbox); + if (await fs.pathExists(providerTemplatePath)) { + await copyTemplate(providerTemplatePath, projectPath); + } + + // Configure environment variables + if (config.configureEnv) { + const envAnswers = await inquirer.prompt(getEnvPrompts(sandbox)); + await createEnvFile(projectPath, sandbox, envAnswers); + } else { + // Create .env.example copy + await createEnvExample(projectPath, sandbox); + } + + // Update package.json with project name + await updatePackageJson(projectPath, name); + + // Update configuration to use the selected sandbox provider + await updateAppConfig(projectPath, sandbox); + + // Install dependencies + if (!skipInstall) { + console.log(chalk.cyan('\n📦 Installing dependencies...')); + execSync('npm install', { + cwd: projectPath, + stdio: 'inherit' + }); + } +} + +async function copyTemplate(src, dest) { + const files = await fs.readdir(src); + + for (const file of files) { + const srcPath = path.join(src, file); + const destPath = path.join(dest, file); + + const stat = await fs.stat(srcPath); + + if (stat.isDirectory()) { + await fs.ensureDir(destPath); + await copyTemplate(srcPath, destPath); + } else { + await fs.copy(srcPath, destPath, { overwrite: true }); + } + } +} + +async function copyMainProject(mainProjectPath, projectPath, sandbox) { + // Copy essential directories and files from the main project + const itemsToCopy = [ + 'app', + 'components', + 'config', + 'lib', + 'types', + 'public', + 'styles', + '.eslintrc.json', + '.gitignore', + 'next.config.js', + 'package.json', + 'tailwind.config.ts', + 'tsconfig.json', + 'postcss.config.mjs' + ]; + + for (const item of itemsToCopy) { + const srcPath = path.join(mainProjectPath, '..', item); + const destPath = path.join(projectPath, item); + + if (await fs.pathExists(srcPath)) { + await fs.copy(srcPath, destPath, { + overwrite: true, + filter: (src) => { + // Skip node_modules and .next + if (src.includes('node_modules') || src.includes('.next')) { + return false; + } + return true; + } + }); + } + } +} + +async function createEnvFile(projectPath, sandbox, answers) { + let envContent = '# Open Lovable Configuration\n\n'; + + // Sandbox provider + envContent += `# Sandbox Provider\n`; + envContent += `SANDBOX_PROVIDER=${sandbox}\n\n`; + + // Required keys + envContent += `# REQUIRED - Web scraping for cloning websites\n`; + envContent += `FIRECRAWL_API_KEY=${answers.firecrawlApiKey || 'your_firecrawl_api_key_here'}\n\n`; + + if (sandbox === 'e2b') { + envContent += `# REQUIRED - E2B Sandboxes\n`; + envContent += `E2B_API_KEY=${answers.e2bApiKey || 'your_e2b_api_key_here'}\n\n`; + } else if (sandbox === 'vercel') { + envContent += `# REQUIRED - Vercel Sandboxes\n`; + if (answers.vercelAuthMethod === 'oidc') { + envContent += `# Using OIDC authentication (automatic in Vercel environment)\n`; + } else { + envContent += `VERCEL_TEAM_ID=${answers.vercelTeamId || 'your_team_id'}\n`; + envContent += `VERCEL_PROJECT_ID=${answers.vercelProjectId || 'your_project_id'}\n`; + envContent += `VERCEL_TOKEN=${answers.vercelToken || 'your_access_token'}\n`; + } + envContent += '\n'; + } + + // Optional AI provider keys + envContent += `# OPTIONAL - AI Providers\n`; + + if (answers.anthropicApiKey) { + envContent += `ANTHROPIC_API_KEY=${answers.anthropicApiKey}\n`; + } else { + envContent += `# ANTHROPIC_API_KEY=your_anthropic_api_key_here\n`; + } + + if (answers.openaiApiKey) { + envContent += `OPENAI_API_KEY=${answers.openaiApiKey}\n`; + } else { + envContent += `# OPENAI_API_KEY=your_openai_api_key_here\n`; + } + + if (answers.geminiApiKey) { + envContent += `GEMINI_API_KEY=${answers.geminiApiKey}\n`; + } else { + envContent += `# GEMINI_API_KEY=your_gemini_api_key_here\n`; + } + + if (answers.groqApiKey) { + envContent += `GROQ_API_KEY=${answers.groqApiKey}\n`; + } else { + envContent += `# GROQ_API_KEY=your_groq_api_key_here\n`; + } + + await fs.writeFile(path.join(projectPath, '.env'), envContent); + await fs.writeFile(path.join(projectPath, '.env.example'), envContent.replace(/=.+/g, '=your_key_here')); +} + +async function createEnvExample(projectPath, sandbox) { + let envContent = '# Open Lovable Configuration\n\n'; + + envContent += `# Sandbox Provider\n`; + envContent += `SANDBOX_PROVIDER=${sandbox}\n\n`; + + envContent += `# REQUIRED - Web scraping for cloning websites\n`; + envContent += `# Get yours at https://firecrawl.dev\n`; + envContent += `FIRECRAWL_API_KEY=your_firecrawl_api_key_here\n\n`; + + if (sandbox === 'e2b') { + envContent += `# REQUIRED - Sandboxes for code execution\n`; + envContent += `# Get yours at https://e2b.dev\n`; + envContent += `E2B_API_KEY=your_e2b_api_key_here\n\n`; + } else if (sandbox === 'vercel') { + envContent += `# REQUIRED - Vercel Sandboxes\n`; + envContent += `# Option 1: OIDC (automatic in Vercel environment)\n`; + envContent += `# Option 2: Personal Access Token\n`; + envContent += `VERCEL_TEAM_ID=your_team_id\n`; + envContent += `VERCEL_PROJECT_ID=your_project_id\n`; + envContent += `VERCEL_TOKEN=your_access_token\n\n`; + } + + envContent += `# OPTIONAL - AI Providers (need at least one)\n`; + envContent += `# Get yours at https://console.anthropic.com\n`; + envContent += `ANTHROPIC_API_KEY=your_anthropic_api_key_here\n\n`; + envContent += `# Get yours at https://platform.openai.com\n`; + envContent += `OPENAI_API_KEY=your_openai_api_key_here\n\n`; + envContent += `# Get yours at https://aistudio.google.com/app/apikey\n`; + envContent += `GEMINI_API_KEY=your_gemini_api_key_here\n\n`; + envContent += `# Get yours at https://console.groq.com\n`; + envContent += `GROQ_API_KEY=your_groq_api_key_here\n`; + + await fs.writeFile(path.join(projectPath, '.env.example'), envContent); +} + +async function updatePackageJson(projectPath, name) { + const packageJsonPath = path.join(projectPath, 'package.json'); + + if (await fs.pathExists(packageJsonPath)) { + const packageJson = await fs.readJson(packageJsonPath); + packageJson.name = name; + await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); + } +} + +async function updateAppConfig(projectPath, sandbox) { + const configPath = path.join(projectPath, 'config', 'app.config.ts'); + + if (await fs.pathExists(configPath)) { + let content = await fs.readFile(configPath, 'utf-8'); + + // Add sandbox provider configuration + const sandboxConfig = ` + // Sandbox Provider Configuration + sandboxProvider: process.env.SANDBOX_PROVIDER || '${sandbox}', +`; + + // Insert after the opening of appConfig + content = content.replace( + 'export const appConfig = {', + `export const appConfig = {${sandboxConfig}` + ); + + await fs.writeFile(configPath, content); + } +} \ No newline at end of file diff --git a/packages/create-open-lovable/lib/prompts.js b/packages/create-open-lovable/lib/prompts.js new file mode 100644 index 0000000..5008049 --- /dev/null +++ b/packages/create-open-lovable/lib/prompts.js @@ -0,0 +1,190 @@ +export function getPrompts(config) { + const prompts = []; + + if (!config.name) { + prompts.push({ + type: 'input', + name: 'name', + message: 'Project name:', + default: 'my-open-lovable', + validate: (input) => { + if (!input || input.trim() === '') { + return 'Project name is required'; + } + if (!/^[a-z0-9-_]+$/i.test(input)) { + return 'Project name can only contain letters, numbers, hyphens, and underscores'; + } + return true; + } + }); + } + + if (!config.sandbox) { + prompts.push({ + type: 'list', + name: 'sandbox', + message: 'Choose your sandbox provider:', + choices: [ + { + name: 'E2B - Full-featured development sandboxes', + value: 'e2b', + short: 'E2B' + }, + { + name: 'Vercel - Lightweight ephemeral VMs', + value: 'vercel', + short: 'Vercel' + } + ], + default: 'e2b' + }); + } + + prompts.push({ + type: 'confirm', + name: 'configureEnv', + message: 'Would you like to configure API keys now?', + default: true + }); + + return prompts; +} + +export function getEnvPrompts(provider) { + const prompts = []; + + // Always include Firecrawl API key + prompts.push({ + type: 'input', + name: 'firecrawlApiKey', + message: 'Firecrawl API key (for web scraping):', + validate: (input) => { + if (!input || input.trim() === '') { + return 'Firecrawl API key is required for web scraping functionality'; + } + return true; + } + }); + + if (provider === 'e2b') { + prompts.push({ + type: 'input', + name: 'e2bApiKey', + message: 'E2B API key:', + validate: (input) => { + if (!input || input.trim() === '') { + return 'E2B API key is required'; + } + return true; + } + }); + } else if (provider === 'vercel') { + prompts.push({ + type: 'list', + name: 'vercelAuthMethod', + message: 'Vercel authentication method:', + choices: [ + { + name: 'OIDC Token (automatic in Vercel environment)', + value: 'oidc', + short: 'OIDC' + }, + { + name: 'Personal Access Token', + value: 'pat', + short: 'PAT' + } + ] + }); + + prompts.push({ + type: 'input', + name: 'vercelTeamId', + message: 'Vercel Team ID:', + when: (answers) => answers.vercelAuthMethod === 'pat', + validate: (input) => { + if (!input || input.trim() === '') { + return 'Team ID is required for PAT authentication'; + } + return true; + } + }); + + prompts.push({ + type: 'input', + name: 'vercelProjectId', + message: 'Vercel Project ID:', + when: (answers) => answers.vercelAuthMethod === 'pat', + validate: (input) => { + if (!input || input.trim() === '') { + return 'Project ID is required for PAT authentication'; + } + return true; + } + }); + + prompts.push({ + type: 'input', + name: 'vercelToken', + message: 'Vercel Access Token:', + when: (answers) => answers.vercelAuthMethod === 'pat', + validate: (input) => { + if (!input || input.trim() === '') { + return 'Access token is required for PAT authentication'; + } + return true; + } + }); + } + + // Optional AI provider keys + prompts.push({ + type: 'confirm', + name: 'addAiKeys', + message: 'Would you like to add AI provider API keys?', + default: true + }); + + prompts.push({ + type: 'checkbox', + name: 'aiProviders', + message: 'Select AI providers to configure:', + when: (answers) => answers.addAiKeys, + choices: [ + { name: 'Anthropic (Claude)', value: 'anthropic' }, + { name: 'OpenAI (GPT)', value: 'openai' }, + { name: 'Google (Gemini)', value: 'gemini' }, + { name: 'Groq', value: 'groq' } + ] + }); + + prompts.push({ + type: 'input', + name: 'anthropicApiKey', + message: 'Anthropic API key:', + when: (answers) => answers.aiProviders && answers.aiProviders.includes('anthropic') + }); + + prompts.push({ + type: 'input', + name: 'openaiApiKey', + message: 'OpenAI API key:', + when: (answers) => answers.aiProviders && answers.aiProviders.includes('openai') + }); + + prompts.push({ + type: 'input', + name: 'geminiApiKey', + message: 'Gemini API key:', + when: (answers) => answers.aiProviders && answers.aiProviders.includes('gemini') + }); + + prompts.push({ + type: 'input', + name: 'groqApiKey', + message: 'Groq API key:', + when: (answers) => answers.aiProviders && answers.aiProviders.includes('groq') + }); + + return prompts; +} \ No newline at end of file diff --git a/packages/create-open-lovable/package.json b/packages/create-open-lovable/package.json new file mode 100644 index 0000000..cbcc8dc --- /dev/null +++ b/packages/create-open-lovable/package.json @@ -0,0 +1,36 @@ +{ + "name": "create-open-lovable", + "version": "1.0.0", + "description": "CLI to bootstrap Open Lovable with your choice of sandbox provider", + "type": "module", + "main": "index.js", + "bin": { + "create-open-lovable": "./index.js" + }, + "scripts": { + "test": "node index.js --dry-run" + }, + "keywords": [ + "lovable", + "sandbox", + "e2b", + "vercel", + "ai", + "code-generation" + ], + "author": "", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^11.1.0", + "fs-extra": "^11.2.0", + "inquirer": "^9.2.12", + "ora": "^7.0.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/create-open-lovable/templates/e2b/.env.example b/packages/create-open-lovable/templates/e2b/.env.example new file mode 100644 index 0000000..9d0b9a5 --- /dev/null +++ b/packages/create-open-lovable/templates/e2b/.env.example @@ -0,0 +1,25 @@ +# Open Lovable Configuration - E2B Provider + +# Sandbox Provider +SANDBOX_PROVIDER=e2b + +# REQUIRED - Sandboxes for code execution +# Get yours at https://e2b.dev +E2B_API_KEY=your_e2b_api_key_here + +# REQUIRED - Web scraping for cloning websites +# Get yours at https://firecrawl.dev +FIRECRAWL_API_KEY=your_firecrawl_api_key_here + +# OPTIONAL - AI Providers (need at least one) +# Get yours at https://console.anthropic.com +ANTHROPIC_API_KEY=your_anthropic_api_key_here + +# Get yours at https://platform.openai.com +OPENAI_API_KEY=your_openai_api_key_here + +# Get yours at https://aistudio.google.com/app/apikey +GEMINI_API_KEY=your_gemini_api_key_here + +# Get yours at https://console.groq.com +GROQ_API_KEY=your_groq_api_key_here \ No newline at end of file diff --git a/packages/create-open-lovable/templates/e2b/README.md b/packages/create-open-lovable/templates/e2b/README.md new file mode 100644 index 0000000..05f0757 --- /dev/null +++ b/packages/create-open-lovable/templates/e2b/README.md @@ -0,0 +1,38 @@ +# Open Lovable - E2B Sandbox + +This project is configured to use E2B sandboxes for code execution. + +## Setup + +1. Get your E2B API key from [https://e2b.dev](https://e2b.dev) +2. Get your Firecrawl API key from [https://firecrawl.dev](https://firecrawl.dev) +3. Copy `.env.example` to `.env` and add your API keys +4. Run `npm install` to install dependencies +5. Run `npm run dev` to start the development server + +## E2B Features + +- Full-featured development sandboxes +- 15-minute default timeout (configurable) +- Persistent file system during session +- Support for complex package installations +- Built-in Python runtime for code execution + +## Configuration + +You can adjust E2B settings in `config/app.config.ts`: + +- `timeoutMinutes`: Sandbox session timeout (default: 15) +- `vitePort`: Development server port (default: 5173) +- `viteStartupDelay`: Time to wait for Vite to start (default: 7000ms) + +## Troubleshooting + +If you encounter issues: + +1. Verify your E2B API key is valid +2. Check the console for detailed error messages +3. Ensure you have a stable internet connection +4. Try refreshing the page and creating a new sandbox + +For more help, visit the [E2B documentation](https://docs.e2b.dev). \ No newline at end of file diff --git a/packages/create-open-lovable/templates/vercel/.env.example b/packages/create-open-lovable/templates/vercel/.env.example new file mode 100644 index 0000000..021f5af --- /dev/null +++ b/packages/create-open-lovable/templates/vercel/.env.example @@ -0,0 +1,28 @@ +# Open Lovable Configuration - Vercel Provider + +# Sandbox Provider +SANDBOX_PROVIDER=vercel + +# REQUIRED - Vercel Sandboxes +# Option 1: OIDC Token (automatic in Vercel environment) +# Option 2: Personal Access Token (configure below) +VERCEL_TEAM_ID=your_team_id +VERCEL_PROJECT_ID=your_project_id +VERCEL_TOKEN=your_access_token + +# REQUIRED - Web scraping for cloning websites +# Get yours at https://firecrawl.dev +FIRECRAWL_API_KEY=your_firecrawl_api_key_here + +# OPTIONAL - AI Providers (need at least one) +# Get yours at https://console.anthropic.com +ANTHROPIC_API_KEY=your_anthropic_api_key_here + +# Get yours at https://platform.openai.com +OPENAI_API_KEY=your_openai_api_key_here + +# Get yours at https://aistudio.google.com/app/apikey +GEMINI_API_KEY=your_gemini_api_key_here + +# Get yours at https://console.groq.com +GROQ_API_KEY=your_groq_api_key_here \ No newline at end of file diff --git a/packages/create-open-lovable/templates/vercel/README.md b/packages/create-open-lovable/templates/vercel/README.md new file mode 100644 index 0000000..5c2bb75 --- /dev/null +++ b/packages/create-open-lovable/templates/vercel/README.md @@ -0,0 +1,52 @@ +# Open Lovable - Vercel Sandbox + +This project is configured to use Vercel Sandboxes for code execution. + +## Setup + +1. Configure Vercel authentication (see below) +2. Get your Firecrawl API key from [https://firecrawl.dev](https://firecrawl.dev) +3. Copy `.env.example` to `.env` and add your credentials +4. Run `npm install` to install dependencies +5. Run `npm run dev` to start the development server + +## Vercel Authentication + +### Option 1: OIDC Token (Recommended for Vercel deployments) +When running in a Vercel environment, authentication happens automatically via OIDC tokens. No configuration needed! + +### Option 2: Personal Access Token (For local development) +1. Create a Personal Access Token in your [Vercel account settings](https://vercel.com/account/tokens) +2. Get your Team ID from your [team settings](https://vercel.com/teams) +3. Create a project and get the Project ID +4. Add these to your `.env` file: + - `VERCEL_TOKEN` + - `VERCEL_TEAM_ID` + - `VERCEL_PROJECT_ID` + +## Vercel Sandbox Features + +- Lightweight ephemeral Linux VMs +- Powered by Firecracker MicroVMs +- 5-minute default timeout (max 45 minutes) +- 8 vCPUs maximum +- Root access for package installation +- Node 22 runtime included + +## Configuration + +You can adjust Vercel settings in `config/app.config.ts`: + +- `maxDuration`: Sandbox session timeout (default: 5 minutes) +- Authentication method (OIDC or PAT) + +## Troubleshooting + +If you encounter issues: + +1. Verify your authentication credentials +2. Check if you're using the correct authentication method +3. Ensure your Vercel account has sandbox access +4. Check the console for detailed error messages + +For more help, visit the [Vercel Sandbox documentation](https://vercel.com/docs/vercel-sandbox). \ No newline at end of file From defd90a0acffffab7fb9e9a235726eb89828942e Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:44:03 -0400 Subject: [PATCH 02/14] instruct towards against creating bespoke svgs --- app/api/generate-ai-code-stream/route.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index 9a7fa4a..8d9e597 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -572,6 +572,11 @@ ${conversationContext} - Simple style/text change = 1 file ONLY - New component = 2 files MAX (component + parent) - If >3 files, YOU'RE DOING TOO MUCH +6. **DO NOT CREATE SVGs FROM SCRATCH**: + - NEVER generate custom SVG code unless explicitly asked + - Use existing icon libraries (lucide-react, heroicons, etc.) + - Or use placeholder elements/text if icons are not critical + - Only create custom SVGs when user specifically requests "create an SVG" or "draw an SVG" COMPONENT RELATIONSHIPS (CHECK THESE FIRST): - Navigation usually lives INSIDE Header.jsx, not separate Nav.jsx From b96d048dbd88a761cefd6dfd022a44f79014c786 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:21:28 -0400 Subject: [PATCH 03/14] Save current v2 sandbox implementation before styling refactor - Modified sandbox API routes for v2 implementation - Updated sandbox providers (E2B and Vercel) - Added styling-reference directory with Firecrawl AI-ready website - Preparing for styling system port from Firecrawl design Co-Authored-By: Claude --- .env.example | 30 ++++- app/api/analyze-edit-intent/route.ts | 22 ++- app/api/apply-ai-code-stream/route.ts | 163 +++++++++++------------ app/api/create-ai-sandbox/route.ts | 75 +++++++++-- app/api/generate-ai-code-stream/route.ts | 17 ++- app/api/install-packages/route.ts | 55 +++----- app/api/kill-sandbox/route.ts | 12 +- app/api/restart-vite/route.ts | 35 +++++ app/api/sandbox-status/route.ts | 13 +- app/page.tsx | 18 ++- config/app.config.ts | 32 ++++- lib/sandbox/providers/e2b-provider.ts | 34 +++-- lib/sandbox/providers/vercel-provider.ts | 4 + lib/sandbox/types.ts | 1 + styling-reference/ai-ready-website | 1 + 15 files changed, 340 insertions(+), 172 deletions(-) create mode 160000 styling-reference/ai-ready-website diff --git a/.env.example b/.env.example index e32df34..0c6a80e 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,38 @@ # Required FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) -# Vercel Sandbox Authentication (choose one method) -# See: https://vercel.com/docs/vercel-sandbox#authentication +# ================================================================================= +# SANDBOX PROVIDER - Choose Option 1 OR 2 +# ================================================================================= -# Method 1: OIDC Token (recommended for development) +# Option 1: E2B Sandbox (recommended for development) +# Set SANDBOX_PROVIDER=e2b and configure E2B_API_KEY below +SANDBOX_PROVIDER=e2b +E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev + +# Option 2: Vercel Sandbox +# Set SANDBOX_PROVIDER=vercel and choose authentication method below +# SANDBOX_PROVIDER=vercel + +# Vercel Authentication - Choose method a OR b +# Method a: OIDC Token (recommended for development) # Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically # VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull -# Method 2: Personal Access Token (for production or when OIDC unavailable) +# Method b: Personal Access Token (for production or when OIDC unavailable) # VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID # VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID # VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard -# Optional (need at least one AI provider) +# ================================================================================= +# AI PROVIDERS - Need at least one +# ================================================================================= + +# Vercel AI Gateway (recommended - provides access to multiple models) +AI_GATEWAY_API_KEY=your_ai_gateway_api_key # Get from https://vercel.com/dashboard/ai-gateway/api-keys + +# Individual provider keys (used when AI_GATEWAY_API_KEY is not set) ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey -GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended) \ No newline at end of file +GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended) diff --git a/app/api/analyze-edit-intent/route.ts b/app/api/analyze-edit-intent/route.ts index 7cf35bc..f99cdad 100644 --- a/app/api/analyze-edit-intent/route.ts +++ b/app/api/analyze-edit-intent/route.ts @@ -7,18 +7,28 @@ import { generateObject } from 'ai'; import { z } from 'zod'; import type { FileManifest } from '@/types/file-manifest'; +// Check if we're using Vercel AI Gateway +const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY; +const aiGatewayBaseURL = 'https://ai-gateway.vercel.sh/v1'; + const groq = createGroq({ - apiKey: process.env.GROQ_API_KEY, + apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GROQ_API_KEY, + baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined, }); const anthropic = createAnthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, - baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1', + apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.ANTHROPIC_API_KEY, + baseURL: isUsingAIGateway ? aiGatewayBaseURL : (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1'), }); const openai = createOpenAI({ - apiKey: process.env.OPENAI_API_KEY, - baseURL: process.env.OPENAI_BASE_URL, + apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.OPENAI_API_KEY, + baseURL: isUsingAIGateway ? aiGatewayBaseURL : process.env.OPENAI_BASE_URL, +}); + +const googleGenerativeAI = createGoogleGenerativeAI({ + apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GEMINI_API_KEY, + baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined, }); // Schema for the AI's search plan - not file selection! @@ -104,7 +114,7 @@ export async function POST(request: NextRequest) { aiModel = openai(model.replace('openai/', '')); } } else if (model.startsWith('google/')) { - aiModel = createGoogleGenerativeAI(model.replace('google/', '')); + aiModel = googleGenerativeAI(model.replace('google/', '')); } else { // Default to groq if model format is unclear aiModel = groq(model); diff --git a/app/api/apply-ai-code-stream/route.ts b/app/api/apply-ai-code-stream/route.ts index ac382d6..442d99c 100644 --- a/app/api/apply-ai-code-stream/route.ts +++ b/app/api/apply-ai-code-stream/route.ts @@ -1,11 +1,11 @@ import { NextRequest, NextResponse } from 'next/server'; -import { Sandbox } from '@vercel/sandbox'; +import { Sandbox } from '@e2b/code-interpreter'; import type { SandboxState } from '@/types/sandbox'; import type { ConversationState } from '@/types/conversation'; declare global { var conversationState: ConversationState | null; - var activeSandbox: any; + var activeSandboxProvider: any; var existingFiles: Set; var sandboxState: SandboxState; } @@ -294,75 +294,88 @@ export async function POST(request: NextRequest) { global.existingFiles = new Set(); } - // First, always check the global state for active sandbox - let sandbox = global.activeSandbox; + // First, always check the global state for active provider + let provider = global.activeSandboxProvider; + + // If we don't have a provider in this instance but we have a sandboxId, + // try to use the existing sandbox data or create a new one + if (!provider && sandboxId) { + console.log(`[apply-ai-code-stream] Provider not in this instance for sandbox ${sandboxId}, checking existing data...`); + + // If we have sandbox data but no provider, we'll create a new provider + // E2B doesn't support reconnection like Vercel does + if (global.sandboxData && global.sandboxData.sandboxId === sandboxId) { + console.log(`[apply-ai-code-stream] Creating new provider for existing sandbox ${sandboxId}`); + + // Create a new provider instance (this will create a new sandbox since E2B doesn't support reconnection) + try { + const { SandboxFactory } = await import('@/lib/sandbox/factory'); + provider = SandboxFactory.create(); + await provider.createSandbox(); + + // Update the global state + global.activeSandboxProvider = provider; + console.log(`[apply-ai-code-stream] Created new provider for sandbox ${sandboxId}`); + } catch (providerError) { + console.error(`[apply-ai-code-stream] Failed to create provider for sandbox ${sandboxId}:`, providerError); + return NextResponse.json({ + success: false, + error: `Failed to create sandbox provider for ${sandboxId}. The sandbox may have expired.`, + results: { + filesCreated: [], + packagesInstalled: [], + commandsExecuted: [], + errors: [`Sandbox provider creation failed: ${(providerError as Error).message}`] + }, + explanation: parsed.explanation, + structure: parsed.structure, + parsedFiles: parsed.files, + message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox reconnection failed.` + }, { status: 500 }); + } + } + } - // If we don't have a sandbox in this instance but we have a sandboxId, - // reconnect to the existing sandbox - if (!sandbox && sandboxId) { - console.log(`[apply-ai-code-stream] Sandbox ${sandboxId} not in this instance, attempting reconnect...`); - + // If we still don't have a provider, create a new one + if (!provider) { + console.log(`[apply-ai-code-stream] No active provider found, creating new sandbox...`); try { - // Reconnect to the existing sandbox using E2B's connect method - sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY }); - console.log(`[apply-ai-code-stream] Successfully reconnected to sandbox ${sandboxId}`); - - // Store the reconnected sandbox globally for this instance - global.activeSandbox = sandbox; - - // Update sandbox data if needed - if (!global.sandboxData) { - const host = (sandbox as any).getHost(5173); + const { SandboxFactory } = await import('@/lib/sandbox/factory'); + provider = SandboxFactory.create(); + await provider.createSandbox(); + + // Store the provider globally + global.activeSandboxProvider = provider; + + // Update sandbox data + const sandboxInfo = provider.getSandboxInfo(); + if (sandboxInfo) { global.sandboxData = { - sandboxId, - url: `https://${host}` + sandboxId: sandboxInfo.sandboxId, + url: sandboxInfo.url }; } - - // Initialize existingFiles if not already - if (!global.existingFiles) { - global.existingFiles = new Set(); - } - } catch (reconnectError) { - console.error(`[apply-ai-code-stream] Failed to reconnect to sandbox ${sandboxId}:`, reconnectError); - - // If reconnection fails, we'll still try to return a meaningful response + + console.log(`[apply-ai-code-stream] Created new sandbox successfully`); + } catch (createError) { + console.error(`[apply-ai-code-stream] Failed to create new sandbox:`, createError); return NextResponse.json({ success: false, - error: `Failed to reconnect to sandbox ${sandboxId}. The sandbox may have expired or been terminated.`, + error: `Failed to create new sandbox: ${createError instanceof Error ? createError.message : 'Unknown error'}`, results: { filesCreated: [], packagesInstalled: [], commandsExecuted: [], - errors: [`Sandbox reconnection failed: ${(reconnectError as Error).message}`] + errors: [`Sandbox creation failed: ${createError instanceof Error ? createError.message : 'Unknown error'}`] }, explanation: parsed.explanation, structure: parsed.structure, parsedFiles: parsed.files, - message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox reconnection failed.` - }); + message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox creation failed.` + }, { status: 500 }); } } - // If no sandbox at all and no sandboxId provided, return an error - if (!sandbox && !sandboxId) { - console.log('[apply-ai-code-stream] No sandbox available and no sandboxId provided'); - return NextResponse.json({ - success: false, - error: 'No active sandbox found. Please create a sandbox first.', - results: { - filesCreated: [], - packagesInstalled: [], - commandsExecuted: [], - errors: ['No sandbox available'] - }, - explanation: parsed.explanation, - structure: parsed.structure, - parsedFiles: parsed.files, - message: `Parsed ${parsed.files.length} files but no sandbox available to apply them.` - }); - } - // Create a response stream for real-time updates const encoder = new TextEncoder(); const stream = new TransformStream(); @@ -374,8 +387,8 @@ export async function POST(request: NextRequest) { await writer.write(encoder.encode(message)); }; - // Start processing in background (pass sandbox and request to the async function) - (async (sandboxInstance, req) => { + // Start processing in background (pass provider and request to the async function) + (async (providerInstance, req) => { const results = { filesCreated: [] as string[], filesUpdated: [] as string[], @@ -432,7 +445,7 @@ export async function POST(request: NextRequest) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ packages: uniquePackages, - sandboxId: sandboxId || (sandboxInstance as any).sandboxId + sandboxId: sandboxId || providerInstance.getSandboxInfo()?.sandboxId }) }); @@ -536,17 +549,11 @@ export async function POST(request: NextRequest) { // Create directory if needed const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : ''; if (dirPath) { - await sandboxInstance.runCommand({ - cmd: 'mkdir', - args: ['-p', dirPath] - }); + await providerInstance.runCommand(`mkdir -p ${dirPath}`); } - - // Write the file using Vercel Sandbox writeFiles - await sandboxInstance.writeFiles([{ - path: normalizedPath, - content: Buffer.from(fileContent) - }]); + + // Write the file using provider + await providerInstance.writeFile(normalizedPath, fileContent); // Update file cache if (global.sandboxState?.fileCache) { @@ -599,20 +606,12 @@ export async function POST(request: NextRequest) { action: 'executing' }); - // Parse command and arguments for Vercel Sandbox - const commandParts = cmd.trim().split(/\s+/); - const cmdName = commandParts[0]; - const args = commandParts.slice(1); - - // Use Vercel Sandbox runCommand - const result = await sandboxInstance.runCommand({ - cmd: cmdName, - args - }); - - // Get command output - const stdout = await result.stdout(); - const stderr = await result.stderr(); + // Use provider runCommand + const result = await providerInstance.runCommand(cmd); + + // Get command output from provider result + const stdout = result.stdout; + const stderr = result.stderr; if (stdout) { await sendProgress({ @@ -697,7 +696,7 @@ export async function POST(request: NextRequest) { } finally { await writer.close(); } - })(sandbox, request); + })(provider, request); // Return the stream return new Response(stream.readable, { @@ -707,7 +706,7 @@ export async function POST(request: NextRequest) { 'Connection': 'keep-alive', }, }); - + } catch (error) { console.error('Apply AI code stream error:', error); return NextResponse.json( diff --git a/app/api/create-ai-sandbox/route.ts b/app/api/create-ai-sandbox/route.ts index 6fc9ebe..b9126a7 100644 --- a/app/api/create-ai-sandbox/route.ts +++ b/app/api/create-ai-sandbox/route.ts @@ -9,9 +9,59 @@ declare global { var sandboxData: any; var existingFiles: Set; var sandboxState: SandboxState; + var sandboxCreationInProgress: boolean; + var sandboxCreationPromise: Promise | null; } export async function POST() { + // Check if sandbox creation is already in progress + if (global.sandboxCreationInProgress && global.sandboxCreationPromise) { + console.log('[create-ai-sandbox] Sandbox creation already in progress, waiting for existing creation...'); + try { + const existingResult = await global.sandboxCreationPromise; + console.log('[create-ai-sandbox] Returning existing sandbox creation result'); + return NextResponse.json(existingResult); + } catch (error) { + console.error('[create-ai-sandbox] Existing sandbox creation failed:', error); + // Continue with new creation if the existing one failed + } + } + + // Check if we already have an active sandbox + if (global.activeSandbox && global.sandboxData) { + console.log('[create-ai-sandbox] Returning existing active sandbox'); + return NextResponse.json({ + success: true, + sandboxId: global.sandboxData.sandboxId, + url: global.sandboxData.url + }); + } + + // Set the creation flag + global.sandboxCreationInProgress = true; + + // Create the promise that other requests can await + global.sandboxCreationPromise = createSandboxInternal(); + + try { + const result = await global.sandboxCreationPromise; + return NextResponse.json(result); + } catch (error) { + console.error('[create-ai-sandbox] Sandbox creation failed:', error); + return NextResponse.json( + { + error: error instanceof Error ? error.message : 'Failed to create sandbox', + details: error instanceof Error ? error.stack : undefined + }, + { status: 500 } + ); + } finally { + global.sandboxCreationInProgress = false; + global.sandboxCreationPromise = null; + } +} + +async function createSandboxInternal() { let sandbox: any = null; try { @@ -26,6 +76,7 @@ export async function POST() { console.error('Failed to stop existing sandbox:', e); } global.activeSandbox = null; + global.sandboxData = null; } // Clear existing files tracking @@ -297,12 +348,20 @@ body { console.log('[create-ai-sandbox] Sandbox ready at:', sandboxUrl); - return NextResponse.json({ + const result = { success: true, sandboxId, url: sandboxUrl, message: 'Vercel sandbox created and Vite React app initialized' - }); + }; + + // Store the result for reuse + global.sandboxData = { + ...global.sandboxData, + ...result + }; + + return result; } catch (error) { console.error('[create-ai-sandbox] Error:', error); @@ -316,12 +375,10 @@ body { } } - return NextResponse.json( - { - error: error instanceof Error ? error.message : 'Failed to create sandbox', - details: error instanceof Error ? error.stack : undefined - }, - { status: 500 } - ); + // Clear global state on error + global.activeSandbox = null; + global.sandboxData = null; + + throw error; // Throw to be caught by the outer handler } } \ No newline at end of file diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index 8d9e597..af86837 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -14,21 +14,28 @@ import { appConfig } from '@/config/app.config'; // Force dynamic route to enable streaming export const dynamic = 'force-dynamic'; +// Check if we're using Vercel AI Gateway +const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY; +const aiGatewayBaseURL = 'https://ai-gateway.vercel.sh/v1'; + const groq = createGroq({ - apiKey: process.env.GROQ_API_KEY, + apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GROQ_API_KEY, + baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined, }); const anthropic = createAnthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, - baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1', + apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.ANTHROPIC_API_KEY, + baseURL: isUsingAIGateway ? aiGatewayBaseURL : (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1'), }); const googleGenerativeAI = createGoogleGenerativeAI({ - apiKey: process.env.GEMINI_API_KEY, + apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GEMINI_API_KEY, + baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined, }); const openai = createOpenAI({ - apiKey: process.env.OPENAI_API_KEY, + apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.OPENAI_API_KEY, + baseURL: isUsingAIGateway ? aiGatewayBaseURL : process.env.OPENAI_BASE_URL, }); // Helper function to analyze user preferences from conversation history diff --git a/app/api/install-packages/route.ts b/app/api/install-packages/route.ts index dd8eb82..08b95a1 100644 --- a/app/api/install-packages/route.ts +++ b/app/api/install-packages/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; declare global { var activeSandbox: any; + var activeSandboxProvider: any; var sandboxData: any; } @@ -35,13 +36,13 @@ export async function POST(request: NextRequest) { console.log(`[install-packages] Cleaned:`, validPackages); } - // Get active sandbox - const sandbox = global.activeSandbox; + // Get active sandbox provider + const provider = global.activeSandboxProvider; - if (!sandbox) { + if (!provider) { return NextResponse.json({ success: false, - error: 'No active sandbox available' + error: 'No active sandbox provider available' }, { status: 400 }); } @@ -59,7 +60,7 @@ export async function POST(request: NextRequest) { }; // Start installation in background - (async (sandboxInstance) => { + (async (providerInstance) => { try { await sendProgress({ type: 'start', @@ -72,10 +73,7 @@ export async function POST(request: NextRequest) { try { // Try to kill any running dev server processes - await sandboxInstance.runCommand({ - cmd: 'pkill', - args: ['-f', 'vite'] - }); + await providerInstance.runCommand('pkill -f vite'); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit } catch (error) { // It's OK if no process is found @@ -92,12 +90,13 @@ export async function POST(request: NextRequest) { try { // Read package.json to check existing dependencies - const catResult = await sandboxInstance.runCommand({ - cmd: 'cat', - args: ['package.json'] - }); - if (catResult.exitCode === 0) { - const packageJsonContent = await catResult.stdout(); + let packageJsonContent = ''; + try { + packageJsonContent = await providerInstance.readFile('package.json'); + } catch (error) { + console.log('[install-packages] Error reading package.json:', error); + } + if (packageJsonContent) { const packageJson = JSON.parse(packageJsonContent); const dependencies = packageJson.dependencies || {}; @@ -144,11 +143,7 @@ export async function POST(request: NextRequest) { // Restart dev server await sendProgress({ type: 'status', message: 'Restarting development server...' }); - const devServerProcess = await sandboxInstance.runCommand({ - cmd: 'npm', - args: ['run', 'dev'], - detached: true - }); + await providerInstance.restartViteServer(); await sendProgress({ type: 'complete', @@ -165,16 +160,12 @@ export async function POST(request: NextRequest) { message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}` }); - // Run npm install - const installArgs = ['install', '--legacy-peer-deps', ...packagesToInstall]; - const installResult = await sandboxInstance.runCommand({ - cmd: 'npm', - args: installArgs - }); + // Install packages using provider method + const installResult = await providerInstance.installPackages(packagesToInstall); // Get install output - const stdout = await installResult.stdout(); - const stderr = await installResult.stderr(); + const stdout = installResult.stdout; + const stderr = installResult.stderr; if (stdout) { const lines = stdout.split('\n').filter(line => line.trim()); @@ -218,11 +209,7 @@ export async function POST(request: NextRequest) { await sendProgress({ type: 'status', message: 'Restarting development server...' }); try { - const devServerProcess = await sandboxInstance.runCommand({ - cmd: 'npm', - args: ['run', 'dev'], - detached: true - }); + await providerInstance.restartViteServer(); // Wait a bit for the server to start await new Promise(resolve => setTimeout(resolve, 3000)); @@ -250,7 +237,7 @@ export async function POST(request: NextRequest) { } finally { await writer.close(); } - })(sandbox); + })(provider); // Return the stream return new Response(stream.readable, { diff --git a/app/api/kill-sandbox/route.ts b/app/api/kill-sandbox/route.ts index accaf7e..f23550b 100644 --- a/app/api/kill-sandbox/route.ts +++ b/app/api/kill-sandbox/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; declare global { - var activeSandbox: any; + var activeSandboxProvider: any; var sandboxData: any; var existingFiles: Set; } @@ -9,19 +9,19 @@ declare global { export async function POST() { try { console.log('[kill-sandbox] Stopping active sandbox...'); - + let sandboxKilled = false; - + // Stop existing sandbox if any - if (global.activeSandbox) { + if (global.activeSandboxProvider) { try { - await global.activeSandbox.stop(); + await global.activeSandboxProvider.terminate(); sandboxKilled = true; console.log('[kill-sandbox] Sandbox stopped successfully'); } catch (e) { console.error('[kill-sandbox] Failed to stop sandbox:', e); } - global.activeSandbox = null; + global.activeSandboxProvider = null; global.sandboxData = null; } diff --git a/app/api/restart-vite/route.ts b/app/api/restart-vite/route.ts index 64bf973..0fb3f30 100644 --- a/app/api/restart-vite/route.ts +++ b/app/api/restart-vite/route.ts @@ -2,8 +2,12 @@ import { NextResponse } from 'next/server'; declare global { var activeSandbox: any; + var lastViteRestartTime: number; + var viteRestartInProgress: boolean; } +const RESTART_COOLDOWN_MS = 5000; // 5 second cooldown between restarts + export async function POST() { try { if (!global.activeSandbox) { @@ -13,6 +17,29 @@ export async function POST() { }, { status: 400 }); } + // Check if restart is already in progress + if (global.viteRestartInProgress) { + console.log('[restart-vite] Vite restart already in progress, skipping...'); + return NextResponse.json({ + success: true, + message: 'Vite restart already in progress' + }); + } + + // Check cooldown + const now = Date.now(); + if (global.lastViteRestartTime && (now - global.lastViteRestartTime) < RESTART_COOLDOWN_MS) { + const remainingTime = Math.ceil((RESTART_COOLDOWN_MS - (now - global.lastViteRestartTime)) / 1000); + console.log(`[restart-vite] Cooldown active, ${remainingTime}s remaining`); + return NextResponse.json({ + success: true, + message: `Vite was recently restarted, cooldown active (${remainingTime}s remaining)` + }); + } + + // Set the restart flag + global.viteRestartInProgress = true; + console.log('[restart-vite] Forcing Vite restart...'); // Kill existing Vite processes @@ -51,6 +78,10 @@ export async function POST() { // Wait for Vite to start up await new Promise(resolve => setTimeout(resolve, 3000)); + // Update global state + global.lastViteRestartTime = Date.now(); + global.viteRestartInProgress = false; + return NextResponse.json({ success: true, message: 'Vite restarted successfully' @@ -58,6 +89,10 @@ export async function POST() { } catch (error) { console.error('[restart-vite] Error:', error); + + // Clear the restart flag on error + global.viteRestartInProgress = false; + return NextResponse.json({ success: false, error: (error as Error).message diff --git a/app/api/sandbox-status/route.ts b/app/api/sandbox-status/route.ts index 7f5e0b5..928c072 100644 --- a/app/api/sandbox-status/route.ts +++ b/app/api/sandbox-status/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; declare global { - var activeSandbox: any; + var activeSandboxProvider: any; var sandboxData: any; var existingFiles: Set; } @@ -9,15 +9,14 @@ declare global { export async function GET() { try { // Check if sandbox exists - const sandboxExists = !!global.activeSandbox; - + const sandboxExists = !!global.activeSandboxProvider; + let sandboxHealthy = false; let sandboxInfo = null; - - if (sandboxExists && global.activeSandbox) { + + if (sandboxExists && global.activeSandboxProvider) { try { - // Since Python isn't available in the Vite template, just check if sandbox exists - // The sandbox object existing is enough to confirm it's healthy + // Check if sandbox is healthy by calling a method that should work sandboxHealthy = true; sandboxInfo = { sandboxId: global.sandboxData?.sandboxId, diff --git a/app/page.tsx b/app/page.tsx index dfe0d89..37da5ca 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -137,8 +137,11 @@ export default function AISandboxPage() { // Clear old conversation data on component mount and create/restore sandbox useEffect(() => { let isMounted = true; + let sandboxCreated = false; // Track if sandbox was created in this effect const initializePage = async () => { + // Prevent double execution in React StrictMode + if (sandboxCreated) return; // Clear old conversation try { await fetch('/api/conversation-state', { @@ -165,9 +168,11 @@ export default function AISandboxPage() { console.log('[home] Attempting to restore sandbox:', sandboxIdParam); // For now, just create a new sandbox - you could enhance this to actually restore // the specific sandbox if your backend supports it + sandboxCreated = true; await createSandbox(true); } else { console.log('[home] No sandbox in URL, creating new sandbox automatically...'); + sandboxCreated = true; await createSandbox(true); } } catch (error) { @@ -369,7 +374,16 @@ export default function AISandboxPage() { } }; + const sandboxCreationRef = useRef(false); + const createSandbox = async (fromHomeScreen = false) => { + // Prevent duplicate sandbox creation + if (sandboxCreationRef.current) { + console.log('[createSandbox] Sandbox creation already in progress, skipping...'); + return; + } + + sandboxCreationRef.current = true; console.log('[createSandbox] Starting sandbox creation...'); setLoading(true); setShowLoadingBackground(true); @@ -378,7 +392,7 @@ export default function AISandboxPage() { setScreenshotError(null); try { - const response = await fetch('/api/create-ai-sandbox', { + const response = await fetch('/api/create-ai-sandbox-v2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) @@ -388,6 +402,7 @@ export default function AISandboxPage() { console.log('[createSandbox] Response data:', data); if (data.success) { + sandboxCreationRef.current = false; // Reset the ref on success setSandboxData(data); updateStatus('Sandbox active', true); log('Sandbox created successfully!'); @@ -454,6 +469,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik addChatMessage(`Failed to create sandbox: ${error.message}`, 'system'); } finally { setLoading(false); + sandboxCreationRef.current = false; // Reset the ref } }; diff --git a/config/app.config.ts b/config/app.config.ts index 1a85a3a..153f7a3 100644 --- a/config/app.config.ts +++ b/config/app.config.ts @@ -6,27 +6,47 @@ export const appConfig = { vercelSandbox: { // Sandbox timeout in minutes timeoutMinutes: 15, - + // Convert to milliseconds for Vercel Sandbox API get timeoutMs() { return this.timeoutMinutes * 60 * 1000; }, - + // Development server port (Vercel Sandbox typically uses 3000 for Next.js/React) devPort: 3000, - + // Time to wait for dev server to be ready (in milliseconds) devServerStartupDelay: 7000, - + // Time to wait for CSS rebuild (in milliseconds) cssRebuildDelay: 2000, - + // Working directory in sandbox workingDirectory: '/app', - + // Default runtime for sandbox runtime: 'node22' // Available: node22, python3.13, v0-next-shadcn, cua-ubuntu-xfce }, + + // E2B Sandbox Configuration + e2b: { + // Sandbox timeout in minutes + timeoutMinutes: 30, + + // Convert to milliseconds for E2B API + get timeoutMs() { + return this.timeoutMinutes * 60 * 1000; + }, + + // Development server port (E2B uses 5173 for Vite) + vitePort: 5173, + + // Time to wait for Vite dev server to be ready (in milliseconds) + viteStartupDelay: 10000, + + // Working directory in sandbox + workingDirectory: '/home/user/app', + }, // AI Model Configuration ai: { diff --git a/lib/sandbox/providers/e2b-provider.ts b/lib/sandbox/providers/e2b-provider.ts index 91fe458..a2c2ea3 100644 --- a/lib/sandbox/providers/e2b-provider.ts +++ b/lib/sandbox/providers/e2b-provider.ts @@ -100,18 +100,28 @@ export class E2BProvider extends SandboxProvider { const fullPath = path.startsWith('/') ? path : `/home/user/app/${path}`; - await this.sandbox.runCode(` - import os + // Use the E2B filesystem API to write the file + // Note: E2B SDK uses files.write() method + if ((this.sandbox as any).files && typeof (this.sandbox as any).files.write === 'function') { + // Use the files.write API if available + await (this.sandbox as any).files.write(fullPath, Buffer.from(content)); + console.log(`[E2BProvider] Written file using files.write: ${fullPath}`); + } else { + // Fallback to Python code execution + await this.sandbox.runCode(` + import os - # Ensure directory exists - dir_path = os.path.dirname("${fullPath}") - os.makedirs(dir_path, exist_ok=True) + # Ensure directory exists + dir_path = os.path.dirname("${fullPath}") + os.makedirs(dir_path, exist_ok=True) - # Write file - with open("${fullPath}", 'w') as f: - f.write(${JSON.stringify(content)}) - print(f"✓ Written: ${fullPath}") - `); + # Write file + with open("${fullPath}", 'w') as f: + f.write(${JSON.stringify(content)}) + print(f"✓ Written: ${fullPath}") + `); + console.log(`[E2BProvider] Written file using Python: ${fullPath}`); + } this.existingFiles.add(path); } @@ -475,6 +485,10 @@ print(f'✓ Vite restarted with PID: {process.pid}') return this.sandboxInfo?.url || null; } + getSandboxInfo(): SandboxInfo | null { + return this.sandboxInfo; + } + async terminate(): Promise { if (this.sandbox) { console.log('[E2BProvider] Terminating sandbox...'); diff --git a/lib/sandbox/providers/vercel-provider.ts b/lib/sandbox/providers/vercel-provider.ts index ed09dea..0af4e61 100644 --- a/lib/sandbox/providers/vercel-provider.ts +++ b/lib/sandbox/providers/vercel-provider.ts @@ -450,6 +450,10 @@ body { return this.sandboxInfo?.url || null; } + getSandboxInfo(): SandboxInfo | null { + return this.sandboxInfo; + } + async terminate(): Promise { if (this.sandbox) { console.log('[VercelProvider] Terminating sandbox...'); diff --git a/lib/sandbox/types.ts b/lib/sandbox/types.ts index 9ab7a3c..3e97a6a 100644 --- a/lib/sandbox/types.ts +++ b/lib/sandbox/types.ts @@ -48,6 +48,7 @@ export abstract class SandboxProvider { abstract listFiles(directory?: string): Promise; abstract installPackages(packages: string[]): Promise; abstract getSandboxUrl(): string | null; + abstract getSandboxInfo(): SandboxInfo | null; abstract terminate(): Promise; abstract isAlive(): boolean; diff --git a/styling-reference/ai-ready-website b/styling-reference/ai-ready-website new file mode 160000 index 0000000..6944f04 --- /dev/null +++ b/styling-reference/ai-ready-website @@ -0,0 +1 @@ +Subproject commit 6944f04ada7ac893e72e5b2af6f702ef88e4eadc From 836b085f75cd1ed2d1425d0f264629a91ef2d37e Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:06:17 -0400 Subject: [PATCH 04/14] continue re-design --- app/api/conversation-state/route.ts | 22 +- app/api/create-ai-sandbox-v2/route.ts | 2 +- app/api/generate-ai-code-stream/route.ts | 58 +- app/api/scrape-screenshot/route.ts | 60 +- app/api/scrape-website/route.ts | 106 + app/builder/page.tsx | 285 ++ app/fonts/GeistMonoVF.woff | Bin 0 -> 58092 bytes app/fonts/GeistVF.woff | Bin 0 -> 58512 bytes app/generation/page.tsx | 3606 ++++++++++++++++ app/globals.css | 206 +- app/landing.tsx | 90 + app/layout.tsx | 29 +- app/page.backup.tsx | 236 ++ app/page.new.tsx | 3659 +++++++++++++++++ app/page.tsx | 3616 +--------------- colors.json | 182 + components/FirecrawlIcon.tsx | 15 + components/FirecrawlLogo.tsx | 48 + components/HeroInput.tsx | 110 + .../sections/ai-readiness/ControlPanel.tsx | 842 ++++ .../sections/ai-readiness/InlineResults.tsx | 341 ++ .../sections/ai-readiness/MetricBars.tsx | 189 + .../sections/ai-readiness/RadarChart.tsx | 220 + .../sections/ai-readiness/ScoreChart.tsx | 104 + .../EndpointsCrawl/EndpointsCrawl.tsx | 188 + .../EndpointsExtract/EndpointsExtract.tsx | 187 + .../endpoints/EndpointsMap/EndpointsMap.tsx | 11 + .../EndpointsScrape/EndpointsScrape.tsx | 181 + .../EndpointsSearch/EndpointsSearch.tsx | 142 + .../sections/endpoints/Extract/Extract.tsx | 134 + .../app/(home)/sections/endpoints/Mcp/Mcp.tsx | 183 + .../(home)/sections/hero-flame/HeroFlame.tsx | 64 + .../app/(home)/sections/hero-flame/data.json | 50 + .../sections/hero-input/Button/Button.tsx | 34 + .../(home)/sections/hero-input/HeroInput.tsx | 84 + .../hero-input/Tabs/Mobile/Mobile.tsx | 196 + .../(home)/sections/hero-input/Tabs/Tabs.tsx | 183 + .../sections/hero-input/_svg/ArrowRight.tsx | 19 + .../(home)/sections/hero-input/_svg/Globe.tsx | 19 + .../sections/hero-scraping/Code/Code.tsx | 159 + .../hero-scraping/Code/Loading/Loading.tsx | 63 + .../hero-scraping/Code/Loading/_svg/Check.tsx | 18 + .../sections/hero-scraping/HeroScraping.css | 21 + .../sections/hero-scraping/HeroScraping.tsx | 314 ++ .../(home)/sections/hero-scraping/Tag/Tag.tsx | 69 + .../hero-scraping/_svg/BrowserMobile.tsx | 137 + .../hero-scraping/_svg/BrowserTab.tsx | 19 + .../sections/hero/Background/Background.tsx | 184 + .../hero/Background/BackgroundOuterPiece.tsx | 56 + .../hero/Background/_svg/CenterStar.tsx | 21 + .../app/(home)/sections/hero/Badge/Badge.tsx | 43 + components/app/(home)/sections/hero/Hero.tsx | 62 + .../app/(home)/sections/hero/Pixi/Pixi.tsx | 46 + .../sections/hero/Pixi/tickers/ascii.ts | 141 + .../hero/Pixi/tickers/features/cell.ts | 65 + .../hero/Pixi/tickers/features/cellReveal.ts | 51 + .../features/components/AnimatedRect.ts | 104 + .../features/components/BlinkingContainer.ts | 83 + .../Pixi/tickers/features/components/Dot.ts | 19 + .../hero/Pixi/tickers/features/crawl.ts | 200 + .../hero/Pixi/tickers/features/index.ts | 124 + .../hero/Pixi/tickers/features/mapping.ts | 207 + .../hero/Pixi/tickers/features/scrape.ts | 219 + .../hero/Pixi/tickers/features/search.ts | 144 + .../app/(home)/sections/hero/Title/Title.tsx | 263 ++ .../app/.cursor/rules/home-page-components.md | 54 + components/app/generation/SidebarInput.tsx | 163 + .../app/generation/SidebarQuickInput.tsx | 47 + components/shared/Playground/Context/types.ts | 48 + components/shared/animated-dot-icon.tsx | 394 ++ components/shared/animated-height.tsx | 58 + components/shared/ascii-background.tsx | 60 + components/shared/ascii-flame-background.tsx | 59 + components/shared/button/Button.css | 72 + components/shared/button/Button.tsx | 67 + components/shared/buttons/capsule-button.tsx | 148 + .../shared/buttons/fire-action-link.tsx | 48 + components/shared/buttons/index.ts | 4 + components/shared/buttons/slate-button.tsx | 122 + .../shared/color-styles/color-styles.tsx | 31 + components/shared/combobox/combobox.tsx | 174 + .../effects/.cursor/rules/flame-effects.md | 73 + components/shared/effects/flame/Flame.tsx | 57 + .../shared/effects/flame/ascii-explosion.tsx | 56 + .../effects/flame/auth-pulse/auth-pulse.tsx | 64 + .../effects/flame/auth-pulse/pulse-data.json | 17 + .../shared/effects/flame/core-flame.json | 22 + .../shared/effects/flame/core-flame.tsx | 58 + .../shared/effects/flame/explosion-data.json | 27 + .../shared/effects/flame/flame-background.tsx | 41 + .../shared/effects/flame/hero-flame-data.json | 50 + .../shared/effects/flame/hero-flame.tsx | 68 + components/shared/effects/flame/index.ts | 7 + .../effects/flame/slate-grid/grid-data.json | 17 + .../effects/flame/slate-grid/slate-grid.tsx | 65 + .../shared/effects/flame/subtle-explosion.tsx | 68 + .../effects/flame/subtle-wave/subtle-wave.tsx | 45 + .../effects/flame/subtle-wave/wave-data.json | 34 + components/shared/effects/index.ts | 3 + .../shared/effects/subtle-ascii-animation.tsx | 71 + .../firecrawl-icon/firecrawl-icon-static.tsx | 23 + .../shared/firecrawl-icon/firecrawl-icon.tsx | 56 + .../shared/header/BrandKit/BrandKit.tsx | 216 + .../shared/header/BrandKit/_svg/Download.tsx | 20 + .../header/BrandKit/_svg/Guidelines.tsx | 20 + .../shared/header/BrandKit/_svg/Icon.tsx | 17 + .../header/Dropdown/Content/Content.tsx | 219 + .../header/Dropdown/Content/NavItemRow.tsx | 91 + .../header/Dropdown/Github/Flame/Flame.tsx | 63 + .../header/Dropdown/Github/Flame/data.json | 25 + .../shared/header/Dropdown/Github/Github.tsx | 27 + .../header/Dropdown/Mobile/Item/Item.tsx | 84 + .../shared/header/Dropdown/Mobile/Mobile.tsx | 49 + .../header/Dropdown/Stories/Flame/Flame.tsx | 62 + .../header/Dropdown/Stories/Stories.tsx | 31 + .../header/Dropdown/Stories/_svg/ArrowUp.tsx | 19 + .../header/Dropdown/Stories/_svg/Replit.tsx | 24 + .../header/Dropdown/Wrapper/Wrapper.tsx | 106 + .../shared/header/Github/GithubClient.tsx | 19 + .../shared/header/Github/_svg/GithubIcon.tsx | 19 + components/shared/header/HeaderContext.tsx | 137 + components/shared/header/Nav/Item/Item.tsx | 53 + .../header/Nav/Item/_svg/ChevronDown.tsx | 20 + components/shared/header/Nav/Nav.tsx | 319 ++ .../shared/header/Nav/RenderEndpointIcon.tsx | 16 + .../shared/header/Nav/_svg/Affiliate.tsx | 35 + components/shared/header/Nav/_svg/Api.tsx | 26 + .../shared/header/Nav/_svg/ArrowRight.tsx | 20 + components/shared/header/Nav/_svg/Careers.tsx | 19 + .../shared/header/Nav/_svg/Changelog.tsx | 19 + components/shared/header/Nav/_svg/Chats.tsx | 28 + components/shared/header/Nav/_svg/Lead.tsx | 18 + components/shared/header/Nav/_svg/MCP.tsx | 16 + .../shared/header/Nav/_svg/Platforms.tsx | 37 + .../shared/header/Nav/_svg/Research.tsx | 47 + components/shared/header/Nav/_svg/Student.tsx | 19 + .../shared/header/Nav/_svg/Templates.tsx | 26 + components/shared/header/Toggle/Toggle.tsx | 65 + components/shared/header/Wrapper/Wrapper.tsx | 40 + components/shared/header/_svg/Logo.tsx | 48 + components/shared/hero-flame.tsx | 122 + components/shared/icons/GitHub.tsx | 21 + components/shared/icons/Logo.tsx | 22 + components/shared/icons/animated-chevron.tsx | 64 + components/shared/icons/animated-icons.tsx | 962 +++++ components/shared/icons/arrow-animated.tsx | 28 + components/shared/icons/check.tsx | 18 + components/shared/icons/chevron-slide.tsx | 52 + components/shared/icons/copied.tsx | 19 + components/shared/icons/copy.tsx | 19 + components/shared/icons/curve.tsx | 24 + components/shared/icons/fingerprint-icon.tsx | 222 + components/shared/icons/openai.tsx | 18 + components/shared/icons/source-icon.tsx | 20 + components/shared/icons/symbol-colored.tsx | 22 + components/shared/icons/symbol-white.tsx | 21 + .../shared/icons/tremor-placeholder.tsx | 62 + components/shared/icons/wordmark-colored.tsx | 57 + components/shared/icons/wordmark-white.tsx | 54 + components/shared/image/Image.tsx | 56 + components/shared/image/getImageSrc.ts | 35 + components/shared/layout/animated-height.tsx | 57 + components/shared/layout/animated-width.tsx | 56 + .../shared/layout/curvy-rect-divider.tsx | 28 + components/shared/layout/curvy-rect.tsx | 185 + components/shared/loading/Shimmer.tsx | 120 + components/shared/loading/usage-loading.tsx | 18 + components/shared/lockBody.tsx | 25 + components/shared/logo-cloud/index.ts | 1 + components/shared/logo-cloud/logo-cloud.tsx | 56 + .../logo-cloud/logo-cloud2/Logocloud.css | 22 + .../logo-cloud/logo-cloud2/Logocloud.tsx | 55 + .../notifications/slack-notification.tsx | 53 + components/shared/pixi/Pixi.tsx | 222 + components/shared/pixi/PixiAssetManager.ts | 27 + components/shared/pixi/utils.ts | 60 + .../shared/portal-to-body/PortalToBody.tsx | 8 + .../shared/preview/json-error-highlighter.tsx | 200 + .../shared/preview/live-preview-frame.tsx | 342 ++ .../shared/preview/multiple-web-browsers.tsx | 76 + components/shared/preview/web-browser.tsx | 84 + components/shared/pylon.tsx | 102 + .../search-params-provider.tsx | 33 + .../shared/section-head/SectionHead.css | 4 + .../shared/section-head/SectionHead.tsx | 100 + .../shared/section-title/SectionTitle.tsx | 55 + components/shared/tabs/Tabs.tsx | 149 + components/shared/ui/app-dialog.tsx | 67 + components/shared/ui/ascii-dot-loader.tsx | 29 + components/shared/ui/dot-grid-loader.tsx | 65 + components/shared/ui/empty-state.tsx | 57 + components/shared/ui/index.ts | 5 + components/shared/ui/loading-state.tsx | 63 + components/shared/ui/mobile-sheet.tsx | 257 ++ components/shared/ui/stat-card.tsx | 98 + components/shared/utils/portal-to-body.tsx | 8 + components/ui/shadcn/accordion.tsx | 58 + components/ui/shadcn/alert-dialog.tsx | 167 + components/ui/shadcn/badge.tsx | 28 + components/ui/shadcn/button.css | 80 + components/ui/shadcn/button.tsx | 139 + components/ui/shadcn/card.tsx | 83 + components/ui/shadcn/checkbox.tsx | 57 + components/ui/shadcn/collapsible.tsx | 11 + components/ui/shadcn/combobox.tsx | 174 + components/ui/shadcn/context-menu.tsx | 200 + components/ui/shadcn/data-table.tsx | 117 + components/ui/shadcn/dialog.tsx | 128 + components/ui/shadcn/dropdown-menu.tsx | 200 + components/ui/shadcn/form.tsx | 179 + components/ui/shadcn/input.tsx | 29 + components/ui/shadcn/label.tsx | 26 + components/ui/shadcn/navigation-menu.tsx | 135 + components/ui/shadcn/popover.tsx | 31 + components/ui/shadcn/progress.tsx | 28 + components/ui/shadcn/scroll-area.tsx | 48 + components/ui/shadcn/select.tsx | 159 + components/ui/shadcn/separator.tsx | 31 + components/ui/shadcn/sheet.tsx | 143 + components/ui/shadcn/slider.tsx | 28 + components/ui/shadcn/switch.tsx | 58 + components/ui/shadcn/tabs.tsx | 55 + components/ui/shadcn/textarea.tsx | 21 + components/ui/shadcn/toast.tsx | 32 + components/ui/shadcn/toggle.tsx | 78 + components/ui/shadcn/tooltip-radix.tsx | 30 + components/ui/shadcn/tooltip.tsx | 120 + config/app.config.ts | 14 +- env.sample | 21 - hooks/useDebouncedCallback.ts | 34 + hooks/useDebouncedEffect.ts | 68 + hooks/useSwitchingCode.ts | 60 + lib/sandbox/providers/vercel-provider.ts | 45 +- lib/utils.ts | 8 +- package-lock.json | 2475 +++++++++-- package.json | 27 +- pnpm-lock.yaml | 770 ++-- postcss.config.mjs | 10 +- public/firecrawl-logo-with-fire.webp | Bin 2024 -> 0 bytes public/next.svg | 1 - public/vercel.svg | 1 - styles/additional-styles/custom-fonts.css | 0 styles/additional-styles/theme.css | 58 + styles/additional-styles/utility-patterns.css | 112 + styles/chrome-bug.css | 12 + styles/colors.json | 182 + .../.cursor/rules/component-styles.md | 32 + styles/components/button.css | 72 + styles/components/code.css | 45 + styles/components/index.css | 10 + .../.cursor/rules/design-system.md | 81 + styles/design-system/animations.css | 92 + styles/design-system/base/body.css | 132 + styles/design-system/base/layout.css | 212 + styles/design-system/base/reset.css | 132 + styles/design-system/colors.css | 135 + styles/design-system/fonts.css | 8 + styles/design-system/typography.css | 243 ++ styles/design-system/utilities.css | 235 ++ styles/fire.css | 103 + styles/inside-border-fix.css | 47 + styles/main.css | 229 ++ styling-reference/ai-ready-website | 1 - tailwind.config.ts | 360 +- test/package-lock.json | 701 ---- test/package.json | 15 - utils/cn.ts | 5 + utils/init-canvas.ts | 30 + utils/set-timeout-on-visible.ts | 130 + utils/sleep.ts | 2 + 270 files changed, 32269 insertions(+), 5182 deletions(-) create mode 100644 app/api/scrape-website/route.ts create mode 100644 app/builder/page.tsx create mode 100644 app/fonts/GeistMonoVF.woff create mode 100644 app/fonts/GeistVF.woff create mode 100644 app/generation/page.tsx create mode 100644 app/landing.tsx create mode 100644 app/page.backup.tsx create mode 100644 app/page.new.tsx create mode 100644 colors.json create mode 100644 components/FirecrawlIcon.tsx create mode 100644 components/FirecrawlLogo.tsx create mode 100644 components/HeroInput.tsx create mode 100644 components/app/(home)/sections/ai-readiness/ControlPanel.tsx create mode 100644 components/app/(home)/sections/ai-readiness/InlineResults.tsx create mode 100644 components/app/(home)/sections/ai-readiness/MetricBars.tsx create mode 100644 components/app/(home)/sections/ai-readiness/RadarChart.tsx create mode 100644 components/app/(home)/sections/ai-readiness/ScoreChart.tsx create mode 100644 components/app/(home)/sections/endpoints/EndpointsCrawl/EndpointsCrawl.tsx create mode 100644 components/app/(home)/sections/endpoints/EndpointsExtract/EndpointsExtract.tsx create mode 100644 components/app/(home)/sections/endpoints/EndpointsMap/EndpointsMap.tsx create mode 100644 components/app/(home)/sections/endpoints/EndpointsScrape/EndpointsScrape.tsx create mode 100644 components/app/(home)/sections/endpoints/EndpointsSearch/EndpointsSearch.tsx create mode 100644 components/app/(home)/sections/endpoints/Extract/Extract.tsx create mode 100644 components/app/(home)/sections/endpoints/Mcp/Mcp.tsx create mode 100644 components/app/(home)/sections/hero-flame/HeroFlame.tsx create mode 100644 components/app/(home)/sections/hero-flame/data.json create mode 100644 components/app/(home)/sections/hero-input/Button/Button.tsx create mode 100644 components/app/(home)/sections/hero-input/HeroInput.tsx create mode 100644 components/app/(home)/sections/hero-input/Tabs/Mobile/Mobile.tsx create mode 100644 components/app/(home)/sections/hero-input/Tabs/Tabs.tsx create mode 100644 components/app/(home)/sections/hero-input/_svg/ArrowRight.tsx create mode 100644 components/app/(home)/sections/hero-input/_svg/Globe.tsx create mode 100644 components/app/(home)/sections/hero-scraping/Code/Code.tsx create mode 100644 components/app/(home)/sections/hero-scraping/Code/Loading/Loading.tsx create mode 100644 components/app/(home)/sections/hero-scraping/Code/Loading/_svg/Check.tsx create mode 100644 components/app/(home)/sections/hero-scraping/HeroScraping.css create mode 100644 components/app/(home)/sections/hero-scraping/HeroScraping.tsx create mode 100644 components/app/(home)/sections/hero-scraping/Tag/Tag.tsx create mode 100644 components/app/(home)/sections/hero-scraping/_svg/BrowserMobile.tsx create mode 100644 components/app/(home)/sections/hero-scraping/_svg/BrowserTab.tsx create mode 100644 components/app/(home)/sections/hero/Background/Background.tsx create mode 100644 components/app/(home)/sections/hero/Background/BackgroundOuterPiece.tsx create mode 100644 components/app/(home)/sections/hero/Background/_svg/CenterStar.tsx create mode 100644 components/app/(home)/sections/hero/Badge/Badge.tsx create mode 100644 components/app/(home)/sections/hero/Hero.tsx create mode 100644 components/app/(home)/sections/hero/Pixi/Pixi.tsx create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/ascii.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/cell.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/cellReveal.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/components/AnimatedRect.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/components/BlinkingContainer.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/components/Dot.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/crawl.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/index.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/mapping.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/scrape.ts create mode 100644 components/app/(home)/sections/hero/Pixi/tickers/features/search.ts create mode 100644 components/app/(home)/sections/hero/Title/Title.tsx create mode 100644 components/app/.cursor/rules/home-page-components.md create mode 100644 components/app/generation/SidebarInput.tsx create mode 100644 components/app/generation/SidebarQuickInput.tsx create mode 100644 components/shared/Playground/Context/types.ts create mode 100644 components/shared/animated-dot-icon.tsx create mode 100644 components/shared/animated-height.tsx create mode 100644 components/shared/ascii-background.tsx create mode 100644 components/shared/ascii-flame-background.tsx create mode 100644 components/shared/button/Button.css create mode 100644 components/shared/button/Button.tsx create mode 100644 components/shared/buttons/capsule-button.tsx create mode 100644 components/shared/buttons/fire-action-link.tsx create mode 100644 components/shared/buttons/index.ts create mode 100644 components/shared/buttons/slate-button.tsx create mode 100644 components/shared/color-styles/color-styles.tsx create mode 100644 components/shared/combobox/combobox.tsx create mode 100644 components/shared/effects/.cursor/rules/flame-effects.md create mode 100644 components/shared/effects/flame/Flame.tsx create mode 100644 components/shared/effects/flame/ascii-explosion.tsx create mode 100644 components/shared/effects/flame/auth-pulse/auth-pulse.tsx create mode 100644 components/shared/effects/flame/auth-pulse/pulse-data.json create mode 100644 components/shared/effects/flame/core-flame.json create mode 100644 components/shared/effects/flame/core-flame.tsx create mode 100644 components/shared/effects/flame/explosion-data.json create mode 100644 components/shared/effects/flame/flame-background.tsx create mode 100644 components/shared/effects/flame/hero-flame-data.json create mode 100644 components/shared/effects/flame/hero-flame.tsx create mode 100644 components/shared/effects/flame/index.ts create mode 100644 components/shared/effects/flame/slate-grid/grid-data.json create mode 100644 components/shared/effects/flame/slate-grid/slate-grid.tsx create mode 100644 components/shared/effects/flame/subtle-explosion.tsx create mode 100644 components/shared/effects/flame/subtle-wave/subtle-wave.tsx create mode 100644 components/shared/effects/flame/subtle-wave/wave-data.json create mode 100644 components/shared/effects/index.ts create mode 100644 components/shared/effects/subtle-ascii-animation.tsx create mode 100644 components/shared/firecrawl-icon/firecrawl-icon-static.tsx create mode 100644 components/shared/firecrawl-icon/firecrawl-icon.tsx create mode 100644 components/shared/header/BrandKit/BrandKit.tsx create mode 100644 components/shared/header/BrandKit/_svg/Download.tsx create mode 100644 components/shared/header/BrandKit/_svg/Guidelines.tsx create mode 100644 components/shared/header/BrandKit/_svg/Icon.tsx create mode 100644 components/shared/header/Dropdown/Content/Content.tsx create mode 100644 components/shared/header/Dropdown/Content/NavItemRow.tsx create mode 100644 components/shared/header/Dropdown/Github/Flame/Flame.tsx create mode 100644 components/shared/header/Dropdown/Github/Flame/data.json create mode 100644 components/shared/header/Dropdown/Github/Github.tsx create mode 100644 components/shared/header/Dropdown/Mobile/Item/Item.tsx create mode 100644 components/shared/header/Dropdown/Mobile/Mobile.tsx create mode 100644 components/shared/header/Dropdown/Stories/Flame/Flame.tsx create mode 100644 components/shared/header/Dropdown/Stories/Stories.tsx create mode 100644 components/shared/header/Dropdown/Stories/_svg/ArrowUp.tsx create mode 100644 components/shared/header/Dropdown/Stories/_svg/Replit.tsx create mode 100644 components/shared/header/Dropdown/Wrapper/Wrapper.tsx create mode 100644 components/shared/header/Github/GithubClient.tsx create mode 100644 components/shared/header/Github/_svg/GithubIcon.tsx create mode 100644 components/shared/header/HeaderContext.tsx create mode 100644 components/shared/header/Nav/Item/Item.tsx create mode 100644 components/shared/header/Nav/Item/_svg/ChevronDown.tsx create mode 100644 components/shared/header/Nav/Nav.tsx create mode 100644 components/shared/header/Nav/RenderEndpointIcon.tsx create mode 100644 components/shared/header/Nav/_svg/Affiliate.tsx create mode 100644 components/shared/header/Nav/_svg/Api.tsx create mode 100644 components/shared/header/Nav/_svg/ArrowRight.tsx create mode 100644 components/shared/header/Nav/_svg/Careers.tsx create mode 100644 components/shared/header/Nav/_svg/Changelog.tsx create mode 100644 components/shared/header/Nav/_svg/Chats.tsx create mode 100644 components/shared/header/Nav/_svg/Lead.tsx create mode 100644 components/shared/header/Nav/_svg/MCP.tsx create mode 100644 components/shared/header/Nav/_svg/Platforms.tsx create mode 100644 components/shared/header/Nav/_svg/Research.tsx create mode 100644 components/shared/header/Nav/_svg/Student.tsx create mode 100644 components/shared/header/Nav/_svg/Templates.tsx create mode 100644 components/shared/header/Toggle/Toggle.tsx create mode 100644 components/shared/header/Wrapper/Wrapper.tsx create mode 100644 components/shared/header/_svg/Logo.tsx create mode 100644 components/shared/hero-flame.tsx create mode 100644 components/shared/icons/GitHub.tsx create mode 100644 components/shared/icons/Logo.tsx create mode 100644 components/shared/icons/animated-chevron.tsx create mode 100644 components/shared/icons/animated-icons.tsx create mode 100644 components/shared/icons/arrow-animated.tsx create mode 100644 components/shared/icons/check.tsx create mode 100644 components/shared/icons/chevron-slide.tsx create mode 100644 components/shared/icons/copied.tsx create mode 100644 components/shared/icons/copy.tsx create mode 100644 components/shared/icons/curve.tsx create mode 100644 components/shared/icons/fingerprint-icon.tsx create mode 100644 components/shared/icons/openai.tsx create mode 100644 components/shared/icons/source-icon.tsx create mode 100644 components/shared/icons/symbol-colored.tsx create mode 100644 components/shared/icons/symbol-white.tsx create mode 100644 components/shared/icons/tremor-placeholder.tsx create mode 100644 components/shared/icons/wordmark-colored.tsx create mode 100644 components/shared/icons/wordmark-white.tsx create mode 100644 components/shared/image/Image.tsx create mode 100644 components/shared/image/getImageSrc.ts create mode 100644 components/shared/layout/animated-height.tsx create mode 100644 components/shared/layout/animated-width.tsx create mode 100644 components/shared/layout/curvy-rect-divider.tsx create mode 100644 components/shared/layout/curvy-rect.tsx create mode 100644 components/shared/loading/Shimmer.tsx create mode 100644 components/shared/loading/usage-loading.tsx create mode 100644 components/shared/lockBody.tsx create mode 100644 components/shared/logo-cloud/index.ts create mode 100644 components/shared/logo-cloud/logo-cloud.tsx create mode 100644 components/shared/logo-cloud/logo-cloud2/Logocloud.css create mode 100644 components/shared/logo-cloud/logo-cloud2/Logocloud.tsx create mode 100644 components/shared/notifications/slack-notification.tsx create mode 100644 components/shared/pixi/Pixi.tsx create mode 100644 components/shared/pixi/PixiAssetManager.ts create mode 100644 components/shared/pixi/utils.ts create mode 100644 components/shared/portal-to-body/PortalToBody.tsx create mode 100644 components/shared/preview/json-error-highlighter.tsx create mode 100644 components/shared/preview/live-preview-frame.tsx create mode 100644 components/shared/preview/multiple-web-browsers.tsx create mode 100644 components/shared/preview/web-browser.tsx create mode 100644 components/shared/pylon.tsx create mode 100644 components/shared/search-params-provider/search-params-provider.tsx create mode 100644 components/shared/section-head/SectionHead.css create mode 100644 components/shared/section-head/SectionHead.tsx create mode 100644 components/shared/section-title/SectionTitle.tsx create mode 100644 components/shared/tabs/Tabs.tsx create mode 100644 components/shared/ui/app-dialog.tsx create mode 100644 components/shared/ui/ascii-dot-loader.tsx create mode 100644 components/shared/ui/dot-grid-loader.tsx create mode 100644 components/shared/ui/empty-state.tsx create mode 100644 components/shared/ui/index.ts create mode 100644 components/shared/ui/loading-state.tsx create mode 100644 components/shared/ui/mobile-sheet.tsx create mode 100644 components/shared/ui/stat-card.tsx create mode 100644 components/shared/utils/portal-to-body.tsx create mode 100644 components/ui/shadcn/accordion.tsx create mode 100644 components/ui/shadcn/alert-dialog.tsx create mode 100644 components/ui/shadcn/badge.tsx create mode 100644 components/ui/shadcn/button.css create mode 100644 components/ui/shadcn/button.tsx create mode 100644 components/ui/shadcn/card.tsx create mode 100644 components/ui/shadcn/checkbox.tsx create mode 100644 components/ui/shadcn/collapsible.tsx create mode 100644 components/ui/shadcn/combobox.tsx create mode 100644 components/ui/shadcn/context-menu.tsx create mode 100644 components/ui/shadcn/data-table.tsx create mode 100644 components/ui/shadcn/dialog.tsx create mode 100644 components/ui/shadcn/dropdown-menu.tsx create mode 100644 components/ui/shadcn/form.tsx create mode 100644 components/ui/shadcn/input.tsx create mode 100644 components/ui/shadcn/label.tsx create mode 100644 components/ui/shadcn/navigation-menu.tsx create mode 100644 components/ui/shadcn/popover.tsx create mode 100644 components/ui/shadcn/progress.tsx create mode 100644 components/ui/shadcn/scroll-area.tsx create mode 100644 components/ui/shadcn/select.tsx create mode 100644 components/ui/shadcn/separator.tsx create mode 100644 components/ui/shadcn/sheet.tsx create mode 100644 components/ui/shadcn/slider.tsx create mode 100644 components/ui/shadcn/switch.tsx create mode 100644 components/ui/shadcn/tabs.tsx create mode 100644 components/ui/shadcn/textarea.tsx create mode 100644 components/ui/shadcn/toast.tsx create mode 100644 components/ui/shadcn/toggle.tsx create mode 100644 components/ui/shadcn/tooltip-radix.tsx create mode 100644 components/ui/shadcn/tooltip.tsx delete mode 100644 env.sample create mode 100644 hooks/useDebouncedCallback.ts create mode 100644 hooks/useDebouncedEffect.ts create mode 100644 hooks/useSwitchingCode.ts delete mode 100644 public/firecrawl-logo-with-fire.webp delete mode 100644 public/next.svg delete mode 100644 public/vercel.svg create mode 100644 styles/additional-styles/custom-fonts.css create mode 100644 styles/additional-styles/theme.css create mode 100644 styles/additional-styles/utility-patterns.css create mode 100644 styles/chrome-bug.css create mode 100644 styles/colors.json create mode 100644 styles/components/.cursor/rules/component-styles.md create mode 100644 styles/components/button.css create mode 100644 styles/components/code.css create mode 100644 styles/components/index.css create mode 100644 styles/design-system/.cursor/rules/design-system.md create mode 100644 styles/design-system/animations.css create mode 100644 styles/design-system/base/body.css create mode 100644 styles/design-system/base/layout.css create mode 100644 styles/design-system/base/reset.css create mode 100644 styles/design-system/colors.css create mode 100644 styles/design-system/fonts.css create mode 100644 styles/design-system/typography.css create mode 100644 styles/design-system/utilities.css create mode 100644 styles/fire.css create mode 100644 styles/inside-border-fix.css create mode 100644 styles/main.css delete mode 160000 styling-reference/ai-ready-website delete mode 100644 test/package-lock.json delete mode 100644 test/package.json create mode 100644 utils/cn.ts create mode 100644 utils/init-canvas.ts create mode 100644 utils/set-timeout-on-visible.ts create mode 100644 utils/sleep.ts diff --git a/app/api/conversation-state/route.ts b/app/api/conversation-state/route.ts index 1a37468..969692c 100644 --- a/app/api/conversation-state/route.ts +++ b/app/api/conversation-state/route.ts @@ -59,10 +59,26 @@ export async function POST(request: NextRequest) { case 'clear-old': // Clear old conversation data but keep recent context if (!global.conversationState) { + // Initialize conversation state if it doesn't exist + global.conversationState = { + conversationId: `conv-${Date.now()}`, + startedAt: Date.now(), + lastUpdated: Date.now(), + context: { + messages: [], + edits: [], + projectEvolution: { majorChanges: [] }, + userPreferences: {} + } + }; + + console.log('[conversation-state] Initialized new conversation state for clear-old'); + return NextResponse.json({ - success: false, - error: 'No active conversation to clear' - }, { status: 400 }); + success: true, + message: 'New conversation state initialized', + state: global.conversationState + }); } // Keep only recent data diff --git a/app/api/create-ai-sandbox-v2/route.ts b/app/api/create-ai-sandbox-v2/route.ts index 93410a1..6545fe8 100644 --- a/app/api/create-ai-sandbox-v2/route.ts +++ b/app/api/create-ai-sandbox-v2/route.ts @@ -5,7 +5,7 @@ import type { SandboxState } from '@/types/sandbox'; // Store active sandbox globally declare global { - var activeSandboxProvider: SandboxProvider | null; + var activeSandboxProvider: any; var sandboxData: any; var existingFiles: Set; var sandboxState: SandboxState; diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index af86837..049f533 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -18,6 +18,12 @@ export const dynamic = 'force-dynamic'; const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY; const aiGatewayBaseURL = 'https://ai-gateway.vercel.sh/v1'; +console.log('[generate-ai-code-stream] AI Gateway config:', { + isUsingAIGateway, + hasGroqKey: !!process.env.GROQ_API_KEY, + hasAIGatewayKey: !!process.env.AI_GATEWAY_API_KEY +}); + const groq = createGroq({ apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GROQ_API_KEY, baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined, @@ -152,10 +158,18 @@ export async function POST(request: NextRequest) { const stream = new TransformStream(); const writer = stream.writable.getWriter(); - // Function to send progress updates + // Function to send progress updates with flushing const sendProgress = async (data: any) => { const message = `data: ${JSON.stringify(data)}\n\n`; - await writer.write(encoder.encode(message)); + try { + await writer.write(encoder.encode(message)); + // Force flush by writing a keep-alive comment + if (data.type === 'stream' || data.type === 'conversation') { + await writer.write(encoder.encode(': keepalive\n\n')); + } + } catch (error) { + console.error('[generate-ai-code-stream] Error writing to stream:', error); + } }; // Start processing in background @@ -1169,15 +1183,22 @@ CRITICAL: When files are provided in the context: // Determine which provider to use based on model const isAnthropic = model.startsWith('anthropic/'); const isGoogle = model.startsWith('google/'); - const isOpenAI = model.startsWith('openai/gpt-5'); - const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq)); + const isOpenAI = model.startsWith('openai/'); + const isKimiGroq = model === 'moonshotai/kimi-k2-instruct-0905'; + const modelProvider = isAnthropic ? anthropic : + (isOpenAI ? openai : + (isGoogle ? googleGenerativeAI : + (isKimiGroq ? groq : groq))); // Fix model name transformation for different providers let actualModel: string; if (isAnthropic) { actualModel = model.replace('anthropic/', ''); - } else if (model === 'openai/gpt-5') { - actualModel = 'gpt-5'; + } else if (isOpenAI) { + actualModel = model.replace('openai/', ''); + } else if (isKimiGroq) { + // Kimi on Groq - use full model string + actualModel = 'moonshotai/kimi-k2-instruct-0905'; } else if (isGoogle) { // Google uses specific model names - convert our naming to theirs actualModel = model.replace('google/', ''); @@ -1186,6 +1207,8 @@ CRITICAL: When files are provided in the context: } console.log(`[generate-ai-code-stream] Using provider: ${isAnthropic ? 'Anthropic' : isGoogle ? 'Google' : isOpenAI ? 'OpenAI' : 'Groq'}, model: ${actualModel}`); + console.log(`[generate-ai-code-stream] AI Gateway enabled: ${isUsingAIGateway}`); + console.log(`[generate-ai-code-stream] Model string: ${model}`); // Make streaming API call with appropriate provider const streamOptions: any = { @@ -1349,6 +1372,11 @@ It's better to have 3 complete files than 10 incomplete files.` raw: true }); + // Debug: Log every 100 characters streamed + if (generatedCode.length % 100 < text.length) { + console.log(`[generate-ai-code-stream] Streamed ${generatedCode.length} chars`); + } + // Check for package tags in buffered text (ONLY for edits, not initial generation) let lastIndex = 0; if (isEdit) { @@ -1638,12 +1666,28 @@ Provide the complete file content without any truncation. Include all necessary completionClient = openai; } else if (model.includes('claude')) { completionClient = anthropic; + } else if (model === 'moonshotai/kimi-k2-instruct-0905') { + completionClient = groq; } else { completionClient = groq; } + // Determine the correct model name for the completion + let completionModelName: string; + if (model === 'moonshotai/kimi-k2-instruct-0905') { + completionModelName = 'moonshotai/kimi-k2-instruct-0905'; + } else if (model.includes('openai')) { + completionModelName = model.replace('openai/', ''); + } else if (model.includes('anthropic')) { + completionModelName = model.replace('anthropic/', ''); + } else if (model.includes('google')) { + completionModelName = model.replace('google/', ''); + } else { + completionModelName = model; + } + const completionResult = await streamText({ - model: completionClient(modelMapping[model] || model), + model: completionClient(completionModelName), messages: [ { role: 'system', diff --git a/app/api/scrape-screenshot/route.ts b/app/api/scrape-screenshot/route.ts index b77820a..feccddf 100644 --- a/app/api/scrape-screenshot/route.ts +++ b/app/api/scrape-screenshot/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import FirecrawlApp from '@mendable/firecrawl-js'; export async function POST(req: NextRequest) { try { @@ -8,43 +9,44 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'URL is required' }, { status: 400 }); } - // Use Firecrawl API to capture screenshot - const firecrawlResponse = await fetch('https://api.firecrawl.dev/v1/scrape', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${process.env.FIRECRAWL_API_KEY}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - url, - formats: ['screenshot'], // Regular viewport screenshot, not full page - waitFor: 3000, // Wait for page to fully load - timeout: 30000, - blockAds: true, - actions: [ - { - type: 'wait', - milliseconds: 2000 // Additional wait for dynamic content - } - ] - }) + // Initialize Firecrawl with API key from environment + const apiKey = process.env.FIRECRAWL_API_KEY; + + if (!apiKey) { + console.error("FIRECRAWL_API_KEY not configured"); + return NextResponse.json({ + error: 'Firecrawl API key not configured' + }, { status: 500 }); + } + + const app = new FirecrawlApp({ apiKey }); + + // Use Firecrawl SDK to capture screenshot with the latest API + const scrapeResult = await app.scrapeUrl(url, { + formats: ['screenshot'], // Request screenshot format + waitFor: 3000, // Wait for page to fully load + timeout: 30000, + onlyMainContent: false, // Get full page for screenshot + actions: [ + { + type: 'wait', + milliseconds: 2000 // Additional wait for dynamic content + } + ] }); - if (!firecrawlResponse.ok) { - const error = await firecrawlResponse.text(); - throw new Error(`Firecrawl API error: ${error}`); + if (!scrapeResult.success) { + throw new Error(scrapeResult.error || 'Failed to capture screenshot'); } - - const data = await firecrawlResponse.json(); - if (!data.success || !data.data?.screenshot) { - throw new Error('Failed to capture screenshot'); + if (!scrapeResult.data?.screenshot) { + throw new Error('Screenshot not available in response'); } return NextResponse.json({ success: true, - screenshot: data.data.screenshot, - metadata: data.data.metadata + screenshot: scrapeResult.data.screenshot, + metadata: scrapeResult.data.metadata || {} }); } catch (error: any) { diff --git a/app/api/scrape-website/route.ts b/app/api/scrape-website/route.ts new file mode 100644 index 0000000..73e9e48 --- /dev/null +++ b/app/api/scrape-website/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from "next/server"; +import FirecrawlApp from '@mendable/firecrawl-js'; + +export async function POST(request: NextRequest) { + try { + const { url, formats = ['markdown', 'html'], options = {} } = await request.json(); + + if (!url) { + return NextResponse.json( + { error: "URL is required" }, + { status: 400 } + ); + } + + // Initialize Firecrawl with API key from environment + const apiKey = process.env.FIRECRAWL_API_KEY; + + if (!apiKey) { + console.error("FIRECRAWL_API_KEY not configured"); + // For demo purposes, return mock data if API key is not set + return NextResponse.json({ + success: true, + data: { + title: "Example Website", + content: `This is a mock response for ${url}. Configure FIRECRAWL_API_KEY to enable real scraping.`, + description: "A sample website", + markdown: `# Example Website\n\nThis is mock content for demonstration purposes.`, + html: `

Example Website

This is mock content for demonstration purposes.

`, + metadata: { + title: "Example Website", + description: "A sample website", + sourceURL: url, + statusCode: 200 + } + } + }); + } + + const app = new FirecrawlApp({ apiKey }); + + // Scrape the website using the latest SDK patterns + // Include screenshot if requested in formats + const scrapeResult = await app.scrapeUrl(url, { + formats: formats, + onlyMainContent: options.onlyMainContent !== false, // Default to true for cleaner content + waitFor: options.waitFor || 2000, // Wait for dynamic content + timeout: options.timeout || 30000, + ...options // Pass through any additional options + }); + + // Handle the response according to the latest SDK structure + if (!scrapeResult.success) { + throw new Error(scrapeResult.error || "Failed to scrape website"); + } + + return NextResponse.json({ + success: true, + data: { + title: scrapeResult.data?.metadata?.title || "Untitled", + content: scrapeResult.data?.markdown || scrapeResult.data?.html || "", + description: scrapeResult.data?.metadata?.description || "", + markdown: scrapeResult.data?.markdown || "", + html: scrapeResult.data?.html || "", + metadata: scrapeResult.data?.metadata || {}, + screenshot: scrapeResult.data?.screenshot || null, + links: scrapeResult.data?.links || [], + // Include raw data for flexibility + raw: scrapeResult.data + } + }); + + } catch (error) { + console.error("Error scraping website:", error); + + // Return a more detailed error response + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : "Failed to scrape website", + // Provide mock data as fallback for development + data: { + title: "Example Website", + content: "This is fallback content due to an error. Please check your configuration.", + description: "Error occurred while scraping", + markdown: `# Error\n\n${error instanceof Error ? error.message : 'Unknown error occurred'}`, + html: `

Error

${error instanceof Error ? error.message : 'Unknown error occurred'}

`, + metadata: { + title: "Error", + description: "Failed to scrape website", + statusCode: 500 + } + } + }, { status: 500 }); + } +} + +// Optional: Add OPTIONS handler for CORS if needed +export async function OPTIONS(request: NextRequest) { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +} \ No newline at end of file diff --git a/app/builder/page.tsx b/app/builder/page.tsx new file mode 100644 index 0000000..9135b63 --- /dev/null +++ b/app/builder/page.tsx @@ -0,0 +1,285 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; + +export default function BuilderPage() { + const [targetUrl, setTargetUrl] = useState(""); + const [selectedStyle, setSelectedStyle] = useState("modern"); + const [isLoading, setIsLoading] = useState(true); + const [previewUrl, setPreviewUrl] = useState(""); + const [progress, setProgress] = useState("Initializing..."); + const [generatedCode, setGeneratedCode] = useState(""); + const router = useRouter(); + + useEffect(() => { + // Get the URL and style from sessionStorage + const url = sessionStorage.getItem('targetUrl'); + const style = sessionStorage.getItem('selectedStyle'); + + if (!url) { + router.push('/'); + return; + } + + setTargetUrl(url); + setSelectedStyle(style || "modern"); + + // Start the website generation process + generateWebsite(url, style || "modern"); + }, [router]); + + const generateWebsite = async (url: string, style: string) => { + try { + setProgress("Analyzing website..."); + + // For demo purposes, we'll generate a simple HTML template + // In production, this would call the actual scraping and generation APIs + const mockGeneratedCode = ` + + + + + + ${style} Website - Reimagined + + + +
+ +
+ +
+
+

Welcome to Your ${style === 'modern' ? 'Modern' : style === 'playful' ? 'Playful' : style === 'professional' ? 'Professional' : 'Artistic'} Website

+

Reimagined from ${url}

+ Get Started +
+ +
+
+

Fast

+

Lightning-fast performance optimized for modern web standards.

+
+
+

Responsive

+

Looks great on all devices, from mobile to desktop.

+
+
+

Beautiful

+

Stunning design that captures attention and drives engagement.

+
+
+
+ +`; + + setGeneratedCode(mockGeneratedCode); + + // Create a blob URL for the preview + const blob = new Blob([mockGeneratedCode], { type: 'text/html' }); + const blobUrl = URL.createObjectURL(blob); + setPreviewUrl(blobUrl); + + setProgress("Website ready!"); + setIsLoading(false); + + // Show success message + toast.success("Website generated successfully!"); + + } catch (error) { + console.error("Error generating website:", error); + toast.error("Failed to generate website. Please try again."); + setProgress("Error occurred"); + setTimeout(() => router.push('/'), 2000); + } + }; + + const downloadCode = () => { + const blob = new Blob([generatedCode], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'website.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success("Code downloaded!"); + }; + + return ( +
+
+ {/* Sidebar */} +
+

Building Your Website

+ +
+
+
Target URL
+
{targetUrl}
+
+ +
+
Style
+
{selectedStyle}
+
+ +
+
Status
+
{progress}
+
+
+ +
+ {!isLoading && ( + + )} + + +
+
+ + {/* Preview */} +
+ {isLoading ? ( +
+
+
+

{progress}

+
+
+ ) : ( + previewUrl && ( +