This commit is contained in:
Developers Digest
2025-08-08 09:04:33 -04:00
parent 0e883102ed
commit 1629e12079
73 changed files with 24502 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# REQUIRED - Sandboxes for code execution
# Get yours at https://e2b.dev
E2B_API_KEY=your_e2b_api_key_here
# REQUIRED - Web scraping for cloning websites
# Get yours at https://firecrawl.dev
FIRECRAWL_API_KEY=your_firecrawl_api_key_here
# OPTIONAL - AI Providers (need at least one)
# Get yours at https://console.anthropic.com
ANTHROPIC_API_KEY=your_anthropic_api_key_here
# Get yours at https://platform.openai.com
OPENAI_API_KEY=your_openai_api_key_here
# Get yours at https://console.groq.com
GROQ_API_KEY=your_groq_api_key_here
+58
View File
@@ -0,0 +1,58 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
**/node_modules/
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
.env.local
!.env.example
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# E2B template builds
*.tar.gz
e2b-template-*
# IDE
.vscode/
.idea/
# Temporary files
*.tmp
*.temp
repomix-output.txt
bun.lockb
View File
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+177
View File
@@ -0,0 +1,177 @@
import { NextRequest, NextResponse } from 'next/server';
import { createGroq } from '@ai-sdk/groq';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from '@ai-sdk/openai';
import { generateObject } from 'ai';
import { z } from 'zod';
import type { FileManifest } from '@/types/file-manifest';
const groq = createGroq({
apiKey: process.env.GROQ_API_KEY,
});
const anthropic = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1',
});
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL,
});
// Schema for the AI's search plan - not file selection!
const searchPlanSchema = z.object({
editType: z.enum([
'UPDATE_COMPONENT',
'ADD_FEATURE',
'FIX_ISSUE',
'UPDATE_STYLE',
'REFACTOR',
'ADD_DEPENDENCY',
'REMOVE_ELEMENT'
]).describe('The type of edit being requested'),
reasoning: z.string().describe('Explanation of the search strategy'),
searchTerms: z.array(z.string()).describe('Specific text to search for (case-insensitive). Be VERY specific - exact button text, class names, etc.'),
regexPatterns: z.array(z.string()).optional().describe('Regex patterns for finding code structures (e.g., "className=[\\"\\\'].*header.*[\\"\\\']")'),
fileTypesToSearch: z.array(z.string()).default(['.jsx', '.tsx', '.js', '.ts']).describe('File extensions to search'),
expectedMatches: z.number().min(1).max(10).default(1).describe('Expected number of matches (helps validate search worked)'),
fallbackSearch: z.object({
terms: z.array(z.string()),
patterns: z.array(z.string()).optional()
}).optional().describe('Backup search if primary fails')
});
export async function POST(request: NextRequest) {
try {
const { prompt, manifest, model = 'openai/gpt-oss-20b' } = await request.json();
console.log('[analyze-edit-intent] Request received');
console.log('[analyze-edit-intent] Prompt:', prompt);
console.log('[analyze-edit-intent] Model:', model);
console.log('[analyze-edit-intent] Manifest files count:', manifest?.files ? Object.keys(manifest.files).length : 0);
if (!prompt || !manifest) {
return NextResponse.json({
error: 'prompt and manifest are required'
}, { status: 400 });
}
// Create a summary of available files for the AI
const validFiles = Object.entries(manifest.files as Record<string, any>)
.filter(([path, info]) => {
// Filter out invalid paths
return path.includes('.') && !path.match(/\/\d+$/);
});
const fileSummary = validFiles
.map(([path, info]: [string, any]) => {
const componentName = info.componentInfo?.name || path.split('/').pop();
const hasImports = info.imports?.length > 0;
const childComponents = info.componentInfo?.childComponents?.join(', ') || 'none';
return `- ${path} (${componentName}, renders: ${childComponents})`;
})
.join('\n');
console.log('[analyze-edit-intent] Valid files found:', validFiles.length);
if (validFiles.length === 0) {
console.error('[analyze-edit-intent] No valid files found in manifest');
return NextResponse.json({
success: false,
error: 'No valid files found in manifest'
}, { status: 400 });
}
console.log('[analyze-edit-intent] Analyzing prompt:', prompt);
console.log('[analyze-edit-intent] File summary preview:', fileSummary.split('\n').slice(0, 5).join('\n'));
// Select the appropriate AI model based on the request
let aiModel;
if (model.startsWith('anthropic/')) {
aiModel = anthropic(model.replace('anthropic/', ''));
} else if (model.startsWith('openai/')) {
if (model.includes('gpt-oss')) {
aiModel = groq(model);
} else {
aiModel = openai(model.replace('openai/', ''));
}
} else {
// Default to groq if model format is unclear
aiModel = groq(model);
}
console.log('[analyze-edit-intent] Using AI model:', model);
// Use AI to create a search plan
const result = await generateObject({
model: aiModel,
schema: searchPlanSchema,
messages: [
{
role: 'system',
content: `You are an expert at planning code searches. Your job is to create a search strategy to find the exact code that needs to be edited.
DO NOT GUESS which files to edit. Instead, provide specific search terms that will locate the code.
SEARCH STRATEGY RULES:
1. For text changes (e.g., "change 'Start Deploying' to 'Go Now'"):
- Search for the EXACT text: "Start Deploying"
2. For style changes (e.g., "make header black"):
- Search for component names: "Header", "<header"
- Search for class names: "header", "navbar"
- Search for className attributes containing relevant words
3. For removing elements (e.g., "remove the deploy button"):
- Search for the button text or aria-label
- Search for relevant IDs or data-testids
4. For navigation/header issues:
- Search for: "navigation", "nav", "Header", "navbar"
- Look for Link components or href attributes
5. Be SPECIFIC:
- Use exact capitalization for user-visible text
- Include multiple search terms for redundancy
- Add regex patterns for structural searches
Current project structure for context:
${fileSummary}`
},
{
role: 'user',
content: `User request: "${prompt}"
Create a search plan to find the exact code that needs to be modified. Include specific search terms and patterns.`
}
]
});
console.log('[analyze-edit-intent] Search plan created:', {
editType: result.object.editType,
searchTerms: result.object.searchTerms,
patterns: result.object.regexPatterns?.length || 0,
reasoning: result.object.reasoning
});
// Return the search plan, not file matches
return NextResponse.json({
success: true,
searchPlan: result.object
});
} catch (error) {
console.error('[analyze-edit-intent] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+707
View File
@@ -0,0 +1,707 @@
import { NextRequest, NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter';
import type { SandboxState } from '@/types/sandbox';
import type { ConversationState } from '@/types/conversation';
declare global {
var conversationState: ConversationState | null;
var activeSandbox: any;
var existingFiles: Set<string>;
var sandboxState: SandboxState;
}
interface ParsedResponse {
explanation: string;
template: string;
files: Array<{ path: string; content: string }>;
packages: string[];
commands: string[];
structure: string | null;
}
function parseAIResponse(response: string): ParsedResponse {
const sections = {
files: [] as Array<{ path: string; content: string }>,
commands: [] as string[],
packages: [] as string[],
structure: null as string | null,
explanation: '',
template: ''
};
// Function to extract packages from import statements
function extractPackagesFromCode(content: string): string[] {
const packages: string[] = [];
// Match ES6 imports
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
let importMatch;
while ((importMatch = importRegex.exec(content)) !== null) {
const importPath = importMatch[1];
// Skip relative imports and built-in React
if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
importPath !== 'react' && importPath !== 'react-dom' &&
!importPath.startsWith('@/')) {
// Extract package name (handle scoped packages like @heroicons/react)
const packageName = importPath.startsWith('@')
? importPath.split('/').slice(0, 2).join('/')
: importPath.split('/')[0];
if (!packages.includes(packageName)) {
packages.push(packageName);
// Log important packages for debugging
if (packageName === 'react-router-dom' || packageName.includes('router') || packageName.includes('icon')) {
console.log(`[apply-ai-code-stream] Detected package from imports: ${packageName}`);
}
}
}
}
return packages;
}
// Parse file sections - handle duplicates and prefer complete versions
const fileMap = new Map<string, { content: string; isComplete: boolean }>();
// First pass: Find all file declarations
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
let match;
while ((match = fileRegex.exec(response)) !== null) {
const filePath = match[1];
const content = match[2].trim();
const hasClosingTag = response.substring(match.index, match.index + match[0].length).includes('</file>');
// Check if this file already exists in our map
const existing = fileMap.get(filePath);
// Decide whether to keep this version
let shouldReplace = false;
if (!existing) {
shouldReplace = true; // First occurrence
} else if (!existing.isComplete && hasClosingTag) {
shouldReplace = true; // Replace incomplete with complete
console.log(`[apply-ai-code-stream] Replacing incomplete ${filePath} with complete version`);
} else if (existing.isComplete && hasClosingTag && content.length > existing.content.length) {
shouldReplace = true; // Replace with longer complete version
console.log(`[apply-ai-code-stream] Replacing ${filePath} with longer complete version`);
} else if (!existing.isComplete && !hasClosingTag && content.length > existing.content.length) {
shouldReplace = true; // Both incomplete, keep longer one
}
if (shouldReplace) {
// Additional validation: reject obviously broken content
if (content.includes('...') && !content.includes('...props') && !content.includes('...rest')) {
console.warn(`[apply-ai-code-stream] Warning: ${filePath} contains ellipsis, may be truncated`);
// Still use it if it's the only version we have
if (!existing) {
fileMap.set(filePath, { content, isComplete: hasClosingTag });
}
} else {
fileMap.set(filePath, { content, isComplete: hasClosingTag });
}
}
}
// Convert map to array for sections.files
for (const [path, { content, isComplete }] of fileMap.entries()) {
if (!isComplete) {
console.log(`[apply-ai-code-stream] Warning: File ${path} appears to be truncated (no closing tag)`);
}
sections.files.push({
path,
content
});
// Extract packages from file content
const filePackages = extractPackagesFromCode(content);
for (const pkg of filePackages) {
if (!sections.packages.includes(pkg)) {
sections.packages.push(pkg);
console.log(`[apply-ai-code-stream] 📦 Package detected from imports: ${pkg}`);
}
}
}
// Also parse markdown code blocks with file paths
const markdownFileRegex = /```(?:file )?path="([^"]+)"\n([\s\S]*?)```/g;
while ((match = markdownFileRegex.exec(response)) !== null) {
const filePath = match[1];
const content = match[2].trim();
sections.files.push({
path: filePath,
content: content
});
// Extract packages from file content
const filePackages = extractPackagesFromCode(content);
for (const pkg of filePackages) {
if (!sections.packages.includes(pkg)) {
sections.packages.push(pkg);
console.log(`[apply-ai-code-stream] 📦 Package detected from imports: ${pkg}`);
}
}
}
// Parse plain text format like "Generated Files: Header.jsx, index.css"
const generatedFilesMatch = response.match(/Generated Files?:\s*([^\n]+)/i);
if (generatedFilesMatch) {
// Split by comma first, then trim whitespace, to preserve filenames with dots
const filesList = generatedFilesMatch[1]
.split(',')
.map(f => f.trim())
.filter(f => f.endsWith('.jsx') || f.endsWith('.js') || f.endsWith('.tsx') || f.endsWith('.ts') || f.endsWith('.css') || f.endsWith('.json') || f.endsWith('.html'));
console.log(`[apply-ai-code-stream] Detected generated files from plain text: ${filesList.join(', ')}`);
// Try to extract the actual file content if it follows
for (const fileName of filesList) {
// Look for the file content after the file name
const fileContentRegex = new RegExp(`${fileName}[\\s\\S]*?(?:import[\\s\\S]+?)(?=Generated Files:|Applying code|$)`, 'i');
const fileContentMatch = response.match(fileContentRegex);
if (fileContentMatch) {
// Extract just the code part (starting from import statements)
const codeMatch = fileContentMatch[0].match(/^(import[\s\S]+)$/m);
if (codeMatch) {
const filePath = fileName.includes('/') ? fileName : `src/components/${fileName}`;
sections.files.push({
path: filePath,
content: codeMatch[1].trim()
});
console.log(`[apply-ai-code-stream] Extracted content for ${filePath}`);
// Extract packages from this file
const filePackages = extractPackagesFromCode(codeMatch[1]);
for (const pkg of filePackages) {
if (!sections.packages.includes(pkg)) {
sections.packages.push(pkg);
console.log(`[apply-ai-code-stream] Package detected from imports: ${pkg}`);
}
}
}
}
}
}
// Also try to parse if the response contains raw JSX/JS code blocks
const codeBlockRegex = /```(?:jsx?|tsx?|javascript|typescript)?\n([\s\S]*?)```/g;
while ((match = codeBlockRegex.exec(response)) !== null) {
const content = match[1].trim();
// Try to detect the file name from comments or context
const fileNameMatch = content.match(/\/\/\s*(?:File:|Component:)\s*([^\n]+)/);
if (fileNameMatch) {
const fileName = fileNameMatch[1].trim();
const filePath = fileName.includes('/') ? fileName : `src/components/${fileName}`;
// Don't add duplicate files
if (!sections.files.some(f => f.path === filePath)) {
sections.files.push({
path: filePath,
content: content
});
// Extract packages
const filePackages = extractPackagesFromCode(content);
for (const pkg of filePackages) {
if (!sections.packages.includes(pkg)) {
sections.packages.push(pkg);
}
}
}
}
}
// Parse commands
const cmdRegex = /<command>(.*?)<\/command>/g;
while ((match = cmdRegex.exec(response)) !== null) {
sections.commands.push(match[1].trim());
}
// Parse packages - support both <package> and <packages> tags
const pkgRegex = /<package>(.*?)<\/package>/g;
while ((match = pkgRegex.exec(response)) !== null) {
sections.packages.push(match[1].trim());
}
// Also parse <packages> tag with multiple packages
const packagesRegex = /<packages>([\s\S]*?)<\/packages>/;
const packagesMatch = response.match(packagesRegex);
if (packagesMatch) {
const packagesContent = packagesMatch[1].trim();
// Split by newlines or commas
const packagesList = packagesContent.split(/[\n,]+/)
.map(pkg => pkg.trim())
.filter(pkg => pkg.length > 0);
sections.packages.push(...packagesList);
}
// Parse structure
const structureMatch = /<structure>([\s\S]*?)<\/structure>/;
const structResult = response.match(structureMatch);
if (structResult) {
sections.structure = structResult[1].trim();
}
// Parse explanation
const explanationMatch = /<explanation>([\s\S]*?)<\/explanation>/;
const explResult = response.match(explanationMatch);
if (explResult) {
sections.explanation = explResult[1].trim();
}
// Parse template
const templateMatch = /<template>(.*?)<\/template>/;
const templResult = response.match(templateMatch);
if (templResult) {
sections.template = templResult[1].trim();
}
return sections;
}
export async function POST(request: NextRequest) {
try {
const { response, isEdit = false, packages = [], sandboxId } = await request.json();
if (!response) {
return NextResponse.json({
error: 'response is required'
}, { status: 400 });
}
// Debug log the response
console.log('[apply-ai-code-stream] Received response to parse:');
console.log('[apply-ai-code-stream] Response length:', response.length);
console.log('[apply-ai-code-stream] Response preview:', response.substring(0, 500));
console.log('[apply-ai-code-stream] isEdit:', isEdit);
console.log('[apply-ai-code-stream] packages:', packages);
// Parse the AI response
const parsed = parseAIResponse(response);
// Log what was parsed
console.log('[apply-ai-code-stream] Parsed result:');
console.log('[apply-ai-code-stream] Files found:', parsed.files.length);
if (parsed.files.length > 0) {
parsed.files.forEach(f => {
console.log(`[apply-ai-code-stream] - ${f.path} (${f.content.length} chars)`);
});
}
console.log('[apply-ai-code-stream] Packages found:', parsed.packages);
// Initialize existingFiles if not already
if (!global.existingFiles) {
global.existingFiles = new Set<string>();
}
// First, always check the global state for active sandbox
let sandbox = global.activeSandbox;
// If we don't have a sandbox in this instance but we have a sandboxId,
// reconnect to the existing sandbox
if (!sandbox && sandboxId) {
console.log(`[apply-ai-code-stream] Sandbox ${sandboxId} not in this instance, attempting reconnect...`);
try {
// Reconnect to the existing sandbox using E2B's connect method
sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY });
console.log(`[apply-ai-code-stream] Successfully reconnected to sandbox ${sandboxId}`);
// Store the reconnected sandbox globally for this instance
global.activeSandbox = sandbox;
// Update sandbox data if needed
if (!global.sandboxData) {
const host = (sandbox as any).getHost(5173);
global.sandboxData = {
sandboxId,
url: `https://${host}`
};
}
// Initialize existingFiles if not already
if (!global.existingFiles) {
global.existingFiles = new Set<string>();
}
} catch (reconnectError) {
console.error(`[apply-ai-code-stream] Failed to reconnect to sandbox ${sandboxId}:`, reconnectError);
// If reconnection fails, we'll still try to return a meaningful response
return NextResponse.json({
success: false,
error: `Failed to reconnect to sandbox ${sandboxId}. The sandbox may have expired or been terminated.`,
results: {
filesCreated: [],
packagesInstalled: [],
commandsExecuted: [],
errors: [`Sandbox reconnection failed: ${(reconnectError 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.`
});
}
}
// If no sandbox at all and no sandboxId provided, return an error
if (!sandbox && !sandboxId) {
console.log('[apply-ai-code-stream] No sandbox available and no sandboxId provided');
return NextResponse.json({
success: false,
error: 'No active sandbox found. Please create a sandbox first.',
results: {
filesCreated: [],
packagesInstalled: [],
commandsExecuted: [],
errors: ['No sandbox available']
},
explanation: parsed.explanation,
structure: parsed.structure,
parsedFiles: parsed.files,
message: `Parsed ${parsed.files.length} files but no sandbox available to apply them.`
});
}
// Create a response stream for real-time updates
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
// Function to send progress updates
const sendProgress = async (data: any) => {
const message = `data: ${JSON.stringify(data)}\n\n`;
await writer.write(encoder.encode(message));
};
// Start processing in background (pass sandbox and request to the async function)
(async (sandboxInstance, req) => {
const results = {
filesCreated: [] as string[],
filesUpdated: [] as string[],
packagesInstalled: [] as string[],
packagesAlreadyInstalled: [] as string[],
packagesFailed: [] as string[],
commandsExecuted: [] as string[],
errors: [] as string[]
};
try {
await sendProgress({
type: 'start',
message: 'Starting code application...',
totalSteps: 3
});
// Step 1: Install packages
const packagesArray = Array.isArray(packages) ? packages : [];
const parsedPackages = Array.isArray(parsed.packages) ? parsed.packages : [];
// Combine and deduplicate packages
const allPackages = [...packagesArray.filter(pkg => pkg && typeof pkg === 'string'), ...parsedPackages];
// Use Set to remove duplicates, then filter out pre-installed packages
const uniquePackages = [...new Set(allPackages)]
.filter(pkg => pkg && typeof pkg === 'string' && pkg.trim() !== '') // Remove empty strings
.filter(pkg => pkg !== 'react' && pkg !== 'react-dom'); // Filter pre-installed
// Log if we found duplicates
if (allPackages.length !== uniquePackages.length) {
console.log(`[apply-ai-code-stream] Removed ${allPackages.length - uniquePackages.length} duplicate packages`);
console.log(`[apply-ai-code-stream] Original packages:`, allPackages);
console.log(`[apply-ai-code-stream] Deduplicated packages:`, uniquePackages);
}
if (uniquePackages.length > 0) {
await sendProgress({
type: 'step',
step: 1,
message: `Installing ${uniquePackages.length} packages...`,
packages: uniquePackages
});
// Use streaming package installation
try {
// Construct the API URL properly for both dev and production
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
const host = req.headers.get('host') || 'localhost:3000';
const apiUrl = `${protocol}://${host}/api/install-packages`;
const installResponse = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packages: uniquePackages,
sandboxId: sandboxId || (sandboxInstance as any).sandboxId
})
});
if (installResponse.ok && installResponse.body) {
const reader = installResponse.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
if (!chunk) continue;
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
// Forward package installation progress
await sendProgress({
type: 'package-progress',
...data
});
// Track results
if (data.type === 'success' && data.installedPackages) {
results.packagesInstalled = data.installedPackages;
}
} catch (e) {
// Ignore parse errors
}
}
}
}
}
} catch (error) {
console.error('[apply-ai-code-stream] Error installing packages:', error);
await sendProgress({
type: 'warning',
message: `Package installation skipped (${(error as Error).message}). Continuing with file creation...`
});
results.errors.push(`Package installation failed: ${(error as Error).message}`);
}
} else {
await sendProgress({
type: 'step',
step: 1,
message: 'No additional packages to install, skipping...'
});
}
// Step 2: Create/update files
const filesArray = Array.isArray(parsed.files) ? parsed.files : [];
await sendProgress({
type: 'step',
step: 2,
message: `Creating ${filesArray.length} files...`
});
// Filter out config files that shouldn't be created
const configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js'];
const filteredFiles = filesArray.filter(file => {
if (!file || typeof file !== 'object') return false;
const fileName = (file.path || '').split('/').pop() || '';
return !configFiles.includes(fileName);
});
for (const [index, file] of filteredFiles.entries()) {
try {
// Send progress for each file
await sendProgress({
type: 'file-progress',
current: index + 1,
total: filteredFiles.length,
fileName: file.path,
action: 'creating'
});
// Normalize the file path
let normalizedPath = file.path;
if (normalizedPath.startsWith('/')) {
normalizedPath = normalizedPath.substring(1);
}
if (!normalizedPath.startsWith('src/') &&
!normalizedPath.startsWith('public/') &&
normalizedPath !== 'index.html' &&
!configFiles.includes(normalizedPath.split('/').pop() || '')) {
normalizedPath = 'src/' + normalizedPath;
}
const fullPath = `/home/user/app/${normalizedPath}`;
const isUpdate = global.existingFiles.has(normalizedPath);
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
let fileContent = file.content;
if (file.path.endsWith('.jsx') || file.path.endsWith('.js') || file.path.endsWith('.tsx') || file.path.endsWith('.ts')) {
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
}
// Write the file using Python (code-interpreter SDK)
const escapedContent = fileContent
.replace(/\\/g, '\\\\')
.replace(/"""/g, '\\"\\"\\"')
.replace(/\$/g, '\\$');
await sandboxInstance.runCode(`
import os
os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True)
with open("${fullPath}", 'w') as f:
f.write("""${escapedContent}""")
print(f"File written: ${fullPath}")
`);
// Update file cache
if (global.sandboxState?.fileCache) {
global.sandboxState.fileCache.files[normalizedPath] = {
content: fileContent,
lastModified: Date.now()
};
}
if (isUpdate) {
if (results.filesUpdated) results.filesUpdated.push(normalizedPath);
} else {
if (results.filesCreated) results.filesCreated.push(normalizedPath);
if (global.existingFiles) global.existingFiles.add(normalizedPath);
}
await sendProgress({
type: 'file-complete',
fileName: normalizedPath,
action: isUpdate ? 'updated' : 'created'
});
} catch (error) {
if (results.errors) {
results.errors.push(`Failed to create ${file.path}: ${(error as Error).message}`);
}
await sendProgress({
type: 'file-error',
fileName: file.path,
error: (error as Error).message
});
}
}
// Step 3: Execute commands
const commandsArray = Array.isArray(parsed.commands) ? parsed.commands : [];
if (commandsArray.length > 0) {
await sendProgress({
type: 'step',
step: 3,
message: `Executing ${commandsArray.length} commands...`
});
for (const [index, cmd] of commandsArray.entries()) {
try {
await sendProgress({
type: 'command-progress',
current: index + 1,
total: parsed.commands.length,
command: cmd,
action: 'executing'
});
// Use E2B commands.run() for cleaner execution
const result = await sandboxInstance.commands.run(cmd, {
cwd: '/home/user/app',
timeout: 60,
on_stdout: async (data: string) => {
await sendProgress({
type: 'command-output',
command: cmd,
output: data,
stream: 'stdout'
});
},
on_stderr: async (data: string) => {
await sendProgress({
type: 'command-output',
command: cmd,
output: data,
stream: 'stderr'
});
}
});
if (results.commandsExecuted) {
results.commandsExecuted.push(cmd);
}
await sendProgress({
type: 'command-complete',
command: cmd,
exitCode: result.exitCode,
success: result.exitCode === 0
});
} catch (error) {
if (results.errors) {
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
}
await sendProgress({
type: 'command-error',
command: cmd,
error: (error as Error).message
});
}
}
}
// Send final results
await sendProgress({
type: 'complete',
results,
explanation: parsed.explanation,
structure: parsed.structure,
message: `Successfully applied ${results.filesCreated.length} files`
});
// Track applied files in conversation state
if (global.conversationState && results.filesCreated.length > 0) {
const messages = global.conversationState.context.messages;
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
if (lastMessage.role === 'user') {
lastMessage.metadata = {
...lastMessage.metadata,
editedFiles: results.filesCreated
};
}
}
// Track applied code in project evolution
if (global.conversationState.context.projectEvolution) {
global.conversationState.context.projectEvolution.majorChanges.push({
timestamp: Date.now(),
description: parsed.explanation || 'Code applied',
filesAffected: results.filesCreated || []
});
}
global.conversationState.lastUpdated = Date.now();
}
} catch (error) {
await sendProgress({
type: 'error',
error: (error as Error).message
});
} finally {
await writer.close();
}
})(sandbox, request);
// Return the stream
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
} catch (error) {
console.error('Apply AI code stream error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to parse AI code' },
{ status: 500 }
);
}
}
+649
View File
@@ -0,0 +1,649 @@
import { NextRequest, NextResponse } from 'next/server';
import type { SandboxState } from '@/types/sandbox';
import type { ConversationState } from '@/types/conversation';
declare global {
var conversationState: ConversationState | null;
}
interface ParsedResponse {
explanation: string;
template: string;
files: Array<{ path: string; content: string }>;
packages: string[];
commands: string[];
structure: string | null;
}
function parseAIResponse(response: string): ParsedResponse {
const sections = {
files: [] as Array<{ path: string; content: string }>,
commands: [] as string[],
packages: [] as string[],
structure: null as string | null,
explanation: '',
template: ''
};
// Parse file sections - handle duplicates and prefer complete versions
const fileMap = new Map<string, { content: string; isComplete: boolean }>();
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
let match;
while ((match = fileRegex.exec(response)) !== null) {
const filePath = match[1];
const content = match[2].trim();
const hasClosingTag = response.substring(match.index, match.index + match[0].length).includes('</file>');
// Check if this file already exists in our map
const existing = fileMap.get(filePath);
// Decide whether to keep this version
let shouldReplace = false;
if (!existing) {
shouldReplace = true; // First occurrence
} else if (!existing.isComplete && hasClosingTag) {
shouldReplace = true; // Replace incomplete with complete
console.log(`[parseAIResponse] Replacing incomplete ${filePath} with complete version`);
} else if (existing.isComplete && hasClosingTag && content.length > existing.content.length) {
shouldReplace = true; // Replace with longer complete version
console.log(`[parseAIResponse] Replacing ${filePath} with longer complete version`);
} else if (!existing.isComplete && !hasClosingTag && content.length > existing.content.length) {
shouldReplace = true; // Both incomplete, keep longer one
}
if (shouldReplace) {
// Additional validation: reject obviously broken content
if (content.includes('...') && !content.includes('...props') && !content.includes('...rest')) {
console.warn(`[parseAIResponse] Warning: ${filePath} contains ellipsis, may be truncated`);
// Still use it if it's the only version we have
if (!existing) {
fileMap.set(filePath, { content, isComplete: hasClosingTag });
}
} else {
fileMap.set(filePath, { content, isComplete: hasClosingTag });
}
}
}
// Convert map to array for sections.files
for (const [path, { content, isComplete }] of fileMap.entries()) {
if (!isComplete) {
console.log(`[parseAIResponse] Warning: File ${path} appears to be truncated (no closing tag)`);
}
sections.files.push({
path,
content
});
}
// Parse commands
const cmdRegex = /<command>(.*?)<\/command>/g;
while ((match = cmdRegex.exec(response)) !== null) {
sections.commands.push(match[1].trim());
}
// Parse packages - support both <package> and <packages> tags
const pkgRegex = /<package>(.*?)<\/package>/g;
while ((match = pkgRegex.exec(response)) !== null) {
sections.packages.push(match[1].trim());
}
// Also parse <packages> tag with multiple packages
const packagesRegex = /<packages>([\s\S]*?)<\/packages>/;
const packagesMatch = response.match(packagesRegex);
if (packagesMatch) {
const packagesContent = packagesMatch[1].trim();
// Split by newlines or commas
const packagesList = packagesContent.split(/[\n,]+/)
.map(pkg => pkg.trim())
.filter(pkg => pkg.length > 0);
sections.packages.push(...packagesList);
}
// Parse structure
const structureMatch = /<structure>([\s\S]*?)<\/structure>/;
const structResult = response.match(structureMatch);
if (structResult) {
sections.structure = structResult[1].trim();
}
// Parse explanation
const explanationMatch = /<explanation>([\s\S]*?)<\/explanation>/;
const explResult = response.match(explanationMatch);
if (explResult) {
sections.explanation = explResult[1].trim();
}
// Parse template
const templateMatch = /<template>(.*?)<\/template>/;
const templResult = response.match(templateMatch);
if (templResult) {
sections.template = templResult[1].trim();
}
return sections;
}
declare global {
var activeSandbox: any;
var existingFiles: Set<string>;
var sandboxState: SandboxState;
}
export async function POST(request: NextRequest) {
try {
const { response, isEdit = false, packages = [] } = await request.json();
if (!response) {
return NextResponse.json({
error: 'response is required'
}, { status: 400 });
}
// Parse the AI response
const parsed = parseAIResponse(response);
// Initialize existingFiles if not already
if (!global.existingFiles) {
global.existingFiles = new Set<string>();
}
// If no active sandbox, just return parsed results
if (!global.activeSandbox) {
return NextResponse.json({
success: true,
results: {
filesCreated: parsed.files.map(f => f.path),
packagesInstalled: parsed.packages,
commandsExecuted: parsed.commands,
errors: []
},
explanation: parsed.explanation,
structure: parsed.structure,
parsedFiles: parsed.files,
message: `Parsed ${parsed.files.length} files successfully. Create a sandbox to apply them.`
});
}
// Apply to active sandbox
console.log('[apply-ai-code] Applying code to sandbox...');
console.log('[apply-ai-code] Is edit mode:', isEdit);
console.log('[apply-ai-code] Files to write:', parsed.files.map(f => f.path));
console.log('[apply-ai-code] Existing files:', Array.from(global.existingFiles));
const results = {
filesCreated: [] as string[],
filesUpdated: [] as string[],
packagesInstalled: [] as string[],
packagesAlreadyInstalled: [] as string[],
packagesFailed: [] as string[],
commandsExecuted: [] as string[],
errors: [] as string[]
};
// Combine packages from tool calls and parsed XML tags
const allPackages = [...packages.filter((pkg: any) => pkg && typeof pkg === 'string'), ...parsed.packages];
const uniquePackages = [...new Set(allPackages)]; // Remove duplicates
if (uniquePackages.length > 0) {
console.log('[apply-ai-code] Installing packages from XML tags and tool calls:', uniquePackages);
try {
const installResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/install-packages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ packages: uniquePackages })
});
if (installResponse.ok) {
const installResult = await installResponse.json();
console.log('[apply-ai-code] Package installation result:', installResult);
if (installResult.installed && installResult.installed.length > 0) {
results.packagesInstalled = installResult.installed;
}
if (installResult.failed && installResult.failed.length > 0) {
results.packagesFailed = installResult.failed;
}
}
} catch (error) {
console.error('[apply-ai-code] Error installing packages:', error);
}
} else {
// Fallback to detecting packages from code
console.log('[apply-ai-code] No packages provided, detecting from generated code...');
console.log('[apply-ai-code] Number of files to scan:', parsed.files.length);
// Filter out config files first
const configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js'];
const filteredFilesForDetection = parsed.files.filter(file => {
const fileName = file.path.split('/').pop() || '';
return !configFiles.includes(fileName);
});
// Build files object for package detection
const filesForPackageDetection: Record<string, string> = {};
for (const file of filteredFilesForDetection) {
filesForPackageDetection[file.path] = file.content;
// Log if heroicons is found
if (file.content.includes('heroicons')) {
console.log(`[apply-ai-code] Found heroicons import in ${file.path}`);
}
}
try {
console.log('[apply-ai-code] Calling detect-and-install-packages...');
const packageResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/detect-and-install-packages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ files: filesForPackageDetection })
});
console.log('[apply-ai-code] Package detection response status:', packageResponse.status);
if (packageResponse.ok) {
const packageResult = await packageResponse.json();
console.log('[apply-ai-code] Package installation result:', JSON.stringify(packageResult, null, 2));
if (packageResult.packagesInstalled && packageResult.packagesInstalled.length > 0) {
results.packagesInstalled = packageResult.packagesInstalled;
console.log(`[apply-ai-code] Installed packages: ${packageResult.packagesInstalled.join(', ')}`);
}
if (packageResult.packagesAlreadyInstalled && packageResult.packagesAlreadyInstalled.length > 0) {
results.packagesAlreadyInstalled = packageResult.packagesAlreadyInstalled;
console.log(`[apply-ai-code] Already installed: ${packageResult.packagesAlreadyInstalled.join(', ')}`);
}
if (packageResult.packagesFailed && packageResult.packagesFailed.length > 0) {
results.packagesFailed = packageResult.packagesFailed;
console.error(`[apply-ai-code] Failed to install packages: ${packageResult.packagesFailed.join(', ')}`);
results.errors.push(`Failed to install packages: ${packageResult.packagesFailed.join(', ')}`);
}
// Force Vite restart after package installation
if (results.packagesInstalled.length > 0) {
console.log('[apply-ai-code] Packages were installed, forcing Vite restart...');
try {
// Call the restart-vite endpoint
const restartResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/restart-vite`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (restartResponse.ok) {
const restartResult = await restartResponse.json();
console.log('[apply-ai-code] Vite restart result:', restartResult.message);
} else {
console.error('[apply-ai-code] Failed to restart Vite:', await restartResponse.text());
}
} catch (e) {
console.error('[apply-ai-code] Error calling restart-vite:', e);
}
// Additional delay to ensure files can be written after restart
await new Promise(resolve => setTimeout(resolve, 1000));
}
} else {
console.error('[apply-ai-code] Package detection/installation failed:', await packageResponse.text());
}
} catch (error) {
console.error('[apply-ai-code] Error detecting/installing packages:', error);
// Continue with file writing even if package installation fails
}
}
// Filter out config files that shouldn't be created
const configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js'];
const filteredFiles = parsed.files.filter(file => {
const fileName = file.path.split('/').pop() || '';
if (configFiles.includes(fileName)) {
console.warn(`[apply-ai-code] Skipping config file: ${file.path} - already exists in template`);
return false;
}
return true;
});
// Create or update files AFTER package installation
for (const file of filteredFiles) {
try {
// Normalize the file path
let normalizedPath = file.path;
// Remove leading slash if present
if (normalizedPath.startsWith('/')) {
normalizedPath = normalizedPath.substring(1);
}
// Ensure src/ prefix for component files
if (!normalizedPath.startsWith('src/') &&
!normalizedPath.startsWith('public/') &&
normalizedPath !== 'index.html' &&
normalizedPath !== 'package.json' &&
normalizedPath !== 'vite.config.js' &&
normalizedPath !== 'tailwind.config.js' &&
normalizedPath !== 'postcss.config.js') {
normalizedPath = 'src/' + normalizedPath;
}
const fullPath = `/home/user/app/${normalizedPath}`;
const isUpdate = global.existingFiles.has(normalizedPath);
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
let fileContent = file.content;
if (file.path.endsWith('.jsx') || file.path.endsWith('.js') || file.path.endsWith('.tsx') || file.path.endsWith('.ts')) {
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
}
console.log(`[apply-ai-code] Writing file using E2B files API: ${fullPath}`);
try {
// Use the correct E2B API - sandbox.files.write()
await global.activeSandbox.files.write(fullPath, fileContent);
console.log(`[apply-ai-code] Successfully wrote file: ${fullPath}`);
// Update file cache
if (global.sandboxState?.fileCache) {
global.sandboxState.fileCache.files[normalizedPath] = {
content: fileContent,
lastModified: Date.now()
};
console.log(`[apply-ai-code] Updated file cache for: ${normalizedPath}`);
}
} catch (writeError) {
console.error(`[apply-ai-code] E2B file write error:`, writeError);
throw writeError;
}
if (isUpdate) {
results.filesUpdated.push(normalizedPath);
} else {
results.filesCreated.push(normalizedPath);
global.existingFiles.add(normalizedPath);
}
} catch (error) {
results.errors.push(`Failed to create ${file.path}: ${(error as Error).message}`);
}
}
// Only create App.jsx if it's not an edit and doesn't exist
const appFileInParsed = parsed.files.some(f => {
const normalized = f.path.replace(/^\//, '').replace(/^src\//, '');
return normalized === 'App.jsx' || normalized === 'App.tsx';
});
const appFileExists = global.existingFiles.has('src/App.jsx') ||
global.existingFiles.has('src/App.tsx') ||
global.existingFiles.has('App.jsx') ||
global.existingFiles.has('App.tsx');
if (!isEdit && !appFileInParsed && !appFileExists && parsed.files.length > 0) {
// Find all component files
const componentFiles = parsed.files.filter(f =>
(f.path.endsWith('.jsx') || f.path.endsWith('.tsx')) &&
f.path.includes('component')
);
// Generate imports for components
const imports = componentFiles
.filter(f => !f.path.includes('App.') && !f.path.includes('main.') && !f.path.includes('index.'))
.map(f => {
const pathParts = f.path.split('/');
const fileName = pathParts[pathParts.length - 1];
const componentName = fileName.replace(/\.(jsx|tsx)$/, '');
// Fix import path - components are in src/components/
const importPath = f.path.startsWith('src/')
? f.path.replace('src/', './').replace(/\.(jsx|tsx)$/, '')
: './' + f.path.replace(/\.(jsx|tsx)$/, '');
return `import ${componentName} from '${importPath}';`;
})
.join('\n');
// Find the main component
const mainComponent = componentFiles.find(f => {
const name = f.path.toLowerCase();
return name.includes('header') ||
name.includes('hero') ||
name.includes('layout') ||
name.includes('main') ||
name.includes('home');
}) || componentFiles[0];
const mainComponentName = mainComponent
? mainComponent.path.split('/').pop()?.replace(/\.(jsx|tsx)$/, '')
: null;
// Create App.jsx with better structure
const appContent = `import React from 'react';
${imports}
function App() {
return (
<div className="min-h-screen bg-gray-900 text-white p-8">
${mainComponentName ? `<${mainComponentName} />` : '<div className="text-center">\n <h1 className="text-4xl font-bold mb-4">Welcome to your React App</h1>\n <p className="text-gray-400">Your components have been created but need to be added here.</p>\n </div>'}
{/* Generated components: ${componentFiles.map(f => f.path).join(', ')} */}
</div>
);
}
export default App;`;
try {
await global.activeSandbox.runCode(`
file_path = "/home/user/app/src/App.jsx"
file_content = """${appContent.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"""
with open(file_path, 'w') as f:
f.write(file_content)
print(f"Auto-generated: {file_path}")
`);
results.filesCreated.push('src/App.jsx (auto-generated)');
} catch (error) {
results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`);
}
// Don't auto-generate App.css - we're using Tailwind CSS
// Only create index.css if it doesn't exist
const indexCssInParsed = parsed.files.some(f => {
const normalized = f.path.replace(/^\//, '').replace(/^src\//, '');
return normalized === 'index.css' || f.path === 'src/index.css';
});
const indexCssExists = global.existingFiles.has('src/index.css') ||
global.existingFiles.has('index.css');
if (!isEdit && !indexCssInParsed && !indexCssExists) {
try {
await global.activeSandbox.runCode(`
file_path = "/home/user/app/src/index.css"
file_content = """@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: dark;
color: rgba(255, 255, 255, 0.87);
background-color: #0a0a0a;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}"""
with open(file_path, 'w') as f:
f.write(file_content)
print(f"Auto-generated: {file_path}")
`);
results.filesCreated.push('src/index.css (with Tailwind)');
} catch (error) {
results.errors.push('Failed to create index.css with Tailwind');
}
}
}
// Execute commands
for (const cmd of parsed.commands) {
try {
await global.activeSandbox.runCode(`
import subprocess
os.chdir('/home/user/app')
result = subprocess.run(${JSON.stringify(cmd.split(' '))}, capture_output=True, text=True)
print(f"Executed: ${cmd}")
print(result.stdout)
if result.stderr:
print(f"Errors: {result.stderr}")
`);
results.commandsExecuted.push(cmd);
} catch (error) {
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
}
}
// Check for missing imports in App.jsx
const missingImports: string[] = [];
const appFile = parsed.files.find(f =>
f.path === 'src/App.jsx' || f.path === 'App.jsx'
);
if (appFile) {
// Extract imports from App.jsx
const importRegex = /import\s+(?:\w+|\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/g;
let match;
const imports: string[] = [];
while ((match = importRegex.exec(appFile.content)) !== null) {
const importPath = match[1];
if (importPath.startsWith('./') || importPath.startsWith('../')) {
imports.push(importPath);
}
}
// Check if all imported files exist
for (const imp of imports) {
// Skip CSS imports for this check
if (imp.endsWith('.css')) continue;
// Convert import path to expected file paths
const basePath = imp.replace('./', 'src/');
const possiblePaths = [
basePath + '.jsx',
basePath + '.js',
basePath + '/index.jsx',
basePath + '/index.js'
];
const fileExists = parsed.files.some(f =>
possiblePaths.some(path => f.path === path)
);
if (!fileExists) {
missingImports.push(imp);
}
}
}
// Prepare response
const responseData: any = {
success: true,
results,
explanation: parsed.explanation,
structure: parsed.structure,
message: `Applied ${results.filesCreated.length} files successfully`
};
// Handle missing imports automatically
if (missingImports.length > 0) {
console.warn('[apply-ai-code] Missing imports detected:', missingImports);
// Automatically generate missing components
try {
console.log('[apply-ai-code] Auto-generating missing components...');
const autoCompleteResponse = await fetch(
`${request.nextUrl.origin}/api/auto-complete-components`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
missingImports,
model: 'claude-sonnet-4-20250514'
})
}
);
const autoCompleteData = await autoCompleteResponse.json();
if (autoCompleteData.success) {
responseData.autoCompleted = true;
responseData.autoCompletedComponents = autoCompleteData.components;
responseData.message = `Applied ${results.filesCreated.length} files + auto-generated ${autoCompleteData.files} missing components`;
// Add auto-completed files to results
results.filesCreated.push(...autoCompleteData.components);
} else {
// If auto-complete fails, still warn the user
responseData.warning = `Missing ${missingImports.length} imported components: ${missingImports.join(', ')}`;
responseData.missingImports = missingImports;
}
} catch (error) {
console.error('[apply-ai-code] Auto-complete failed:', error);
responseData.warning = `Missing ${missingImports.length} imported components: ${missingImports.join(', ')}`;
responseData.missingImports = missingImports;
}
}
// Track applied files in conversation state
if (global.conversationState && results.filesCreated.length > 0) {
// Update the last message metadata with edited files
const messages = global.conversationState.context.messages;
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
if (lastMessage.role === 'user') {
lastMessage.metadata = {
...lastMessage.metadata,
editedFiles: results.filesCreated
};
}
}
// Track applied code in project evolution
if (global.conversationState.context.projectEvolution) {
global.conversationState.context.projectEvolution.majorChanges.push({
timestamp: Date.now(),
description: parsed.explanation || 'Code applied',
filesAffected: results.filesCreated
});
}
// Update last updated timestamp
global.conversationState.lastUpdated = Date.now();
console.log('[apply-ai-code] Updated conversation state with applied files:', results.filesCreated);
}
return NextResponse.json(responseData);
} catch (error) {
console.error('Apply AI code error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to parse AI code' },
{ status: 500 }
);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { NextResponse } from 'next/server';
// Stub endpoint to prevent 404 errors
// This endpoint is being called but the source is unknown
// Returns empty errors array to satisfy any calling code
export async function GET() {
return NextResponse.json({
success: true,
errors: [],
message: 'No Vite errors detected'
});
}
+26
View File
@@ -0,0 +1,26 @@
import { NextResponse } from 'next/server';
declare global {
var viteErrorsCache: { errors: any[], timestamp: number } | null;
}
export async function POST() {
try {
// Clear the cache
global.viteErrorsCache = null;
console.log('[clear-vite-errors-cache] Cache cleared');
return NextResponse.json({
success: true,
message: 'Vite errors cache cleared'
});
} catch (error) {
console.error('[clear-vite-errors-cache] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+144
View File
@@ -0,0 +1,144 @@
import { NextRequest, NextResponse } from 'next/server';
import type { ConversationState } from '@/types/conversation';
declare global {
var conversationState: ConversationState | null;
}
// GET: Retrieve current conversation state
export async function GET() {
try {
if (!global.conversationState) {
return NextResponse.json({
success: true,
state: null,
message: 'No active conversation'
});
}
return NextResponse.json({
success: true,
state: global.conversationState
});
} catch (error) {
console.error('[conversation-state] Error getting state:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
// POST: Reset or update conversation state
export async function POST(request: NextRequest) {
try {
const { action, data } = await request.json();
switch (action) {
case 'reset':
global.conversationState = {
conversationId: `conv-${Date.now()}`,
startedAt: Date.now(),
lastUpdated: Date.now(),
context: {
messages: [],
edits: [],
projectEvolution: { majorChanges: [] },
userPreferences: {}
}
};
console.log('[conversation-state] Reset conversation state');
return NextResponse.json({
success: true,
message: 'Conversation state reset',
state: global.conversationState
});
case 'clear-old':
// Clear old conversation data but keep recent context
if (!global.conversationState) {
return NextResponse.json({
success: false,
error: 'No active conversation to clear'
}, { status: 400 });
}
// Keep only recent data
global.conversationState.context.messages = global.conversationState.context.messages.slice(-5);
global.conversationState.context.edits = global.conversationState.context.edits.slice(-3);
global.conversationState.context.projectEvolution.majorChanges =
global.conversationState.context.projectEvolution.majorChanges.slice(-2);
console.log('[conversation-state] Cleared old conversation data');
return NextResponse.json({
success: true,
message: 'Old conversation data cleared',
state: global.conversationState
});
case 'update':
if (!global.conversationState) {
return NextResponse.json({
success: false,
error: 'No active conversation to update'
}, { status: 400 });
}
// Update specific fields if provided
if (data) {
if (data.currentTopic) {
global.conversationState.context.currentTopic = data.currentTopic;
}
if (data.userPreferences) {
global.conversationState.context.userPreferences = {
...global.conversationState.context.userPreferences,
...data.userPreferences
};
}
global.conversationState.lastUpdated = Date.now();
}
return NextResponse.json({
success: true,
message: 'Conversation state updated',
state: global.conversationState
});
default:
return NextResponse.json({
success: false,
error: 'Invalid action. Use "reset" or "update"'
}, { status: 400 });
}
} catch (error) {
console.error('[conversation-state] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
// DELETE: Clear conversation state
export async function DELETE() {
try {
global.conversationState = null;
console.log('[conversation-state] Cleared conversation state');
return NextResponse.json({
success: true,
message: 'Conversation state cleared'
});
} catch (error) {
console.error('[conversation-state] Error clearing state:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+365
View File
@@ -0,0 +1,365 @@
import { NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter';
import type { SandboxState } from '@/types/sandbox';
import { appConfig } from '@/config/app.config';
// Store active sandbox globally
declare global {
var activeSandbox: any;
var sandboxData: any;
var existingFiles: Set<string>;
var sandboxState: SandboxState;
}
export async function POST() {
let sandbox: any = null;
try {
console.log('[create-ai-sandbox] Creating base sandbox...');
// Kill existing sandbox if any
if (global.activeSandbox) {
console.log('[create-ai-sandbox] Killing existing sandbox...');
try {
await global.activeSandbox.kill();
} catch (e) {
console.error('Failed to close existing sandbox:', e);
}
global.activeSandbox = null;
}
// Clear existing files tracking
if (global.existingFiles) {
global.existingFiles.clear();
} else {
global.existingFiles = new Set<string>();
}
// Create base sandbox - we'll set up Vite ourselves for full control
console.log(`[create-ai-sandbox] Creating base E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`);
sandbox = await Sandbox.create({
apiKey: process.env.E2B_API_KEY,
timeoutMs: appConfig.e2b.timeoutMs
});
const sandboxId = (sandbox as any).sandboxId || Date.now().toString();
const host = (sandbox as any).getHost(appConfig.e2b.vitePort);
console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`);
console.log(`[create-ai-sandbox] Sandbox host: ${host}`);
// Set up a basic Vite React app using Python to write files
console.log('[create-ai-sandbox] Setting up Vite React app...');
// Write all files in a single Python script to avoid multiple executions
const setupScript = `
import os
import json
print('Setting up React app with Vite and Tailwind...')
# Create directory structure
os.makedirs('/home/user/app/src', exist_ok=True)
# Package.json
package_json = {
"name": "sandbox-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.0",
"vite": "^4.3.9",
"tailwindcss": "^3.3.0",
"postcss": "^8.4.31",
"autoprefixer": "^10.4.16"
}
}
with open('/home/user/app/package.json', 'w') as f:
json.dump(package_json, f, indent=2)
print('✓ package.json')
# Vite config for E2B - with allowedHosts
vite_config = """import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// E2B-compatible Vite configuration
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
hmr: false,
allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1']
}
})"""
with open('/home/user/app/vite.config.js', 'w') as f:
f.write(vite_config)
print('✓ vite.config.js')
# Tailwind config - standard without custom design tokens
tailwind_config = """/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}"""
with open('/home/user/app/tailwind.config.js', 'w') as f:
f.write(tailwind_config)
print('✓ tailwind.config.js')
# PostCSS config
postcss_config = """export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}"""
with open('/home/user/app/postcss.config.js', 'w') as f:
f.write(postcss_config)
print('✓ postcss.config.js')
# Index.html
index_html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>"""
with open('/home/user/app/index.html', 'w') as f:
f.write(index_html)
print('✓ index.html')
# Main.jsx
main_jsx = """import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)"""
with open('/home/user/app/src/main.jsx', 'w') as f:
f.write(main_jsx)
print('✓ src/main.jsx')
# App.jsx with explicit Tailwind test
app_jsx = """function App() {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
<div className="text-center max-w-2xl">
<p className="text-lg text-gray-400">
Sandbox Ready<br/>
Start building your React app with Vite and Tailwind CSS!
</p>
</div>
</div>
)
}
export default App"""
with open('/home/user/app/src/App.jsx', 'w') as f:
f.write(app_jsx)
print('✓ src/App.jsx')
# Index.css with explicit Tailwind directives
index_css = """@tailwind base;
@tailwind components;
@tailwind utilities;
/* Force Tailwind to load */
@layer base {
:root {
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: rgb(17 24 39);
}"""
with open('/home/user/app/src/index.css', 'w') as f:
f.write(index_css)
print('✓ src/index.css')
print('\\nAll files created successfully!')
`;
// Execute the setup script
await sandbox.runCode(setupScript);
// Install dependencies
console.log('[create-ai-sandbox] Installing dependencies...');
await sandbox.runCode(`
import subprocess
import sys
print('Installing npm packages...')
result = subprocess.run(
['npm', 'install'],
cwd='/home/user/app',
capture_output=True,
text=True
)
if result.returncode == 0:
print('✓ Dependencies installed successfully')
else:
print(f'⚠ Warning: npm install had issues: {result.stderr}')
# Continue anyway as it might still work
`);
// Start Vite dev server
console.log('[create-ai-sandbox] Starting Vite dev server...');
await sandbox.runCode(`
import subprocess
import os
import time
os.chdir('/home/user/app')
# Kill any existing Vite processes
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
time.sleep(1)
# Start Vite dev server
env = os.environ.copy()
env['FORCE_COLOR'] = '0'
process = subprocess.Popen(
['npm', 'run', 'dev'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env
)
print(f'✓ Vite dev server started with PID: {process.pid}')
print('Waiting for server to be ready...')
`);
// Wait for Vite to be fully ready
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
// Force Tailwind CSS to rebuild by touching the CSS file
await sandbox.runCode(`
import os
import time
# Touch the CSS file to trigger rebuild
css_file = '/home/user/app/src/index.css'
if os.path.exists(css_file):
os.utime(css_file, None)
print('✓ Triggered CSS rebuild')
# Also ensure PostCSS processes it
time.sleep(2)
print('✓ Tailwind CSS should be loaded')
`);
// Store sandbox globally
global.activeSandbox = sandbox;
global.sandboxData = {
sandboxId,
url: `https://${host}`
};
// Set extended timeout on the sandbox instance if method available
if (typeof sandbox.setTimeout === 'function') {
sandbox.setTimeout(appConfig.e2b.timeoutMs);
console.log(`[create-ai-sandbox] Set sandbox timeout to ${appConfig.e2b.timeoutMinutes} minutes`);
}
// Initialize sandbox state
global.sandboxState = {
fileCache: {
files: {},
lastSync: Date.now(),
sandboxId
},
sandbox,
sandboxData: {
sandboxId,
url: `https://${host}`
}
};
// Track initial files
global.existingFiles.add('src/App.jsx');
global.existingFiles.add('src/main.jsx');
global.existingFiles.add('src/index.css');
global.existingFiles.add('index.html');
global.existingFiles.add('package.json');
global.existingFiles.add('vite.config.js');
global.existingFiles.add('tailwind.config.js');
global.existingFiles.add('postcss.config.js');
console.log('[create-ai-sandbox] Sandbox ready at:', `https://${host}`);
return NextResponse.json({
success: true,
sandboxId,
url: `https://${host}`,
message: 'Sandbox created and Vite React app initialized'
});
} catch (error) {
console.error('[create-ai-sandbox] Error:', error);
// Clean up on error
if (sandbox) {
try {
await sandbox.kill();
} catch (e) {
console.error('Failed to close sandbox on error:', e);
}
}
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Failed to create sandbox',
details: error instanceof Error ? error.stack : undefined
},
{ status: 500 }
);
}
}
+71
View File
@@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from 'next/server';
declare global {
var activeSandbox: any;
}
export async function POST(request: NextRequest) {
try {
if (!global.activeSandbox) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 400 });
}
console.log('[create-zip] Creating project zip...');
// Create zip file in sandbox
const result = await global.activeSandbox.runCode(`
import zipfile
import os
import json
os.chdir('/home/user/app')
# Create zip file
with zipfile.ZipFile('/tmp/project.zip', 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk('.'):
# Skip node_modules and .git
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist']]
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, '.')
zipf.write(file_path, arcname)
# Get file size
file_size = os.path.getsize('/tmp/project.zip')
print(f" Created project.zip ({file_size} bytes)")
`);
// Read the zip file and convert to base64
const readResult = await global.activeSandbox.runCode(`
import base64
with open('/tmp/project.zip', 'rb') as f:
content = f.read()
encoded = base64.b64encode(content).decode('utf-8')
print(encoded)
`);
const base64Content = readResult.logs.stdout.join('').trim();
// Create a data URL for download
const dataUrl = `data:application/zip;base64,${base64Content}`;
return NextResponse.json({
success: true,
dataUrl,
fileName: 'e2b-project.zip',
message: 'Zip file created successfully'
});
} catch (error) {
console.error('[create-zip] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
@@ -0,0 +1,260 @@
import { NextRequest, NextResponse } from 'next/server';
declare global {
var activeSandbox: any;
}
export async function POST(request: NextRequest) {
try {
const { files } = await request.json();
if (!files || typeof files !== 'object') {
return NextResponse.json({
success: false,
error: 'Files object is required'
}, { status: 400 });
}
if (!global.activeSandbox) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 404 });
}
console.log('[detect-and-install-packages] Processing files:', Object.keys(files));
// Extract all import statements from the files
const imports = new Set<string>();
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s*,?\s*)*(?:from\s+)?['"]([^'"]+)['"]/g;
const requireRegex = /require\s*\(['"]([^'"]+)['"]\)/g;
for (const [filePath, content] of Object.entries(files)) {
if (typeof content !== 'string') continue;
// Skip non-JS/JSX/TS/TSX files
if (!filePath.match(/\.(jsx?|tsx?)$/)) continue;
// Find ES6 imports
let match;
while ((match = importRegex.exec(content)) !== null) {
imports.add(match[1]);
}
// Find CommonJS requires
while ((match = requireRegex.exec(content)) !== null) {
imports.add(match[1]);
}
}
console.log('[detect-and-install-packages] Found imports:', Array.from(imports));
// Log specific heroicons imports
const heroiconImports = Array.from(imports).filter(imp => imp.includes('heroicons'));
if (heroiconImports.length > 0) {
console.log('[detect-and-install-packages] Heroicon imports:', heroiconImports);
}
// Filter out relative imports and built-in modules
const packages = Array.from(imports).filter(imp => {
// Skip relative imports
if (imp.startsWith('.') || imp.startsWith('/')) return false;
// Skip built-in Node modules
const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process'];
if (builtins.includes(imp)) return false;
// Extract package name (handle scoped packages and subpaths)
const parts = imp.split('/');
if (imp.startsWith('@')) {
// Scoped package like @vitejs/plugin-react
return true;
} else {
// Regular package, return just the first part
return true;
}
});
// Extract just the package names (without subpaths)
const packageNames = packages.map(pkg => {
if (pkg.startsWith('@')) {
// Scoped package: @scope/package or @scope/package/subpath
const parts = pkg.split('/');
return parts.slice(0, 2).join('/');
} else {
// Regular package: package or package/subpath
return pkg.split('/')[0];
}
});
// Remove duplicates
const uniquePackages = [...new Set(packageNames)];
console.log('[detect-and-install-packages] Packages to install:', uniquePackages);
if (uniquePackages.length === 0) {
return NextResponse.json({
success: true,
packagesInstalled: [],
message: 'No new packages to install'
});
}
// Check which packages are already installed
const checkResult = await global.activeSandbox.runCode(`
import os
import json
installed = []
missing = []
packages = ${JSON.stringify(uniquePackages)}
for package in packages:
# Handle scoped packages
if package.startswith('@'):
package_path = f"/home/user/app/node_modules/{package}"
else:
package_path = f"/home/user/app/node_modules/{package}"
if os.path.exists(package_path):
installed.append(package)
else:
missing.append(package)
result = {
'installed': installed,
'missing': missing
}
print(json.dumps(result))
`);
const status = JSON.parse(checkResult.logs.stdout.join(''));
console.log('[detect-and-install-packages] Package status:', status);
if (status.missing.length === 0) {
return NextResponse.json({
success: true,
packagesInstalled: [],
packagesAlreadyInstalled: status.installed,
message: 'All packages already installed'
});
}
// Install missing packages
console.log('[detect-and-install-packages] Installing packages:', status.missing);
const installResult = await global.activeSandbox.runCode(`
import subprocess
import os
import json
os.chdir('/home/user/app')
packages_to_install = ${JSON.stringify(status.missing)}
# Join packages into a single install command
packages_str = ' '.join(packages_to_install)
cmd = f'npm install {packages_str} --save'
print(f"Running: {cmd}")
# Run npm install with explicit save flag
result = subprocess.run(['npm', 'install', '--save'] + packages_to_install,
capture_output=True,
text=True,
cwd='/home/user/app',
timeout=60)
print("stdout:", result.stdout)
if result.stderr:
print("stderr:", result.stderr)
# Verify installation
installed = []
failed = []
for package in packages_to_install:
# Handle scoped packages correctly
if package.startswith('@'):
# For scoped packages like @heroicons/react
package_path = f"/home/user/app/node_modules/{package}"
else:
package_path = f"/home/user/app/node_modules/{package}"
if os.path.exists(package_path):
installed.append(package)
print(f"✓ Verified installation of {package}")
else:
# Check if it's a submodule of an installed package
base_package = package.split('/')[0]
if package.startswith('@'):
# For @scope/package, the base is @scope/package
base_package = '/'.join(package.split('/')[:2])
base_path = f"/home/user/app/node_modules/{base_package}"
if os.path.exists(base_path):
installed.append(package)
print(f"✓ Verified installation of {package} (via {base_package})")
else:
failed.append(package)
print(f"✗ Failed to verify installation of {package}")
result_data = {
'installed': installed,
'failed': failed,
'returncode': result.returncode
}
print("\\nResult:", json.dumps(result_data))
`, { timeout: 60000 });
// Parse the result more safely
let installStatus;
try {
const stdout = installResult.logs.stdout.join('');
const resultMatch = stdout.match(/Result:\s*({.*})/);
if (resultMatch) {
installStatus = JSON.parse(resultMatch[1]);
} else {
// Fallback parsing
const lines = stdout.split('\n');
const resultLine = lines.find((line: string) => line.includes('Result:'));
if (resultLine) {
installStatus = JSON.parse(resultLine.split('Result:')[1].trim());
} else {
throw new Error('Could not find Result in output');
}
}
} catch (parseError) {
console.error('[detect-and-install-packages] Failed to parse install result:', parseError);
console.error('[detect-and-install-packages] stdout:', installResult.logs.stdout.join(''));
// Fallback to assuming all packages were installed
installStatus = {
installed: status.missing,
failed: [],
returncode: 0
};
}
if (installStatus.failed.length > 0) {
console.error('[detect-and-install-packages] Failed to install:', installStatus.failed);
}
return NextResponse.json({
success: true,
packagesInstalled: installStatus.installed,
packagesFailed: installStatus.failed,
packagesAlreadyInstalled: status.installed,
message: `Installed ${installStatus.installed.length} packages`,
logs: installResult.logs.stdout.join('\n')
});
} catch (error) {
console.error('[detect-and-install-packages] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
File diff suppressed because it is too large Load Diff
+183
View File
@@ -0,0 +1,183 @@
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';
declare global {
var activeSandbox: any;
}
export async function GET() {
try {
if (!global.activeSandbox) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 404 });
}
console.log('[get-sandbox-files] Fetching and analyzing file structure...');
// Get all React/JS/CSS files
const result = await global.activeSandbox.runCode(`
import os
import json
def get_files_content(directory='/home/user/app', extensions=['.jsx', '.js', '.tsx', '.ts', '.css', '.json']):
files_content = {}
for root, dirs, files in os.walk(directory):
# Skip node_modules and other unwanted directories
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'dist', 'build']]
for file in files:
if any(file.endswith(ext) for ext in extensions):
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, '/home/user/app')
try:
with open(file_path, 'r') as f:
content = f.read()
# Only include files under 10KB to avoid huge responses
if len(content) < 10000:
files_content[relative_path] = content
except:
pass
return files_content
# Get the files
files = get_files_content()
# Also get the directory structure
structure = []
for root, dirs, files in os.walk('/home/user/app'):
level = root.replace('/home/user/app', '').count(os.sep)
indent = ' ' * 2 * level
structure.append(f"{indent}{os.path.basename(root)}/")
sub_indent = ' ' * 2 * (level + 1)
for file in files:
if not any(skip in root for skip in ['node_modules', '.git', 'dist', 'build']):
structure.append(f"{sub_indent}{file}")
result = {
'files': files,
'structure': '\\n'.join(structure[:50]) # Limit structure to 50 lines
}
print(json.dumps(result))
`);
const output = result.logs.stdout.join('');
const parsedResult = JSON.parse(output);
// Build enhanced file manifest
const fileManifest: FileManifest = {
files: {},
routes: [],
componentTree: {},
entryPoint: '',
styleFiles: [],
timestamp: Date.now(),
};
// Process each file
for (const [relativePath, content] of Object.entries(parsedResult.files)) {
const fullPath = `/home/user/app/${relativePath}`;
// Create base file info
const fileInfo: FileInfo = {
content: content as string,
type: 'utility',
path: fullPath,
relativePath,
lastModified: Date.now(),
};
// Parse JavaScript/JSX files
if (relativePath.match(/\.(jsx?|tsx?)$/)) {
const parseResult = parseJavaScriptFile(content as string, fullPath);
Object.assign(fileInfo, parseResult);
// Identify entry point
if (relativePath === 'src/main.jsx' || relativePath === 'src/index.jsx') {
fileManifest.entryPoint = fullPath;
}
// Identify App.jsx
if (relativePath === 'src/App.jsx' || relativePath === 'App.jsx') {
fileManifest.entryPoint = fileManifest.entryPoint || fullPath;
}
}
// Track style files
if (relativePath.endsWith('.css')) {
fileManifest.styleFiles.push(fullPath);
fileInfo.type = 'style';
}
fileManifest.files[fullPath] = fileInfo;
}
// Build component tree
fileManifest.componentTree = buildComponentTree(fileManifest.files);
// Extract routes (simplified - looks for Route components or page pattern)
fileManifest.routes = extractRoutes(fileManifest.files);
// Update global file cache with manifest
if (global.sandboxState?.fileCache) {
global.sandboxState.fileCache.manifest = fileManifest;
}
return NextResponse.json({
success: true,
files: parsedResult.files,
structure: parsedResult.structure,
fileCount: Object.keys(parsedResult.files).length,
manifest: fileManifest,
});
} catch (error) {
console.error('[get-sandbox-files] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
function extractRoutes(files: Record<string, FileInfo>): RouteInfo[] {
const routes: RouteInfo[] = [];
// Look for React Router usage
for (const [path, fileInfo] of Object.entries(files)) {
if (fileInfo.content.includes('<Route') || fileInfo.content.includes('createBrowserRouter')) {
// Extract route definitions (simplified)
const routeMatches = fileInfo.content.matchAll(/path=["']([^"']+)["'].*(?:element|component)={([^}]+)}/g);
for (const match of routeMatches) {
const [, routePath, componentRef] = match;
routes.push({
path: routePath,
component: path,
});
}
}
// Check for Next.js style pages
if (fileInfo.relativePath.startsWith('pages/') || fileInfo.relativePath.startsWith('src/pages/')) {
const routePath = '/' + fileInfo.relativePath
.replace(/^(src\/)?pages\//, '')
.replace(/\.(jsx?|tsx?)$/, '')
.replace(/index$/, '');
routes.push({
path: routePath,
component: path,
});
}
}
return routes;
}
+369
View File
@@ -0,0 +1,369 @@
import { NextRequest, NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter';
declare global {
var activeSandbox: any;
var sandboxData: any;
}
export async function POST(request: NextRequest) {
try {
const { packages, sandboxId } = await request.json();
if (!packages || !Array.isArray(packages) || packages.length === 0) {
return NextResponse.json({
success: false,
error: 'Packages array is required'
}, { status: 400 });
}
// Validate and deduplicate package names
const validPackages = [...new Set(packages)]
.filter(pkg => pkg && typeof pkg === 'string' && pkg.trim() !== '')
.map(pkg => pkg.trim());
if (validPackages.length === 0) {
return NextResponse.json({
success: false,
error: 'No valid package names provided'
}, { status: 400 });
}
// Log if duplicates were found
if (packages.length !== validPackages.length) {
console.log(`[install-packages] Cleaned packages: removed ${packages.length - validPackages.length} invalid/duplicate entries`);
console.log(`[install-packages] Original:`, packages);
console.log(`[install-packages] Cleaned:`, validPackages);
}
// Try to get sandbox - either from global or reconnect
let sandbox = global.activeSandbox;
if (!sandbox && sandboxId) {
console.log(`[install-packages] Reconnecting to sandbox ${sandboxId}...`);
try {
sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY });
global.activeSandbox = sandbox;
console.log(`[install-packages] Successfully reconnected to sandbox ${sandboxId}`);
} catch (error) {
console.error(`[install-packages] Failed to reconnect to sandbox:`, error);
return NextResponse.json({
success: false,
error: `Failed to reconnect to sandbox: ${(error as Error).message}`
}, { status: 500 });
}
}
if (!sandbox) {
return NextResponse.json({
success: false,
error: 'No active sandbox available'
}, { status: 400 });
}
console.log('[install-packages] Installing packages:', packages);
// Create a response stream for real-time updates
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
// Function to send progress updates
const sendProgress = async (data: any) => {
const message = `data: ${JSON.stringify(data)}\n\n`;
await writer.write(encoder.encode(message));
};
// Start installation in background
(async (sandboxInstance) => {
try {
await sendProgress({
type: 'start',
message: `Installing ${validPackages.length} package${validPackages.length > 1 ? 's' : ''}...`,
packages: validPackages
});
// Kill any existing Vite process first
await sendProgress({ type: 'status', message: 'Stopping development server...' });
await sandboxInstance.runCode(`
import subprocess
import os
import signal
# Try to kill any existing Vite process
try:
with open('/tmp/vite-process.pid', 'r') as f:
pid = int(f.read().strip())
os.kill(pid, signal.SIGTERM)
print("Stopped existing Vite process")
except:
print("No existing Vite process found")
`);
// Check which packages are already installed
await sendProgress({
type: 'status',
message: 'Checking installed packages...'
});
const checkResult = await sandboxInstance.runCode(`
import os
import json
os.chdir('/home/user/app')
# Read package.json to check installed packages
try:
with open('package.json', 'r') as f:
package_json = json.load(f)
dependencies = package_json.get('dependencies', {})
dev_dependencies = package_json.get('devDependencies', {})
all_deps = {**dependencies, **dev_dependencies}
# Check which packages need to be installed
packages_to_check = ${JSON.stringify(validPackages)}
already_installed = []
need_install = []
for pkg in packages_to_check:
# Handle scoped packages
if pkg.startswith('@'):
pkg_name = pkg
else:
# Extract package name without version
pkg_name = pkg.split('@')[0]
if pkg_name in all_deps:
already_installed.append(pkg_name)
else:
need_install.append(pkg)
print(f"Already installed: {already_installed}")
print(f"Need to install: {need_install}")
print(f"NEED_INSTALL:{json.dumps(need_install)}")
except Exception as e:
print(f"Error checking packages: {e}")
print(f"NEED_INSTALL:{json.dumps(packages_to_check)}")
`);
// Parse packages that need installation
let packagesToInstall = validPackages;
// Check if checkResult has the expected structure
if (checkResult && checkResult.results && checkResult.results[0] && checkResult.results[0].text) {
const outputLines = checkResult.results[0].text.split('\n');
for (const line of outputLines) {
if (line.startsWith('NEED_INSTALL:')) {
try {
packagesToInstall = JSON.parse(line.substring('NEED_INSTALL:'.length));
} catch (e) {
console.error('Failed to parse packages to install:', e);
}
}
}
} else {
console.error('[install-packages] Invalid checkResult structure:', checkResult);
// If we can't check, just try to install all packages
packagesToInstall = validPackages;
}
if (packagesToInstall.length === 0) {
await sendProgress({
type: 'success',
message: 'All packages are already installed',
installedPackages: [],
alreadyInstalled: validPackages
});
return;
}
// Install only packages that aren't already installed
const packageList = packagesToInstall.join(' ');
await sendProgress({
type: 'command',
command: `npm install ${packageList}`,
message: `Installing ${packagesToInstall.length} new package(s)...`
});
const installResult = await sandboxInstance.runCode(`
import subprocess
import os
os.chdir('/home/user/app')
# Run npm install with output capture
packages_to_install = ${JSON.stringify(packagesToInstall)}
cmd_args = ['npm', 'install', '--legacy-peer-deps'] + packages_to_install
print(f"Running command: {' '.join(cmd_args)}")
process = subprocess.Popen(
cmd_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Stream output
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.strip())
# Get the return code
rc = process.poll()
# Capture any stderr
stderr = process.stderr.read()
if stderr:
print("STDERR:", stderr)
if 'ERESOLVE' in stderr:
print("ERESOLVE_ERROR: Dependency conflict detected - using --legacy-peer-deps flag")
print(f"\\nInstallation completed with code: {rc}")
# Verify packages were installed
import json
with open('/home/user/app/package.json', 'r') as f:
package_json = json.load(f)
installed = []
for pkg in ${JSON.stringify(packagesToInstall)}:
if pkg in package_json.get('dependencies', {}):
installed.append(pkg)
print(f"✓ Verified {pkg}")
else:
print(f"✗ Package {pkg} not found in dependencies")
print(f"\\nVerified installed packages: {installed}")
`, { timeout: 60000 }); // 60 second timeout for npm install
// Send npm output
const output = installResult?.output || installResult?.logs?.stdout?.join('\n') || '';
const npmOutputLines = output.split('\n').filter((line: string) => line.trim());
for (const line of npmOutputLines) {
if (line.includes('STDERR:')) {
const errorMsg = line.replace('STDERR:', '').trim();
if (errorMsg && errorMsg !== 'undefined') {
await sendProgress({ type: 'error', message: errorMsg });
}
} else if (line.includes('ERESOLVE_ERROR:')) {
const msg = line.replace('ERESOLVE_ERROR:', '').trim();
await sendProgress({
type: 'warning',
message: `Dependency conflict resolved with --legacy-peer-deps: ${msg}`
});
} else if (line.includes('npm WARN')) {
await sendProgress({ type: 'warning', message: line });
} else if (line.trim() && !line.includes('undefined')) {
await sendProgress({ type: 'output', message: line });
}
}
// Check if installation was successful
const installedMatch = output.match(/Verified installed packages: \[(.*?)\]/);
let installedPackages: string[] = [];
if (installedMatch && installedMatch[1]) {
installedPackages = installedMatch[1]
.split(',')
.map((p: string) => p.trim().replace(/'/g, ''))
.filter((p: string) => p.length > 0);
}
if (installedPackages.length > 0) {
await sendProgress({
type: 'success',
message: `Successfully installed: ${installedPackages.join(', ')}`,
installedPackages
});
} else {
await sendProgress({
type: 'error',
message: 'Failed to verify package installation'
});
}
// Restart Vite dev server
await sendProgress({ type: 'status', message: 'Restarting development server...' });
await sandboxInstance.runCode(`
import subprocess
import os
import time
os.chdir('/home/user/app')
# Kill any existing Vite processes
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
time.sleep(1)
# Start Vite dev server
env = os.environ.copy()
env['FORCE_COLOR'] = '0'
process = subprocess.Popen(
['npm', 'run', 'dev'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env
)
print(f'✓ Vite dev server restarted with PID: {process.pid}')
# Store process info for later
with open('/tmp/vite-process.pid', 'w') as f:
f.write(str(process.pid))
# Wait a bit for Vite to start up
time.sleep(3)
# Touch files to trigger Vite reload
subprocess.run(['touch', '/home/user/app/package.json'])
subprocess.run(['touch', '/home/user/app/vite.config.js'])
print("Vite restarted and should now recognize all packages")
`);
await sendProgress({
type: 'complete',
message: 'Package installation complete and dev server restarted!',
installedPackages
});
} catch (error) {
const errorMessage = (error as Error).message;
if (errorMessage && errorMessage !== 'undefined') {
await sendProgress({
type: 'error',
message: errorMessage
});
}
} finally {
await writer.close();
}
})(sandbox);
// Return the stream
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
} catch (error) {
console.error('[install-packages] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+49
View File
@@ -0,0 +1,49 @@
import { NextResponse } from 'next/server';
declare global {
var activeSandbox: any;
var sandboxData: any;
var existingFiles: Set<string>;
}
export async function POST() {
try {
console.log('[kill-sandbox] Killing active sandbox...');
let sandboxKilled = false;
// Kill existing sandbox if any
if (global.activeSandbox) {
try {
await global.activeSandbox.close();
sandboxKilled = true;
console.log('[kill-sandbox] Sandbox closed successfully');
} catch (e) {
console.error('[kill-sandbox] Failed to close sandbox:', e);
}
global.activeSandbox = null;
global.sandboxData = null;
}
// Clear existing files tracking
if (global.existingFiles) {
global.existingFiles.clear();
}
return NextResponse.json({
success: true,
sandboxKilled,
message: 'Sandbox cleaned up successfully'
});
} catch (error) {
console.error('[kill-sandbox] Error:', error);
return NextResponse.json(
{
success: false,
error: (error as Error).message
},
{ status: 500 }
);
}
}
+118
View File
@@ -0,0 +1,118 @@
import { NextResponse } from 'next/server';
declare global {
var activeSandbox: any;
}
export async function GET() {
try {
if (!global.activeSandbox) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 400 });
}
console.log('[monitor-vite-logs] Checking Vite process logs...');
// Check both the error file and recent logs
const result = await global.activeSandbox.runCode(`
import json
import subprocess
import re
errors = []
# First check the error file
try:
with open('/tmp/vite-errors.json', 'r') as f:
data = json.load(f)
errors.extend(data.get('errors', []))
except:
pass
# Also check if we can get recent Vite logs
try:
# Try to get the Vite process PID
with open('/tmp/vite-process.pid', 'r') as f:
pid = int(f.read().strip())
# Check if process is still running and get its logs
# This is a bit hacky but works for our use case
result = subprocess.run(['ps', '-p', str(pid)], capture_output=True, text=True)
if result.returncode == 0:
# Process is running, try to check for errors in output
# Note: We can't easily get stdout/stderr from a running process
# but we can check if there are new errors
pass
except:
pass
# Also scan the current console output for any HMR errors
# This won't catch everything but helps with recent errors
try:
# Check if there's a log file we can read
import os
log_files = []
for root, dirs, files in os.walk('/tmp'):
for file in files:
if 'vite' in file.lower() and file.endswith('.log'):
log_files.append(os.path.join(root, file))
for log_file in log_files[:5]: # Check up to 5 log files
try:
with open(log_file, 'r') as f:
content = f.read()
# Look for import errors
import_errors = re.findall(r'Failed to resolve import "([^"]+)"', content)
for pkg in import_errors:
if not pkg.startswith('.'):
# Extract base package name
if pkg.startswith('@'):
parts = pkg.split('/')
final_pkg = '/'.join(parts[:2]) if len(parts) >= 2 else pkg
else:
final_pkg = pkg.split('/')[0]
error_obj = {
"type": "npm-missing",
"package": final_pkg,
"message": f"Failed to resolve import \\"{pkg}\\"",
"file": "Unknown"
}
# Avoid duplicates
if not any(e['package'] == error_obj['package'] for e in errors):
errors.append(error_obj)
except:
pass
except Exception as e:
print(f"Error scanning logs: {e}")
# Deduplicate errors
unique_errors = []
seen_packages = set()
for error in errors:
if error.get('package') and error['package'] not in seen_packages:
seen_packages.add(error['package'])
unique_errors.append(error)
print(json.dumps({"errors": unique_errors}))
`, { timeout: 5000 });
const data = JSON.parse(result.output || '{"errors": []}');
return NextResponse.json({
success: true,
hasErrors: data.errors.length > 0,
errors: data.errors
});
} catch (error) {
console.error('[monitor-vite-logs] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+62
View File
@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from 'next/server';
declare global {
var viteErrors: any[];
}
// Initialize global viteErrors array if it doesn't exist
if (!global.viteErrors) {
global.viteErrors = [];
}
export async function POST(request: NextRequest) {
try {
const { error, file, type = 'runtime-error' } = await request.json();
if (!error) {
return NextResponse.json({
success: false,
error: 'Error message is required'
}, { status: 400 });
}
// Parse the error to extract useful information
const errorObj: any = {
type,
message: error,
file: file || 'unknown',
timestamp: new Date().toISOString()
};
// Extract import information if it's an import error
const importMatch = error.match(/Failed to resolve import ['"]([^'"]+)['"] from ['"]([^'"]+)['"]/);
if (importMatch) {
errorObj.type = 'import-error';
errorObj.import = importMatch[1];
errorObj.file = importMatch[2];
}
// Add to global errors array
global.viteErrors.push(errorObj);
// Keep only last 50 errors
if (global.viteErrors.length > 50) {
global.viteErrors = global.viteErrors.slice(-50);
}
console.log('[report-vite-error] Error reported:', errorObj);
return NextResponse.json({
success: true,
message: 'Error reported successfully',
error: errorObj
});
} catch (error) {
console.error('[report-vite-error] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+136
View File
@@ -0,0 +1,136 @@
import { NextResponse } from 'next/server';
declare global {
var activeSandbox: any;
}
export async function POST() {
try {
if (!global.activeSandbox) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 400 });
}
console.log('[restart-vite] Forcing Vite restart...');
// Kill existing Vite process and restart
const result = await global.activeSandbox.runCode(`
import subprocess
import os
import signal
import time
import threading
import json
import sys
# Kill existing Vite process
try:
with open('/tmp/vite-process.pid', 'r') as f:
pid = int(f.read().strip())
os.kill(pid, signal.SIGTERM)
print("Killed existing Vite process")
time.sleep(1)
except:
print("No existing Vite process found")
os.chdir('/home/user/app')
# Clear error file
error_file = '/tmp/vite-errors.json'
with open(error_file, 'w') as f:
json.dump({"errors": [], "lastChecked": time.time()}, f)
# Function to monitor Vite output for errors
def monitor_output(proc, error_file):
while True:
line = proc.stderr.readline()
if not line:
break
sys.stdout.write(line) # Also print to console
# Check for import resolution errors
if "Failed to resolve import" in line:
try:
# Extract package name from error
import_match = line.find('"')
if import_match != -1:
end_match = line.find('"', import_match + 1)
if end_match != -1:
package_name = line[import_match + 1:end_match]
# Skip relative imports
if not package_name.startswith('.'):
with open(error_file, 'r') as f:
data = json.load(f)
# Handle scoped packages correctly
if package_name.startswith('@'):
# For @scope/package, keep the scope
pkg_parts = package_name.split('/')
if len(pkg_parts) >= 2:
final_package = '/'.join(pkg_parts[:2])
else:
final_package = package_name
else:
# For regular packages, just take the first part
final_package = package_name.split('/')[0]
error_obj = {
"type": "npm-missing",
"package": final_package,
"message": line.strip(),
"timestamp": time.time()
}
# Avoid duplicates
if not any(e['package'] == error_obj['package'] for e in data['errors']):
data['errors'].append(error_obj)
with open(error_file, 'w') as f:
json.dump(data, f)
print(f"WARNING: Detected missing package: {error_obj['package']}")
except Exception as e:
print(f"Error parsing Vite error: {e}")
# Start Vite with error monitoring
process = subprocess.Popen(
['npm', 'run', 'dev'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
# Start monitoring thread
monitor_thread = threading.Thread(target=monitor_output, args=(process, error_file))
monitor_thread.daemon = True
monitor_thread.start()
print("Vite restarted successfully!")
# Store process info for later
with open('/tmp/vite-process.pid', 'w') as f:
f.write(str(process.pid))
# Wait for Vite to fully start
time.sleep(5)
print("Vite is ready")
`);
return NextResponse.json({
success: true,
message: 'Vite restarted successfully',
output: result.output
});
} catch (error) {
console.error('[restart-vite] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+62
View File
@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter';
// Get active sandbox from global state (in production, use a proper state management solution)
declare global {
var activeSandbox: any;
}
export async function POST(request: NextRequest) {
try {
const { command } = await request.json();
if (!command) {
return NextResponse.json({
success: false,
error: 'Command is required'
}, { status: 400 });
}
if (!global.activeSandbox) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 400 });
}
console.log(`[run-command] Executing: ${command}`);
const result = await global.activeSandbox.runCode(`
import subprocess
import os
os.chdir('/home/user/app')
result = subprocess.run(${JSON.stringify(command.split(' '))},
capture_output=True,
text=True,
shell=False)
print("STDOUT:")
print(result.stdout)
if result.stderr:
print("\\nSTDERR:")
print(result.stderr)
print(f"\\nReturn code: {result.returncode}")
`);
const output = result.logs.stdout.join('\n');
return NextResponse.json({
success: true,
output,
message: 'Command executed successfully'
});
} catch (error) {
console.error('[run-command] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+74
View File
@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from 'next/server';
declare global {
var activeSandbox: any;
}
export async function GET(request: NextRequest) {
try {
if (!global.activeSandbox) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 400 });
}
console.log('[sandbox-logs] Fetching Vite dev server logs...');
// Get the last N lines of the Vite dev server output
const result = await global.activeSandbox.runCode(`
import subprocess
import os
# Try to get the Vite process output
try:
# Read the last 100 lines of any log files
log_content = []
# Check if there are any node processes running
ps_result = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
vite_processes = [line for line in ps_result.stdout.split('\\n') if 'vite' in line.lower()]
if vite_processes:
log_content.append("Vite is running")
else:
log_content.append("Vite process not found")
# Try to capture recent console output (this is a simplified approach)
# In a real implementation, you'd want to capture the Vite process output directly
print(json.dumps({
"hasErrors": False,
"logs": log_content,
"status": "running" if vite_processes else "stopped"
}))
except Exception as e:
print(json.dumps({
"hasErrors": True,
"logs": [str(e)],
"status": "error"
}))
`);
try {
const logData = JSON.parse(result.output || '{}');
return NextResponse.json({
success: true,
...logData
});
} catch {
return NextResponse.json({
success: true,
hasErrors: false,
logs: [result.output],
status: 'unknown'
});
}
} catch (error) {
console.error('[sandbox-logs] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+54
View File
@@ -0,0 +1,54 @@
import { NextResponse } from 'next/server';
declare global {
var activeSandbox: any;
var sandboxData: any;
var existingFiles: Set<string>;
}
export async function GET() {
try {
// Check if sandbox exists
const sandboxExists = !!global.activeSandbox;
let sandboxHealthy = false;
let sandboxInfo = null;
if (sandboxExists && global.activeSandbox) {
try {
// Since Python isn't available in the Vite template, just check if sandbox exists
// The sandbox object existing is enough to confirm it's healthy
sandboxHealthy = true;
sandboxInfo = {
sandboxId: global.sandboxData?.sandboxId,
url: global.sandboxData?.url,
filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [],
lastHealthCheck: new Date().toISOString()
};
} catch (error) {
console.error('[sandbox-status] Health check failed:', error);
sandboxHealthy = false;
}
}
return NextResponse.json({
success: true,
active: sandboxExists,
healthy: sandboxHealthy,
sandboxData: sandboxInfo,
message: sandboxHealthy
? 'Sandbox is active and healthy'
: sandboxExists
? 'Sandbox exists but is not responding'
: 'No active sandbox'
});
} catch (error) {
console.error('[sandbox-status] Error:', error);
return NextResponse.json({
success: false,
active: false,
error: (error as Error).message
}, { status: 500 });
}
}
+56
View File
@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
try {
const { url } = await req.json();
if (!url) {
return NextResponse.json({ error: 'URL is required' }, { status: 400 });
}
// Use Firecrawl API to capture screenshot
const firecrawlResponse = await fetch('https://api.firecrawl.dev/v1/scrape', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FIRECRAWL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
url,
formats: ['screenshot'], // Regular viewport screenshot, not full page
waitFor: 3000, // Wait for page to fully load
timeout: 30000,
blockAds: true,
actions: [
{
type: 'wait',
milliseconds: 2000 // Additional wait for dynamic content
}
]
})
});
if (!firecrawlResponse.ok) {
const error = await firecrawlResponse.text();
throw new Error(`Firecrawl API error: ${error}`);
}
const data = await firecrawlResponse.json();
if (!data.success || !data.data?.screenshot) {
throw new Error('Failed to capture screenshot');
}
return NextResponse.json({
success: true,
screenshot: data.data.screenshot,
metadata: data.data.metadata
});
} catch (error: any) {
console.error('Screenshot capture error:', error);
return NextResponse.json({
error: error.message || 'Failed to capture screenshot'
}, { status: 500 });
}
}
+117
View File
@@ -0,0 +1,117 @@
import { NextRequest, NextResponse } from 'next/server';
// Function to sanitize smart quotes and other problematic characters
function sanitizeQuotes(text: string): string {
return text
// Replace smart single quotes
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
// Replace smart double quotes
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
// Replace other quote-like characters
.replace(/[\u00AB\u00BB]/g, '"') // Guillemets
.replace(/[\u2039\u203A]/g, "'") // Single guillemets
// Replace other problematic characters
.replace(/[\u2013\u2014]/g, '-') // En dash and em dash
.replace(/[\u2026]/g, '...') // Ellipsis
.replace(/[\u00A0]/g, ' '); // Non-breaking space
}
export async function POST(request: NextRequest) {
try {
const { url } = await request.json();
if (!url) {
return NextResponse.json({
success: false,
error: 'URL is required'
}, { status: 400 });
}
console.log('[scrape-url-enhanced] Scraping with Firecrawl:', url);
const FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY;
if (!FIRECRAWL_API_KEY) {
throw new Error('FIRECRAWL_API_KEY environment variable is not set');
}
// Make request to Firecrawl API with maxAge for 500% faster scraping
const firecrawlResponse = await fetch('https://api.firecrawl.dev/v1/scrape', {
method: 'POST',
headers: {
'Authorization': `Bearer ${FIRECRAWL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
url,
formats: ['markdown', 'html'],
waitFor: 3000,
timeout: 30000,
blockAds: true,
maxAge: 3600000, // Use cached data if less than 1 hour old (500% faster!)
actions: [
{
type: 'wait',
milliseconds: 2000
}
]
})
});
if (!firecrawlResponse.ok) {
const error = await firecrawlResponse.text();
throw new Error(`Firecrawl API error: ${error}`);
}
const data = await firecrawlResponse.json();
if (!data.success || !data.data) {
throw new Error('Failed to scrape content');
}
const { markdown, html, metadata } = data.data;
// Sanitize the markdown content
const sanitizedMarkdown = sanitizeQuotes(markdown || '');
// Extract structured data from the response
const title = metadata?.title || '';
const description = metadata?.description || '';
// Format content for AI
const formattedContent = `
Title: ${sanitizeQuotes(title)}
Description: ${sanitizeQuotes(description)}
URL: ${url}
Main Content:
${sanitizedMarkdown}
`.trim();
return NextResponse.json({
success: true,
url,
content: formattedContent,
structured: {
title: sanitizeQuotes(title),
description: sanitizeQuotes(description),
content: sanitizedMarkdown,
url
},
metadata: {
scraper: 'firecrawl-enhanced',
timestamp: new Date().toISOString(),
contentLength: formattedContent.length,
cached: data.data.cached || false, // Indicates if data came from cache
...metadata
},
message: 'URL scraped successfully with Firecrawl (with caching for 500% faster performance)'
});
} catch (error) {
console.error('[scrape-url-enhanced] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+30
View File
@@ -0,0 +1,30 @@
'use client';
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform",
"data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
+52
View File
@@ -0,0 +1,52 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ToggleProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof toggleVariants> {
pressed?: boolean
onPressedChange?: (pressed: boolean) => void
}
const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
({ className, variant, size, pressed, onPressedChange, ...props }, ref) => {
return (
<button
ref={ref}
type="button"
aria-pressed={pressed}
data-state={pressed ? "on" : "off"}
onClick={() => onPressedChange?.(!pressed)}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
)
Toggle.displayName = "Toggle"
export { Toggle, toggleVariants }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+205
View File
@@ -0,0 +1,205 @@
@import "tailwindcss";
@keyframes slide {
0% { transform: translate(0, 0); }
100% { transform: translate(70px, 70px); }
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes sunPulse {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.5;
}
50% {
transform: translate(-50%, -50%) scale(1.05);
opacity: 0.7;
}
}
@keyframes orbShrink {
0% {
transform: translateX(-50%) translateY(45%) scale(1.5);
opacity: 0.2;
}
100% {
transform: translateX(-50%) translateY(45%) scale(1);
opacity: 1;
}
}
@keyframes gradient-shift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes screenshot-pulse {
0%, 100% {
opacity: 0.2;
transform: scale(0.98);
}
50% {
opacity: 0.4;
transform: scale(1);
}
}
@keyframes camera-float {
0%, 100% {
transform: translateY(0px) rotate(0deg);
}
25% {
transform: translateY(-10px) rotate(-5deg);
}
75% {
transform: translateY(5px) rotate(5deg);
}
}
@keyframes lens-rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes pushUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInSmooth {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* Theme configuration for Tailwind CSS v4 */
@theme {
--color-background: hsl(0 0% 100%);
--color-foreground: hsl(240 10% 3.9%);
--color-card: hsl(0 0% 100%);
--color-card-foreground: hsl(240 10% 3.9%);
--color-popover: hsl(0 0% 100%);
--color-popover-foreground: hsl(240 10% 3.9%);
--color-primary: hsl(25 95% 53%);
--color-primary-foreground: hsl(0 0% 98%);
--color-secondary: hsl(240 4.8% 95.9%);
--color-secondary-foreground: hsl(240 5.9% 10%);
--color-muted: hsl(240 4.8% 95.9%);
--color-muted-foreground: hsl(240 3.8% 46.1%);
--color-accent: hsl(240 4.8% 95.9%);
--color-accent-foreground: hsl(240 5.9% 10%);
--color-destructive: hsl(0 84.2% 60.2%);
--color-destructive-foreground: hsl(0 0% 98%);
--color-border: hsl(240 5.9% 90%);
--color-input: hsl(240 5.9% 90%);
--color-ring: hsl(25 95% 53%);
--radius: 0.5rem;
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Radial gradient utilities */
.bg-gradient-radial {
background-image: radial-gradient(circle, var(--tw-gradient-stops));
}
/* Conic gradient utilities */
.bg-gradient-conic {
background-image: conic-gradient(var(--tw-gradient-stops));
}
}
@layer base {
* {
border-color: theme('colors.border');
}
body {
background-color: theme('colors.background');
color: theme('colors.foreground');
}
}
@layer utilities {
.animate-gradient-shift {
background-size: 400% 400%;
animation: gradient-shift 8s ease infinite;
}
.animate-camera-float {
animation: camera-float 3s ease-in-out infinite;
}
.animate-lens-rotate {
animation: lens-rotate 2s linear infinite;
}
.animation-delay-200 {
animation-delay: 200ms;
}
.animate-push-up {
animation: pushUp 0.4s ease-out forwards;
}
.animate-fade-in-smooth {
opacity: 0;
animation: fadeInSmooth 0.6s ease-out forwards;
}
.animate-fade-in-up {
opacity: 0;
animation: fadeInUp 0.5s ease-out forwards;
}
}
+24
View File
@@ -0,0 +1,24 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Open Lovable",
description: "Re-imagine any website in seconds with AI-powered website builder.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}
+3362
View File
File diff suppressed because it is too large Load Diff
+59
View File
@@ -0,0 +1,59 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
export interface CodeApplicationState {
stage: 'analyzing' | 'installing' | 'applying' | 'complete' | null;
packages?: string[];
installedPackages?: string[];
filesGenerated?: string[];
message?: string;
}
interface CodeApplicationProgressProps {
state: CodeApplicationState;
}
export default function CodeApplicationProgress({ state }: CodeApplicationProgressProps) {
if (!state.stage || state.stage === 'complete') return null;
return (
<AnimatePresence mode="wait">
<motion.div
key="loading"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="inline-block bg-gray-100 rounded-[10px] p-3 mt-2"
>
<div className="flex items-center gap-3">
{/* Rotating loading indicator */}
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-4 h-4"
>
<svg className="w-full h-full" viewBox="0 0 24 24" fill="none">
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeDasharray="31.416"
strokeDashoffset="10"
className="text-gray-700"
/>
</svg>
</motion.div>
{/* Simple loading text */}
<div className="text-sm font-medium text-gray-700">
Applying to sandbox...
</div>
</div>
</motion.div>
</AnimatePresence>
);
}
+67
View File
@@ -0,0 +1,67 @@
import { useEffect, useRef } from 'react';
interface HMRErrorDetectorProps {
iframeRef: React.RefObject<HTMLIFrameElement>;
onErrorDetected: (errors: Array<{ type: string; message: string; package?: string }>) => void;
}
export default function HMRErrorDetector({ iframeRef, onErrorDetected }: HMRErrorDetectorProps) {
const checkIntervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const checkForHMRErrors = () => {
if (!iframeRef.current) return;
try {
const iframeDoc = iframeRef.current.contentDocument;
if (!iframeDoc) return;
// Check for Vite error overlay
const errorOverlay = iframeDoc.querySelector('vite-error-overlay');
if (errorOverlay) {
// Try to extract error message
const messageElement = errorOverlay.shadowRoot?.querySelector('.message-body');
if (messageElement) {
const errorText = messageElement.textContent || '';
// Parse import errors
const importMatch = errorText.match(/Failed to resolve import "([^"]+)"/);
if (importMatch) {
const packageName = importMatch[1];
if (!packageName.startsWith('.')) {
// Extract base package name
let finalPackage = packageName;
if (packageName.startsWith('@')) {
const parts = packageName.split('/');
finalPackage = parts.length >= 2 ? parts.slice(0, 2).join('/') : packageName;
} else {
finalPackage = packageName.split('/')[0];
}
onErrorDetected([{
type: 'npm-missing',
message: `Failed to resolve import "${packageName}"`,
package: finalPackage
}]);
}
}
}
}
} catch (error) {
// Cross-origin errors are expected, ignore them
}
};
// Check immediately and then every 2 seconds
checkForHMRErrors();
checkIntervalRef.current = setInterval(checkForHMRErrors, 2000);
return () => {
if (checkIntervalRef.current) {
clearInterval(checkIntervalRef.current);
}
};
}, [iframeRef, onErrorDetected]);
return null;
}
+119
View File
@@ -0,0 +1,119 @@
import { useState, useEffect } from 'react';
import { Loader2, ExternalLink, RefreshCw, Terminal } from 'lucide-react';
interface SandboxPreviewProps {
sandboxId: string;
port: number;
type: 'vite' | 'nextjs' | 'console';
output?: string;
isLoading?: boolean;
}
export default function SandboxPreview({
sandboxId,
port,
type,
output,
isLoading = false
}: SandboxPreviewProps) {
const [previewUrl, setPreviewUrl] = useState<string>('');
const [showConsole, setShowConsole] = useState(false);
const [iframeKey, setIframeKey] = useState(0);
useEffect(() => {
if (sandboxId && type !== 'console') {
// In production, this would be the actual E2B sandbox URL
// Format: https://{sandboxId}-{port}.e2b.dev
setPreviewUrl(`https://${sandboxId}-${port}.e2b.dev`);
}
}, [sandboxId, port, type]);
const handleRefresh = () => {
setIframeKey(prev => prev + 1);
};
if (type === 'console') {
return (
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
<div className="font-mono text-sm whitespace-pre-wrap text-gray-300">
{output || 'No output yet...'}
</div>
</div>
);
}
return (
<div className="space-y-4">
{/* Preview Controls */}
<div className="flex items-center justify-between bg-gray-800 rounded-lg p-3 border border-gray-700">
<div className="flex items-center gap-3">
<span className="text-sm text-gray-400">
{type === 'vite' ? '⚡ Vite' : '▲ Next.js'} Preview
</span>
<code className="text-xs bg-gray-900 px-2 py-1 rounded text-blue-400">
{previewUrl}
</code>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowConsole(!showConsole)}
className="p-2 hover:bg-gray-700 rounded transition-colors"
title="Toggle console"
>
<Terminal className="w-4 h-4" />
</button>
<button
onClick={handleRefresh}
className="p-2 hover:bg-gray-700 rounded transition-colors"
title="Refresh preview"
>
<RefreshCw className="w-4 h-4" />
</button>
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="p-2 hover:bg-gray-700 rounded transition-colors"
title="Open in new tab"
>
<ExternalLink className="w-4 h-4" />
</a>
</div>
</div>
{/* Main Preview */}
<div className="relative bg-gray-900 rounded-lg overflow-hidden border border-gray-700">
{isLoading && (
<div className="absolute inset-0 bg-gray-900/80 flex items-center justify-center z-10">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2" />
<p className="text-sm text-gray-400">
{type === 'vite' ? 'Starting Vite dev server...' : 'Starting Next.js dev server...'}
</p>
</div>
</div>
)}
<iframe
key={iframeKey}
src={previewUrl}
className="w-full h-[600px] bg-white"
title={`${type} preview`}
sandbox="allow-scripts allow-same-origin allow-forms"
/>
</div>
{/* Console Output (Toggle) */}
{showConsole && output && (
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-gray-400">Console Output</span>
</div>
<div className="font-mono text-xs whitespace-pre-wrap text-gray-300 max-h-48 overflow-y-auto">
{output}
</div>
</div>
)}
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-[10px] text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-zinc-900 text-white hover:bg-zinc-800 [box-shadow:inset_0px_-2px_0px_0px_#18181b,_0px_1px_6px_0px_rgba(24,_24,_27,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#18181b,_0px_1px_3px_0px_rgba(24,_24,_27,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#18181b,_0px_1px_2px_0px_rgba(24,_24,_27,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
secondary: "bg-zinc-100 text-zinc-900 hover:bg-zinc-200 [box-shadow:inset_0px_-2px_0px_0px_#d4d4d8,_0px_1px_6px_0px_rgba(161,_161,_170,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#d4d4d8,_0px_1px_3px_0px_rgba(161,_161,_170,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#d4d4d8,_0px_1px_2px_0px_rgba(161,_161,_170,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
outline: "border border-zinc-300 bg-transparent hover:bg-zinc-50 text-zinc-900 [box-shadow:inset_0px_-2px_0px_0px_#e4e4e7,_0px_1px_6px_0px_rgba(228,_228,_231,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#e4e4e7,_0px_1px_3px_0px_rgba(228,_228,_231,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#e4e4e7,_0px_1px_2px_0px_rgba(228,_228,_231,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
destructive: "bg-red-500 text-white hover:bg-red-600 [box-shadow:inset_0px_-2px_0px_0px_#dc2626,_0px_1px_6px_0px_rgba(239,_68,_68,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#dc2626,_0px_1px_3px_0px_rgba(239,_68,_68,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#dc2626,_0px_1px_2px_0px_rgba(239,_68,_68,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
code: "bg-[#36322F] text-white hover:bg-[#4a4542] [box-shadow:inset_0px_-2px_0px_0px_#171310,_0px_1px_6px_0px_rgba(58,_33,_8,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#171310,_0px_1px_3px_0px_rgba(58,_33,_8,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#171310,_0px_1px_2px_0px_rgba(58,_33,_8,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
orange: "bg-orange-500 text-white hover:bg-orange-600 [box-shadow:inset_0px_-2px_0px_0px_#c2410c,_0px_1px_6px_0px_rgba(234,_88,_12,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#c2410c,_0px_1px_3px_0px_rgba(234,_88,_12,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#c2410c,_0px_1px_2px_0px_rgba(234,_88,_12,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 px-3 py-1 text-sm",
lg: "h-12 px-6 py-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? "button" : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
+63
View File
@@ -0,0 +1,63 @@
import * as React from "react"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
export interface CheckboxProps {
label?: string
defaultChecked?: boolean
disabled?: boolean
className?: string
onChange?: (checked: boolean) => void
}
const Checkbox = React.forwardRef<HTMLDivElement, CheckboxProps>(
({ label, defaultChecked = false, disabled = false, className, onChange }, ref) => {
const [checked, setChecked] = React.useState(defaultChecked)
const handleToggle = () => {
if (!disabled) {
const newChecked = !checked
setChecked(newChecked)
onChange?.(newChecked)
}
}
return (
<div
ref={ref}
className={cn("flex items-center gap-2", className)}
>
<button
type="button"
onClick={handleToggle}
disabled={disabled}
className={cn(
"h-4 w-4 rounded border border-zinc-300 flex items-center justify-center transition-all duration-200",
"[box-shadow:inset_0px_-1px_0px_0px_#e4e4e7,_0px_1px_3px_0px_rgba(228,_228,_231,_20%)]",
!disabled && "hover:[box-shadow:inset_0px_-1px_0px_0px_#d4d4d8,_0px_1px_3px_0px_rgba(212,_212,_216,_30%)]",
checked && "bg-orange-500 border-orange-500 [box-shadow:inset_0px_-1px_0px_0px_#c2410c,_0px_1px_3px_0px_rgba(234,_88,_12,_30%)]",
disabled && "opacity-50 cursor-not-allowed"
)}
>
{checked && <Check className="h-3 w-3 text-white" />}
</button>
{label && (
<label
onClick={handleToggle}
className={cn(
"text-sm select-none",
!disabled && "cursor-pointer",
disabled && "opacity-50 cursor-not-allowed"
)}
>
{label}
</label>
)}
</div>
)
}
)
Checkbox.displayName = "Checkbox"
export { Checkbox }
+24
View File
@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-[10px] border border-zinc-300 bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [box-shadow:inset_0px_-2px_0px_0px_#e4e4e7,_0px_1px_6px_0px_rgba(228,_228,_231,_30%)] hover:[box-shadow:inset_0px_-2px_0px_0px_#d4d4d8,_0px_1px_6px_0px_rgba(212,_212,_216,_40%)] focus-visible:[box-shadow:inset_0px_-2px_0px_0px_#f97316,_0px_1px_6px_0px_rgba(249,_115,_22,_30%)] transition-all duration-200",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
HTMLLabelElement,
React.ComponentPropsWithoutRef<"label"> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<label
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = "Label"
export { Label }
+25
View File
@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export type SelectProps = React.SelectHTMLAttributes<HTMLSelectElement>
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => {
return (
<select
className={cn(
"flex h-10 w-full rounded-[10px] border border-zinc-300 bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [box-shadow:inset_0px_-2px_0px_0px_#e4e4e7,_0px_1px_6px_0px_rgba(228,_228,_231,_30%)] hover:[box-shadow:inset_0px_-2px_0px_0px_#d4d4d8,_0px_1px_6px_0px_rgba(212,_212,_216,_40%)] focus-visible:[box-shadow:inset_0px_-2px_0px_0px_#f97316,_0px_1px_6px_0px_rgba(249,_115,_22,_30%)] transition-all duration-200",
className
)}
ref={ref}
{...props}
>
{children}
</select>
)
}
)
Select.displayName = "Select"
export { Select }
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-[10px] border border-zinc-300 bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [box-shadow:inset_0px_-2px_0px_0px_#e4e4e7,_0px_1px_6px_0px_rgba(228,_228,_231,_30%)] hover:[box-shadow:inset_0px_-2px_0px_0px_#d4d4d8,_0px_1px_6px_0px_rgba(212,_212,_216,_40%)] focus-visible:[box-shadow:inset_0px_-2px_0px_0px_#f97316,_0px_1px_6px_0px_rgba(249,_115,_22,_30%)] transition-all duration-200",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }
+167
View File
@@ -0,0 +1,167 @@
// Application Configuration
// This file contains all configurable settings for the application
export const appConfig = {
// E2B Sandbox Configuration
e2b: {
// Sandbox timeout in minutes
timeoutMinutes: 15,
// Convert to milliseconds for E2B API
get timeoutMs() {
return this.timeoutMinutes * 60 * 1000;
},
// Vite development server port
vitePort: 5173,
// Time to wait for Vite to be ready (in milliseconds)
viteStartupDelay: 7000,
// Time to wait for CSS rebuild (in milliseconds)
cssRebuildDelay: 2000,
// Default sandbox template (if using templates)
defaultTemplate: undefined, // or specify a template ID
},
// AI Model Configuration
ai: {
// Default AI model
defaultModel: 'openai/gpt-5-nano',
// Available models
availableModels: [
'openai/gpt-5-nano',
'openai/gpt-5-mini',
'openai/gpt-5',
'moonshotai/kimi-k2-instruct',
'anthropic/claude-3-5-sonnet-20241022'
],
// Model display names
modelDisplayNames: {
'openai/gpt-5-nano': 'GPT-5 Nano',
'openai/gpt-5-mini': 'GPT-5 Mini',
'openai/gpt-5': 'GPT-5',
'moonshotai/kimi-k2-instruct': 'Kimi K2 Instruct',
'anthropic/claude-3-5-sonnet-20241022': 'Claude 3.5 Sonnet'
},
// Temperature settings for non-reasoning models
defaultTemperature: 0.7,
// Max tokens for code generation
maxTokens: 8000,
// Max tokens for truncation recovery
truncationRecoveryMaxTokens: 4000,
},
// Code Application Configuration
codeApplication: {
// Delay after applying code before refreshing iframe (milliseconds)
defaultRefreshDelay: 2000,
// Delay when packages are installed (milliseconds)
packageInstallRefreshDelay: 5000,
// Enable/disable automatic truncation recovery
enableTruncationRecovery: true,
// Maximum number of truncation recovery attempts per file
maxTruncationRecoveryAttempts: 1,
},
// UI Configuration
ui: {
// Show/hide certain UI elements
showModelSelector: true,
showStatusIndicator: true,
// Animation durations (milliseconds)
animationDuration: 200,
// Toast notification duration (milliseconds)
toastDuration: 3000,
// Maximum chat messages to keep in memory
maxChatMessages: 100,
// Maximum recent messages to send as context
maxRecentMessagesContext: 20,
},
// Development Configuration
dev: {
// Enable debug logging
enableDebugLogging: true,
// Enable performance monitoring
enablePerformanceMonitoring: false,
// Log API responses
logApiResponses: true,
},
// Package Installation Configuration
packages: {
// Use --legacy-peer-deps flag for npm install
useLegacyPeerDeps: true,
// Package installation timeout (milliseconds)
installTimeout: 60000,
// Auto-restart Vite after package installation
autoRestartVite: true,
},
// File Management Configuration
files: {
// Excluded file patterns (files to ignore)
excludePatterns: [
'node_modules/**',
'.git/**',
'.next/**',
'dist/**',
'build/**',
'*.log',
'.DS_Store'
],
// Maximum file size to read (bytes)
maxFileSize: 1024 * 1024, // 1MB
// File extensions to treat as text
textFileExtensions: [
'.js', '.jsx', '.ts', '.tsx',
'.css', '.scss', '.sass',
'.html', '.xml', '.svg',
'.json', '.yml', '.yaml',
'.md', '.txt', '.env',
'.gitignore', '.dockerignore'
],
},
// API Endpoints Configuration (for external services)
api: {
// Retry configuration
maxRetries: 3,
retryDelay: 1000, // milliseconds
// Request timeout (milliseconds)
requestTimeout: 30000,
}
};
// Type-safe config getter
export function getConfig<K extends keyof typeof appConfig>(key: K): typeof appConfig[K] {
return appConfig[key];
}
// Helper to get nested config values
export function getConfigValue(path: string): any {
return path.split('.').reduce((obj, key) => obj?.[key], appConfig as any);
}
export default appConfig;
+263
View File
@@ -0,0 +1,263 @@
# Package Detection and Installation Guide
This document explains how to use the XML-based package detection and installation mechanism in the E2B sandbox environment.
## Overview
The E2B sandbox can automatically detect and install packages from XML tags in AI-generated code responses. This mechanism works alongside the existing file detection system.
## XML Tag Formats
### Individual Package Tags
Use `<package>` tags for individual packages:
```xml
<package>react-router-dom</package>
<package>axios</package>
<package>@heroicons/react</package>
```
### Multiple Packages Tag
Use `<packages>` tag for multiple packages (comma or newline separated):
```xml
<packages>
react-router-dom
axios
@heroicons/react
framer-motion
</packages>
```
Or comma-separated:
```xml
<packages>react-router-dom, axios, @heroicons/react, framer-motion</packages>
```
### Command Execution
Use `<command>` tags to execute shell commands in the sandbox:
```xml
<command>npm run build</command>
<command>npm run test</command>
```
## Complete Example
Here's a complete example of an AI response with files, packages, and commands:
```xml
<explanation>
Creating a React application with routing and API integration.
</explanation>
<packages>
react-router-dom
axios
@heroicons/react
</packages>
<file path="src/App.jsx">
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { HomeIcon } from '@heroicons/react/24/solid';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
function App() {
return (
<Router>
<div className="min-h-screen bg-gray-100">
<nav className="bg-white shadow-lg p-4">
<HomeIcon className="h-6 w-6" />
</nav>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</div>
</Router>
);
}
export default App;
</file>
<file path="src/pages/HomePage.jsx">
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function HomePage() {
const [data, setData] = useState(null);
useEffect(() => {
axios.get('/api/data')
.then(response => setData(response.data))
.catch(error => console.error('Error fetching data:', error));
}, []);
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold">Home Page</h1>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
export default HomePage;
</file>
<file path="src/pages/AboutPage.jsx">
import React from 'react';
function AboutPage() {
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold">About Page</h1>
<p>This is the about page of our application.</p>
</div>
);
}
export default AboutPage;
</file>
<command>npm run dev</command>
</xml>
```
## How It Works
1. **Parsing**: The `parseAIResponse` function in `/app/api/apply-ai-code/route.ts` extracts:
- Files from `<file>` tags
- Packages from `<package>` and `<packages>` tags
- Commands from `<command>` tags
2. **Package Installation**:
- Packages are automatically installed using npm
- Both scoped packages (e.g., `@heroicons/react`) and regular packages are supported
- The system checks if packages are already installed to avoid redundant installations
3. **File Creation**: Files are created in the sandbox after packages are installed
4. **Command Execution**: Commands are executed in the sandbox environment
## API Endpoints
### `/api/apply-ai-code`
Main endpoint that processes AI responses containing XML tags.
**Request body:**
```json
{
"response": "<AI response with XML tags>",
"isEdit": false,
"packages": [] // Optional array of packages
}
```
### `/api/detect-and-install-packages`
Detects packages from import statements in code files.
**Request body:**
```json
{
"files": {
"src/App.jsx": "import React from 'react'...",
"src/utils.js": "import axios from 'axios'..."
}
}
```
### `/api/install-packages`
Directly installs packages in the sandbox.
**Request body:**
```json
{
"packages": ["react-router-dom", "axios", "@heroicons/react"]
}
```
## Features
- **Automatic Package Detection**: Extracts packages from import statements
- **Duplicate Prevention**: Avoids installing already-installed packages
- **Scoped Package Support**: Handles packages like `@heroicons/react`
- **Built-in Module Filtering**: Skips Node.js built-in modules
- **Real-time Feedback**: Provides installation progress updates
- **Error Handling**: Reports failed installations
## Best Practices
1. **Specify packages explicitly** using XML tags when possible
2. **Group related packages** in a single `<packages>` tag
3. **Order matters**: Packages are installed before files are created
4. **Use commands** for post-installation tasks like building or testing
## Integration with E2B Sandbox
The package detection mechanism integrates seamlessly with the E2B sandbox:
1. Packages are installed in `/home/user/app/node_modules`
2. The Vite dev server is automatically restarted after package installation
3. All npm operations run within the sandbox environment
4. Package.json is automatically updated with new dependencies
## E2B Command Execution Methods
### Method 1: Using runCode() with Python subprocess
```javascript
// Current implementation pattern
await global.activeSandbox.runCode(`
import subprocess
import os
os.chdir('/home/user/app')
result = subprocess.run(['npm', 'install', 'axios'], capture_output=True, text=True)
print(result.stdout)
`);
```
### Method 2: Using commands.run() directly (Recommended)
```javascript
// Direct command execution - cleaner approach
const result = await global.activeSandbox.commands.run('npm install axios', {
cwd: '/home/user/app',
timeout: 60000
});
console.log(result.stdout);
```
### Command Execution Options
When using `sandbox.commands.run()`, you can specify:
- `cmd`: Command string to execute
- `background`: Run in background (true) or wait for completion (false)
- `envs`: Environment variables as key-value pairs
- `user`: User to run command as (default: "user")
- `cwd`: Working directory
- `on_stdout`: Callback for stdout output
- `on_stderr`: Callback for stderr output
- `timeout`: Command timeout in seconds (default: 60)
### Example: Installing packages with commands.run()
```javascript
// Install multiple packages
const packages = ['react-router-dom', 'axios', '@heroicons/react'];
const result = await global.activeSandbox.commands.run(
`npm install ${packages.join(' ')}`,
{
cwd: '/home/user/app',
timeout: 120,
on_stdout: (data) => console.log('npm:', data),
on_stderr: (data) => console.error('npm error:', data)
}
);
if (result.exitCode === 0) {
console.log('Packages installed successfully');
} else {
console.error('Installation failed:', result.stderr);
}
+63
View File
@@ -0,0 +1,63 @@
# Streaming API Fixes Summary
## Issues Fixed
### 1. "Cannot read properties of undefined (reading 'split')"
**Location**: `/api/install-packages/route.ts` line 119
**Cause**: `installResult.output` was undefined
**Fix**: Added fallback to handle different output formats:
```typescript
const output = installResult?.output || installResult?.logs?.stdout?.join('\n') || '';
```
### 2. "Cannot read properties of undefined (reading 'push')"
**Location**: `/api/apply-ai-code-stream/route.ts` various lines
**Causes**:
- Arrays not properly initialized
- Results object properties accessed without checks
**Fixes**:
- Added array checks before operations:
```typescript
const packagesArray = Array.isArray(packages) ? packages : [];
const parsedPackages = Array.isArray(parsed.packages) ? parsed.packages : [];
const filesArray = Array.isArray(parsed.files) ? parsed.files : [];
const commandsArray = Array.isArray(parsed.commands) ? parsed.commands : [];
```
- Added null checks before push operations:
```typescript
if (results.filesCreated) results.filesCreated.push(normalizedPath);
if (results.errors) results.errors.push(`Failed to create ${file.path}`);
```
### 3. Improved Error Handling
- Added checks for undefined chunks in streaming
- Added proper error messages for all failure cases
- Ensured all arrays are initialized before use
## Current Status
✅ Package detection working via XML tags
✅ Real-time streaming feedback operational
✅ File creation/update tracking functional
✅ Command execution with output streaming
✅ Error messages properly displayed
## Known Issues
1. **NPM Resolution Errors**: When packages have conflicting dependencies, npm may show ERESOLVE errors. This is expected behavior and doesn't break the functionality.
2. **Package Installation Verification**: The current implementation tries to verify package installation by checking the filesystem. This might not always work for all package types.
## UI Feedback Flow
Users now see:
1. 🔍 Analyzing code and detecting dependencies
2. 📦 Starting code application
3. Step 1: Installing X packages (with real-time npm output)
4. Step 2: Creating Y files (with progress indicators)
5. Step 3: Executing Z commands (with output streaming)
6. ✅ Success message with summary
All errors are displayed inline with context, making debugging easier.
+67
View File
@@ -0,0 +1,67 @@
# Tool Call Validation Fix Summary
## Issue
The error message "tool call validation failed: parameters for tool installPackage did not match schema" was occurring when the AI tried to install packages.
## Root Cause
The Groq models (including `moonshotai/kimi-k2-instruct`) do not support function/tool calling. This is a limitation of most Groq models - only specific models like `llama3-groq-70b-8192-tool-use-preview` support tools.
## Solution
Instead of using the Vercel AI SDK's tool calling feature, we switched to XML-based package detection:
### 1. Removed Tool Support
- Removed the `tool` import and `installPackage` tool definition
- Removed the `tools` configuration from the `streamText` call
### 2. Updated System Prompt
Changed from:
```
Use the installPackage tool with parameters: {name: "package-name", reason: "why you need it"}
```
To:
```
You MUST specify packages using <package> tags BEFORE using them in your code.
For example: <package>three</package> or <package>@heroicons/react</package>
```
### 3. Implemented XML Tag Detection
- Added streaming detection for `<package>` tags during response generation
- Implemented buffering to handle tags split across chunks
- Added support for both individual `<package>` tags and grouped `<packages>` tags
### 4. Real-time Package Detection
Packages are now detected in real-time as the AI generates the response:
```javascript
// Buffer incomplete tags across chunks
const searchText = tagBuffer + text;
const packageRegex = /<package>([^<]+)<\/package>/g;
while ((packageMatch = packageRegex.exec(searchText)) !== null) {
const packageName = packageMatch[1].trim();
if (packageName && !packagesToInstall.includes(packageName)) {
packagesToInstall.push(packageName);
await sendProgress({
type: 'package',
name: packageName,
message: `📦 Package detected: ${packageName}`
});
}
}
```
## Results
- ✅ Package detection now works reliably
- ✅ Real-time UI feedback shows packages as they're detected
- ✅ No more tool validation errors
- ✅ Compatible with all Groq models
## UI Feedback
Users now see:
```
📦 Package detected: three
📦 Package detected: @react-three/fiber
📦 Package detected: @react-three/drei
```
As packages are detected in the AI's response, providing immediate feedback about dependencies that will be installed.
+133
View File
@@ -0,0 +1,133 @@
# UI Feedback Demonstration
This document demonstrates the new real-time feedback mechanism for package installation and command execution in the E2B sandbox UI.
## What's New
### 1. Real-time Package Installation Feedback
When packages are detected and installed from XML tags, users now see:
- 🔍 **Initial Analysis**: "Analyzing code and detecting dependencies..."
- 📦 **Package Detection**: "Step 1: Installing X packages..."
- **NPM Output**: Real-time npm install output with proper formatting
- Blue text for commands (`$ npm install react-router-dom`)
- Gray text for standard output
- Red text for errors
-**Success Messages**: Clear confirmation when packages are installed
### 2. File Creation Progress
- 📝 **File Creation Start**: "Creating X files..."
- **Individual File Updates**: Progress for each file being created/updated
-**Completion Status**: Visual confirmation for each file
### 3. Command Execution Feedback
When `<command>` tags are executed:
-**Command Start**: Shows the command being executed
- **Real-time Output**: Displays stdout/stderr as it happens
- ✅/❌ **Exit Status**: Clear success/failure indicators
## Example Flow
Here's what users see when applying code with packages and commands:
```
🔍 Analyzing code and detecting dependencies...
📦 Starting code application...
Step 1: Installing 3 packages...
$ npm install react-router-dom
> added 3 packages in 2.3s
$ npm install axios
> added 1 package in 1.1s
$ npm install @heroicons/react
> added 1 package in 0.9s
✅ Successfully installed: react-router-dom, axios, @heroicons/react
Step 2: Creating 5 files...
📝 Creating 5 files...
Step 3: Executing 1 commands...
⚡ executing command: npm run dev
> app@0.0.0 dev
> vite
> VITE ready in 523ms
✅ Command completed successfully
```
## UI Components
### Chat Message Types
The UI now supports these message types with distinct styling:
1. **System Messages** (`bg-[#36322F] text-white text-sm`)
- General information and status updates
2. **Command Messages** (`bg-gray-100 text-gray-600 font-mono text-sm`)
- Input commands: Blue prefix (`$`)
- Output: Gray text
- Errors: Red text
- Success: Green text
3. **User Messages** (`bg-[#36322F] text-white`)
- User input and queries
4. **AI Messages** (`bg-secondary text-foreground`)
- AI responses
### Visual Indicators
- 🔍 Analyzing/Detection phase
- 📦 Package operations
- 📝 File operations
- ⚡ Command execution
- ✅ Success states
- ❌ Error states
- ⚠️ Warnings
## Implementation Details
### Streaming Response Format
The new `/api/apply-ai-code-stream` endpoint sends Server-Sent Events:
```typescript
data: {"type": "start", "message": "Starting code application...", "totalSteps": 3}
data: {"type": "step", "step": 1, "message": "Installing 3 packages..."}
data: {"type": "package-progress", "type": "output", "message": "added 3 packages"}
data: {"type": "file-progress", "current": 1, "total": 5, "fileName": "App.jsx"}
data: {"type": "command-output", "command": "npm run dev", "output": "VITE ready", "stream": "stdout"}
data: {"type": "complete", "results": {...}, "message": "Success"}
```
### Error Handling
Errors are displayed inline with context:
- Package installation failures
- File creation errors
- Command execution failures
Each error includes the specific operation that failed and helpful error messages.
## Benefits
1. **Transparency**: Users see exactly what's happening in real-time
2. **Debugging**: Easy to identify where issues occur
3. **Progress Tracking**: Clear indication of progress through multi-step operations
4. **Professional Feel**: Terminal-like output for technical operations
5. **Accessibility**: Color-coded output for quick scanning
## Usage
The feedback system automatically activates when:
1. Code with `<package>` or `<packages>` tags is applied
2. Files are created or updated
3. Commands from `<command>` tags are executed
4. Packages are auto-detected from import statements
No additional configuration is required - the UI automatically provides rich feedback for all operations.
+25
View File
@@ -0,0 +1,25 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "warn",
"react-hooks/exhaustive-deps": "warn",
"react/no-unescaped-entities": "off",
"prefer-const": "warn"
}
}
];
export default eslintConfig;
+363
View File
@@ -0,0 +1,363 @@
import { FileManifest, EditIntent, EditType } from '@/types/file-manifest';
import { analyzeEditIntent } from '@/lib/edit-intent-analyzer';
import { getEditExamplesPrompt, getComponentPatternPrompt } from '@/lib/edit-examples';
export interface FileContext {
primaryFiles: string[]; // Files to edit
contextFiles: string[]; // Files to include for reference
systemPrompt: string; // Enhanced prompt with file info
editIntent: EditIntent;
}
/**
* Select files and build context based on user prompt
*/
export function selectFilesForEdit(
userPrompt: string,
manifest: FileManifest
): FileContext {
// Analyze the edit intent
const editIntent = analyzeEditIntent(userPrompt, manifest);
// Get the files based on intent - only edit target files, but provide all others as context
const primaryFiles = editIntent.targetFiles;
const allFiles = Object.keys(manifest.files);
let contextFiles = allFiles.filter(file => !primaryFiles.includes(file));
// ALWAYS include key files in context if they exist and aren't already primary files
const keyFiles: string[] = [];
// App.jsx is most important - shows component structure
const appFile = allFiles.find(f => f.endsWith('App.jsx') || f.endsWith('App.tsx'));
if (appFile && !primaryFiles.includes(appFile)) {
keyFiles.push(appFile);
}
// Include design system files for style context
const tailwindConfig = allFiles.find(f => f.endsWith('tailwind.config.js') || f.endsWith('tailwind.config.ts'));
if (tailwindConfig && !primaryFiles.includes(tailwindConfig)) {
keyFiles.push(tailwindConfig);
}
const indexCss = allFiles.find(f => f.endsWith('index.css') || f.endsWith('globals.css'));
if (indexCss && !primaryFiles.includes(indexCss)) {
keyFiles.push(indexCss);
}
// Include package.json to understand dependencies
const packageJson = allFiles.find(f => f.endsWith('package.json'));
if (packageJson && !primaryFiles.includes(packageJson)) {
keyFiles.push(packageJson);
}
// Put key files at the beginning of context for visibility
contextFiles = [...keyFiles, ...contextFiles.filter(f => !keyFiles.includes(f))];
// Build enhanced system prompt
const systemPrompt = buildSystemPrompt(
userPrompt,
editIntent,
primaryFiles,
contextFiles,
manifest
);
return {
primaryFiles,
contextFiles,
systemPrompt,
editIntent,
};
}
/**
* Build an enhanced system prompt with file structure context
*/
function buildSystemPrompt(
userPrompt: string,
editIntent: EditIntent,
primaryFiles: string[],
contextFiles: string[],
manifest: FileManifest
): string {
const sections: string[] = [];
// Add edit examples first for better understanding
if (editIntent.type !== EditType.FULL_REBUILD) {
sections.push(getEditExamplesPrompt());
}
// Add edit intent section
sections.push(`## Edit Intent
Type: ${editIntent.type}
Description: ${editIntent.description}
Confidence: ${(editIntent.confidence * 100).toFixed(0)}%
User Request: "${userPrompt}"`);
// Add file structure overview
sections.push(buildFileStructureSection(manifest));
// Add component patterns
const fileList = Object.keys(manifest.files).map(f => f.replace('/home/user/app/', '')).join('\n');
sections.push(getComponentPatternPrompt(fileList));
// Add primary files section
if (primaryFiles.length > 0) {
sections.push(`## Files to Edit
${primaryFiles.map(f => {
const fileInfo = manifest.files[f];
return `- ${f}${fileInfo?.componentInfo ? ` (${fileInfo.componentInfo.name} component)` : ''}`;
}).join('\n')}`);
}
// Add context files section
if (contextFiles.length > 0) {
sections.push(`## Context Files (for reference only)
${contextFiles.map(f => {
const fileInfo = manifest.files[f];
return `- ${f}${fileInfo?.componentInfo ? ` (${fileInfo.componentInfo.name} component)` : ''}`;
}).join('\n')}`);
}
// Add specific instructions based on edit type
sections.push(buildEditInstructions(editIntent.type));
// Add component relationships if relevant
if (editIntent.type === EditType.UPDATE_COMPONENT ||
editIntent.type === EditType.ADD_FEATURE) {
sections.push(buildComponentRelationships(primaryFiles, manifest));
}
return sections.join('\n\n');
}
/**
* Build file structure overview section
*/
function buildFileStructureSection(manifest: FileManifest): string {
const allFiles = Object.entries(manifest.files)
.map(([path]) => path.replace('/home/user/app/', ''))
.filter(path => !path.includes('node_modules'))
.sort();
const componentFiles = Object.entries(manifest.files)
.filter(([, info]) => info.type === 'component' || info.type === 'page')
.map(([path, info]) => ({
path: path.replace('/home/user/app/', ''),
name: info.componentInfo?.name || path.split('/').pop(),
type: info.type,
}));
return `## 🚨 EXISTING PROJECT FILES - DO NOT CREATE NEW FILES WITH SIMILAR NAMES 🚨
### ALL PROJECT FILES (${allFiles.length} files)
\`\`\`
${allFiles.join('\n')}
\`\`\`
### Component Files (USE THESE EXACT NAMES)
${componentFiles.map(f =>
`- ${f.name}${f.path} (${f.type})`
).join('\n')}
### CRITICAL: Component Relationships
**ALWAYS CHECK App.jsx FIRST** to understand what components exist and how they're imported!
Common component overlaps to watch for:
- "nav" or "navigation" → Often INSIDE Header.jsx, not a separate file
- "menu" → Usually part of Header/Nav, not separate
- "logo" → Typically in Header, not standalone
When user says "nav" or "navigation":
1. First check if Header.jsx exists
2. Look inside Header.jsx for navigation elements
3. Only create Nav.jsx if navigation doesn't exist anywhere
Entry Point: ${manifest.entryPoint}
### Routes
${manifest.routes.map(r =>
`- ${r.path}${r.component.split('/').pop()}`
).join('\n') || 'No routes detected'}`;
}
/**
* Build edit-type specific instructions
*/
function buildEditInstructions(editType: EditType): string {
const instructions: Record<EditType, string> = {
[EditType.UPDATE_COMPONENT]: `## SURGICAL EDIT INSTRUCTIONS
- You MUST preserve 99% of the original code
- ONLY edit the specific component(s) mentioned
- Make ONLY the minimal change requested
- DO NOT rewrite or refactor unless explicitly asked
- DO NOT remove any existing code unless explicitly asked
- DO NOT change formatting or structure
- Preserve all imports and exports
- Maintain the existing code style
- Return the COMPLETE file with the surgical change applied
- Think of yourself as a surgeon making a precise incision, not an artist repainting`,
[EditType.ADD_FEATURE]: `## Instructions
- Create new components in appropriate directories
- IMPORTANT: Update parent components to import and use the new component
- Update routing if adding new pages
- Follow existing patterns and conventions
- Add necessary styles to match existing design
- Example workflow:
1. Create NewComponent.jsx
2. Import it in the parent: import NewComponent from './NewComponent'
3. Use it in the parent's render: <NewComponent />`,
[EditType.FIX_ISSUE]: `## Instructions
- Identify and fix the specific issue
- Test the fix doesn't break other functionality
- Preserve existing behavior except for the bug
- Add error handling if needed`,
[EditType.UPDATE_STYLE]: `## SURGICAL STYLE EDIT INSTRUCTIONS
- Change ONLY the specific style/class mentioned
- If user says "change background to blue", change ONLY the background class
- DO NOT touch any other styles, classes, or attributes
- DO NOT refactor or "improve" the styling
- DO NOT change the component structure
- Preserve ALL other classes and styles exactly as they are
- Return the COMPLETE file with only the specific style change`,
[EditType.REFACTOR]: `## Instructions
- Improve code quality without changing functionality
- Follow project conventions
- Maintain all existing features
- Improve readability and maintainability`,
[EditType.FULL_REBUILD]: `## Instructions
- You may rebuild the entire application
- Keep the same core functionality
- Improve upon the existing design
- Use modern best practices`,
[EditType.ADD_DEPENDENCY]: `## Instructions
- Update package.json with new dependency
- Add necessary import statements
- Configure the dependency if needed
- Update any build configuration`,
};
return instructions[editType] || instructions[EditType.UPDATE_COMPONENT];
}
/**
* Build component relationship information
*/
function buildComponentRelationships(
files: string[],
manifest: FileManifest
): string {
const relationships: string[] = ['## Component Relationships'];
for (const file of files) {
const fileInfo = manifest.files[file];
if (!fileInfo?.componentInfo) continue;
const componentName = fileInfo.componentInfo.name;
const treeNode = manifest.componentTree[componentName];
if (treeNode) {
relationships.push(`\n### ${componentName}`);
if (treeNode.imports.length > 0) {
relationships.push(`Imports: ${treeNode.imports.join(', ')}`);
}
if (treeNode.importedBy.length > 0) {
relationships.push(`Used by: ${treeNode.importedBy.join(', ')}`);
}
if (fileInfo.componentInfo.childComponents?.length) {
relationships.push(`Renders: ${fileInfo.componentInfo.childComponents.join(', ')}`);
}
}
}
return relationships.join('\n');
}
/**
* Get file content for selected files
*/
export async function getFileContents(
files: string[],
manifest: FileManifest
): Promise<Record<string, string>> {
const contents: Record<string, string> = {};
for (const file of files) {
const fileInfo = manifest.files[file];
if (fileInfo) {
contents[file] = fileInfo.content;
}
}
return contents;
}
/**
* Format files for AI context
*/
export function formatFilesForAI(
primaryFiles: Record<string, string>,
contextFiles: Record<string, string>
): string {
const sections: string[] = [];
// Add primary files
sections.push('## Files to Edit (ONLY OUTPUT THESE FILES)\n');
sections.push('🚨 You MUST ONLY generate the files listed below. Do NOT generate any other files! 🚨\n');
sections.push('⚠️ CRITICAL: Return the COMPLETE file - NEVER truncate with "..." or skip any lines! ⚠️\n');
sections.push('The file MUST include ALL imports, ALL functions, ALL JSX, and ALL closing tags.\n\n');
for (const [path, content] of Object.entries(primaryFiles)) {
sections.push(`### ${path}
**IMPORTANT: This is the COMPLETE file. Your output must include EVERY line shown below, modified only where necessary.**
\`\`\`${getFileExtension(path)}
${content}
\`\`\`
`);
}
// Add context files if any - but truncate large files
if (Object.keys(contextFiles).length > 0) {
sections.push('\n## Context Files (Reference Only - Do Not Edit)\n');
for (const [path, content] of Object.entries(contextFiles)) {
// Truncate very large context files to save tokens
let truncatedContent = content;
if (content.length > 2000) {
truncatedContent = content.substring(0, 2000) + '\n// ... [truncated for context length]';
}
sections.push(`### ${path}
\`\`\`${getFileExtension(path)}
${truncatedContent}
\`\`\`
`);
}
}
return sections.join('\n');
}
/**
* Get file extension for syntax highlighting
*/
function getFileExtension(path: string): string {
const ext = path.split('.').pop() || '';
const mapping: Record<string, string> = {
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'css': 'css',
'json': 'json',
};
return mapping[ext] || ext;
}
+252
View File
@@ -0,0 +1,252 @@
/**
* Example-based prompts for teaching AI proper edit behavior
*/
export const EDIT_EXAMPLES = `
## Edit Strategy Examples
### Example 1: Update Header Color
USER: "Make the header background black"
CORRECT APPROACH:
1. Identify Header component location
2. Edit ONLY Header.jsx
3. Change background color class/style
INCORRECT APPROACH:
- Regenerating entire App.jsx
- Creating new Header.jsx from scratch
- Modifying unrelated files
EXPECTED OUTPUT:
<file path="src/components/Header.jsx">
// Only the Header component with updated background
// Preserving all existing functionality
</file>
### Example 2: Add New Page
USER: "Add a videos page"
CORRECT APPROACH:
1. Create Videos.jsx component
2. Update routing in App.jsx or Router component
3. Add navigation link if needed
INCORRECT APPROACH:
- Regenerating entire application
- Recreating all existing pages
EXPECTED OUTPUT:
<file path="src/components/Videos.jsx">
// New Videos component
</file>
<file path="src/App.jsx">
// ONLY the routing update, preserving everything else
</file>
### Example 3: Fix Styling Issue
USER: "Fix the button styling on mobile"
CORRECT APPROACH:
1. Identify which component has the button
2. Update only that component's Tailwind classes
3. Add responsive modifiers (sm:, md:, etc)
INCORRECT APPROACH:
- Regenerating all components
- Creating new CSS files
- Modifying global styles unnecessarily
### Example 4: Add Feature to Component
USER: "Add a search bar to the header"
CORRECT APPROACH:
1. Modify Header.jsx to add search functionality
2. Preserve all existing header content
3. Integrate search seamlessly
INCORRECT APPROACH:
- Creating Header.jsx from scratch
- Losing existing navigation/branding
### Example 5: Add New Component
USER: "Add a newsletter signup to the footer"
CORRECT APPROACH:
1. Create Newsletter.jsx component
2. UPDATE Footer.jsx to import Newsletter
3. Add <Newsletter /> in the appropriate place in Footer
EXPECTED OUTPUT:
<file path="src/components/Newsletter.jsx">
// New Newsletter component
</file>
<file path="src/components/Footer.jsx">
// Updated Footer with Newsletter import and usage
import Newsletter from './Newsletter';
// ... existing code ...
// Add <Newsletter /> in the render
</file>
### Example 6: Add External Library
USER: "Add animations with framer-motion to the hero"
CORRECT APPROACH:
1. Import framer-motion in Hero.jsx
2. Use motion components
3. System will auto-install framer-motion
EXPECTED OUTPUT:
<file path="src/components/Hero.jsx">
import { motion } from 'framer-motion';
// ... rest of Hero with motion animations
</file>
### Example 7: Remove Element
USER: "Remove start deploying button"
CORRECT APPROACH:
1. Search for "start deploying" in all component files
2. Find it in Hero.jsx
3. Edit ONLY Hero.jsx to remove that button
INCORRECT APPROACH:
- Creating a new file
- Editing multiple files
- Redesigning the entire Hero
EXPECTED OUTPUT:
<file path="src/components/Hero.jsx">
// Hero component with "start deploying" button removed
// All other content preserved
</file>
### Example 8: Delete Section
USER: "Delete the testimonials section"
CORRECT APPROACH:
1. Find which file contains testimonials
2. Remove only that section from the file
3. Keep all other content intact
INCORRECT APPROACH:
- Deleting the entire file
- Recreating the page without testimonials
### Example 9: Change a Single Style (CRITICAL EXAMPLE)
USER: "update the hero to bg blue"
CORRECT APPROACH:
1. Identify the Hero component file: 'src/components/Hero.jsx'.
2. Locate the outermost 'div' or container element.
3. Find the existing background color class (e.g., 'bg-gray-900').
4. Replace ONLY that class with 'bg-blue-500'.
5. Return the entire file, completely unchanged except for that single class modification.
**Original File Content (BEFORE):**
<file path="src/components/Hero.jsx">
import React from 'react';
export default function Hero() {
return (
<div className="w-full bg-gray-900 text-white py-20 px-4">
<h1 className="text-5xl font-bold">Welcome to the App</h1>
<p className="mt-4 text-lg">This is the original hero section.</p>
<button className="mt-6 px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg">
Get Started
</button>
</div>
);
}
</file>
**Expected Output (AFTER):**
<file path="src/components/Hero.jsx">
import React from 'react';
export default function Hero() {
return (
<div className="w-full bg-blue-500 text-white py-20 px-4">
<h1 className="text-5xl font-bold">Welcome to the App</h1>
<p className="mt-4 text-lg">This is the original hero section.</p>
<button className="mt-6 px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg">
Get Started
</button>
</div>
);
}
</file>
NOTICE: Everything remains EXACTLY the same except 'bg-gray-900' → 'bg-blue-500'.
- The button still has bg-blue-600 (unchanged)
- All text, structure, imports are identical
- No reformatting, no "improvements", no cleanup
## Key Principles
1. **Minimal Changes**: Only modify what's necessary
2. **Preserve Functionality**: Keep all existing features
3. **Respect Structure**: Follow existing patterns
4. **Target Precision**: Edit specific files, not everything
5. **Context Awareness**: Use imports/exports to understand relationships
## File Identification Patterns
- "header" → src/components/Header.jsx
- "navigation" → src/components/Nav.jsx or Header.jsx
- "footer" → src/components/Footer.jsx
- "home page" → src/App.jsx or src/pages/Home.jsx
- "styling" → Component files (Tailwind) or index.css
- "routing" → App.jsx or Router component
- "layout" → Layout components or App.jsx
## Edit Intent Classification
UPDATE_COMPONENT: Modify existing component
- Keywords: update, change, modify, edit, fix
- Action: Edit single file
ADD_FEATURE: Add new functionality
- Keywords: add, create, implement, build
- Action: Create new files + minimal edits
FIX_ISSUE: Resolve problems
- Keywords: fix, resolve, debug, repair
- Action: Targeted fixes
UPDATE_STYLE: Change appearance
- Keywords: style, color, theme, design
- Action: Update Tailwind classes
REFACTOR: Improve code quality
- Keywords: refactor, clean, optimize
- Action: Restructure without changing behavior
`;
export function getEditExamplesPrompt(): string {
return EDIT_EXAMPLES;
}
export function getComponentPatternPrompt(fileStructure: string): string {
return `
## Current Project Structure
${fileStructure}
## Component Naming Patterns
Based on your file structure, here are the patterns to follow:
1. Component files are in: src/components/
2. Page components might be in: src/pages/ or src/components/
3. Utility functions are in: src/utils/ or src/lib/
4. Styles use Tailwind classes inline
5. Main app entry is: src/App.jsx
When the user mentions a component by name, look for:
- Exact matches first (Header → Header.jsx)
- Partial matches (nav → Navigation.jsx, NavBar.jsx)
- Semantic matches (top bar → Header.jsx)
`;
}
+510
View File
@@ -0,0 +1,510 @@
import { FileManifest, EditType, EditIntent, IntentPattern } from '@/types/file-manifest';
/**
* Analyze user prompts to determine edit intent and select relevant files
*/
export function analyzeEditIntent(
prompt: string,
manifest: FileManifest
): EditIntent {
const lowerPrompt = prompt.toLowerCase();
// Define intent patterns
const patterns: IntentPattern[] = [
{
patterns: [
/update\s+(the\s+)?(\w+)\s+(component|section|page)/i,
/change\s+(the\s+)?(\w+)/i,
/modify\s+(the\s+)?(\w+)/i,
/edit\s+(the\s+)?(\w+)/i,
/fix\s+(the\s+)?(\w+)\s+(styling|style|css|layout)/i,
/remove\s+.*\s+(button|link|text|element|section)/i,
/delete\s+.*\s+(button|link|text|element|section)/i,
/hide\s+.*\s+(button|link|text|element|section)/i,
],
type: EditType.UPDATE_COMPONENT,
fileResolver: (p, m) => findComponentByContent(p, m),
},
{
patterns: [
/add\s+(a\s+)?new\s+(\w+)\s+(page|section|feature|component)/i,
/create\s+(a\s+)?(\w+)\s+(page|section|feature|component)/i,
/implement\s+(a\s+)?(\w+)\s+(page|section|feature)/i,
/build\s+(a\s+)?(\w+)\s+(page|section|feature)/i,
/add\s+(\w+)\s+to\s+(?:the\s+)?(\w+)/i,
/add\s+(?:a\s+)?(\w+)\s+(?:component|section)/i,
/include\s+(?:a\s+)?(\w+)/i,
],
type: EditType.ADD_FEATURE,
fileResolver: (p, m) => findFeatureInsertionPoints(p, m),
},
{
patterns: [
/fix\s+(the\s+)?(\w+|\w+\s+\w+)(?!\s+styling|\s+style)/i,
/resolve\s+(the\s+)?error/i,
/debug\s+(the\s+)?(\w+)/i,
/repair\s+(the\s+)?(\w+)/i,
],
type: EditType.FIX_ISSUE,
fileResolver: (p, m) => findProblemFiles(p, m),
},
{
patterns: [
/change\s+(the\s+)?(color|theme|style|styling|css)/i,
/update\s+(the\s+)?(color|theme|style|styling|css)/i,
/make\s+it\s+(dark|light|blue|red|green)/i,
/style\s+(the\s+)?(\w+)/i,
],
type: EditType.UPDATE_STYLE,
fileResolver: (p, m) => findStyleFiles(p, m),
},
{
patterns: [
/refactor\s+(the\s+)?(\w+)/i,
/clean\s+up\s+(the\s+)?code/i,
/reorganize\s+(the\s+)?(\w+)/i,
/optimize\s+(the\s+)?(\w+)/i,
],
type: EditType.REFACTOR,
fileResolver: (p, m) => findRefactorTargets(p, m),
},
{
patterns: [
/start\s+over/i,
/recreate\s+everything/i,
/rebuild\s+(the\s+)?app/i,
/new\s+app/i,
/from\s+scratch/i,
],
type: EditType.FULL_REBUILD,
fileResolver: (p, m) => [m.entryPoint],
},
{
patterns: [
/install\s+(\w+)/i,
/add\s+(\w+)\s+(package|library|dependency)/i,
/use\s+(\w+)\s+(library|framework)/i,
],
type: EditType.ADD_DEPENDENCY,
fileResolver: (p, m) => findPackageFiles(m),
},
];
// Find matching pattern
for (const pattern of patterns) {
for (const regex of pattern.patterns) {
if (regex.test(lowerPrompt)) {
const targetFiles = pattern.fileResolver(prompt, manifest);
const suggestedContext = getSuggestedContext(targetFiles, manifest);
return {
type: pattern.type,
targetFiles,
confidence: calculateConfidence(prompt, pattern, targetFiles),
description: generateDescription(pattern.type, prompt, targetFiles),
suggestedContext,
};
}
}
}
// Default to component update if no pattern matches
return {
type: EditType.UPDATE_COMPONENT,
targetFiles: [manifest.entryPoint],
confidence: 0.3,
description: 'General update to application',
suggestedContext: [],
};
}
/**
* Find component files mentioned in the prompt
*/
function findComponentFiles(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
const lowerPrompt = prompt.toLowerCase();
// Extract component names from prompt
const componentWords = extractComponentNames(prompt);
console.log('[findComponentFiles] Extracted words:', componentWords);
// First pass: Look for exact component file matches
for (const [path, fileInfo] of Object.entries(manifest.files)) {
// Check if file name or component name matches
const fileName = path.split('/').pop()?.toLowerCase() || '';
const componentName = fileInfo.componentInfo?.name.toLowerCase();
for (const word of componentWords) {
if (fileName.includes(word) || componentName?.includes(word)) {
console.log(`[findComponentFiles] Match found: word="${word}" in file="${path}"`);
files.push(path);
break; // Stop after first match to avoid duplicates
}
}
}
// If no specific component found, check for common UI elements
if (files.length === 0) {
const uiElements = ['header', 'footer', 'nav', 'sidebar', 'button', 'card', 'modal', 'hero', 'banner', 'about', 'services', 'features', 'testimonials', 'gallery', 'contact', 'team', 'pricing'];
for (const element of uiElements) {
if (lowerPrompt.includes(element)) {
// Look for exact component file matches first
for (const [path, fileInfo] of Object.entries(manifest.files)) {
const fileName = path.split('/').pop()?.toLowerCase() || '';
// Only match if the filename contains the element name
if (fileName.includes(element + '.') || fileName === element) {
files.push(path);
console.log(`[findComponentFiles] UI element match: element="${element}" in file="${path}"`);
return files; // Return immediately with just this file
}
}
// If no exact file match, look for the element in file names (but be more selective)
for (const [path, fileInfo] of Object.entries(manifest.files)) {
const fileName = path.split('/').pop()?.toLowerCase() || '';
if (fileName.includes(element)) {
files.push(path);
console.log(`[findComponentFiles] UI element partial match: element="${element}" in file="${path}"`);
return files; // Return immediately with just this file
}
}
}
}
}
// Limit results to most specific matches
if (files.length > 1) {
console.log(`[findComponentFiles] Multiple files found (${files.length}), limiting to first match`);
return [files[0]]; // Only return the first match
}
return files.length > 0 ? files : [manifest.entryPoint];
}
/**
* Find where to add new features
*/
function findFeatureInsertionPoints(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
const lowerPrompt = prompt.toLowerCase();
// For new pages, we need routing files and layout
if (lowerPrompt.includes('page')) {
// Find router configuration
for (const [path, fileInfo] of Object.entries(manifest.files)) {
if (fileInfo.content.includes('Route') ||
fileInfo.content.includes('createBrowserRouter') ||
path.includes('router') ||
path.includes('routes')) {
files.push(path);
}
}
// Also include App.jsx for navigation updates
if (manifest.entryPoint) {
files.push(manifest.entryPoint);
}
}
// For new components, find the most appropriate parent
if (lowerPrompt.includes('component') || lowerPrompt.includes('section') ||
lowerPrompt.includes('add') || lowerPrompt.includes('create')) {
// Extract where to add it (e.g., "to the footer", "in header")
const locationMatch = prompt.match(/(?:in|to|on|inside)\s+(?:the\s+)?(\w+)/i);
if (locationMatch) {
const location = locationMatch[1];
const parentFiles = findComponentFiles(location, manifest);
files.push(...parentFiles);
console.log(`[findFeatureInsertionPoints] Adding to ${location}, parent files:`, parentFiles);
} else {
// Look for component mentions in the prompt
const componentWords = extractComponentNames(prompt);
for (const word of componentWords) {
const relatedFiles = findComponentFiles(word, manifest);
if (relatedFiles.length > 0 && relatedFiles[0] !== manifest.entryPoint) {
files.push(...relatedFiles);
}
}
// Default to App.jsx if no specific location found
if (files.length === 0) {
files.push(manifest.entryPoint);
}
}
}
// Remove duplicates
return [...new Set(files)];
}
/**
* Find files that might have problems
*/
function findProblemFiles(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
// Look for error keywords
if (prompt.match(/error|bug|issue|problem|broken|not working/i)) {
// Check recently modified files first
const sortedFiles = Object.entries(manifest.files)
.sort(([, a], [, b]) => b.lastModified - a.lastModified)
.slice(0, 5);
files.push(...sortedFiles.map(([path]) => path));
}
// Also check for specific component mentions
const componentFiles = findComponentFiles(prompt, manifest);
files.push(...componentFiles);
return [...new Set(files)];
}
/**
* Find style-related files
*/
function findStyleFiles(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
// Add all CSS files
files.push(...manifest.styleFiles);
// Check for Tailwind config
const tailwindConfig = Object.keys(manifest.files).find(
path => path.includes('tailwind.config')
);
if (tailwindConfig) files.push(tailwindConfig);
// If specific component styling mentioned, include that component
const componentFiles = findComponentFiles(prompt, manifest);
files.push(...componentFiles);
return files;
}
/**
* Find files to refactor
*/
function findRefactorTargets(prompt: string, manifest: FileManifest): string[] {
// Similar to findComponentFiles but broader
return findComponentFiles(prompt, manifest);
}
/**
* Find package configuration files
*/
function findPackageFiles(manifest: FileManifest): string[] {
const files: string[] = [];
for (const path of Object.keys(manifest.files)) {
if (path.endsWith('package.json') ||
path.endsWith('vite.config.js') ||
path.endsWith('tsconfig.json')) {
files.push(path);
}
}
return files;
}
/**
* Find component by searching for content mentioned in the prompt
*/
function findComponentByContent(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
const lowerPrompt = prompt.toLowerCase();
console.log('[findComponentByContent] Searching for content in prompt:', prompt);
// Extract quoted strings or specific button/link text
const quotedStrings = prompt.match(/["']([^"']+)["']/g) || [];
const searchTerms: string[] = quotedStrings.map(s => s.replace(/["']/g, ''));
// Also look for specific terms after 'remove', 'delete', 'hide'
const actionMatch = prompt.match(/(?:remove|delete|hide)\s+(?:the\s+)?(.+?)(?:\s+button|\s+link|\s+text|\s+element|\s+section|$)/i);
if (actionMatch) {
searchTerms.push(actionMatch[1].trim());
}
console.log('[findComponentByContent] Search terms:', searchTerms);
// If we have search terms, look for them in file contents
if (searchTerms.length > 0) {
for (const [path, fileInfo] of Object.entries(manifest.files)) {
// Only search in component files
if (!path.includes('.jsx') && !path.includes('.tsx')) continue;
const content = fileInfo.content.toLowerCase();
for (const term of searchTerms) {
if (content.includes(term.toLowerCase())) {
console.log(`[findComponentByContent] Found "${term}" in ${path}`);
files.push(path);
break; // Only add file once
}
}
}
}
// If no files found by content, fall back to component name search
if (files.length === 0) {
console.log('[findComponentByContent] No files found by content, falling back to component name search');
return findComponentFiles(prompt, manifest);
}
// Return only the first match to avoid editing multiple files
return [files[0]];
}
/**
* Extract component names from prompt
*/
function extractComponentNames(prompt: string): string[] {
const words: string[] = [];
// Remove common words but keep component-related words
const cleanPrompt = prompt
.replace(/\b(the|a|an|in|on|to|from|update|change|modify|edit|fix|make)\b/gi, '')
.toLowerCase();
// Extract potential component names (words that might be components)
const matches = cleanPrompt.match(/\b\w+\b/g) || [];
for (const match of matches) {
if (match.length > 2) { // Skip very short words
words.push(match);
}
}
return words;
}
/**
* Get additional files for context - returns ALL files for comprehensive context
*/
function getSuggestedContext(
targetFiles: string[],
manifest: FileManifest
): string[] {
// Return all files except the ones being edited
const allFiles = Object.keys(manifest.files);
return allFiles.filter(file => !targetFiles.includes(file));
}
/**
* Resolve import path to actual file path
*/
function resolveImportPath(
fromFile: string,
importPath: string,
manifest: FileManifest
): string | null {
// Handle relative imports
if (importPath.startsWith('./') || importPath.startsWith('../')) {
const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
const resolved = resolveRelativePath(fromDir, importPath);
// Try with different extensions
const extensions = ['.jsx', '.js', '.tsx', '.ts', ''];
for (const ext of extensions) {
const fullPath = resolved + ext;
if (manifest.files[fullPath]) {
return fullPath;
}
// Try index file
const indexPath = resolved + '/index' + ext;
if (manifest.files[indexPath]) {
return indexPath;
}
}
}
// Handle @/ alias (common in Vite projects)
if (importPath.startsWith('@/')) {
const srcPath = importPath.replace('@/', '/home/user/app/src/');
return resolveImportPath(fromFile, srcPath, manifest);
}
return null;
}
/**
* Resolve relative path
*/
function resolveRelativePath(fromDir: string, relativePath: string): string {
const parts = fromDir.split('/');
const relParts = relativePath.split('/');
for (const part of relParts) {
if (part === '..') {
parts.pop();
} else if (part !== '.') {
parts.push(part);
}
}
return parts.join('/');
}
/**
* Calculate confidence score
*/
function calculateConfidence(
prompt: string,
pattern: IntentPattern,
targetFiles: string[]
): number {
let confidence = 0.5; // Base confidence
// Higher confidence if we found specific files
if (targetFiles.length > 0 && targetFiles[0] !== '') {
confidence += 0.2;
}
// Higher confidence for more specific prompts
if (prompt.split(' ').length > 5) {
confidence += 0.1;
}
// Higher confidence for exact pattern matches
for (const regex of pattern.patterns) {
if (regex.test(prompt)) {
confidence += 0.2;
break;
}
}
return Math.min(confidence, 1.0);
}
/**
* Generate human-readable description
*/
function generateDescription(
type: EditType,
prompt: string,
targetFiles: string[]
): string {
const fileNames = targetFiles.map(f => f.split('/').pop()).join(', ');
switch (type) {
case EditType.UPDATE_COMPONENT:
return `Updating component(s): ${fileNames}`;
case EditType.ADD_FEATURE:
return `Adding new feature to: ${fileNames}`;
case EditType.FIX_ISSUE:
return `Fixing issue in: ${fileNames}`;
case EditType.UPDATE_STYLE:
return `Updating styles in: ${fileNames}`;
case EditType.REFACTOR:
return `Refactoring: ${fileNames}`;
case EditType.FULL_REBUILD:
return 'Rebuilding entire application';
case EditType.ADD_DEPENDENCY:
return 'Adding new dependency';
default:
return `Editing: ${fileNames}`;
}
}
+265
View File
@@ -0,0 +1,265 @@
import { FileInfo, ImportInfo, ComponentInfo } from '@/types/file-manifest';
/**
* Parse a JavaScript/JSX file to extract imports, exports, and component info
*/
export function parseJavaScriptFile(content: string, filePath: string): Partial<FileInfo> {
const imports = extractImports(content);
const exports = extractExports(content);
const componentInfo = extractComponentInfo(content, filePath);
const fileType = determineFileType(filePath, content);
return {
imports,
exports,
componentInfo,
type: fileType,
};
}
/**
* Extract import statements from file content
*/
function extractImports(content: string): ImportInfo[] {
const imports: ImportInfo[] = [];
// Match import statements
const importRegex = /import\s+(?:(.+?)\s+from\s+)?['"](.+?)['"]/g;
const matches = content.matchAll(importRegex);
for (const match of matches) {
const [, importClause, source] = match;
const importInfo: ImportInfo = {
source,
imports: [],
isLocal: source.startsWith('./') || source.startsWith('../') || source.startsWith('@/'),
};
if (importClause) {
// Handle default import
const defaultMatch = importClause.match(/^(\w+)(?:,|$)/);
if (defaultMatch) {
importInfo.defaultImport = defaultMatch[1];
}
// Handle named imports
const namedMatch = importClause.match(/\{([^}]+)\}/);
if (namedMatch) {
importInfo.imports = namedMatch[1]
.split(',')
.map(imp => imp.trim())
.map(imp => imp.split(/\s+as\s+/)[0].trim());
}
}
imports.push(importInfo);
}
return imports;
}
/**
* Extract export statements from file content
*/
function extractExports(content: string): string[] {
const exports: string[] = [];
// Match default export
if (/export\s+default\s+/m.test(content)) {
// Try to find the name of the default export
const defaultExportMatch = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
if (defaultExportMatch) {
exports.push(`default:${defaultExportMatch[1]}`);
} else {
exports.push('default');
}
}
// Match named exports
const namedExportRegex = /export\s+(?:const|let|var|function|class)\s+(\w+)/g;
const namedMatches = content.matchAll(namedExportRegex);
for (const match of namedMatches) {
exports.push(match[1]);
}
// Match export { ... } statements
const exportBlockRegex = /export\s+\{([^}]+)\}/g;
const blockMatches = content.matchAll(exportBlockRegex);
for (const match of blockMatches) {
const names = match[1]
.split(',')
.map(exp => exp.trim())
.map(exp => exp.split(/\s+as\s+/)[0].trim());
exports.push(...names);
}
return exports;
}
/**
* Extract React component information
*/
function extractComponentInfo(content: string, filePath: string): ComponentInfo | undefined {
// Check if this is likely a React component
const hasJSX = /<[A-Z]\w*|<[a-z]+\s+[^>]*\/?>/.test(content);
if (!hasJSX && !content.includes('React')) return undefined;
// Try to find component name
let componentName = '';
// Check for function component
const funcComponentMatch = content.match(/(?:export\s+)?(?:default\s+)?function\s+([A-Z]\w*)\s*\(/);
if (funcComponentMatch) {
componentName = funcComponentMatch[1];
} else {
// Check for arrow function component
const arrowComponentMatch = content.match(/(?:export\s+)?(?:default\s+)?(?:const|let)\s+([A-Z]\w*)\s*=\s*(?:\([^)]*\)|[^=])*=>/);
if (arrowComponentMatch) {
componentName = arrowComponentMatch[1];
}
}
// If no component name found, try to get from filename
if (!componentName) {
const fileName = filePath.split('/').pop()?.replace(/\.(jsx?|tsx?)$/, '');
if (fileName && /^[A-Z]/.test(fileName)) {
componentName = fileName;
}
}
if (!componentName) return undefined;
// Extract hooks used
const hooks: string[] = [];
const hookRegex = /use[A-Z]\w*/g;
const hookMatches = content.matchAll(hookRegex);
for (const match of hookMatches) {
if (!hooks.includes(match[0])) {
hooks.push(match[0]);
}
}
// Check if component has state
const hasState = hooks.includes('useState') || hooks.includes('useReducer');
// Extract child components (rough approximation)
const childComponents: string[] = [];
const componentRegex = /<([A-Z]\w*)[^>]*(?:\/?>|>)/g;
const componentMatches = content.matchAll(componentRegex);
for (const match of componentMatches) {
const comp = match[1];
if (!childComponents.includes(comp) && comp !== componentName) {
childComponents.push(comp);
}
}
return {
name: componentName,
hooks,
hasState,
childComponents,
};
}
/**
* Determine file type based on path and content
*/
function determineFileType(
filePath: string,
content: string
): FileInfo['type'] {
const fileName = filePath.split('/').pop()?.toLowerCase() || '';
const dirPath = filePath.toLowerCase();
// Style files
if (fileName.endsWith('.css')) return 'style';
// Config files
if (fileName.includes('config') ||
fileName === 'vite.config.js' ||
fileName === 'tailwind.config.js' ||
fileName === 'postcss.config.js') {
return 'config';
}
// Hook files
if (dirPath.includes('/hooks/') || fileName.startsWith('use')) {
return 'hook';
}
// Context files
if (dirPath.includes('/context/') || fileName.includes('context')) {
return 'context';
}
// Layout components
if (fileName.includes('layout') || content.includes('children')) {
return 'layout';
}
// Page components (in pages directory or have routing)
if (dirPath.includes('/pages/') ||
content.includes('useRouter') ||
content.includes('useParams')) {
return 'page';
}
// Utility files
if (dirPath.includes('/utils/') ||
dirPath.includes('/lib/') ||
!content.includes('export default')) {
return 'utility';
}
// Default to component
return 'component';
}
/**
* Build component dependency tree
*/
export function buildComponentTree(files: Record<string, FileInfo>) {
const tree: Record<string, {
file: string;
imports: string[];
importedBy: string[];
type: 'page' | 'layout' | 'component';
}> = {};
// First pass: collect all components
for (const [path, fileInfo] of Object.entries(files)) {
if (fileInfo.componentInfo) {
const componentName = fileInfo.componentInfo.name;
tree[componentName] = {
file: path,
imports: [],
importedBy: [],
type: fileInfo.type === 'page' ? 'page' :
fileInfo.type === 'layout' ? 'layout' : 'component',
};
}
}
// Second pass: build relationships
for (const [path, fileInfo] of Object.entries(files)) {
if (fileInfo.componentInfo && fileInfo.imports) {
const componentName = fileInfo.componentInfo.name;
// Find imported components
for (const imp of fileInfo.imports) {
if (imp.isLocal && imp.defaultImport) {
// Check if this import is a component we know about
if (tree[imp.defaultImport]) {
tree[componentName].imports.push(imp.defaultImport);
tree[imp.defaultImport].importedBy.push(componentName);
}
}
}
}
}
return tree;
}
+268
View File
@@ -0,0 +1,268 @@
/**
* Agentic file search executor
* Executes search plans to find exact code locations before editing
*/
export interface SearchResult {
filePath: string;
lineNumber: number;
lineContent: string;
matchedTerm?: string;
matchedPattern?: string;
contextBefore: string[];
contextAfter: string[];
confidence: 'high' | 'medium' | 'low';
}
export interface SearchPlan {
editType: string;
reasoning: string;
searchTerms: string[];
regexPatterns?: string[];
fileTypesToSearch?: string[];
expectedMatches?: number;
fallbackSearch?: {
terms: string[];
patterns?: string[];
};
}
export interface SearchExecutionResult {
success: boolean;
results: SearchResult[];
filesSearched: number;
executionTime: number;
usedFallback: boolean;
error?: string;
}
/**
* Execute a search plan against the codebase
*/
export function executeSearchPlan(
searchPlan: SearchPlan,
files: Record<string, string>
): SearchExecutionResult {
const startTime = Date.now();
const results: SearchResult[] = [];
let filesSearched = 0;
let usedFallback = false;
const {
searchTerms = [],
regexPatterns = [],
fileTypesToSearch = ['.jsx', '.tsx', '.js', '.ts'],
fallbackSearch
} = searchPlan;
// Helper function to perform search
const performSearch = (terms: string[], patterns?: string[]): SearchResult[] => {
const searchResults: SearchResult[] = [];
for (const [filePath, content] of Object.entries(files)) {
// Skip files that don't match the desired extensions
const shouldSearch = fileTypesToSearch.some(ext => filePath.endsWith(ext));
if (!shouldSearch) continue;
filesSearched++;
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
let matched = false;
let matchedTerm: string | undefined;
let matchedPattern: string | undefined;
// Check simple search terms (case-insensitive)
for (const term of terms) {
if (line.toLowerCase().includes(term.toLowerCase())) {
matched = true;
matchedTerm = term;
break;
}
}
// Check regex patterns if no term match
if (!matched && patterns) {
for (const pattern of patterns) {
try {
const regex = new RegExp(pattern, 'i');
if (regex.test(line)) {
matched = true;
matchedPattern = pattern;
break;
}
} catch (e) {
console.warn(`[file-search] Invalid regex pattern: ${pattern}`);
}
}
}
if (matched) {
// Get context lines (3 before, 3 after)
const contextBefore = lines.slice(Math.max(0, i - 3), i);
const contextAfter = lines.slice(i + 1, Math.min(lines.length, i + 4));
// Determine confidence based on match type and context
let confidence: 'high' | 'medium' | 'low' = 'medium';
// High confidence if it's an exact match or in a component definition
if (matchedTerm && line.includes(matchedTerm)) {
confidence = 'high';
} else if (line.includes('function') || line.includes('export') || line.includes('return')) {
confidence = 'high';
} else if (matchedPattern) {
confidence = 'medium';
}
searchResults.push({
filePath,
lineNumber: i + 1,
lineContent: line.trim(),
matchedTerm,
matchedPattern,
contextBefore,
contextAfter,
confidence
});
}
}
}
return searchResults;
};
// Execute primary search
results.push(...performSearch(searchTerms, regexPatterns));
// If no results and we have a fallback, try it
if (results.length === 0 && fallbackSearch) {
console.log('[file-search] No results from primary search, trying fallback...');
usedFallback = true;
results.push(...performSearch(
fallbackSearch.terms,
fallbackSearch.patterns
));
}
const executionTime = Date.now() - startTime;
// Sort results by confidence
results.sort((a, b) => {
const confidenceOrder = { high: 3, medium: 2, low: 1 };
return confidenceOrder[b.confidence] - confidenceOrder[a.confidence];
});
return {
success: results.length > 0,
results,
filesSearched,
executionTime,
usedFallback,
error: results.length === 0 ? 'No matches found for search terms' : undefined
};
}
/**
* Format search results for AI consumption
*/
export function formatSearchResultsForAI(results: SearchResult[]): string {
if (results.length === 0) {
return 'No search results found.';
}
const sections: string[] = [];
sections.push('🔍 SEARCH RESULTS - EXACT LOCATIONS FOUND:\n');
// Group by file for better readability
const resultsByFile = new Map<string, SearchResult[]>();
for (const result of results) {
if (!resultsByFile.has(result.filePath)) {
resultsByFile.set(result.filePath, []);
}
resultsByFile.get(result.filePath)!.push(result);
}
for (const [filePath, fileResults] of resultsByFile) {
sections.push(`\n📄 FILE: ${filePath}`);
for (const result of fileResults) {
sections.push(`\n 📍 Line ${result.lineNumber} (${result.confidence} confidence)`);
if (result.matchedTerm) {
sections.push(` Matched: "${result.matchedTerm}"`);
} else if (result.matchedPattern) {
sections.push(` Pattern: ${result.matchedPattern}`);
}
sections.push(` Code: ${result.lineContent}`);
if (result.contextBefore.length > 0 || result.contextAfter.length > 0) {
sections.push(` Context:`);
for (const line of result.contextBefore) {
sections.push(` ${line}`);
}
sections.push(`${result.lineContent}`);
for (const line of result.contextAfter) {
sections.push(` ${line}`);
}
}
}
}
sections.push('\n\n🎯 RECOMMENDED ACTION:');
// Recommend the highest confidence result
const bestResult = results[0];
sections.push(`Edit ${bestResult.filePath} at line ${bestResult.lineNumber}`);
return sections.join('\n');
}
/**
* Select the best file to edit based on search results
*/
export function selectTargetFile(
results: SearchResult[],
editType: string
): { filePath: string; lineNumber: number; reason: string } | null {
if (results.length === 0) return null;
// For style updates, prefer components over CSS files
if (editType === 'UPDATE_STYLE') {
const componentResult = results.find(r =>
r.filePath.endsWith('.jsx') || r.filePath.endsWith('.tsx')
);
if (componentResult) {
return {
filePath: componentResult.filePath,
lineNumber: componentResult.lineNumber,
reason: 'Found component with style to update'
};
}
}
// For remove operations, find the component that renders the element
if (editType === 'REMOVE_ELEMENT') {
const renderResult = results.find(r =>
r.lineContent.includes('return') ||
r.lineContent.includes('<')
);
if (renderResult) {
return {
filePath: renderResult.filePath,
lineNumber: renderResult.lineNumber,
reason: 'Found element to remove in render output'
};
}
}
// Default: use highest confidence result
const best = results[0];
return {
filePath: best.filePath,
lineNumber: best.lineNumber,
reason: `Highest confidence match (${best.confidence})`
};
}
+21
View File
@@ -0,0 +1,21 @@
// Centralized icon exports to avoid Turbopack chunk loading issues
// This file pre-loads all icons to prevent dynamic import errors
export {
FiFile,
FiChevronRight,
FiChevronDown,
FiGithub
} from 'react-icons/fi';
export {
BsFolderFill,
BsFolder2Open
} from 'react-icons/bs';
export {
SiJavascript,
SiReact,
SiCss3,
SiJson
} from 'react-icons/si';
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+6229
View File
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
{
"name": "open-lovable",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test:integration": "node tests/e2b-integration.test.js",
"test:api": "node tests/api-endpoints.test.js",
"test:code": "node tests/code-execution.test.js",
"test:all": "npm run test:integration && npm run test:api && npm run test:code"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.1",
"@ai-sdk/groq": "^2.0.0",
"@ai-sdk/openai": "^2.0.4",
"@anthropic-ai/sdk": "^0.57.0",
"@e2b/code-interpreter": "^1.5.1",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@types/react-syntax-highlighter": "^15.5.13",
"ai": "^5.0.0",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"e2b": "^1.13.2",
"express": "^5.1.0",
"framer-motion": "^12.23.12",
"groq-sdk": "^0.29.0",
"lucide-react": "^0.532.0",
"next": "15.4.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-icons": "^5.5.0",
"react-syntax-highlighter": "^15.6.1",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.11",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.4.3",
"tailwindcss": "^4.1.11",
"typescript": "^5"
}
}
+5133
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};
export default config;
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+68
View File
@@ -0,0 +1,68 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: "class",
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: ["var(--font-inter)", "ui-sans-serif", "system-ui", "sans-serif"],
mono: ["ui-monospace", "SFMono-Regular", "monospace"],
},
},
},
plugins: [],
};
export default config;
+701
View File
@@ -0,0 +1,701 @@
{
"name": "e2b-tests",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "e2b-tests",
"version": "1.0.0",
"dependencies": {
"@e2b/code-interpreter": "^1.5.1",
"@e2b/sdk": "^0.12.5",
"dotenv": "^16.0.3"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.3.tgz",
"integrity": "sha512-w/gJKME9mYN7ZoUAmSMAWXk4hkVpxRKvEJCb3dV5g9wwWdxTJJ0ayOJAVcNxtdqaxDyFuC0uz4RSGVacJ030PQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@connectrpc/connect": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.0.0-rc.3.tgz",
"integrity": "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@bufbuild/protobuf": "^2.2.0"
}
},
"node_modules/@connectrpc/connect-web": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-2.0.0-rc.3.tgz",
"integrity": "sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw==",
"license": "Apache-2.0",
"peerDependencies": {
"@bufbuild/protobuf": "^2.2.0",
"@connectrpc/connect": "2.0.0-rc.3"
}
},
"node_modules/@e2b/code-interpreter": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@e2b/code-interpreter/-/code-interpreter-1.5.1.tgz",
"integrity": "sha512-mkyKjAW2KN5Yt0R1I+1lbH3lo+W/g/1+C2lnwlitXk5wqi/g94SEO41XKdmDf5WWpKG3mnxWDR5d6S/lyjmMEw==",
"license": "MIT",
"dependencies": {
"e2b": "^1.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@e2b/sdk": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/@e2b/sdk/-/sdk-0.12.5.tgz",
"integrity": "sha512-oXmb8/LhjyNp048cSSSul4PZAqpQ+1qj/FM99AssmKrW9xXSES8w5t2AGeqTo+g3IOOm4Kf8DND/Jl3QP8RIgA==",
"license": "MIT",
"dependencies": {
"isomorphic-ws": "^5.0.0",
"normalize-path": "^3.0.0",
"openapi-typescript-fetch": "^1.1.3",
"path-browserify": "^1.0.1",
"platform": "^1.3.6",
"ws": "^8.15.1"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"bufferutil": "^4.0.8",
"utf-8-validate": "^6.0.3"
},
"peerDependencies": {
"openai": "^4.17.4"
}
},
"node_modules/@types/node": {
"version": "18.19.121",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.121.tgz",
"integrity": "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.4"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"peer": true,
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT",
"peer": true
},
"node_modules/bufferutil": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz",
"integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/compare-versions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/e2b": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/e2b/-/e2b-1.13.2.tgz",
"integrity": "sha512-m8acE/MzMAJo1A57DakR2X1Sl5Mt1tcQO2aJfygNaQHLXby/4xsjF0UeJUB70jF7xntiR41pAMbZEHnkzrT9tw==",
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.6.2",
"@connectrpc/connect": "2.0.0-rc.3",
"@connectrpc/connect-web": "2.0.0-rc.3",
"compare-versions": "^6.1.0",
"openapi-fetch": "^0.9.7",
"platform": "^1.3.6"
},
"engines": {
"node": ">=18"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
"license": "MIT",
"peer": true
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"peer": true,
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"peer": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/isomorphic-ws": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
"license": "MIT",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT",
"peer": true
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"peer": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/openai": {
"version": "4.104.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
"integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/openapi-fetch": {
"version": "0.9.8",
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz",
"integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==",
"license": "MIT",
"dependencies": {
"openapi-typescript-helpers": "^0.0.8"
}
},
"node_modules/openapi-typescript-fetch": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/openapi-typescript-fetch/-/openapi-typescript-fetch-1.1.3.tgz",
"integrity": "sha512-smLZPck4OkKMNExcw8jMgrMOGgVGx2N/s6DbKL2ftNl77g5HfoGpZGFy79RBzU/EkaO0OZpwBnslfdBfh7ZcWg==",
"license": "MIT",
"engines": {
"node": ">= 12.0.0",
"npm": ">= 7.0.0"
}
},
"node_modules/openapi-typescript-helpers": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.8.tgz",
"integrity": "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g==",
"license": "MIT"
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
"license": "MIT"
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT",
"peer": true
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT",
"peer": true
},
"node_modules/utf-8-validate": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.5.tgz",
"integrity": "sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 14"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause",
"peer": true
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"peer": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "e2b-tests",
"version": "1.0.0",
"type": "module",
"scripts": {
"test:basic": "node test-sandbox-basic.js",
"test:operations": "node test-sandbox-operations.js",
"test:all": "npm run test:basic && npm run test:operations"
},
"dependencies": {
"@e2b/code-interpreter": "^1.5.1",
"@e2b/sdk": "^0.12.5",
"dotenv": "^16.0.3"
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "examples", "tests", "lib/e2b-backends/archive", "styles"]
}
+50
View File
@@ -0,0 +1,50 @@
// Conversation tracking types for maintaining context across interactions
export interface ConversationMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: number;
metadata?: {
editedFiles?: string[]; // Files edited in this interaction
addedPackages?: string[]; // Packages added in this interaction
editType?: string; // Type of edit performed
sandboxId?: string; // Sandbox ID at time of message
};
}
export interface ConversationEdit {
timestamp: number;
userRequest: string;
editType: string;
targetFiles: string[];
confidence: number;
outcome: 'success' | 'partial' | 'failed';
errorMessage?: string;
}
export interface ConversationContext {
messages: ConversationMessage[];
edits: ConversationEdit[];
currentTopic?: string; // Current focus area (e.g., "header styling", "hero section")
projectEvolution: {
initialState?: string; // Description of initial project state
majorChanges: Array<{
timestamp: number;
description: string;
filesAffected: string[];
}>;
};
userPreferences: {
editStyle?: 'targeted' | 'comprehensive'; // How the user prefers edits
commonRequests?: string[]; // Common patterns in user requests
packagePreferences?: string[]; // Commonly used packages
};
}
export interface ConversationState {
conversationId: string;
startedAt: number;
lastUpdated: number;
context: ConversationContext;
}
+77
View File
@@ -0,0 +1,77 @@
// File manifest types for enhanced edit tracking
export interface FileInfo {
content: string;
type: 'component' | 'page' | 'style' | 'config' | 'utility' | 'layout' | 'hook' | 'context';
exports?: string[]; // Named exports and default export
imports?: ImportInfo[]; // Dependencies
lastModified: number;
componentInfo?: ComponentInfo; // For React components
path: string;
relativePath: string; // Path relative to src/
}
export interface ImportInfo {
source: string; // e.g., './Header', 'react', '@/components/Button'
imports: string[]; // Named imports
defaultImport?: string; // Default import name
isLocal: boolean; // true if starts with './' or '@/'
}
export interface ComponentInfo {
name: string;
props?: string[]; // Prop names if detectable
hooks?: string[]; // Hooks used (useState, useEffect, etc)
hasState: boolean;
childComponents?: string[]; // Components rendered inside
}
export interface RouteInfo {
path: string; // Route path (e.g., '/videos', '/about')
component: string; // Component file path
layout?: string; // Layout component if any
}
export interface ComponentTree {
[componentName: string]: {
file: string;
imports: string[]; // Components it imports
importedBy: string[]; // Components that import it
type: 'page' | 'layout' | 'component';
}
}
export interface FileManifest {
files: Record<string, FileInfo>;
routes: RouteInfo[];
componentTree: ComponentTree;
entryPoint: string; // Usually App.jsx or main.jsx
styleFiles: string[]; // All CSS files
timestamp: number;
}
// Edit classification types
export enum EditType {
UPDATE_COMPONENT = 'UPDATE_COMPONENT', // "update the header", "change button color"
ADD_FEATURE = 'ADD_FEATURE', // "add a videos page", "create new component"
FIX_ISSUE = 'FIX_ISSUE', // "fix the styling", "resolve error"
REFACTOR = 'REFACTOR', // "reorganize", "clean up"
FULL_REBUILD = 'FULL_REBUILD', // "start over", "recreate everything"
UPDATE_STYLE = 'UPDATE_STYLE', // "change colors", "update theme"
ADD_DEPENDENCY = 'ADD_DEPENDENCY' // "install package", "add library"
}
export interface EditIntent {
type: EditType;
targetFiles: string[]; // Predicted files to edit
confidence: number; // 0-1 confidence score
description: string; // Human-readable description
suggestedContext: string[]; // Additional files to include for context
}
// Patterns for intent detection
export interface IntentPattern {
patterns: RegExp[];
type: EditType;
fileResolver: (prompt: string, manifest: FileManifest) => string[];
}
+31
View File
@@ -0,0 +1,31 @@
// Global types for sandbox file management
export interface SandboxFile {
content: string;
lastModified: number;
}
export interface SandboxFileCache {
files: Record<string, SandboxFile>;
lastSync: number;
sandboxId: string;
manifest?: any; // FileManifest type from file-manifest.ts
}
export interface SandboxState {
fileCache: SandboxFileCache | null;
sandbox: any; // E2B sandbox instance
sandboxData: {
sandboxId: string;
url: string;
} | null;
}
// Declare global types
declare global {
var activeSandbox: any;
var sandboxState: SandboxState;
var existingFiles: Set<string>;
}
export {};