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