update vercel sandbox support
This commit is contained in:
@@ -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<string, any>)
|
||||
.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})`;
|
||||
})
|
||||
|
||||
@@ -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,30 +295,34 @@ export async function POST(request: NextRequest) {
|
||||
global.existingFiles = new Set<string>();
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// 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
|
||||
// Fall back to global state if not found in manager
|
||||
if (!provider) {
|
||||
provider = global.activeSandboxProvider;
|
||||
}
|
||||
|
||||
// If we have a sandboxId but no provider, try to get or create one
|
||||
if (!provider && sandboxId) {
|
||||
console.log(`[apply-ai-code-stream] Provider not in this instance for sandbox ${sandboxId}, checking existing data...`);
|
||||
console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`);
|
||||
|
||||
// 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();
|
||||
provider = await sandboxManager.getOrCreateProvider(sandboxId);
|
||||
|
||||
// Update the global state
|
||||
// If we got a new provider (not reconnected), we need to create a new sandbox
|
||||
if (!provider.getSandboxInfo()) {
|
||||
console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`);
|
||||
await provider.createSandbox();
|
||||
await provider.setupViteApp();
|
||||
sandboxManager.registerSandbox(sandboxId, provider);
|
||||
}
|
||||
|
||||
// Update legacy global state
|
||||
global.activeSandboxProvider = provider;
|
||||
console.log(`[apply-ai-code-stream] Created new provider for sandbox ${sandboxId}`);
|
||||
console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`);
|
||||
} catch (providerError) {
|
||||
console.error(`[apply-ai-code-stream] Failed to create provider for sandbox ${sandboxId}:`, 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.`,
|
||||
@@ -334,7 +339,6 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have a provider, create a new one
|
||||
if (!provider) {
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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<string, FileInfo>): 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
if (!scrapeResult.success) {
|
||||
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');
|
||||
}
|
||||
|
||||
if (!scrapeResult.data?.screenshot) {
|
||||
throw new Error('Screenshot not available in response');
|
||||
}
|
||||
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));
|
||||
|
||||
// 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);
|
||||
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');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[scrape-screenshot] Screenshot capture error:', error);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
+519
-429
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
// useState not currently used but kept for future interactivity
|
||||
import Link from "next/link";
|
||||
|
||||
// Import shared components
|
||||
import { HeaderProvider } from "@/components/shared/header/HeaderContext";
|
||||
import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit";
|
||||
// import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit"; // Not used in current implementation
|
||||
import HeaderWrapper from "@/components/shared/header/Wrapper/Wrapper";
|
||||
import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/Wrapper";
|
||||
import ButtonUI from "@/components/ui/shadcn/button";
|
||||
|
||||
+2
-5
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
@@ -3649,11 +3650,7 @@ Focus on the key sections and content, making it clean and modern.`;
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
)}
|
||||
</HeaderProvider>
|
||||
);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export default function HMRErrorDetector({ iframeRef, onErrorDetected }: HMRErro
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Cross-origin errors are expected, ignore them
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<HTMLTextAreaElement>(null);
|
||||
|
||||
// Reset textarea height when value changes (especially when cleared)
|
||||
|
||||
@@ -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<CheckItem[]>([]);
|
||||
const [isAnalyzingAI, setIsAnalyzingAI] = useState(false);
|
||||
const [combinedChecks, setCombinedChecks] = useState<CheckItem[]>([]);
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -92,7 +92,7 @@ export function executeSearchPlan(
|
||||
matchedPattern = pattern;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.warn(`[file-search] Invalid regex pattern: ${pattern}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> = new Set();
|
||||
|
||||
/**
|
||||
* Attempt to reconnect to an existing E2B sandbox
|
||||
*/
|
||||
async reconnect(sandboxId: string): Promise<boolean> {
|
||||
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<SandboxInfo> {
|
||||
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']
|
||||
}
|
||||
})"""
|
||||
|
||||
|
||||
@@ -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<string> = 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({
|
||||
if (dir) {
|
||||
console.log(`[VercelProvider] Creating directory: ${dir}`);
|
||||
const mkdirResult = await this.sandbox.runCommand({
|
||||
cmd: 'mkdir',
|
||||
args: ['-p', dir],
|
||||
cwd: '/'
|
||||
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}`);
|
||||
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<string[]> {
|
||||
async listFiles(directory: string = '/vercel/sandbox'): Promise<string[]> {
|
||||
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');
|
||||
|
||||
@@ -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<string, SandboxInfo> = new Map();
|
||||
private activeSandboxId: string | null = null;
|
||||
|
||||
/**
|
||||
* Get or create a sandbox provider for the given sandbox ID
|
||||
*/
|
||||
async getOrCreateProvider(sandboxId: string): Promise<SandboxProvider> {
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user