diff --git a/app/api/apply-ai-code-stream/route.ts b/app/api/apply-ai-code-stream/route.ts index 3676105..22795cc 100644 --- a/app/api/apply-ai-code-stream/route.ts +++ b/app/api/apply-ai-code-stream/route.ts @@ -30,28 +30,28 @@ function parseAIResponse(response: string): ParsedResponse { 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('@/')) { + 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('@') + 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}`); @@ -59,13 +59,13 @@ function parseAIResponse(response: string): ParsedResponse { } } } - + 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; @@ -73,10 +73,10 @@ function parseAIResponse(response: string): ParsedResponse { 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) { @@ -90,7 +90,7 @@ function parseAIResponse(response: string): ParsedResponse { } 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')) { @@ -104,18 +104,18 @@ function parseAIResponse(response: string): ParsedResponse { } } } - + // 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) { @@ -125,7 +125,7 @@ function parseAIResponse(response: string): ParsedResponse { } } } - + // Also parse markdown code blocks with file paths const markdownFileRegex = /```(?:file )?path="([^"]+)"\n([\s\S]*?)```/g; while ((match = markdownFileRegex.exec(response)) !== null) { @@ -135,7 +135,7 @@ function parseAIResponse(response: string): ParsedResponse { path: filePath, content: content }); - + // Extract packages from file content const filePackages = extractPackagesFromCode(content); for (const pkg of filePackages) { @@ -145,7 +145,7 @@ function parseAIResponse(response: string): ParsedResponse { } } } - + // Parse plain text format like "Generated Files: Header.jsx, index.css" const generatedFilesMatch = response.match(/Generated Files?:\s*([^\n]+)/i); if (generatedFilesMatch) { @@ -155,7 +155,7 @@ function parseAIResponse(response: string): ParsedResponse { .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 @@ -171,7 +171,7 @@ function parseAIResponse(response: string): ParsedResponse { 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) { @@ -184,7 +184,7 @@ function parseAIResponse(response: string): ParsedResponse { } } } - + // 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) { @@ -194,14 +194,14 @@ function parseAIResponse(response: string): ParsedResponse { 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) { @@ -224,7 +224,7 @@ function parseAIResponse(response: string): ParsedResponse { 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); @@ -264,20 +264,20 @@ function parseAIResponse(response: string): ParsedResponse { 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); const morphEnabled = Boolean(isEdit && process.env.MORPH_API_KEY); @@ -296,15 +296,15 @@ export async function POST(request: NextRequest) { }); } console.log('[apply-ai-code-stream] Packages found:', parsed.packages); - + // Initialize existingFiles if not already if (!global.existingFiles) { global.existingFiles = new Set(); } - + // Try to get provider from sandbox manager first let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider(); - + // Fall back to global state if not found in manager if (!provider) { provider = global.activeSandboxProvider; @@ -313,10 +313,10 @@ export async function POST(request: NextRequest) { // If we have a sandboxId but no provider, try to get or create one if (!provider && sandboxId) { console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`); - + try { provider = await sandboxManager.getOrCreateProvider(sandboxId); - + // If we got a new provider (not reconnected), we need to create a new sandbox if (!provider.getSandboxInfo()) { console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`); @@ -324,7 +324,7 @@ export async function POST(request: NextRequest) { await provider.setupViteApp(); sandboxManager.registerSandbox(sandboxId, provider); } - + // Update legacy global state global.activeSandboxProvider = provider; console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`); @@ -346,7 +346,7 @@ export async function POST(request: NextRequest) { }, { status: 500 }); } } - + // If we still don't have a provider, create a new one if (!provider) { console.log(`[apply-ai-code-stream] No active provider found, creating new sandbox...`); @@ -358,7 +358,7 @@ export async function POST(request: NextRequest) { // Register with sandbox manager sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider); - + // Store in legacy global state global.activeSandboxProvider = provider; global.sandboxData = { @@ -385,18 +385,18 @@ export async function POST(request: NextRequest) { }, { status: 500 }); } } - + // 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 provider and request to the async function) (async (providerInstance, req) => { const results = { @@ -408,10 +408,10 @@ export async function POST(request: NextRequest) { commandsExecuted: [] as string[], errors: [] as string[] }; - + try { - await sendProgress({ - type: 'start', + await sendProgress({ + type: 'start', message: 'Starting code application...', totalSteps: 3 }); @@ -427,75 +427,75 @@ export async function POST(request: NextRequest) { // 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', + 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({ + body: JSON.stringify({ packages: uniquePackages, sandboxId: sandboxId || providerInstance.getSandboxInfo()?.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 (parseError) { - console.debug('Error parsing terminal output:', parseError); + console.debug('Error parsing terminal output:', parseError); } } } @@ -510,21 +510,21 @@ export async function POST(request: NextRequest) { results.errors.push(`Package installation failed: ${(error as Error).message}`); } } else { - await sendProgress({ - type: 'step', + 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', + 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']; let filteredFiles = filesArray.filter(file => { @@ -592,27 +592,27 @@ export async function POST(request: NextRequest) { 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() || '')) { + if (!normalizedPath.startsWith('src/') && + !normalizedPath.startsWith('public/') && + normalizedPath !== 'index.html' && + !configFiles.includes(normalizedPath.split('/').pop() || '')) { normalizedPath = 'src/' + 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, ''); } - + // Fix common Tailwind CSS errors in CSS files if (file.path.endsWith('.css')) { // Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist) @@ -621,7 +621,7 @@ export async function POST(request: NextRequest) { fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl'); fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl'); } - + // Create directory if needed const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : ''; if (dirPath) { @@ -630,7 +630,7 @@ export async function POST(request: NextRequest) { // Write the file using provider await providerInstance.writeFile(normalizedPath, fileContent); - + // Update file cache if (global.sandboxState?.fileCache) { global.sandboxState.fileCache.files[normalizedPath] = { @@ -638,14 +638,14 @@ export async function POST(request: NextRequest) { 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, @@ -662,16 +662,16 @@ export async function POST(request: NextRequest) { }); } } - + // Step 3: Execute commands const commandsArray = Array.isArray(parsed.commands) ? parsed.commands : []; if (commandsArray.length > 0) { - await sendProgress({ - type: 'step', + await sendProgress({ + type: 'step', step: 3, message: `Executing ${commandsArray.length} commands...` }); - + for (const [index, cmd] of commandsArray.entries()) { try { await sendProgress({ @@ -681,14 +681,14 @@ export async function POST(request: NextRequest) { command: cmd, action: 'executing' }); - + // Use provider runCommand const result = await providerInstance.runCommand(cmd); // Get command output from provider result const stdout = result.stdout; const stderr = result.stderr; - + if (stdout) { await sendProgress({ type: 'command-output', @@ -697,7 +697,7 @@ export async function POST(request: NextRequest) { stream: 'stdout' }); } - + if (stderr) { await sendProgress({ type: 'command-output', @@ -706,11 +706,11 @@ export async function POST(request: NextRequest) { stream: 'stderr' }); } - + if (results.commandsExecuted) { results.commandsExecuted.push(cmd); } - + await sendProgress({ type: 'command-complete', command: cmd, @@ -729,7 +729,7 @@ export async function POST(request: NextRequest) { } } } - + // Send final results await sendProgress({ type: 'complete', @@ -738,7 +738,7 @@ export async function POST(request: NextRequest) { 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; @@ -751,7 +751,7 @@ export async function POST(request: NextRequest) { }; } } - + // Track applied code in project evolution if (global.conversationState.context.projectEvolution) { global.conversationState.context.projectEvolution.majorChanges.push({ @@ -760,10 +760,10 @@ export async function POST(request: NextRequest) { filesAffected: results.filesCreated || [] }); } - + global.conversationState.lastUpdated = Date.now(); } - + } catch (error) { await sendProgress({ type: 'error', @@ -773,7 +773,7 @@ export async function POST(request: NextRequest) { await writer.close(); } })(provider, request); - + // Return the stream return new Response(stream.readable, { headers: {