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(); 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(`[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>/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 = /