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; 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(); // First pass: Find all file declarations const fileRegex = /([\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(''); // 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>/g; while ((match = cmdRegex.exec(response)) !== null) { sections.commands.push(match[1].trim()); } // Parse packages - support both and tags const pkgRegex = /(.*?)<\/package>/g; while ((match = pkgRegex.exec(response)) !== null) { sections.packages.push(match[1].trim()); } // Also parse tag with multiple packages const packagesRegex = /([\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 = /([\s\S]*?)<\/structure>/; const structResult = response.match(structureMatch); if (structResult) { sections.structure = structResult[1].trim(); } // Parse explanation const explanationMatch = /([\s\S]*?)<\/explanation>/; const explResult = response.match(explanationMatch); if (explResult) { sections.explanation = explResult[1].trim(); } // Parse template const templateMatch = /