diff --git a/.env.example b/.env.example
index c8b9f67..0c6a80e 100644
--- a/.env.example
+++ b/.env.example
@@ -1,20 +1,38 @@
-# REQUIRED - Sandboxes for code execution
-# Get yours at https://e2b.dev
-E2B_API_KEY=your_e2b_api_key_here
+# Required
+FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
-# REQUIRED - Web scraping for cloning websites
-# Get yours at https://firecrawl.dev
-FIRECRAWL_API_KEY=your_firecrawl_api_key_here
+# =================================================================================
+# SANDBOX PROVIDER - Choose Option 1 OR 2
+# =================================================================================
-# OPTIONAL - AI Providers (need at least one)
-# Get yours at https://console.anthropic.com
-ANTHROPIC_API_KEY=your_anthropic_api_key_here
+# 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
-# Get yours at https://platform.openai.com
-OPENAI_API_KEY=your_openai_api_key_here
+# Option 2: Vercel Sandbox
+# Set SANDBOX_PROVIDER=vercel and choose authentication method below
+# SANDBOX_PROVIDER=vercel
-# Get yours at https://aistudio.google.com/app/apikey
-GEMINI_API_KEY=your_gemini_api_key_here
+# 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
-# Get yours at https://console.groq.com
-GROQ_API_KEY=your_groq_api_key_here
\ No newline at end of file
+# 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
+
+# =================================================================================
+# 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)
diff --git a/.gitignore b/.gitignore
index ac59fa8..79f47d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,3 +56,4 @@ e2b-template-*
*.temp
repomix-output.txt
bun.lockb
+.env*.local
diff --git a/README.md b/README.md
index 803cc92..d394bcf 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,53 @@
# Open Lovable
-Chat with AI to build React apps instantly. An example app made by the [Firecrawl](https://firecrawl.dev/?ref=open-lovable-github) team. For a complete cloud solution, check out [Lovable.dev ❤️](https://lovable.dev/).
+Chat with AI to build React apps instantly. An example app made by the [Firecrawl](https://firecrawl.dev/?ref=open-lovable-github) team. For a complete cloud solution, check out [Lovable.dev](https://lovable.dev/) ❤️.
-
-
## Setup
1. **Clone & Install**
```bash
-git clone https://github.com/mendableai/open-lovable.git
+git clone https://github.com/firecrawl/open-lovable.git
cd open-lovable
-npm install
+pnpm install # or npm install / yarn install
```
2. **Add `.env.local`**
-```env
-# Required
-E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev (Sandboxes)
-FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
-# Optional (need at least one AI provider)
-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)
+```env
+# =================================================================
+# REQUIRED
+# =================================================================
+FIRECRAWL_API_KEY=your_firecrawl_api_key # https://firecrawl.dev
+
+# =================================================================
+# AI PROVIDER - Choose your LLM
+# =================================================================
+ANTHROPIC_API_KEY=your_anthropic_api_key # https://console.anthropic.com
+OPENAI_API_KEY=your_openai_api_key # https://platform.openai.com
+GEMINI_API_KEY=your_gemini_api_key # https://aistudio.google.com/app/apikey
+GROQ_API_KEY=your_groq_api_key # https://console.groq.com
+
+# =================================================================
+# SANDBOX PROVIDER - Choose ONE: E2B or Vercel
+# =================================================================
+SANDBOX_PROVIDER=e2b # or 'vercel'
+
+# E2B Sandbox (default)
+E2B_API_KEY=your_e2b_api_key # https://e2b.dev
+
+# OR Vercel Sandbox
+VERCEL_OIDC_TOKEN=your_vercel_oidc_token # https://vercel.com
```
3. **Run**
```bash
-npm run dev
+pnpm dev # or npm run dev / yarn dev
```
-Open [http://localhost:3000](http://localhost:3000)
+Open [http://localhost:3000](http://localhost:3000)
## License
-MIT
+MIT
\ No newline at end of file
diff --git a/app/api/analyze-edit-intent/route.ts b/app/api/analyze-edit-intent/route.ts
index 7cf35bc..07798a0 100644
--- a/app/api/analyze-edit-intent/route.ts
+++ b/app/api/analyze-edit-intent/route.ts
@@ -5,20 +5,30 @@ import { createOpenAI } from '@ai-sdk/openai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { generateObject } from 'ai';
import { z } from 'zod';
-import type { FileManifest } from '@/types/file-manifest';
+// import type { FileManifest } from '@/types/file-manifest'; // Type is used implicitly through manifest parameter
+
+// 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!
@@ -66,7 +76,7 @@ export async function POST(request: NextRequest) {
// Create a summary of available files for the AI
const validFiles = Object.entries(manifest.files as Record)
- .filter(([path, info]) => {
+ .filter(([path]) => {
// Filter out invalid paths
return path.includes('.') && !path.match(/\/\d+$/);
});
@@ -74,7 +84,7 @@ export async function POST(request: NextRequest) {
const fileSummary = validFiles
.map(([path, info]: [string, any]) => {
const componentName = info.componentInfo?.name || path.split('/').pop();
- const hasImports = info.imports?.length > 0;
+ // const hasImports = info.imports?.length > 0; // Kept for future use
const childComponents = info.componentInfo?.childComponents?.join(', ') || 'none';
return `- ${path} (${componentName}, renders: ${childComponents})`;
})
@@ -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 c91bf11..3812ac0 100644
--- a/app/api/apply-ai-code-stream/route.ts
+++ b/app/api/apply-ai-code-stream/route.ts
@@ -1,11 +1,12 @@
import { NextRequest, NextResponse } from 'next/server';
-import { Sandbox } from '@e2b/code-interpreter';
+// Sandbox import not needed - using global sandbox from sandbox-manager
import type { SandboxState } from '@/types/sandbox';
import type { ConversationState } from '@/types/conversation';
+import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
declare global {
var conversationState: ConversationState | null;
- var activeSandbox: any;
+ var activeSandboxProvider: any;
var existingFiles: Set;
var sandboxState: SandboxState;
}
@@ -294,73 +295,88 @@ export async function POST(request: NextRequest) {
global.existingFiles = new Set();
}
- // First, always check the global state for active sandbox
- let sandbox = global.activeSandbox;
+ // Try to get provider from sandbox manager first
+ let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
- // 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...`);
+ // Fall back to global state if not found in manager
+ if (!provider) {
+ provider = global.activeSandboxProvider;
+ }
+
+ // If we have a sandboxId but no provider, try to get or create one
+ if (!provider && sandboxId) {
+ console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`);
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}`);
+ provider = await sandboxManager.getOrCreateProvider(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);
- global.sandboxData = {
- sandboxId,
- url: `https://${host}`
- };
+ // If we got a new provider (not reconnected), we need to create a new sandbox
+ if (!provider.getSandboxInfo()) {
+ console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`);
+ await provider.createSandbox();
+ await provider.setupViteApp();
+ sandboxManager.registerSandbox(sandboxId, provider);
}
- // 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
+ // Update legacy global state
+ global.activeSandboxProvider = provider;
+ console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`);
+ } catch (providerError) {
+ console.error(`[apply-ai-code-stream] Failed to get or create provider for sandbox ${sandboxId}:`, providerError);
return NextResponse.json({
success: false,
- error: `Failed to reconnect to sandbox ${sandboxId}. The sandbox may have expired or been terminated.`,
+ error: `Failed to create sandbox provider for ${sandboxId}. The sandbox may have expired.`,
results: {
filesCreated: [],
packagesInstalled: [],
commandsExecuted: [],
- errors: [`Sandbox reconnection failed: ${(reconnectError as Error).message}`]
+ 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 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.`
- });
+ // 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 {
+ const { SandboxFactory } = await import('@/lib/sandbox/factory');
+ provider = SandboxFactory.create();
+ const sandboxInfo = await provider.createSandbox();
+ await provider.setupViteApp();
+
+ // Register with sandbox manager
+ sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
+
+ // Store in legacy global state
+ global.activeSandboxProvider = provider;
+ global.sandboxData = {
+ sandboxId: sandboxInfo.sandboxId,
+ url: sandboxInfo.url
+ };
+
+ 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 create new sandbox: ${createError instanceof Error ? createError.message : 'Unknown error'}`,
+ results: {
+ filesCreated: [],
+ packagesInstalled: [],
+ commandsExecuted: [],
+ 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 creation failed.`
+ }, { status: 500 });
+ }
}
// Create a response stream for real-time updates
@@ -374,8 +390,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 +448,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
})
});
@@ -463,7 +479,8 @@ export async function POST(request: NextRequest) {
if (data.type === 'success' && data.installedPackages) {
results.packagesInstalled = data.installedPackages;
}
- } catch (e) {
+ } catch (parseError) {
+ console.debug('Error parsing terminal output:', parseError);
// Ignore parse errors
}
}
@@ -525,7 +542,6 @@ export async function POST(request: NextRequest) {
normalizedPath = 'src/' + normalizedPath;
}
- const fullPath = `/home/user/app/${normalizedPath}`;
const isUpdate = global.existingFiles.has(normalizedPath);
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
@@ -534,19 +550,23 @@ export async function POST(request: NextRequest) {
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
}
- // Write the file using Python (code-interpreter SDK)
- const escapedContent = fileContent
- .replace(/\\/g, '\\\\')
- .replace(/"""/g, '\\"\\"\\"')
- .replace(/\$/g, '\\$');
+ // Fix common Tailwind CSS errors in CSS files
+ if (file.path.endsWith('.css')) {
+ // Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
+ fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl');
+ // Replace any other non-existent shadow utilities
+ fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl');
+ fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
+ }
- await sandboxInstance.runCode(`
-import os
-os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True)
-with open("${fullPath}", 'w') as f:
- f.write("""${escapedContent}""")
-print(f"File written: ${fullPath}")
- `);
+ // Create directory if needed
+ const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
+ if (dirPath) {
+ await providerInstance.runCommand(`mkdir -p ${dirPath}`);
+ }
+
+ // Write the file using provider
+ await providerInstance.writeFile(normalizedPath, fileContent);
// Update file cache
if (global.sandboxState?.fileCache) {
@@ -599,27 +619,30 @@ print(f"File written: ${fullPath}")
action: 'executing'
});
- // Use E2B commands.run() for cleaner execution
- const result = await sandboxInstance.commands.run(cmd, {
- cwd: '/home/user/app',
- timeout: 60,
- on_stdout: async (data: string) => {
- await sendProgress({
- type: 'command-output',
- command: cmd,
- output: data,
- stream: 'stdout'
- });
- },
- on_stderr: async (data: string) => {
- await sendProgress({
- type: 'command-output',
- command: cmd,
- output: data,
- stream: '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({
+ type: 'command-output',
+ command: cmd,
+ output: stdout,
+ stream: 'stdout'
+ });
+ }
+
+ if (stderr) {
+ await sendProgress({
+ type: 'command-output',
+ command: cmd,
+ output: stderr,
+ stream: 'stderr'
+ });
+ }
if (results.commandsExecuted) {
results.commandsExecuted.push(cmd);
@@ -686,7 +709,7 @@ print(f"File written: ${fullPath}")
} finally {
await writer.close();
}
- })(sandbox, request);
+ })(provider, request);
// Return the stream
return new Response(stream.readable, {
@@ -696,7 +719,7 @@ print(f"File written: ${fullPath}")
'Connection': 'keep-alive',
},
});
-
+
} catch (error) {
console.error('Apply AI code stream error:', error);
return NextResponse.json(
diff --git a/app/api/apply-ai-code/route.ts b/app/api/apply-ai-code/route.ts
index f00f08a..da2cb37 100644
--- a/app/api/apply-ai-code/route.ts
+++ b/app/api/apply-ai-code/route.ts
@@ -128,6 +128,7 @@ function parseAIResponse(response: string): ParsedResponse {
declare global {
var activeSandbox: any;
+ var activeSandboxProvider: any;
var existingFiles: Set;
var sandboxState: SandboxState;
}
@@ -150,8 +151,11 @@ export async function POST(request: NextRequest) {
global.existingFiles = new Set();
}
+ // Get the active sandbox or provider
+ const sandbox = global.activeSandbox || global.activeSandboxProvider;
+
// If no active sandbox, just return parsed results
- if (!global.activeSandbox) {
+ if (!sandbox) {
return NextResponse.json({
success: true,
results: {
@@ -167,6 +171,30 @@ export async function POST(request: NextRequest) {
});
}
+ // Verify sandbox is ready before applying code
+ console.log('[apply-ai-code] Verifying sandbox is ready...');
+
+ // For Vercel sandboxes, check if Vite is running
+ if (sandbox.constructor?.name === 'VercelProvider' || sandbox.getSandboxInfo?.()?.provider === 'vercel') {
+ console.log('[apply-ai-code] Detected Vercel sandbox, checking Vite status...');
+ try {
+ // Check if Vite process is running
+ const checkResult = await sandbox.runCommand('pgrep -f vite');
+ if (!checkResult || !checkResult.stdout) {
+ console.log('[apply-ai-code] Vite not running, starting it...');
+ // Start Vite if not running
+ await sandbox.runCommand('sh -c "cd /vercel/sandbox && nohup npm run dev > /tmp/vite.log 2>&1 &"');
+ // Wait for Vite to start
+ await new Promise(resolve => setTimeout(resolve, 5000));
+ console.log('[apply-ai-code] Vite started, proceeding with code application');
+ } else {
+ console.log('[apply-ai-code] Vite is already running');
+ }
+ } catch (e) {
+ console.log('[apply-ai-code] Could not check Vite status, proceeding anyway:', e);
+ }
+ }
+
// Apply to active sandbox
console.log('[apply-ai-code] Applying code to sandbox...');
console.log('[apply-ai-code] Is edit mode:', isEdit);
@@ -336,11 +364,28 @@ export async function POST(request: NextRequest) {
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
}
+ // Fix common Tailwind CSS errors in CSS files
+ if (file.path.endsWith('.css')) {
+ // Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
+ fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl');
+ // Replace any other non-existent shadow utilities
+ fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl');
+ fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
+ }
+
console.log(`[apply-ai-code] Writing file using E2B files API: ${fullPath}`);
try {
- // Use the correct E2B API - sandbox.files.write()
- await global.activeSandbox.files.write(fullPath, fileContent);
+ // Check if we're using provider pattern (v2) or direct sandbox (v1)
+ if (sandbox.writeFile) {
+ // V2: Provider pattern (Vercel/E2B provider)
+ await sandbox.writeFile(file.path, fileContent);
+ } else if (sandbox.files?.write) {
+ // V1: Direct E2B sandbox
+ await sandbox.files.write(fullPath, fileContent);
+ } else {
+ throw new Error('Unsupported sandbox type');
+ }
console.log(`[apply-ai-code] Successfully wrote file: ${fullPath}`);
// Update file cache
@@ -432,15 +477,17 @@ function App() {
export default App;`;
try {
- await global.activeSandbox.runCode(`
-file_path = "/home/user/app/src/App.jsx"
-file_content = """${appContent.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"""
-
-with open(file_path, 'w') as f:
- f.write(file_content)
-
-print(f"Auto-generated: {file_path}")
- `);
+ // Use provider pattern if available
+ if (sandbox.writeFile) {
+ await sandbox.writeFile('src/App.jsx', appContent);
+ } else if (sandbox.writeFiles) {
+ await sandbox.writeFiles([{
+ path: 'src/App.jsx',
+ content: Buffer.from(appContent)
+ }]);
+ }
+
+ console.log('Auto-generated: src/App.jsx');
results.filesCreated.push('src/App.jsx (auto-generated)');
} catch (error) {
results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`);
@@ -459,9 +506,7 @@ print(f"Auto-generated: {file_path}")
if (!isEdit && !indexCssInParsed && !indexCssExists) {
try {
- await global.activeSandbox.runCode(`
-file_path = "/home/user/app/src/index.css"
-file_content = """@tailwind base;
+ const indexCssContent = `@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -483,15 +528,22 @@ body {
margin: 0;
min-width: 320px;
min-height: 100vh;
-}"""
+}`;
-with open(file_path, 'w') as f:
- f.write(file_content)
-
-print(f"Auto-generated: {file_path}")
- `);
+ // Use provider pattern if available
+ if (sandbox.writeFile) {
+ await sandbox.writeFile('src/index.css', indexCssContent);
+ } else if (sandbox.writeFiles) {
+ await sandbox.writeFiles([{
+ path: 'src/index.css',
+ content: Buffer.from(indexCssContent)
+ }]);
+ }
+
+ console.log('Auto-generated: src/index.css');
results.filesCreated.push('src/index.css (with Tailwind)');
} catch (error) {
+ console.error('Failed to create index.css:', error);
results.errors.push('Failed to create index.css with Tailwind');
}
}
@@ -500,15 +552,47 @@ print(f"Auto-generated: {file_path}")
// Execute commands
for (const cmd of parsed.commands) {
try {
- await global.activeSandbox.runCode(`
-import subprocess
-os.chdir('/home/user/app')
-result = subprocess.run(${JSON.stringify(cmd.split(' '))}, capture_output=True, text=True)
-print(f"Executed: ${cmd}")
-print(result.stdout)
-if result.stderr:
- print(f"Errors: {result.stderr}")
- `);
+ // Parse command and arguments
+ const commandParts = cmd.trim().split(/\s+/);
+ const cmdName = commandParts[0];
+ const args = commandParts.slice(1);
+
+ // Execute command using sandbox
+ let result;
+ if (sandbox.runCommand && typeof sandbox.runCommand === 'function') {
+ // Check if this is a provider pattern sandbox
+ const testResult = await sandbox.runCommand(cmd);
+ if (testResult && typeof testResult === 'object' && 'stdout' in testResult) {
+ // Provider returns CommandResult directly
+ result = testResult;
+ } else {
+ // Direct sandbox - expects object with cmd and args
+ result = await sandbox.runCommand({
+ cmd: cmdName,
+ args
+ });
+ }
+ }
+
+ console.log(`Executed: ${cmd}`);
+
+ // Handle result based on type
+ let stdout = '';
+ let stderr = '';
+
+ if (result) {
+ if (typeof result.stdout === 'string') {
+ stdout = result.stdout;
+ stderr = result.stderr || '';
+ } else if (typeof result.stdout === 'function') {
+ stdout = await result.stdout();
+ stderr = await result.stderr();
+ }
+ }
+
+ if (stdout) console.log(stdout);
+ if (stderr) console.log(`Errors: ${stderr}`);
+
results.commandsExecuted.push(cmd);
} catch (error) {
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
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
new file mode 100644
index 0000000..cd72a74
--- /dev/null
+++ b/app/api/create-ai-sandbox-v2/route.ts
@@ -0,0 +1,103 @@
+import { NextResponse } from 'next/server';
+import { SandboxFactory } from '@/lib/sandbox/factory';
+// SandboxProvider type is used through SandboxFactory
+import type { SandboxState } from '@/types/sandbox';
+import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
+
+// Store active sandbox globally
+declare global {
+ var activeSandboxProvider: any;
+ var sandboxData: any;
+ var existingFiles: Set;
+ var sandboxState: SandboxState;
+}
+
+export async function POST() {
+ try {
+ console.log('[create-ai-sandbox-v2] Creating sandbox...');
+
+ // Clean up all existing sandboxes
+ console.log('[create-ai-sandbox-v2] Cleaning up existing sandboxes...');
+ await sandboxManager.terminateAll();
+
+ // Also clean up legacy global state
+ if (global.activeSandboxProvider) {
+ try {
+ await global.activeSandboxProvider.terminate();
+ } catch (e) {
+ console.error('Failed to terminate legacy global 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();
+
+ // Register with sandbox manager
+ sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
+
+ // Also store in legacy global state for backward compatibility
+ 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
+ await sandboxManager.terminateAll();
+ 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/create-ai-sandbox/route.ts b/app/api/create-ai-sandbox/route.ts
index 257ce1d..daf9b84 100644
--- a/app/api/create-ai-sandbox/route.ts
+++ b/app/api/create-ai-sandbox/route.ts
@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server';
-import { Sandbox } from '@e2b/code-interpreter';
+import { Sandbox } from '@vercel/sandbox';
import type { SandboxState } from '@/types/sandbox';
import { appConfig } from '@/config/app.config';
@@ -9,23 +9,74 @@ 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 {
- console.log('[create-ai-sandbox] Creating base sandbox...');
+ console.log('[create-ai-sandbox] Creating Vercel sandbox...');
// Kill existing sandbox if any
if (global.activeSandbox) {
- console.log('[create-ai-sandbox] Killing existing sandbox...');
+ console.log('[create-ai-sandbox] Stopping existing sandbox...');
try {
- await global.activeSandbox.kill();
+ await global.activeSandbox.stop();
} catch (e) {
- console.error('Failed to close existing sandbox:', e);
+ console.error('Failed to stop existing sandbox:', e);
}
global.activeSandbox = null;
+ global.sandboxData = null;
}
// Clear existing files tracking
@@ -35,81 +86,102 @@ export async function POST() {
global.existingFiles = new Set();
}
- // Create base sandbox - we'll set up Vite ourselves for full control
- console.log(`[create-ai-sandbox] Creating base E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`);
- sandbox = await Sandbox.create({
- apiKey: process.env.E2B_API_KEY,
- timeoutMs: appConfig.e2b.timeoutMs
- });
+ // Create Vercel sandbox with flexible authentication
+ console.log(`[create-ai-sandbox] Creating Vercel sandbox with ${appConfig.vercelSandbox.timeoutMinutes} minute timeout...`);
- const sandboxId = (sandbox as any).sandboxId || Date.now().toString();
- const host = (sandbox as any).getHost(appConfig.e2b.vitePort);
+ // Prepare sandbox configuration
+ const sandboxConfig: any = {
+ timeout: appConfig.vercelSandbox.timeoutMs,
+ runtime: appConfig.vercelSandbox.runtime,
+ ports: [appConfig.vercelSandbox.devPort]
+ };
+ // Add authentication parameters if using personal access token
+ if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
+ console.log('[create-ai-sandbox] 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('[create-ai-sandbox] Using OIDC token authentication');
+ } else {
+ console.log('[create-ai-sandbox] No authentication found - relying on default Vercel authentication');
+ }
+
+ sandbox = await Sandbox.create(sandboxConfig);
+
+ const sandboxId = sandbox.sandboxId;
console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`);
- console.log(`[create-ai-sandbox] Sandbox host: ${host}`);
- // Set up a basic Vite React app using Python to write files
+ // Set up a basic Vite React app
console.log('[create-ai-sandbox] Setting up Vite React app...');
- // Write all files in a single Python script to avoid multiple executions
- const setupScript = `
-import os
-import json
+ // First, change to the working directory
+ await sandbox.runCommand('pwd');
+ // workDir is defined in appConfig - not needed here
+
+ // Get the sandbox URL using the correct Vercel Sandbox API
+ const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort);
+
+ // Extract the hostname from the sandbox URL for Vite config
+ const sandboxHostname = new URL(sandboxUrl).hostname;
+ console.log(`[create-ai-sandbox] Sandbox hostname: ${sandboxHostname}`);
-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 for E2B - with allowedHosts
-vite_config = """import { defineConfig } from 'vite'
+ // Create the Vite config content with the proper hostname (using string concatenation)
+ const viteConfigContent = `import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
-// E2B-compatible Vite configuration
+// Vercel Sandbox compatible Vite configuration
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
- port: 5173,
+ port: ${appConfig.vercelSandbox.devPort},
strictPort: true,
- hmr: false,
- allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1']
+ hmr: true,
+ allowedHosts: [
+ 'localhost',
+ '127.0.0.1',
+ '` + sandboxHostname + `', // Allow the Vercel Sandbox domain
+ '.vercel.run', // Allow all Vercel sandbox domains
+ '.vercel-sandbox.dev' // Fallback pattern
+ ]
}
-})"""
+})`;
-with open('/home/user/app/vite.config.js', 'w') as f:
- f.write(vite_config)
-print('✓ vite.config.js')
-
-# Tailwind config - standard without custom design tokens
-tailwind_config = """/** @type {import('tailwindcss').Config} */
+ // Create the project files (now we have the sandbox hostname)
+ const projectFiles = [
+ {
+ path: 'package.json',
+ content: Buffer.from(JSON.stringify({
+ "name": "sandbox-app",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host --port 3000",
+ "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"
+ }
+ }, null, 2))
+ },
+ {
+ path: 'vite.config.js',
+ content: Buffer.from(viteConfigContent)
+ },
+ {
+ path: 'tailwind.config.js',
+ content: Buffer.from(`/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
@@ -119,26 +191,20 @@ export default {
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 {
+}`)
+ },
+ {
+ path: 'postcss.config.js',
+ content: Buffer.from(`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 = """
+}`)
+ },
+ {
+ path: 'index.html',
+ content: Buffer.from(`
@@ -149,14 +215,11 @@ index_html = """
-"""
-
-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'
+