diff --git a/app/api/analyze-edit-intent/route.ts b/app/api/analyze-edit-intent/route.ts index f99cdad..5d7da65 100644 --- a/app/api/analyze-edit-intent/route.ts +++ b/app/api/analyze-edit-intent/route.ts @@ -5,7 +5,7 @@ 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; @@ -76,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, _info]) => { // Filter out invalid paths return path.includes('.') && !path.match(/\/\d+$/); }); @@ -84,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})`; }) diff --git a/app/api/apply-ai-code-stream/route.ts b/app/api/apply-ai-code-stream/route.ts index 442d99c..e94bca9 100644 --- a/app/api/apply-ai-code-stream/route.ts +++ b/app/api/apply-ai-code-stream/route.ts @@ -1,7 +1,8 @@ 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; @@ -294,45 +295,48 @@ export async function POST(request: NextRequest) { global.existingFiles = new Set(); } - // First, always check the global state for active provider - let provider = global.activeSandboxProvider; + // Try to get provider from sandbox manager first + let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider(); + + // Fall back to global state if not found in manager + if (!provider) { + 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 we have a sandboxId but no provider, try to get or create 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(); + console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`); + + try { + provider = await sandboxManager.getOrCreateProvider(sandboxId); + + // 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(); - - // 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 }); + await provider.setupViteApp(); + sandboxManager.registerSandbox(sandboxId, provider); } + + // 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 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 }); } } @@ -342,19 +346,18 @@ export async function POST(request: NextRequest) { try { const { SandboxFactory } = await import('@/lib/sandbox/factory'); provider = SandboxFactory.create(); - await provider.createSandbox(); + const sandboxInfo = await provider.createSandbox(); + await provider.setupViteApp(); - // Store the provider globally + // Register with sandbox manager + sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider); + + // Store in legacy global state global.activeSandboxProvider = provider; - - // Update sandbox data - const sandboxInfo = provider.getSandboxInfo(); - if (sandboxInfo) { - global.sandboxData = { - sandboxId: sandboxInfo.sandboxId, - url: sandboxInfo.url - }; - } + global.sandboxData = { + sandboxId: sandboxInfo.sandboxId, + url: sandboxInfo.url + }; console.log(`[apply-ai-code-stream] Created new sandbox successfully`); } catch (createError) { @@ -476,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 } } diff --git a/app/api/apply-ai-code/route.ts b/app/api/apply-ai-code/route.ts index f051da4..e98cbbb 100644 --- a/app/api/apply-ai-code/route.ts +++ b/app/api/apply-ai-code/route.ts @@ -488,6 +488,7 @@ body { 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'); } } diff --git a/app/api/create-ai-sandbox-v2/route.ts b/app/api/create-ai-sandbox-v2/route.ts index 6545fe8..cd72a74 100644 --- a/app/api/create-ai-sandbox-v2/route.ts +++ b/app/api/create-ai-sandbox-v2/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server'; import { SandboxFactory } from '@/lib/sandbox/factory'; -import { SandboxProvider } from '@/lib/sandbox/types'; +// 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 { @@ -15,13 +16,16 @@ export async function POST() { try { console.log('[create-ai-sandbox-v2] Creating sandbox...'); - // Clean up existing sandbox if any + // 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) { - console.log('[create-ai-sandbox-v2] Terminating existing sandbox...'); try { await global.activeSandboxProvider.terminate(); } catch (e) { - console.error('Failed to terminate existing sandbox:', e); + console.error('Failed to terminate legacy global sandbox:', e); } global.activeSandboxProvider = null; } @@ -40,7 +44,10 @@ export async function POST() { console.log('[create-ai-sandbox-v2] Setting up Vite React app...'); await provider.setupViteApp(); - // Store provider globally + // 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, @@ -75,6 +82,7 @@ export async function POST() { console.error('[create-ai-sandbox-v2] Error:', error); // Clean up on error + await sandboxManager.terminateAll(); if (global.activeSandboxProvider) { try { await global.activeSandboxProvider.terminate(); diff --git a/app/api/create-ai-sandbox/route.ts b/app/api/create-ai-sandbox/route.ts index b9126a7..daf9b84 100644 --- a/app/api/create-ai-sandbox/route.ts +++ b/app/api/create-ai-sandbox/route.ts @@ -118,7 +118,7 @@ async function createSandboxInternal() { // First, change to the working directory await sandbox.runCommand('pwd'); - const workDir = appConfig.vercelSandbox.workingDirectory; + // 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); diff --git a/app/api/create-zip/route.ts b/app/api/create-zip/route.ts index 2030a39..fae6c39 100644 --- a/app/api/create-zip/route.ts +++ b/app/api/create-zip/route.ts @@ -4,7 +4,7 @@ declare global { var activeSandbox: any; } -export async function POST(request: NextRequest) { +export async function POST(_request: NextRequest) { try { if (!global.activeSandbox) { return NextResponse.json({ diff --git a/app/api/detect-and-install-packages/route.ts b/app/api/detect-and-install-packages/route.ts index facbd51..a2feaf9 100644 --- a/app/api/detect-and-install-packages/route.ts +++ b/app/api/detect-and-install-packages/route.ts @@ -108,8 +108,9 @@ export async function POST(request: NextRequest) { } else { missing.push(packageName); } - } catch (error) { + } catch (checkError) { // If test command fails, assume package is missing + console.debug(`Package check failed for ${packageName}:`, checkError); missing.push(packageName); } } diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index a97c11a..417f5cf 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -245,7 +245,8 @@ export async function POST(request: NextRequest) { // Create surgical edit context with exact location const normalizedPath = target.filePath.replace('/home/user/app/', ''); - const fileContent = fileContents[normalizedPath]?.content || ''; + // fileContent available but not used in current implementation + // const fileContent = fileContents[normalizedPath]?.content || ''; // Build enhanced context with search results enhancedSystemPrompt = ` diff --git a/app/api/get-sandbox-files/route.ts b/app/api/get-sandbox-files/route.ts index 9dad8b1..81ad76f 100644 --- a/app/api/get-sandbox-files/route.ts +++ b/app/api/get-sandbox-files/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import { parseJavaScriptFile, buildComponentTree } from '@/lib/file-parser'; import { FileManifest, FileInfo, RouteInfo } from '@/types/file-manifest'; -import type { SandboxState } from '@/types/sandbox'; +// SandboxState type used implicitly through global.activeSandbox declare global { var activeSandbox: any; @@ -76,7 +76,8 @@ export async function GET() { } } } - } catch (error) { + } catch (parseError) { + console.debug('Error parsing component info:', parseError); // Skip files that can't be read continue; } @@ -180,7 +181,8 @@ function extractRoutes(files: Record): RouteInfo[] { const routeMatches = fileInfo.content.matchAll(/path=["']([^"']+)["'].*(?:element|component)={([^}]+)}/g); for (const match of routeMatches) { - const [, routePath, componentRef] = match; + const [, routePath] = match; + // componentRef available in match but not used currently routes.push({ path: routePath, component: path, diff --git a/app/api/install-packages-v2/route.ts b/app/api/install-packages-v2/route.ts index 646e44b..01643f7 100644 --- a/app/api/install-packages-v2/route.ts +++ b/app/api/install-packages-v2/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { SandboxProvider } from '@/lib/sandbox/types'; +import { sandboxManager } from '@/lib/sandbox/sandbox-manager'; declare global { var activeSandboxProvider: SandboxProvider | null; @@ -16,7 +17,10 @@ export async function POST(request: NextRequest) { }, { status: 400 }); } - if (!global.activeSandboxProvider) { + // Get provider from sandbox manager or global state + const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider; + + if (!provider) { return NextResponse.json({ success: false, error: 'No active sandbox' @@ -25,7 +29,7 @@ export async function POST(request: NextRequest) { console.log(`[install-packages-v2] Installing: ${packages.join(', ')}`); - const result = await global.activeSandboxProvider.installPackages(packages); + const result = await provider.installPackages(packages); return NextResponse.json({ success: result.success, diff --git a/app/api/install-packages/route.ts b/app/api/install-packages/route.ts index 08b95a1..90a9c5d 100644 --- a/app/api/install-packages/route.ts +++ b/app/api/install-packages/route.ts @@ -8,7 +8,8 @@ declare global { export async function POST(request: NextRequest) { try { - const { packages, sandboxId } = await request.json(); + const { packages } = await request.json(); + // sandboxId not used - using global sandbox if (!packages || !Array.isArray(packages) || packages.length === 0) { return NextResponse.json({ @@ -75,9 +76,9 @@ export async function POST(request: NextRequest) { // Try to kill any running dev server processes await providerInstance.runCommand('pkill -f vite'); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit - } catch (error) { + } catch (killError) { // It's OK if no process is found - console.log('[install-packages] No existing dev server found'); + console.debug('[install-packages] No existing dev server found:', killError); } // Check which packages are already installed diff --git a/app/api/monitor-vite-logs/route.ts b/app/api/monitor-vite-logs/route.ts index 3cb2a9b..2c6dc60 100644 --- a/app/api/monitor-vite-logs/route.ts +++ b/app/api/monitor-vite-logs/route.ts @@ -29,7 +29,7 @@ export async function GET() { const data = JSON.parse(errorFileContent); errors.push(...(data.errors || [])); } - } catch (error) { + } catch { // No error file exists, that's OK } @@ -85,12 +85,12 @@ export async function GET() { } } } - } catch (error) { + } catch { // Skip if grep fails } } } - } catch (error) { + } catch { // No log files found, that's OK } diff --git a/app/api/restart-vite/route.ts b/app/api/restart-vite/route.ts index 0fb3f30..9472287 100644 --- a/app/api/restart-vite/route.ts +++ b/app/api/restart-vite/route.ts @@ -52,7 +52,7 @@ export async function POST() { // Wait a moment for processes to terminate await new Promise(resolve => setTimeout(resolve, 2000)); - } catch (error) { + } catch { console.log('[restart-vite] No existing Vite processes found'); } @@ -62,12 +62,13 @@ export async function POST() { cmd: 'bash', args: ['-c', 'echo \'{"errors": [], "lastChecked": '+ Date.now() +'}\' > /tmp/vite-errors.json'] }); - } catch (error) { + } catch { // Ignore if this fails } // Start Vite dev server in detached mode - const viteProcess = await global.activeSandbox.runCommand({ + // Start Vite dev server in detached mode + await global.activeSandbox.runCommand({ cmd: 'npm', args: ['run', 'dev'], detached: true diff --git a/app/api/run-command-v2/route.ts b/app/api/run-command-v2/route.ts index 6ea58b3..cca755a 100644 --- a/app/api/run-command-v2/route.ts +++ b/app/api/run-command-v2/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { SandboxProvider } from '@/lib/sandbox/types'; +import { sandboxManager } from '@/lib/sandbox/sandbox-manager'; // Get active sandbox provider from global state declare global { @@ -17,7 +18,10 @@ export async function POST(request: NextRequest) { }, { status: 400 }); } - if (!global.activeSandboxProvider) { + // Get provider from sandbox manager or global state + const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider; + + if (!provider) { return NextResponse.json({ success: false, error: 'No active sandbox' @@ -26,7 +30,7 @@ export async function POST(request: NextRequest) { console.log(`[run-command-v2] Executing: ${command}`); - const result = await global.activeSandboxProvider.runCommand(command); + const result = await provider.runCommand(command); return NextResponse.json({ success: result.success, diff --git a/app/api/sandbox-logs/route.ts b/app/api/sandbox-logs/route.ts index 177c370..8404da1 100644 --- a/app/api/sandbox-logs/route.ts +++ b/app/api/sandbox-logs/route.ts @@ -4,7 +4,7 @@ declare global { var activeSandbox: any; } -export async function GET(request: NextRequest) { +export async function GET(_request: NextRequest) { try { if (!global.activeSandbox) { return NextResponse.json({ @@ -22,7 +22,7 @@ export async function GET(request: NextRequest) { }); let viteRunning = false; - let logContent: string[] = []; + const logContent: string[] = []; if (psResult.exitCode === 0) { const psOutput = await psResult.stdout(); @@ -63,12 +63,12 @@ export async function GET(request: NextRequest) { logContent.push(`--- ${logFile} ---`); logContent.push(logFileContent); } - } catch (error) { + } catch { // Skip if can't read log file } } } - } catch (error) { + } catch { // No log files found, that's OK } diff --git a/app/api/sandbox-status/route.ts b/app/api/sandbox-status/route.ts index 928c072..0d7bde0 100644 --- a/app/api/sandbox-status/route.ts +++ b/app/api/sandbox-status/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server'; +import { sandboxManager } from '@/lib/sandbox/sandbox-manager'; declare global { var activeSandboxProvider: any; @@ -8,19 +9,22 @@ declare global { export async function GET() { try { - // Check if sandbox exists - const sandboxExists = !!global.activeSandboxProvider; + // Check sandbox manager first, then fall back to global state + const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider; + const sandboxExists = !!provider; let sandboxHealthy = false; let sandboxInfo = null; - if (sandboxExists && global.activeSandboxProvider) { + if (sandboxExists && provider) { try { - // Check if sandbox is healthy by calling a method that should work - sandboxHealthy = true; + // Check if sandbox is healthy by getting its info + const providerInfo = provider.getSandboxInfo(); + sandboxHealthy = !!providerInfo; + sandboxInfo = { - sandboxId: global.sandboxData?.sandboxId, - url: global.sandboxData?.url, + sandboxId: providerInfo?.sandboxId || global.sandboxData?.sandboxId, + url: providerInfo?.url || global.sandboxData?.url, filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [], lastHealthCheck: new Date().toISOString() }; diff --git a/app/api/scrape-screenshot/route.ts b/app/api/scrape-screenshot/route.ts index 55fc9ea..cdd4ea2 100644 --- a/app/api/scrape-screenshot/route.ts +++ b/app/api/scrape-screenshot/route.ts @@ -38,24 +38,35 @@ export async function POST(req: NextRequest) { ] }); - console.log('[scrape-screenshot] Scrape result success:', scrapeResult.success); - console.log('[scrape-screenshot] Scrape result data:', scrapeResult.data ? Object.keys(scrapeResult.data) : 'No data'); + console.log('[scrape-screenshot] Full scrape result:', JSON.stringify(scrapeResult, null, 2)); + console.log('[scrape-screenshot] Scrape result type:', typeof scrapeResult); + console.log('[scrape-screenshot] Scrape result keys:', Object.keys(scrapeResult)); - if (!scrapeResult.success) { + // The Firecrawl v4 API might return data directly without a success flag + // Check if we have data with screenshot + if (scrapeResult && scrapeResult.screenshot) { + // Direct screenshot response + return NextResponse.json({ + success: true, + screenshot: scrapeResult.screenshot, + metadata: scrapeResult.metadata || {} + }); + } else if (scrapeResult?.data?.screenshot) { + // Nested data structure + return NextResponse.json({ + success: true, + screenshot: scrapeResult.data.screenshot, + metadata: scrapeResult.data.metadata || {} + }); + } else if (scrapeResult?.success === false) { + // Explicit failure console.error('[scrape-screenshot] Firecrawl API error:', scrapeResult.error); - console.error('[scrape-screenshot] Full scrapeResult:', JSON.stringify(scrapeResult, null, 2)); throw new Error(scrapeResult.error || 'Failed to capture screenshot'); + } else { + // No screenshot in response + console.error('[scrape-screenshot] No screenshot in response. Full response:', JSON.stringify(scrapeResult, null, 2)); + throw new Error('Screenshot not available in response - check console for full response structure'); } - - if (!scrapeResult.data?.screenshot) { - throw new Error('Screenshot not available in response'); - } - - return NextResponse.json({ - success: true, - screenshot: scrapeResult.data.screenshot, - metadata: scrapeResult.data.metadata || {} - }); } catch (error: any) { console.error('[scrape-screenshot] Screenshot capture error:', error); diff --git a/app/api/scrape-url-enhanced/route.ts b/app/api/scrape-url-enhanced/route.ts index 699fd3a..1baa682 100644 --- a/app/api/scrape-url-enhanced/route.ts +++ b/app/api/scrape-url-enhanced/route.ts @@ -72,7 +72,8 @@ export async function POST(request: NextRequest) { throw new Error('Failed to scrape content'); } - const { markdown, html, metadata, screenshot, actions } = data.data; + const { markdown, metadata, screenshot, actions } = data.data; + // html available but not used in current implementation // Get screenshot from either direct field or actions result const screenshotUrl = screenshot || actions?.screenshots?.[0] || null; diff --git a/app/api/scrape-website/route.ts b/app/api/scrape-website/route.ts index a1d95a6..2990b33 100644 --- a/app/api/scrape-website/route.ts +++ b/app/api/scrape-website/route.ts @@ -94,7 +94,7 @@ export async function POST(request: NextRequest) { } // Optional: Add OPTIONS handler for CORS if needed -export async function OPTIONS(request: NextRequest) { +export async function OPTIONS(_request: NextRequest) { return new NextResponse(null, { status: 200, headers: { diff --git a/app/builder/page.tsx b/app/builder/page.tsx index 9135b63..56c4a2a 100644 --- a/app/builder/page.tsx +++ b/app/builder/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; @@ -28,6 +28,7 @@ export default function BuilderPage() { // Start the website generation process generateWebsite(url, style || "modern"); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [router]); const generateWebsite = async (url: string, style: string) => { diff --git a/app/generation/page.tsx b/app/generation/page.tsx index 0b6ab1a..b2d5aad 100644 --- a/app/generation/page.tsx +++ b/app/generation/page.tsx @@ -22,7 +22,7 @@ import { SiCss3, SiJson } from '@/lib/icons'; -import { motion, AnimatePresence } from 'framer-motion'; +import { motion } from 'framer-motion'; import CodeApplicationProgress, { type CodeApplicationState } from '@/components/CodeApplicationProgress'; interface SandboxData { @@ -47,7 +47,9 @@ interface ChatMessage { export default function AISandboxPage() { const [sandboxData, setSandboxData] = useState(null); const [loading, setLoading] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [status, setStatus] = useState({ text: 'Not connected', active: false }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [responseArea, setResponseArea] = useState([]); const [structureContent, setStructureContent] = useState('No sandbox created yet'); const [promptInput, setPromptInput] = useState(''); @@ -66,8 +68,11 @@ export default function AISandboxPage() { const modelParam = searchParams.get('model'); return appConfig.ai.availableModels.includes(modelParam || '') ? modelParam! : appConfig.ai.defaultModel; }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [urlOverlayVisible, setUrlOverlayVisible] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [urlInput, setUrlInput] = useState(''); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [urlStatus, setUrlStatus] = useState([]); const [showHomeScreen, setShowHomeScreen] = useState(true); const [expandedFolders, setExpandedFolders] = useState>(new Set(['app', 'src', 'src/components'])); @@ -78,18 +83,21 @@ export default function AISandboxPage() { const [activeTab, setActiveTab] = useState<'generation' | 'preview'>('preview'); const [showStyleSelector, setShowStyleSelector] = useState(false); const [selectedStyle, setSelectedStyle] = useState(null); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [showLoadingBackground, setShowLoadingBackground] = useState(false); const [urlScreenshot, setUrlScreenshot] = useState(null); const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false); const [screenshotError, setScreenshotError] = useState(null); const [isPreparingDesign, setIsPreparingDesign] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [targetUrl, setTargetUrl] = useState(''); const [sidebarScrolled, setSidebarScrolled] = useState(false); const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [sandboxFiles, setSandboxFiles] = useState>({}); const [hasInitialSubmission, setHasInitialSubmission] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [fileStructure, setFileStructure] = useState(''); - const [isApplyingCode, setIsApplyingCode] = useState(false); const [conversationContext, setConversationContext] = useState<{ scrapedWebsites: Array<{ url: string; content: any; timestamp: Date }>; @@ -124,7 +132,7 @@ export default function AISandboxPage() { thinkingText?: string; thinkingDuration?: number; currentFile?: { path: string; content: string; type: string }; - files: Array<{ path: string; content: string; type: string; completed: boolean }>; + files: Array<{ path: string; content: string; type: string; completed: boolean; edited?: boolean }>; lastProcessedPosition: number; isEdit?: boolean; }>({ @@ -278,6 +286,7 @@ export default function AISandboxPage() { return () => { isMounted = false; }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Run only on mount useEffect(() => { @@ -348,6 +357,7 @@ export default function AISandboxPage() { return () => clearTimeout(timer); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldAutoGenerate, homeUrlInput, showHomeScreen]); const updateStatus = (text: string, active: boolean) => { @@ -372,16 +382,19 @@ export default function AISandboxPage() { }; const checkAndInstallPackages = async () => { + // This function is only called when user explicitly requests it + // Don't show error if no sandbox - it's likely being created if (!sandboxData) { - addChatMessage('No active sandbox. Create a sandbox first!', 'system'); + console.log('[checkAndInstallPackages] No sandbox data available yet'); return; } // Vite error checking removed - handled by template setup - addChatMessage('Sandbox is ready. Vite configuration is handled by the template.', 'system'); + addChatMessage('Checking packages... Sandbox is ready with Vite configuration.', 'system'); }; - const handleSurfaceError = (errors: any[]) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handleSurfaceError = (_errors: any[]) => { // Function kept for compatibility but Vite errors are now handled by template // Focus the input @@ -391,6 +404,7 @@ export default function AISandboxPage() { } }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const installPackages = async (packages: string[]) => { if (!sandboxData) { addChatMessage('No active sandbox. Create a sandbox first!', 'system'); @@ -503,7 +517,7 @@ export default function AISandboxPage() { // Prevent duplicate sandbox creation if (sandboxCreationRef.current) { console.log('[createSandbox] Sandbox creation already in progress, skipping...'); - return; + return null; } sandboxCreationRef.current = true; @@ -583,6 +597,9 @@ Tip: I automatically detect and install npm packages from your code imports (lik iframeRef.current.src = data.url; } }, 100); + + // Return the sandbox data so it can be used immediately + return data; } else { throw new Error(data.error || 'Unknown error'); } @@ -591,6 +608,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik updateStatus('Error', false); log(`Failed to create sandbox: ${error.message}`, 'error'); addChatMessage(`Failed to create sandbox: ${error.message}`, 'system'); + throw error; } finally { setLoading(false); sandboxCreationRef.current = false; // Reset the ref @@ -670,7 +688,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik } else if (data.message.includes('Creating files') || data.message.includes('Applying')) { setCodeApplicationState({ stage: 'applying', - filesGenerated: results.filesCreated + filesGenerated: [] // Files will be populated when complete }); } break; @@ -751,7 +769,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik } break; } - } catch (e) { + } catch { // Ignore parse errors } } @@ -760,12 +778,17 @@ Tip: I automatically detect and install npm packages from your code imports (lik // Process final data if (finalData && finalData.type === 'complete') { - const data = { + const data: any = { success: true, results: finalData.results, explanation: finalData.explanation, structure: finalData.structure, - message: finalData.message + message: finalData.message, + autoCompleted: finalData.autoCompleted, + autoCompletedComponents: finalData.autoCompletedComponents, + warning: finalData.warning, + missingImports: finalData.missingImports, + debug: finalData.debug }; if (data.success) { @@ -862,9 +885,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik console.log('[applyGeneratedCode] Current iframe src:', iframeRef.current?.src); // Set applying code state for edits to show loading overlay - if (isEdit && sandboxData) { - setIsApplyingCode(true); - } + // Removed overlay - changes apply directly if (results.filesCreated?.length > 0) { setConversationContext(prev => ({ @@ -905,8 +926,8 @@ Tip: I automatically detect and install npm packages from your code imports (lik // Fetch updated file structure await fetchSandboxFiles(); - // Automatically check and install any missing packages - await checkAndInstallPackages(); + // Skip automatic package check - it's not needed here and can cause false "no sandbox" messages + // Packages are already installed during the apply-ai-code-stream process // Test build to ensure everything compiles correctly // Skip build test for now - it's causing errors with undefined activeSandbox @@ -935,8 +956,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik } catch (e) { console.log('[home] Could not reload iframe (cross-origin):', e); } - // Clear applying code state after reload - setIsApplyingCode(false); + // Reload completed }, 1000); } }, refreshDelay); @@ -979,7 +999,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik console.log('[applyGeneratedCode] Iframe loaded successfully'); return; } - } catch (e) { + } catch { console.log('[applyGeneratedCode] Cannot access iframe content (CORS), assuming loaded'); return; } @@ -1062,56 +1082,56 @@ Tip: I automatically detect and install npm packages from your code imports (lik } }; - const restartViteServer = async () => { - try { - addChatMessage('Restarting Vite dev server...', 'system'); - - const response = await fetch('/api/restart-vite', { - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); - - if (response.ok) { - const data = await response.json(); - if (data.success) { - addChatMessage('✓ Vite dev server restarted successfully!', 'system'); - - // Refresh the iframe after a short delay - setTimeout(() => { - if (iframeRef.current && sandboxData?.url) { - iframeRef.current.src = `${sandboxData.url}?t=${Date.now()}`; - } - }, 2000); - } else { - addChatMessage(`Failed to restart Vite: ${data.error}`, 'error'); - } - } else { - addChatMessage('Failed to restart Vite server', 'error'); - } - } catch (error) { - console.error('[restartViteServer] Error:', error); - addChatMessage(`Error restarting Vite: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error'); - } - }; +// const restartViteServer = async () => { +// try { +// addChatMessage('Restarting Vite dev server...', 'system'); +// +// const response = await fetch('/api/restart-vite', { +// method: 'POST', +// headers: { 'Content-Type': 'application/json' } +// }); +// +// if (response.ok) { +// const data = await response.json(); +// if (data.success) { +// addChatMessage('✓ Vite dev server restarted successfully!', 'system'); +// +// // Refresh the iframe after a short delay +// setTimeout(() => { +// if (iframeRef.current && sandboxData?.url) { +// iframeRef.current.src = `${sandboxData.url}?t=${Date.now()}`; +// } +// }, 2000); +// } else { +// addChatMessage(`Failed to restart Vite: ${data.error}`, 'error'); +// } +// } else { +// addChatMessage('Failed to restart Vite server', 'error'); +// } +// } catch (error) { +// console.error('[restartViteServer] Error:', error); +// addChatMessage(`Error restarting Vite: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error'); +// } +// }; - const applyCode = async () => { - const code = promptInput.trim(); - if (!code) { - log('Please enter some code first', 'error'); - addChatMessage('No code to apply. Please generate code first.', 'system'); - return; - } - - // Prevent double clicks - if (loading) { - console.log('[applyCode] Already loading, skipping...'); - return; - } - - // Determine if this is an edit based on whether we have applied code before - const isEdit = conversationContext.appliedCode.length > 0; - await applyGeneratedCode(code, isEdit); - }; +// const applyCode = async () => { +// const code = promptInput.trim(); +// if (!code) { +// log('Please enter some code first', 'error'); +// addChatMessage('No code to apply. Please generate code first.', 'system'); +// return; +// } +// +// // Prevent double clicks +// if (loading) { +// console.log('[applyCode] Already loading, skipping...'); +// return; +// } +// +// // Determine if this is an edit based on whether we have applied code before +// const isEdit = conversationContext.appliedCode.length > 0; +// await applyGeneratedCode(code, isEdit); +// }; const renderMainContent = () => { if (activeTab === 'generation' && (generationProgress.isGenerating || generationProgress.files.length > 0)) { @@ -1133,7 +1153,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
{/* Root app folder */}
toggleFolder('app')} > {expandedFolders.has('app') ? ( @@ -1156,11 +1176,11 @@ Tip: I automatically detect and install npm packages from your code imports (lik const fileTree: { [key: string]: Array<{ name: string; edited?: boolean }> } = {}; // Create a map of edited files - const editedFiles = new Set( - generationProgress.files - .filter(f => f.edited) - .map(f => f.path) - ); + // const editedFiles = new Set( + // generationProgress.files + // .filter(f => f.edited) + // .map(f => f.path) + // ); // Process all files from generation progress generationProgress.files.forEach(file => { @@ -1179,7 +1199,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
{dir && (
toggleFolder(dir)} > {expandedFolders.has(dir) ? ( @@ -1204,7 +1224,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik return (
{/* Screenshot as background when available */} {urlScreenshot && ( + /* eslint-disable-next-line @next/next/no-img-element */ Website preview - {/* Show loading overlay after applying code during edits */} - {isApplyingCode && ( -
-
-
-

Applying changes...

-

Reloading environment

+ {/* Package installation overlay - shows when installing packages or applying code */} + {codeApplicationState.stage && codeApplicationState.stage !== 'complete' && ( +
+
+
+ {/* Animated icon based on stage */} + {codeApplicationState.stage === 'installing' ? ( +
+ + + + +
+ ) : ( +
+ + + +
+ )} +
+ +

+ {codeApplicationState.stage === 'analyzing' && 'Analyzing code...'} + {codeApplicationState.stage === 'installing' && 'Installing packages...'} + {codeApplicationState.stage === 'applying' && 'Applying changes...'} +

+ + {/* Package list during installation */} + {codeApplicationState.stage === 'installing' && codeApplicationState.packages && ( +
+
+ {codeApplicationState.packages.map((pkg, index) => ( + + {pkg} + {codeApplicationState.installedPackages?.includes(pkg) && ( + + )} + + ))} +
+
+ )} + + {/* Files being generated */} + {codeApplicationState.stage === 'applying' && codeApplicationState.filesGenerated && ( +
+ Creating {codeApplicationState.filesGenerated.length} files... +
+ )} + +

+ {codeApplicationState.stage === 'analyzing' && 'Parsing generated code and detecting dependencies...'} + {codeApplicationState.stage === 'installing' && 'This may take a moment while npm installs the required packages...'} + {codeApplicationState.stage === 'applying' && 'Writing files to your sandbox environment...'} +

)} - {/* Show a subtle indicator when code is being edited/generated (before applying) */} - {generationProgress.isGenerating && generationProgress.isEdit && !isApplyingCode && ( + {/* Show a subtle indicator when code is being edited/generated */} + {generationProgress.isGenerating && generationProgress.isEdit && !codeApplicationState.stage && (
Generating code... @@ -1625,7 +1702,8 @@ Tip: I automatically detect and install npm packages from your code imports (lik const lowerMessage = message.toLowerCase().trim(); if (lowerMessage === 'check packages' || lowerMessage === 'install packages' || lowerMessage === 'npm install') { if (!sandboxData) { - addChatMessage('No active sandbox. Create a sandbox first!', 'system'); + // More helpful message - user might be trying to run this too early + addChatMessage('The sandbox is still being set up. Please wait for the generation to complete, then try again.', 'system'); return; } await checkAndInstallPackages(); @@ -2030,7 +2108,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik const downloadZip = async () => { if (!sandboxData) { - addChatMessage('No active sandbox to download. Create a sandbox first!', 'system'); + addChatMessage('Please wait for the sandbox to be created before downloading.', 'system'); return; } @@ -2130,338 +2208,346 @@ Tip: I automatically detect and install npm packages from your code imports (lik } }; - const clearChatHistory = () => { - setChatMessages([{ - content: 'Chat history cleared. How can I help you?', - type: 'system', - timestamp: new Date() - }]); - }; +// const clearChatHistory = () => { +// setChatMessages([{ +// content: 'Chat history cleared. How can I help you?', +// type: 'system', +// timestamp: new Date() +// }]); +// }; +// - - const cloneWebsite = async () => { - let url = urlInput.trim(); - if (!url) { - setUrlStatus(prev => [...prev, 'Please enter a URL']); - return; - } - - if (!url.match(/^https?:\/\//i)) { - url = 'https://' + url; - } - - setUrlStatus([`Using: ${url}`, 'Starting to scrape...']); - - setUrlOverlayVisible(false); - - // Remove protocol for cleaner display - const cleanUrl = url.replace(/^https?:\/\//i, ''); - addChatMessage(`Starting to clone ${cleanUrl}...`, 'system'); - - // Capture screenshot immediately and switch to preview tab - captureUrlScreenshot(url); - - try { - addChatMessage('Scraping website content...', 'system'); - const scrapeResponse = await fetch('/api/scrape-url-enhanced', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url }) - }); - - if (!scrapeResponse.ok) { - throw new Error(`Scraping failed: ${scrapeResponse.status}`); - } - - const scrapeData = await scrapeResponse.json(); - - if (!scrapeData.success) { - throw new Error(scrapeData.error || 'Failed to scrape website'); - } - - addChatMessage(`Scraped ${scrapeData.content.length} characters from ${url}`, 'system'); - - // Clear preparing design state and switch to generation tab - setIsPreparingDesign(false); - setActiveTab('generation'); - - setConversationContext(prev => ({ - ...prev, - scrapedWebsites: [...prev.scrapedWebsites, { - url, - content: scrapeData, - timestamp: new Date() - }], - currentProject: `Clone of ${url}` - })); - - // Start sandbox creation in parallel with code generation - let sandboxPromise: Promise | null = null; - if (!sandboxData) { - addChatMessage('Creating sandbox while generating your React app...', 'system'); - sandboxPromise = createSandbox(true); - } - - addChatMessage('Analyzing and generating React recreation...', 'system'); - - const recreatePrompt = `I scraped this website and want you to recreate it as a modern React application. - -URL: ${url} - -SCRAPED CONTENT: -${scrapeData.content} - -${homeContextInput ? `ADDITIONAL CONTEXT/REQUIREMENTS FROM USER: -${homeContextInput} - -Please incorporate these requirements into the design and implementation.` : ''} - -REQUIREMENTS: -1. Create a COMPLETE React application with App.jsx as the main component -2. App.jsx MUST import and render all other components -3. Recreate the main sections and layout from the scraped content -4. ${homeContextInput ? `Apply the user's context/theme: "${homeContextInput}"` : `Use a modern dark theme with excellent contrast: - - Background: #0a0a0a - - Text: #ffffff - - Links: #60a5fa - - Accent: #3b82f6`} -5. Make it fully responsive -6. Include hover effects and smooth transitions -7. Create separate components for major sections (Header, Hero, Features, etc.) -8. Use semantic HTML5 elements - -IMPORTANT CONSTRAINTS: -- DO NOT use React Router or any routing libraries -- Use regular tags with href="#section" for navigation, NOT Link or NavLink components -- This is a single-page application, no routing needed -- ALWAYS create src/App.jsx that imports ALL components -- Each component should be in src/components/ -- Use Tailwind CSS for ALL styling (no custom CSS files) -- Make sure the app actually renders visible content -- Create ALL components that you reference in imports - -IMAGE HANDLING RULES: -- When the scraped content includes images, USE THE ORIGINAL IMAGE URLS whenever appropriate -- Keep existing images from the scraped site (logos, product images, hero images, icons, etc.) -- Use the actual image URLs provided in the scraped content, not placeholders -- Only use placeholder images or generic services when no real images are available -- For company logos and brand images, ALWAYS use the original URLs to maintain brand identity -- If scraped data contains image URLs, include them in your img tags -- Example: If you see "https://example.com/logo.png" in the scraped content, use that exact URL - -Focus on the key sections and content, making it clean and modern while preserving visual assets.`; - - setGenerationProgress(prev => ({ - isGenerating: true, - status: 'Initializing AI...', - components: [], - currentComponent: 0, - streamedCode: '', - isStreaming: true, - isThinking: false, - thinkingText: undefined, - thinkingDuration: undefined, - // Keep previous files until new ones are generated - files: prev.files || [], - currentFile: undefined, - lastProcessedPosition: 0 - })); - - // Switch to generation tab when starting - setActiveTab('generation'); - - const aiResponse = await fetch('/api/generate-ai-code-stream', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - prompt: recreatePrompt, - model: aiModel, - context: { - sandboxId: sandboxData?.id, - structure: structureContent, - conversationContext: conversationContext - } - }) - }); - - if (!aiResponse.ok) { - throw new Error(`AI generation failed: ${aiResponse.status}`); - } - - const reader = aiResponse.body?.getReader(); - const decoder = new TextDecoder(); - let generatedCode = ''; - let explanation = ''; - - if (reader) { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value); - const lines = chunk.split('\n'); - - for (const line of lines) { - if (line.startsWith('data: ')) { - try { - const data = JSON.parse(line.slice(6)); - - if (data.type === 'status') { - setGenerationProgress(prev => ({ ...prev, status: data.message })); - } else if (data.type === 'thinking') { - setGenerationProgress(prev => ({ - ...prev, - isThinking: true, - thinkingText: (prev.thinkingText || '') + data.text - })); - } else if (data.type === 'thinking_complete') { - setGenerationProgress(prev => ({ - ...prev, - isThinking: false, - thinkingDuration: data.duration - })); - } else if (data.type === 'conversation') { - // Add conversational text to chat only if it's not code - let text = data.text || ''; - - // Remove package tags from the text - text = text.replace(/[^<]*<\/package>/g, ''); - text = text.replace(/[^<]*<\/packages>/g, ''); - - // Filter out any XML tags and file content that slipped through - if (!text.includes(' ({ - ...prev, - streamedCode: prev.streamedCode + data.text, - lastProcessedPosition: prev.lastProcessedPosition || 0 - })); - } else if (data.type === 'component') { - setGenerationProgress(prev => ({ - ...prev, - status: `Generated ${data.name}`, - components: [...prev.components, { - name: data.name, - path: data.path, - completed: true - }], - currentComponent: prev.currentComponent + 1 - })); - } else if (data.type === 'complete') { - generatedCode = data.generatedCode; - explanation = data.explanation; - - // Save the last generated code - setConversationContext(prev => ({ - ...prev, - lastGeneratedCode: generatedCode - })); - } - } catch (e) { - console.error('Error parsing streaming data:', e); - } - } - } - } - } - - setGenerationProgress(prev => ({ - ...prev, - isGenerating: false, - isStreaming: false, - status: 'Generation complete!', - isEdit: prev.isEdit - })); - - if (generatedCode) { - addChatMessage('AI recreation generated!', 'system'); - - // Add the explanation to chat if available - if (explanation && explanation.trim()) { - addChatMessage(explanation, 'ai'); - } - - setPromptInput(generatedCode); - // Don't show the Generated Code panel by default - // setLeftPanelVisible(true); - - // Wait for sandbox creation if it's still in progress - if (sandboxPromise) { - addChatMessage('Waiting for sandbox to be ready...', 'system'); - try { - await sandboxPromise; - // Remove the waiting message - setChatMessages(prev => prev.filter(msg => msg.content !== 'Waiting for sandbox to be ready...')); - } catch (error: any) { - addChatMessage('Sandbox creation failed. Cannot apply code.', 'system'); - throw error; - } - } - - // First application for cloned site should not be in edit mode - await applyGeneratedCode(generatedCode, false); - - addChatMessage( - `Successfully recreated ${url} as a modern React app${homeContextInput ? ` with your requested context: "${homeContextInput}"` : ''}! The scraped content is now in my context, so you can ask me to modify specific sections or add features based on the original site.`, - 'ai', - { - scrapedUrl: url, - scrapedContent: scrapeData, - generatedCode: generatedCode - } - ); - - setUrlInput(''); - setUrlStatus([]); - setHomeContextInput(''); - - // Clear generation progress and all screenshot/design states - setGenerationProgress(prev => ({ - ...prev, - isGenerating: false, - isStreaming: false, - status: 'Generation complete!' - })); - - // Clear screenshot and preparing design states to prevent them from showing on next run - setUrlScreenshot(null); - setIsPreparingDesign(false); - setTargetUrl(''); - setScreenshotError(null); - setLoadingStage(null); // Clear loading stage - - setTimeout(() => { - // Switch back to preview tab but keep files - setActiveTab('preview'); - }, 1000); // Show completion briefly then switch - } else { - throw new Error('Failed to generate recreation'); - } - - } catch (error: any) { - addChatMessage(`Failed to clone website: ${error.message}`, 'system'); - setUrlStatus([]); - setIsPreparingDesign(false); - // Clear all states on error - setUrlScreenshot(null); - setTargetUrl(''); - setScreenshotError(null); - setLoadingStage(null); - setGenerationProgress(prev => ({ - ...prev, - isGenerating: false, - isStreaming: false, - status: '', - // Keep files to display in sidebar - files: prev.files - })); - setActiveTab('preview'); - } - }; +// const cloneWebsite = async () => { +// let url = urlInput.trim(); +// if (!url) { +// setUrlStatus(prev => [...prev, 'Please enter a URL']); +// return; +// } +// +// if (!url.match(/^https?:\/\//i)) { +// url = 'https://' + url; +// } +// +// setUrlStatus([`Using: ${url}`, 'Starting to scrape...']); +// +// setUrlOverlayVisible(false); +// +// // Remove protocol for cleaner display +// const cleanUrl = url.replace(/^https?:\/\//i, ''); +// addChatMessage(`Starting to clone ${cleanUrl}...`, 'system'); +// +// // Capture screenshot immediately and switch to preview tab +// captureUrlScreenshot(url); +// +// try { +// addChatMessage('Scraping website content...', 'system'); +// const scrapeResponse = await fetch('/api/scrape-url-enhanced', { +// method: 'POST', +// headers: { 'Content-Type': 'application/json' }, +// body: JSON.stringify({ url }) +// }); +// +// if (!scrapeResponse.ok) { +// throw new Error(`Scraping failed: ${scrapeResponse.status}`); +// } +// +// const scrapeData = await scrapeResponse.json(); +// +// if (!scrapeData.success) { +// throw new Error(scrapeData.error || 'Failed to scrape website'); +// } +// +// addChatMessage(`Scraped ${scrapeData.content.length} characters from ${url}`, 'system'); +// +// // Clear preparing design state and switch to generation tab +// setIsPreparingDesign(false); +// setActiveTab('generation'); +// +// setConversationContext(prev => ({ +// ...prev, +// scrapedWebsites: [...prev.scrapedWebsites, { +// url, +// content: scrapeData, +// timestamp: new Date() +// }], +// currentProject: `Clone of ${url}` +// })); +// +// // Start sandbox creation in parallel with code generation +// let sandboxPromise: Promise | null = null; +// if (!sandboxData) { +// addChatMessage('Creating sandbox while generating your React app...', 'system'); +// sandboxPromise = createSandbox(true); +// } +// +// addChatMessage('Analyzing and generating React recreation...', 'system'); +// +// const recreatePrompt = `I scraped this website and want you to recreate it as a modern React application. +// +// URL: ${url} +// +// SCRAPED CONTENT: +// ${scrapeData.content} +// +// ${homeContextInput ? `ADDITIONAL CONTEXT/REQUIREMENTS FROM USER: +// ${homeContextInput} +// +// Please incorporate these requirements into the design and implementation.` : ''} +// +// REQUIREMENTS: +// 1. Create a COMPLETE React application with App.jsx as the main component +// 2. App.jsx MUST import and render all other components +// 3. Recreate the main sections and layout from the scraped content +// 4. ${homeContextInput ? `Apply the user's context/theme: "${homeContextInput}"` : `Use a modern dark theme with excellent contrast: +// - Background: #0a0a0a +// - Text: #ffffff +// - Links: #60a5fa +// - Accent: #3b82f6`} +// 5. Make it fully responsive +// 6. Include hover effects and smooth transitions +// 7. Create separate components for major sections (Header, Hero, Features, etc.) +// 8. Use semantic HTML5 elements +// +// IMPORTANT CONSTRAINTS: +// - DO NOT use React Router or any routing libraries +// - Use regular tags with href="#section" for navigation, NOT Link or NavLink components +// - This is a single-page application, no routing needed +// - ALWAYS create src/App.jsx that imports ALL components +// - Each component should be in src/components/ +// - Use Tailwind CSS for ALL styling (no custom CSS files) +// - Make sure the app actually renders visible content +// - Create ALL components that you reference in imports +// +// IMAGE HANDLING RULES: +// - When the scraped content includes images, USE THE ORIGINAL IMAGE URLS whenever appropriate +// - Keep existing images from the scraped site (logos, product images, hero images, icons, etc.) +// - Use the actual image URLs provided in the scraped content, not placeholders +// - Only use placeholder images or generic services when no real images are available +// - For company logos and brand images, ALWAYS use the original URLs to maintain brand identity +// - If scraped data contains image URLs, include them in your img tags +// - Example: If you see "https://example.com/logo.png" in the scraped content, use that exact URL +// +// Focus on the key sections and content, making it clean and modern while preserving visual assets.`; +// +// setGenerationProgress(prev => ({ +// isGenerating: true, +// status: 'Initializing AI...', +// components: [], +// currentComponent: 0, +// streamedCode: '', +// isStreaming: true, +// isThinking: false, +// thinkingText: undefined, +// thinkingDuration: undefined, +// // Keep previous files until new ones are generated +// files: prev.files || [], +// currentFile: undefined, +// lastProcessedPosition: 0 +// })); +// +// // Switch to generation tab when starting +// setActiveTab('generation'); +// +// const aiResponse = await fetch('/api/generate-ai-code-stream', { +// method: 'POST', +// headers: { 'Content-Type': 'application/json' }, +// body: JSON.stringify({ +// prompt: recreatePrompt, +// model: aiModel, +// context: { +// sandboxId: sandboxData?.id, +// structure: structureContent, +// conversationContext: conversationContext +// } +// }) +// }); +// +// if (!aiResponse.ok) { +// throw new Error(`AI generation failed: ${aiResponse.status}`); +// } +// +// const reader = aiResponse.body?.getReader(); +// const decoder = new TextDecoder(); +// let generatedCode = ''; +// let explanation = ''; +// +// if (reader) { +// while (true) { +// const { done, value } = await reader.read(); +// if (done) break; +// +// const chunk = decoder.decode(value); +// const lines = chunk.split('\n'); +// +// for (const line of lines) { +// if (line.startsWith('data: ')) { +// try { +// const data = JSON.parse(line.slice(6)); +// +// if (data.type === 'status') { +// setGenerationProgress(prev => ({ ...prev, status: data.message })); +// } else if (data.type === 'thinking') { +// setGenerationProgress(prev => ({ +// ...prev, +// isThinking: true, +// thinkingText: (prev.thinkingText || '') + data.text +// })); +// } else if (data.type === 'thinking_complete') { +// setGenerationProgress(prev => ({ +// ...prev, +// isThinking: false, +// thinkingDuration: data.duration +// })); +// } else if (data.type === 'conversation') { +// // Add conversational text to chat only if it's not code +// let text = data.text || ''; +// +// // Remove package tags from the text +// text = text.replace(/[^<]*<\/package>/g, ''); +// text = text.replace(/[^<]*<\/packages>/g, ''); +// +// // Filter out any XML tags and file content that slipped through +// if (!text.includes(' ({ +// ...prev, +// streamedCode: prev.streamedCode + data.text, +// lastProcessedPosition: prev.lastProcessedPosition || 0 +// })); +// } else if (data.type === 'component') { +// setGenerationProgress(prev => ({ +// ...prev, +// status: `Generated ${data.name}`, +// components: [...prev.components, { +// name: data.name, +// path: data.path, +// completed: true +// }], +// currentComponent: prev.currentComponent + 1 +// })); +// } else if (data.type === 'complete') { +// generatedCode = data.generatedCode; +// explanation = data.explanation; +// +// // Save the last generated code +// setConversationContext(prev => ({ +// ...prev, +// lastGeneratedCode: generatedCode +// })); +// } +// } catch (e) { +// console.error('Error parsing streaming data:', e); +// } +// } +// } +// } +// } +// +// setGenerationProgress(prev => ({ +// ...prev, +// isGenerating: false, +// isStreaming: false, +// status: 'Generation complete!', +// isEdit: prev.isEdit +// })); +// +// if (generatedCode) { +// addChatMessage('AI recreation generated!', 'system'); +// +// // Add the explanation to chat if available +// if (explanation && explanation.trim()) { +// addChatMessage(explanation, 'ai'); +// } +// +// setPromptInput(generatedCode); +// // Don't show the Generated Code panel by default +// // setLeftPanelVisible(true); +// +// // Wait for sandbox creation if it's still in progress +// let activeSandboxData = sandboxData; +// if (sandboxPromise) { +// addChatMessage('Waiting for sandbox to be ready...', 'system'); +// try { +// const newSandboxData = await sandboxPromise; +// if (newSandboxData) { +// activeSandboxData = newSandboxData; +// } +// // Remove the waiting message +// setChatMessages(prev => prev.filter(msg => msg.content !== 'Waiting for sandbox to be ready...')); +// } catch (error: any) { +// addChatMessage('Sandbox creation failed. Cannot apply code.', 'system'); +// throw error; +// } +// } +// +// // Only apply code if we have sandbox data +// if (activeSandboxData) { +// // First application for cloned site should not be in edit mode +// await applyGeneratedCode(generatedCode, false); +// } +// +// addChatMessage( +// `Successfully recreated ${url} as a modern React app${homeContextInput ? ` with your requested context: "${homeContextInput}"` : ''}! The scraped content is now in my context, so you can ask me to modify specific sections or add features based on the original site.`, +// 'ai', +// { +// scrapedUrl: url, +// scrapedContent: scrapeData, +// generatedCode: generatedCode +// } +// ); +// +// setUrlInput(''); +// setUrlStatus([]); +// setHomeContextInput(''); +// +// // Clear generation progress and all screenshot/design states +// setGenerationProgress(prev => ({ +// ...prev, +// isGenerating: false, +// isStreaming: false, +// status: 'Generation complete!' +// })); +// +// // Clear screenshot and preparing design states to prevent them from showing on next run +// setUrlScreenshot(null); +// setIsPreparingDesign(false); +// setTargetUrl(''); +// setScreenshotError(null); +// setLoadingStage(null); // Clear loading stage +// setShowLoadingBackground(false); // Clear loading background +// +// setTimeout(() => { +// // Switch back to preview tab but keep files +// setActiveTab('preview'); +// }, 1000); // Show completion briefly then switch +// } else { +// throw new Error('Failed to generate recreation'); +// } +// +// } catch (error: any) { +// addChatMessage(`Failed to clone website: ${error.message}`, 'system'); +// setUrlStatus([]); +// setIsPreparingDesign(false); +// // Clear all states on error +// setUrlScreenshot(null); +// setTargetUrl(''); +// setScreenshotError(null); +// setLoadingStage(null); +// setGenerationProgress(prev => ({ +// ...prev, +// isGenerating: false, +// isStreaming: false, +// status: '', +// // Keep files to display in sidebar +// files: prev.files +// })); +// setActiveTab('preview'); +// } +// }; const captureUrlScreenshot = async (url: string) => { setIsCapturingScreenshot(true); @@ -2506,6 +2592,9 @@ Focus on the key sections and content, making it clean and modern while preservi setHomeScreenFading(true); + // Set loading background to ensure proper visual feedback + setShowLoadingBackground(true); + // Clear messages and immediately show the cloning message setChatMessages([]); let displayUrl = homeUrlInput.trim(); @@ -2517,19 +2606,17 @@ Focus on the key sections and content, making it clean and modern while preservi addChatMessage(`Starting to clone ${cleanUrl}...`, 'system'); // Start creating sandbox and capturing screenshot immediately in parallel - const sandboxPromise = !sandboxData ? createSandbox(true) : Promise.resolve(); - - // Only capture screenshot if we don't already have a sandbox (first generation) - // After sandbox is set up, skip the screenshot phase for faster generation - if (!sandboxData) { - captureUrlScreenshot(displayUrl); - } + const sandboxPromise = !sandboxData ? createSandbox(true) : Promise.resolve(null); // Set loading stage immediately before hiding home screen setLoadingStage('gathering'); // Also ensure we're on preview tab to show the loading overlay setActiveTab('preview'); + // Always capture screenshot for new URLs, even if sandbox exists + // This ensures the loading screen shows properly + captureUrlScreenshot(displayUrl); + setTimeout(async () => { setShowHomeScreen(false); setHomeScreenFading(false); @@ -2867,6 +2954,7 @@ Focus on the key sections and content, making it clean and modern.`; setTargetUrl(''); setScreenshotError(null); setLoadingStage(null); // Clear loading stage + setShowLoadingBackground(false); // Clear loading background setTimeout(() => { // Switch back to preview tab but keep files @@ -3235,6 +3323,7 @@ Focus on the key sections and content, making it clean and modern.`;
{/* Site info with favicon */}
+ {/* eslint-disable-next-line @next/next/no-img-element */} {siteName} + {/* eslint-disable-next-line @next/next/no-img-element */} {`${siteName} @@ -3382,7 +3472,7 @@ Focus on the key sections and content, making it clean and modern.`; {generationProgress.files.map((file, fileIdx) => (
(
@@ -3455,7 +3545,7 @@ Focus on the key sections and content, making it clean and modern.`;
-
+
- - - - -
+ )} ); } \ No newline at end of file diff --git a/components/HMRErrorDetector.tsx b/components/HMRErrorDetector.tsx index 1b0d011..46f232b 100644 --- a/components/HMRErrorDetector.tsx +++ b/components/HMRErrorDetector.tsx @@ -47,7 +47,7 @@ export default function HMRErrorDetector({ iframeRef, onErrorDetected }: HMRErro } } } - } catch (error) { + } catch { // Cross-origin errors are expected, ignore them } }; diff --git a/components/HeroInput.tsx b/components/HeroInput.tsx index 4e11d7c..2d88944 100644 --- a/components/HeroInput.tsx +++ b/components/HeroInput.tsx @@ -17,7 +17,7 @@ export default function HeroInput({ placeholder = "Describe what you want to build...", className = "" }: HeroInputProps) { - const [isFocused, setIsFocused] = useState(false); + // const [isFocused, setIsFocused] = useState(false); // Reserved for future focus effects const textareaRef = useRef(null); // Reset textarea height when value changes (especially when cleared) diff --git a/components/app/(home)/sections/ai-readiness/ControlPanel.tsx b/components/app/(home)/sections/ai-readiness/ControlPanel.tsx index dce0cc4..c80cda9 100644 --- a/components/app/(home)/sections/ai-readiness/ControlPanel.tsx +++ b/components/app/(home)/sections/ai-readiness/ControlPanel.tsx @@ -6,10 +6,10 @@ import { FileText, Code, Shield, - Search, + // Search, // Not used in current implementation Zap, Database, - Lock, + // Lock, // Not used in current implementation CheckCircle2, XCircle, Loader2, @@ -54,7 +54,7 @@ export default function ControlPanel({ analysisData, onReset, }: ControlPanelProps) { - const [showAIAnalysis, setShowAIAnalysis] = useState(false); + // const [showAIAnalysis, setShowAIAnalysis] = useState(false); // Reserved for AI analysis feature const [aiInsights, setAiInsights] = useState([]); const [isAnalyzingAI, setIsAnalyzingAI] = useState(false); const [combinedChecks, setCombinedChecks] = useState([]); @@ -298,6 +298,7 @@ export default function ControlPanel({ return () => clearInterval(checkInterval); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAnalyzing, showResults, analysisData]); useEffect(() => { @@ -341,6 +342,8 @@ export default function ControlPanel({ } }; + // Utility function available but not used in current render + // eslint-disable-next-line @typescript-eslint/no-unused-vars const getScoreColor = (score: number) => { if (score >= 80) return "text-accent-black"; if (score >= 60) return "text-accent-black"; diff --git a/components/app/(home)/sections/ai-readiness/InlineResults.tsx b/components/app/(home)/sections/ai-readiness/InlineResults.tsx index bd812e3..6f5ccb5 100644 --- a/components/app/(home)/sections/ai-readiness/InlineResults.tsx +++ b/components/app/(home)/sections/ai-readiness/InlineResults.tsx @@ -1,7 +1,8 @@ "use client"; import { motion, AnimatePresence } from "framer-motion"; -import { Check, X, Zap, FileText, Shield, Globe, Code, Sparkles, AlertCircle } from "lucide-react"; +import { Check, X, FileText, Globe, Code, Sparkles, AlertCircle } from "lucide-react"; +// import { Zap, Shield } from "lucide-react"; // Reserved for future features import { useEffect, useState } from "react"; interface InlineResultsProps { @@ -35,7 +36,7 @@ export default function InlineResults({ isAnalyzing, showResults, analysisStep, - url, + url: _url, // URL prop available but not used in current implementation onReset, }: InlineResultsProps) { const [displayScore, setDisplayScore] = useState(0); diff --git a/components/app/(home)/sections/ai-readiness/MetricBars.tsx b/components/app/(home)/sections/ai-readiness/MetricBars.tsx index 787f1c7..80b7114 100644 --- a/components/app/(home)/sections/ai-readiness/MetricBars.tsx +++ b/components/app/(home)/sections/ai-readiness/MetricBars.tsx @@ -27,7 +27,7 @@ export default function MetricBars({ metrics }: MetricBarsProps) { return 'bg-heat-20'; }; - const getBulletColor = (score: number) => { + const getBulletColor = (_score: number) => { // Always use heat-100 for all bullets for consistency return 'bg-heat-100'; }; diff --git a/components/app/(home)/sections/ai-readiness/ScoreChart.tsx b/components/app/(home)/sections/ai-readiness/ScoreChart.tsx index 32e237f..1daa90e 100644 --- a/components/app/(home)/sections/ai-readiness/ScoreChart.tsx +++ b/components/app/(home)/sections/ai-readiness/ScoreChart.tsx @@ -1,7 +1,7 @@ "use client"; import { motion } from "framer-motion"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; interface ScoreChartProps { score: number; diff --git a/lib/file-search-executor.ts b/lib/file-search-executor.ts index 01803a4..7ed21cf 100644 --- a/lib/file-search-executor.ts +++ b/lib/file-search-executor.ts @@ -92,7 +92,7 @@ export function executeSearchPlan( matchedPattern = pattern; break; } - } catch (e) { + } catch { console.warn(`[file-search] Invalid regex pattern: ${pattern}`); } } diff --git a/lib/sandbox/providers/e2b-provider.ts b/lib/sandbox/providers/e2b-provider.ts index a2c2ea3..cd62890 100644 --- a/lib/sandbox/providers/e2b-provider.ts +++ b/lib/sandbox/providers/e2b-provider.ts @@ -1,10 +1,30 @@ import { Sandbox } from '@e2b/code-interpreter'; -import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types'; +import { SandboxProvider, SandboxInfo, CommandResult } from '../types'; +// SandboxProviderConfig available through parent class import { appConfig } from '@/config/app.config'; export class E2BProvider extends SandboxProvider { private existingFiles: Set = new Set(); + /** + * Attempt to reconnect to an existing E2B sandbox + */ + async reconnect(sandboxId: string): Promise { + try { + console.log(`[E2BProvider] Attempting to reconnect to sandbox ${sandboxId}...`); + + // Try to connect to existing sandbox + // Note: E2B SDK doesn't directly support reconnection, but we can try to recreate + // For now, return false to indicate reconnection isn't supported + // In the future, E2B may add this capability + + return false; + } catch (error) { + console.error(`[E2BProvider] Failed to reconnect to sandbox ${sandboxId}:`, error); + return false; + } + } + async createSandbox(): Promise { try { console.log('[E2BProvider] Creating sandbox...'); @@ -274,7 +294,7 @@ export default defineConfig({ port: 5173, strictPort: true, hmr: false, - allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1'] + allowedHosts: ['.e2b.app', '.e2b.dev', '.vercel.run', 'localhost', '127.0.0.1'] } })""" diff --git a/lib/sandbox/providers/vercel-provider.ts b/lib/sandbox/providers/vercel-provider.ts index 37e5d76..4810179 100644 --- a/lib/sandbox/providers/vercel-provider.ts +++ b/lib/sandbox/providers/vercel-provider.ts @@ -1,5 +1,6 @@ import { Sandbox } from '@vercel/sandbox'; -import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types'; +import { SandboxProvider, SandboxInfo, CommandResult } from '../types'; +// SandboxProviderConfig available through parent class export class VercelProvider extends SandboxProvider { private existingFiles: Set = new Set(); @@ -47,10 +48,22 @@ export class VercelProvider extends SandboxProvider { console.log('[VercelProvider] Available env vars:', Object.keys(process.env).filter(k => k.startsWith('VERCEL'))); } + console.log('[VercelProvider] Creating sandbox with config:', { + runtime: sandboxConfig.runtime, + timeout: sandboxConfig.timeout, + ports: sandboxConfig.ports, + hasTeamId: !!sandboxConfig.teamId, + hasProjectId: !!sandboxConfig.projectId, + hasToken: !!sandboxConfig.token + }); + this.sandbox = await Sandbox.create(sandboxConfig); const sandboxId = this.sandbox.sandboxId; - console.log(`[VercelProvider] Sandbox created: ${sandboxId}`); + console.log(`[VercelProvider] Sandbox created successfully:`, { + sandboxId: sandboxId, + status: this.sandbox.status + }); // Get the sandbox URL using the correct Vercel Sandbox API const sandboxUrl = this.sandbox.domain(5173); @@ -88,7 +101,7 @@ export class VercelProvider extends SandboxProvider { const result = await this.sandbox.runCommand({ cmd: cmd, args: args, - cwd: '/app', + cwd: '/vercel/sandbox', env: {} }); @@ -113,28 +126,61 @@ export class VercelProvider extends SandboxProvider { throw new Error('No active sandbox'); } - const fullPath = path.startsWith('/') ? path : `/app/${path}`; + // Vercel sandbox default working directory is /vercel/sandbox + const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`; - // Based on PR, Vercel SDK has a writeFiles method that takes an array + console.log(`[VercelProvider] writeFile called:`, { + originalPath: path, + fullPath: fullPath, + contentLength: content.length, + contentPreview: content.substring(0, 100) + (content.length > 100 ? '...' : ''), + sandboxId: this.sandbox.sandboxId, + sandboxStatus: this.sandbox.status + }); + + // Based on Vercel SDK docs, writeFiles expects path and Buffer content try { + const buffer = Buffer.from(content, 'utf-8'); + console.log(`[VercelProvider] Calling sandbox.writeFiles with:`, { + path: fullPath, + bufferLength: buffer.length, + isBuffer: Buffer.isBuffer(buffer) + }); + await this.sandbox.writeFiles([{ path: fullPath, - content: Buffer.from(content) + content: buffer }]); - console.log(`[VercelProvider] Written: ${fullPath}`); + console.log(`[VercelProvider] Successfully 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`); + } catch (writeError: any) { + // Log detailed error information + console.error(`[VercelProvider] writeFiles failed for ${fullPath}:`, { + error: writeError, + message: writeError?.message, + response: writeError?.response, + statusCode: writeError?.response?.status, + responseData: writeError?.response?.data + }); + + // Fallback to command-based approach if writeFiles fails + console.log(`[VercelProvider] Attempting command fallback for ${fullPath}`); // Ensure directory exists const dir = fullPath.substring(0, fullPath.lastIndexOf('/')); - await this.sandbox.runCommand({ - cmd: 'mkdir', - args: ['-p', dir], - cwd: '/' - }); + if (dir) { + console.log(`[VercelProvider] Creating directory: ${dir}`); + const mkdirResult = await this.sandbox.runCommand({ + cmd: 'mkdir', + args: ['-p', dir] + }); + console.log(`[VercelProvider] mkdir result:`, { + exitCode: mkdirResult.exitCode, + stdout: mkdirResult.stdout, + stderr: mkdirResult.stderr + }); + } // Write file using echo and redirection const escapedContent = content @@ -144,14 +190,24 @@ export class VercelProvider extends SandboxProvider { .replace(/`/g, '\\`') .replace(/\n/g, '\\n'); - await this.sandbox.runCommand({ + console.log(`[VercelProvider] Writing file via echo command to: ${fullPath}`); + const writeResult = await this.sandbox.runCommand({ cmd: 'sh', - args: ['-c', `echo "${escapedContent}" > ${fullPath}`], - cwd: '/' + args: ['-c', `echo "${escapedContent}" > "${fullPath}"`] }); - console.log(`[VercelProvider] Written via command: ${fullPath}`); - this.existingFiles.add(path); + console.log(`[VercelProvider] Write command result:`, { + exitCode: writeResult.exitCode, + stdout: writeResult.stdout, + stderr: writeResult.stderr + }); + + if (writeResult.exitCode === 0) { + console.log(`[VercelProvider] Successfully written via command: ${fullPath}`); + this.existingFiles.add(path); + } else { + throw new Error(`Failed to write file via command: ${writeResult.stderr}`); + } } } @@ -160,12 +216,12 @@ export class VercelProvider extends SandboxProvider { throw new Error('No active sandbox'); } - const fullPath = path.startsWith('/') ? path : `/app/${path}`; + // Vercel sandbox default working directory is /vercel/sandbox + const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`; const result = await this.sandbox.runCommand({ cmd: 'cat', - args: [fullPath], - cwd: '/' + args: [fullPath] }); if (result.exitCode !== 0) { @@ -175,7 +231,7 @@ export class VercelProvider extends SandboxProvider { return result.stdout || ''; } - async listFiles(directory: string = '/app'): Promise { + async listFiles(directory: string = '/vercel/sandbox'): Promise { if (!this.sandbox) { throw new Error('No active sandbox'); } @@ -212,7 +268,7 @@ export class VercelProvider extends SandboxProvider { const result = await this.sandbox.runCommand({ cmd: 'npm', args: args, - cwd: '/app' + cwd: '/vercel/sandbox' }); // Restart Vite if configured and successful @@ -234,12 +290,21 @@ export class VercelProvider extends SandboxProvider { } console.log('[VercelProvider] Setting up Vite React app...'); + console.log('[VercelProvider] Sandbox details:', { + sandboxId: this.sandbox.sandboxId, + status: this.sandbox.status + }); // Create directory structure - await this.sandbox.runCommand({ + console.log('[VercelProvider] Creating directory structure...'); + const mkdirResult = await this.sandbox.runCommand({ cmd: 'mkdir', - args: ['-p', '/app/src'], - cwd: '/' + args: ['-p', '/vercel/sandbox/src'] + }); + console.log('[VercelProvider] mkdir /vercel/sandbox/src result:', { + exitCode: mkdirResult.exitCode, + stdout: mkdirResult.stdout, + stderr: mkdirResult.stderr }); // Create package.json @@ -265,6 +330,7 @@ export class VercelProvider extends SandboxProvider { } }; + console.log('[VercelProvider] Writing package.json...'); await this.writeFile('package.json', JSON.stringify(packageJson, null, 2)); // Create vite.config.js @@ -277,6 +343,11 @@ export default defineConfig({ host: '0.0.0.0', port: 5173, strictPort: true, + allowedHosts: [ + '.vercel.run', // Allow all Vercel sandbox domains + '.e2b.dev', // Allow all E2B sandbox domains + 'localhost' + ], hmr: { clientPort: 443, protocol: 'wss' @@ -375,11 +446,18 @@ body { // Install dependencies console.log('[VercelProvider] Installing dependencies...'); + console.log('[VercelProvider] Running npm install in /vercel/sandbox'); try { const installResult = await this.sandbox.runCommand({ cmd: 'npm', args: ['install'], - cwd: '/app' + cwd: '/vercel/sandbox' + }); + + console.log('[VercelProvider] npm install result:', { + exitCode: installResult.exitCode, + stdout: typeof installResult.stdout === 'function' ? 'function' : installResult.stdout, + stderr: typeof installResult.stderr === 'function' ? 'function' : installResult.stderr }); if (installResult.exitCode === 0) { @@ -388,14 +466,18 @@ body { console.warn('[VercelProvider] npm install had issues:', installResult.stderr); } } catch (error: any) { - console.error('[VercelProvider] npm install error:', error); + console.error('[VercelProvider] npm install error:', { + message: error?.message, + response: error?.response?.status, + responseText: error?.text + }); // Try alternative approach - run as shell command console.log('[VercelProvider] Trying alternative npm install approach...'); try { const altResult = await this.sandbox.runCommand({ cmd: 'sh', - args: ['-c', 'cd /app && npm install'], - cwd: '/' + args: ['-c', 'cd /vercel/sandbox && npm install'], + cwd: '/vercel/sandbox' }); if (altResult.exitCode === 0) { console.log('[VercelProvider] Dependencies installed successfully (alternative method)'); @@ -422,7 +504,7 @@ body { await this.sandbox.runCommand({ cmd: 'sh', args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'], - cwd: '/app' + cwd: '/vercel/sandbox' }); console.log('[VercelProvider] Vite dev server started'); @@ -462,7 +544,7 @@ body { await this.sandbox.runCommand({ cmd: 'sh', args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'], - cwd: '/app' + cwd: '/vercel/sandbox' }); console.log('[VercelProvider] Vite restarted'); diff --git a/lib/sandbox/sandbox-manager.ts b/lib/sandbox/sandbox-manager.ts new file mode 100644 index 0000000..ef02fdf --- /dev/null +++ b/lib/sandbox/sandbox-manager.ts @@ -0,0 +1,177 @@ +import { SandboxProvider } from './types'; +import { SandboxFactory } from './factory'; + +interface SandboxInfo { + sandboxId: string; + provider: SandboxProvider; + createdAt: Date; + lastAccessed: Date; +} + +class SandboxManager { + private sandboxes: Map = new Map(); + private activeSandboxId: string | null = null; + + /** + * Get or create a sandbox provider for the given sandbox ID + */ + async getOrCreateProvider(sandboxId: string): Promise { + // Check if we already have this sandbox + const existing = this.sandboxes.get(sandboxId); + if (existing) { + existing.lastAccessed = new Date(); + return existing.provider; + } + + // Try to reconnect to existing sandbox + console.log(`[SandboxManager] Attempting to reconnect to sandbox ${sandboxId}`); + + try { + const provider = SandboxFactory.create(); + + // For E2B provider, try to reconnect + if (provider.constructor.name === 'E2BProvider') { + // E2B sandboxes can be reconnected using the sandbox ID + const reconnected = await (provider as any).reconnect(sandboxId); + if (reconnected) { + this.sandboxes.set(sandboxId, { + sandboxId, + provider, + createdAt: new Date(), + lastAccessed: new Date() + }); + this.activeSandboxId = sandboxId; + console.log(`[SandboxManager] Successfully reconnected to sandbox ${sandboxId}`); + return provider; + } + } + + // For Vercel or if reconnection failed, return the new provider + // The caller will need to handle creating a new sandbox + console.log(`[SandboxManager] Could not reconnect to ${sandboxId}, returning new provider`); + return provider; + } catch (error) { + console.error(`[SandboxManager] Error reconnecting to sandbox ${sandboxId}:`, error); + throw error; + } + } + + /** + * Register a new sandbox + */ + registerSandbox(sandboxId: string, provider: SandboxProvider): void { + this.sandboxes.set(sandboxId, { + sandboxId, + provider, + createdAt: new Date(), + lastAccessed: new Date() + }); + this.activeSandboxId = sandboxId; + console.log(`[SandboxManager] Registered sandbox ${sandboxId}`); + } + + /** + * Get the active sandbox provider + */ + getActiveProvider(): SandboxProvider | null { + if (!this.activeSandboxId) { + return null; + } + + const sandbox = this.sandboxes.get(this.activeSandboxId); + if (sandbox) { + sandbox.lastAccessed = new Date(); + return sandbox.provider; + } + + return null; + } + + /** + * Get a specific sandbox provider + */ + getProvider(sandboxId: string): SandboxProvider | null { + const sandbox = this.sandboxes.get(sandboxId); + if (sandbox) { + sandbox.lastAccessed = new Date(); + return sandbox.provider; + } + return null; + } + + /** + * Set the active sandbox + */ + setActiveSandbox(sandboxId: string): boolean { + if (this.sandboxes.has(sandboxId)) { + this.activeSandboxId = sandboxId; + return true; + } + return false; + } + + /** + * Terminate a sandbox + */ + async terminateSandbox(sandboxId: string): Promise { + const sandbox = this.sandboxes.get(sandboxId); + if (sandbox) { + try { + await sandbox.provider.terminate(); + } catch (error) { + console.error(`[SandboxManager] Error terminating sandbox ${sandboxId}:`, error); + } + this.sandboxes.delete(sandboxId); + + if (this.activeSandboxId === sandboxId) { + this.activeSandboxId = null; + } + } + } + + /** + * Terminate all sandboxes + */ + async terminateAll(): Promise { + const promises = Array.from(this.sandboxes.values()).map(sandbox => + sandbox.provider.terminate().catch(err => + console.error(`[SandboxManager] Error terminating sandbox ${sandbox.sandboxId}:`, err) + ) + ); + + await Promise.all(promises); + this.sandboxes.clear(); + this.activeSandboxId = null; + } + + /** + * Clean up old sandboxes (older than maxAge milliseconds) + */ + async cleanup(maxAge: number = 3600000): Promise { + const now = new Date(); + const toDelete: string[] = []; + + for (const [id, info] of this.sandboxes.entries()) { + const age = now.getTime() - info.lastAccessed.getTime(); + if (age > maxAge) { + toDelete.push(id); + } + } + + for (const id of toDelete) { + await this.terminateSandbox(id); + console.log(`[SandboxManager] Cleaned up old sandbox ${id}`); + } + } +} + +// Export singleton instance +export const sandboxManager = new SandboxManager(); + +// Also maintain backward compatibility with global state +declare global { + var sandboxManager: SandboxManager; +} + +// Ensure the global reference points to our singleton +global.sandboxManager = sandboxManager; \ No newline at end of file diff --git a/styles/components/code.css b/styles/components/code.css index 43232e7..aa85f75 100644 --- a/styles/components/code.css +++ b/styles/components/code.css @@ -38,8 +38,7 @@ code:not(.language-html) .function, } .linenumber { - width: 48px; padding: 0; + color:white!important; font-style: normal; - @apply !text-black-alpha-12 !pl-20 !pr-0 !text-left; }