added fast apply
if isEdit === true & Morph API Key is in .env: - Morph Fast Apply is activated.
This commit is contained in:
@@ -30,28 +30,28 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
explanation: '',
|
explanation: '',
|
||||||
template: ''
|
template: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to extract packages from import statements
|
// Function to extract packages from import statements
|
||||||
function extractPackagesFromCode(content: string): string[] {
|
function extractPackagesFromCode(content: string): string[] {
|
||||||
const packages: string[] = [];
|
const packages: string[] = [];
|
||||||
// Match ES6 imports
|
// Match ES6 imports
|
||||||
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
|
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
|
||||||
let importMatch;
|
let importMatch;
|
||||||
|
|
||||||
while ((importMatch = importRegex.exec(content)) !== null) {
|
while ((importMatch = importRegex.exec(content)) !== null) {
|
||||||
const importPath = importMatch[1];
|
const importPath = importMatch[1];
|
||||||
// Skip relative imports and built-in React
|
// Skip relative imports and built-in React
|
||||||
if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
|
if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
|
||||||
importPath !== 'react' && importPath !== 'react-dom' &&
|
importPath !== 'react' && importPath !== 'react-dom' &&
|
||||||
!importPath.startsWith('@/')) {
|
!importPath.startsWith('@/')) {
|
||||||
// Extract package name (handle scoped packages like @heroicons/react)
|
// 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('/').slice(0, 2).join('/')
|
||||||
: importPath.split('/')[0];
|
: importPath.split('/')[0];
|
||||||
|
|
||||||
if (!packages.includes(packageName)) {
|
if (!packages.includes(packageName)) {
|
||||||
packages.push(packageName);
|
packages.push(packageName);
|
||||||
|
|
||||||
// Log important packages for debugging
|
// Log important packages for debugging
|
||||||
if (packageName === 'react-router-dom' || packageName.includes('router') || packageName.includes('icon')) {
|
if (packageName === 'react-router-dom' || packageName.includes('router') || packageName.includes('icon')) {
|
||||||
console.log(`[apply-ai-code-stream] Detected package from imports: ${packageName}`);
|
console.log(`[apply-ai-code-stream] Detected package from imports: ${packageName}`);
|
||||||
@@ -59,13 +59,13 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse file sections - handle duplicates and prefer complete versions
|
// Parse file sections - handle duplicates and prefer complete versions
|
||||||
const fileMap = new Map<string, { content: string; isComplete: boolean }>();
|
const fileMap = new Map<string, { content: string; isComplete: boolean }>();
|
||||||
|
|
||||||
// First pass: Find all file declarations
|
// First pass: Find all file declarations
|
||||||
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
|
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
|
||||||
let match;
|
let match;
|
||||||
@@ -73,10 +73,10 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
const filePath = match[1];
|
const filePath = match[1];
|
||||||
const content = match[2].trim();
|
const content = match[2].trim();
|
||||||
const hasClosingTag = response.substring(match.index, match.index + match[0].length).includes('</file>');
|
const hasClosingTag = response.substring(match.index, match.index + match[0].length).includes('</file>');
|
||||||
|
|
||||||
// Check if this file already exists in our map
|
// Check if this file already exists in our map
|
||||||
const existing = fileMap.get(filePath);
|
const existing = fileMap.get(filePath);
|
||||||
|
|
||||||
// Decide whether to keep this version
|
// Decide whether to keep this version
|
||||||
let shouldReplace = false;
|
let shouldReplace = false;
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -90,7 +90,7 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
} else if (!existing.isComplete && !hasClosingTag && content.length > existing.content.length) {
|
} else if (!existing.isComplete && !hasClosingTag && content.length > existing.content.length) {
|
||||||
shouldReplace = true; // Both incomplete, keep longer one
|
shouldReplace = true; // Both incomplete, keep longer one
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldReplace) {
|
if (shouldReplace) {
|
||||||
// Additional validation: reject obviously broken content
|
// Additional validation: reject obviously broken content
|
||||||
if (content.includes('...') && !content.includes('...props') && !content.includes('...rest')) {
|
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
|
// Convert map to array for sections.files
|
||||||
for (const [path, { content, isComplete }] of fileMap.entries()) {
|
for (const [path, { content, isComplete }] of fileMap.entries()) {
|
||||||
if (!isComplete) {
|
if (!isComplete) {
|
||||||
console.log(`[apply-ai-code-stream] Warning: File ${path} appears to be truncated (no closing tag)`);
|
console.log(`[apply-ai-code-stream] Warning: File ${path} appears to be truncated (no closing tag)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
sections.files.push({
|
sections.files.push({
|
||||||
path,
|
path,
|
||||||
content
|
content
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract packages from file content
|
// Extract packages from file content
|
||||||
const filePackages = extractPackagesFromCode(content);
|
const filePackages = extractPackagesFromCode(content);
|
||||||
for (const pkg of filePackages) {
|
for (const pkg of filePackages) {
|
||||||
@@ -125,7 +125,7 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also parse markdown code blocks with file paths
|
// Also parse markdown code blocks with file paths
|
||||||
const markdownFileRegex = /```(?:file )?path="([^"]+)"\n([\s\S]*?)```/g;
|
const markdownFileRegex = /```(?:file )?path="([^"]+)"\n([\s\S]*?)```/g;
|
||||||
while ((match = markdownFileRegex.exec(response)) !== null) {
|
while ((match = markdownFileRegex.exec(response)) !== null) {
|
||||||
@@ -135,7 +135,7 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
path: filePath,
|
path: filePath,
|
||||||
content: content
|
content: content
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract packages from file content
|
// Extract packages from file content
|
||||||
const filePackages = extractPackagesFromCode(content);
|
const filePackages = extractPackagesFromCode(content);
|
||||||
for (const pkg of filePackages) {
|
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"
|
// Parse plain text format like "Generated Files: Header.jsx, index.css"
|
||||||
const generatedFilesMatch = response.match(/Generated Files?:\s*([^\n]+)/i);
|
const generatedFilesMatch = response.match(/Generated Files?:\s*([^\n]+)/i);
|
||||||
if (generatedFilesMatch) {
|
if (generatedFilesMatch) {
|
||||||
@@ -155,7 +155,7 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
.map(f => f.trim())
|
.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'));
|
.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(', ')}`);
|
console.log(`[apply-ai-code-stream] Detected generated files from plain text: ${filesList.join(', ')}`);
|
||||||
|
|
||||||
// Try to extract the actual file content if it follows
|
// Try to extract the actual file content if it follows
|
||||||
for (const fileName of filesList) {
|
for (const fileName of filesList) {
|
||||||
// Look for the file content after the file name
|
// Look for the file content after the file name
|
||||||
@@ -171,7 +171,7 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
content: codeMatch[1].trim()
|
content: codeMatch[1].trim()
|
||||||
});
|
});
|
||||||
console.log(`[apply-ai-code-stream] Extracted content for ${filePath}`);
|
console.log(`[apply-ai-code-stream] Extracted content for ${filePath}`);
|
||||||
|
|
||||||
// Extract packages from this file
|
// Extract packages from this file
|
||||||
const filePackages = extractPackagesFromCode(codeMatch[1]);
|
const filePackages = extractPackagesFromCode(codeMatch[1]);
|
||||||
for (const pkg of filePackages) {
|
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
|
// Also try to parse if the response contains raw JSX/JS code blocks
|
||||||
const codeBlockRegex = /```(?:jsx?|tsx?|javascript|typescript)?\n([\s\S]*?)```/g;
|
const codeBlockRegex = /```(?:jsx?|tsx?|javascript|typescript)?\n([\s\S]*?)```/g;
|
||||||
while ((match = codeBlockRegex.exec(response)) !== null) {
|
while ((match = codeBlockRegex.exec(response)) !== null) {
|
||||||
@@ -194,14 +194,14 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
if (fileNameMatch) {
|
if (fileNameMatch) {
|
||||||
const fileName = fileNameMatch[1].trim();
|
const fileName = fileNameMatch[1].trim();
|
||||||
const filePath = fileName.includes('/') ? fileName : `src/components/${fileName}`;
|
const filePath = fileName.includes('/') ? fileName : `src/components/${fileName}`;
|
||||||
|
|
||||||
// Don't add duplicate files
|
// Don't add duplicate files
|
||||||
if (!sections.files.some(f => f.path === filePath)) {
|
if (!sections.files.some(f => f.path === filePath)) {
|
||||||
sections.files.push({
|
sections.files.push({
|
||||||
path: filePath,
|
path: filePath,
|
||||||
content: content
|
content: content
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract packages
|
// Extract packages
|
||||||
const filePackages = extractPackagesFromCode(content);
|
const filePackages = extractPackagesFromCode(content);
|
||||||
for (const pkg of filePackages) {
|
for (const pkg of filePackages) {
|
||||||
@@ -224,7 +224,7 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
while ((match = pkgRegex.exec(response)) !== null) {
|
while ((match = pkgRegex.exec(response)) !== null) {
|
||||||
sections.packages.push(match[1].trim());
|
sections.packages.push(match[1].trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also parse <packages> tag with multiple packages
|
// Also parse <packages> tag with multiple packages
|
||||||
const packagesRegex = /<packages>([\s\S]*?)<\/packages>/;
|
const packagesRegex = /<packages>([\s\S]*?)<\/packages>/;
|
||||||
const packagesMatch = response.match(packagesRegex);
|
const packagesMatch = response.match(packagesRegex);
|
||||||
@@ -264,20 +264,20 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { response, isEdit = false, packages = [], sandboxId } = await request.json();
|
const { response, isEdit = false, packages = [], sandboxId } = await request.json();
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
error: 'response is required'
|
error: 'response is required'
|
||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug log the response
|
// Debug log the response
|
||||||
console.log('[apply-ai-code-stream] Received response to parse:');
|
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 length:', response.length);
|
||||||
console.log('[apply-ai-code-stream] Response preview:', response.substring(0, 500));
|
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] isEdit:', isEdit);
|
||||||
console.log('[apply-ai-code-stream] packages:', packages);
|
console.log('[apply-ai-code-stream] packages:', packages);
|
||||||
|
|
||||||
// Parse the AI response
|
// Parse the AI response
|
||||||
const parsed = parseAIResponse(response);
|
const parsed = parseAIResponse(response);
|
||||||
const morphEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
|
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);
|
console.log('[apply-ai-code-stream] Packages found:', parsed.packages);
|
||||||
|
|
||||||
// Initialize existingFiles if not already
|
// Initialize existingFiles if not already
|
||||||
if (!global.existingFiles) {
|
if (!global.existingFiles) {
|
||||||
global.existingFiles = new Set<string>();
|
global.existingFiles = new Set<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get provider from sandbox manager first
|
// Try to get provider from sandbox manager first
|
||||||
let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
|
let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
|
||||||
|
|
||||||
// Fall back to global state if not found in manager
|
// Fall back to global state if not found in manager
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
provider = global.activeSandboxProvider;
|
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 we have a sandboxId but no provider, try to get or create one
|
||||||
if (!provider && sandboxId) {
|
if (!provider && sandboxId) {
|
||||||
console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`);
|
console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
provider = await sandboxManager.getOrCreateProvider(sandboxId);
|
provider = await sandboxManager.getOrCreateProvider(sandboxId);
|
||||||
|
|
||||||
// If we got a new provider (not reconnected), we need to create a new sandbox
|
// If we got a new provider (not reconnected), we need to create a new sandbox
|
||||||
if (!provider.getSandboxInfo()) {
|
if (!provider.getSandboxInfo()) {
|
||||||
console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`);
|
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();
|
await provider.setupViteApp();
|
||||||
sandboxManager.registerSandbox(sandboxId, provider);
|
sandboxManager.registerSandbox(sandboxId, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update legacy global state
|
// Update legacy global state
|
||||||
global.activeSandboxProvider = provider;
|
global.activeSandboxProvider = provider;
|
||||||
console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`);
|
console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`);
|
||||||
@@ -346,7 +346,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we still don't have a provider, create a new one
|
// If we still don't have a provider, create a new one
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
console.log(`[apply-ai-code-stream] No active provider found, creating new sandbox...`);
|
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
|
// Register with sandbox manager
|
||||||
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
|
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
|
||||||
|
|
||||||
// Store in legacy global state
|
// Store in legacy global state
|
||||||
global.activeSandboxProvider = provider;
|
global.activeSandboxProvider = provider;
|
||||||
global.sandboxData = {
|
global.sandboxData = {
|
||||||
@@ -385,18 +385,18 @@ export async function POST(request: NextRequest) {
|
|||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a response stream for real-time updates
|
// Create a response stream for real-time updates
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const stream = new TransformStream();
|
const stream = new TransformStream();
|
||||||
const writer = stream.writable.getWriter();
|
const writer = stream.writable.getWriter();
|
||||||
|
|
||||||
// Function to send progress updates
|
// Function to send progress updates
|
||||||
const sendProgress = async (data: any) => {
|
const sendProgress = async (data: any) => {
|
||||||
const message = `data: ${JSON.stringify(data)}\n\n`;
|
const message = `data: ${JSON.stringify(data)}\n\n`;
|
||||||
await writer.write(encoder.encode(message));
|
await writer.write(encoder.encode(message));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start processing in background (pass provider and request to the async function)
|
// Start processing in background (pass provider and request to the async function)
|
||||||
(async (providerInstance, req) => {
|
(async (providerInstance, req) => {
|
||||||
const results = {
|
const results = {
|
||||||
@@ -408,10 +408,10 @@ export async function POST(request: NextRequest) {
|
|||||||
commandsExecuted: [] as string[],
|
commandsExecuted: [] as string[],
|
||||||
errors: [] as string[]
|
errors: [] as string[]
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'start',
|
type: 'start',
|
||||||
message: 'Starting code application...',
|
message: 'Starting code application...',
|
||||||
totalSteps: 3
|
totalSteps: 3
|
||||||
});
|
});
|
||||||
@@ -427,75 +427,75 @@ export async function POST(request: NextRequest) {
|
|||||||
// Step 1: Install packages
|
// Step 1: Install packages
|
||||||
const packagesArray = Array.isArray(packages) ? packages : [];
|
const packagesArray = Array.isArray(packages) ? packages : [];
|
||||||
const parsedPackages = Array.isArray(parsed.packages) ? parsed.packages : [];
|
const parsedPackages = Array.isArray(parsed.packages) ? parsed.packages : [];
|
||||||
|
|
||||||
// Combine and deduplicate packages
|
// Combine and deduplicate packages
|
||||||
const allPackages = [...packagesArray.filter(pkg => pkg && typeof pkg === 'string'), ...parsedPackages];
|
const allPackages = [...packagesArray.filter(pkg => pkg && typeof pkg === 'string'), ...parsedPackages];
|
||||||
|
|
||||||
// Use Set to remove duplicates, then filter out pre-installed packages
|
// Use Set to remove duplicates, then filter out pre-installed packages
|
||||||
const uniquePackages = [...new Set(allPackages)]
|
const uniquePackages = [...new Set(allPackages)]
|
||||||
.filter(pkg => pkg && typeof pkg === 'string' && pkg.trim() !== '') // Remove empty strings
|
.filter(pkg => pkg && typeof pkg === 'string' && pkg.trim() !== '') // Remove empty strings
|
||||||
.filter(pkg => pkg !== 'react' && pkg !== 'react-dom'); // Filter pre-installed
|
.filter(pkg => pkg !== 'react' && pkg !== 'react-dom'); // Filter pre-installed
|
||||||
|
|
||||||
// Log if we found duplicates
|
// Log if we found duplicates
|
||||||
if (allPackages.length !== uniquePackages.length) {
|
if (allPackages.length !== uniquePackages.length) {
|
||||||
console.log(`[apply-ai-code-stream] Removed ${allPackages.length - uniquePackages.length} duplicate packages`);
|
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] Original packages:`, allPackages);
|
||||||
console.log(`[apply-ai-code-stream] Deduplicated packages:`, uniquePackages);
|
console.log(`[apply-ai-code-stream] Deduplicated packages:`, uniquePackages);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uniquePackages.length > 0) {
|
if (uniquePackages.length > 0) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'step',
|
type: 'step',
|
||||||
step: 1,
|
step: 1,
|
||||||
message: `Installing ${uniquePackages.length} packages...`,
|
message: `Installing ${uniquePackages.length} packages...`,
|
||||||
packages: uniquePackages
|
packages: uniquePackages
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use streaming package installation
|
// Use streaming package installation
|
||||||
try {
|
try {
|
||||||
// Construct the API URL properly for both dev and production
|
// Construct the API URL properly for both dev and production
|
||||||
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
const host = req.headers.get('host') || 'localhost:3000';
|
const host = req.headers.get('host') || 'localhost:3000';
|
||||||
const apiUrl = `${protocol}://${host}/api/install-packages`;
|
const apiUrl = `${protocol}://${host}/api/install-packages`;
|
||||||
|
|
||||||
const installResponse = await fetch(apiUrl, {
|
const installResponse = await fetch(apiUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
packages: uniquePackages,
|
packages: uniquePackages,
|
||||||
sandboxId: sandboxId || providerInstance.getSandboxInfo()?.sandboxId
|
sandboxId: sandboxId || providerInstance.getSandboxInfo()?.sandboxId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (installResponse.ok && installResponse.body) {
|
if (installResponse.ok && installResponse.body) {
|
||||||
const reader = installResponse.body.getReader();
|
const reader = installResponse.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
|
|
||||||
const chunk = decoder.decode(value);
|
const chunk = decoder.decode(value);
|
||||||
if (!chunk) continue;
|
if (!chunk) continue;
|
||||||
const lines = chunk.split('\n');
|
const lines = chunk.split('\n');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) {
|
if (line.startsWith('data: ')) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(line.slice(6));
|
const data = JSON.parse(line.slice(6));
|
||||||
|
|
||||||
// Forward package installation progress
|
// Forward package installation progress
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'package-progress',
|
type: 'package-progress',
|
||||||
...data
|
...data
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track results
|
// Track results
|
||||||
if (data.type === 'success' && data.installedPackages) {
|
if (data.type === 'success' && data.installedPackages) {
|
||||||
results.packagesInstalled = data.installedPackages;
|
results.packagesInstalled = data.installedPackages;
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} 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}`);
|
results.errors.push(`Package installation failed: ${(error as Error).message}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'step',
|
type: 'step',
|
||||||
step: 1,
|
step: 1,
|
||||||
message: 'No additional packages to install, skipping...'
|
message: 'No additional packages to install, skipping...'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Create/update files
|
// Step 2: Create/update files
|
||||||
const filesArray = Array.isArray(parsed.files) ? parsed.files : [];
|
const filesArray = Array.isArray(parsed.files) ? parsed.files : [];
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'step',
|
type: 'step',
|
||||||
step: 2,
|
step: 2,
|
||||||
message: `Creating ${filesArray.length} files...`
|
message: `Creating ${filesArray.length} files...`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter out config files that shouldn't be created
|
// 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 configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js'];
|
||||||
let filteredFiles = filesArray.filter(file => {
|
let filteredFiles = filesArray.filter(file => {
|
||||||
@@ -592,27 +592,27 @@ export async function POST(request: NextRequest) {
|
|||||||
fileName: file.path,
|
fileName: file.path,
|
||||||
action: 'creating'
|
action: 'creating'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Normalize the file path
|
// Normalize the file path
|
||||||
let normalizedPath = file.path;
|
let normalizedPath = file.path;
|
||||||
if (normalizedPath.startsWith('/')) {
|
if (normalizedPath.startsWith('/')) {
|
||||||
normalizedPath = normalizedPath.substring(1);
|
normalizedPath = normalizedPath.substring(1);
|
||||||
}
|
}
|
||||||
if (!normalizedPath.startsWith('src/') &&
|
if (!normalizedPath.startsWith('src/') &&
|
||||||
!normalizedPath.startsWith('public/') &&
|
!normalizedPath.startsWith('public/') &&
|
||||||
normalizedPath !== 'index.html' &&
|
normalizedPath !== 'index.html' &&
|
||||||
!configFiles.includes(normalizedPath.split('/').pop() || '')) {
|
!configFiles.includes(normalizedPath.split('/').pop() || '')) {
|
||||||
normalizedPath = 'src/' + normalizedPath;
|
normalizedPath = 'src/' + normalizedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUpdate = global.existingFiles.has(normalizedPath);
|
const isUpdate = global.existingFiles.has(normalizedPath);
|
||||||
|
|
||||||
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
|
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
|
||||||
let fileContent = file.content;
|
let fileContent = file.content;
|
||||||
if (file.path.endsWith('.jsx') || file.path.endsWith('.js') || file.path.endsWith('.tsx') || file.path.endsWith('.ts')) {
|
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, '');
|
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix common Tailwind CSS errors in CSS files
|
// Fix common Tailwind CSS errors in CSS files
|
||||||
if (file.path.endsWith('.css')) {
|
if (file.path.endsWith('.css')) {
|
||||||
// Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
|
// 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-4xl/g, 'shadow-2xl');
|
||||||
fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
|
fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create directory if needed
|
// Create directory if needed
|
||||||
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
|
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
|
||||||
if (dirPath) {
|
if (dirPath) {
|
||||||
@@ -630,7 +630,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
// Write the file using provider
|
// Write the file using provider
|
||||||
await providerInstance.writeFile(normalizedPath, fileContent);
|
await providerInstance.writeFile(normalizedPath, fileContent);
|
||||||
|
|
||||||
// Update file cache
|
// Update file cache
|
||||||
if (global.sandboxState?.fileCache) {
|
if (global.sandboxState?.fileCache) {
|
||||||
global.sandboxState.fileCache.files[normalizedPath] = {
|
global.sandboxState.fileCache.files[normalizedPath] = {
|
||||||
@@ -638,14 +638,14 @@ export async function POST(request: NextRequest) {
|
|||||||
lastModified: Date.now()
|
lastModified: Date.now()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUpdate) {
|
if (isUpdate) {
|
||||||
if (results.filesUpdated) results.filesUpdated.push(normalizedPath);
|
if (results.filesUpdated) results.filesUpdated.push(normalizedPath);
|
||||||
} else {
|
} else {
|
||||||
if (results.filesCreated) results.filesCreated.push(normalizedPath);
|
if (results.filesCreated) results.filesCreated.push(normalizedPath);
|
||||||
if (global.existingFiles) global.existingFiles.add(normalizedPath);
|
if (global.existingFiles) global.existingFiles.add(normalizedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'file-complete',
|
type: 'file-complete',
|
||||||
fileName: normalizedPath,
|
fileName: normalizedPath,
|
||||||
@@ -662,16 +662,16 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Execute commands
|
// Step 3: Execute commands
|
||||||
const commandsArray = Array.isArray(parsed.commands) ? parsed.commands : [];
|
const commandsArray = Array.isArray(parsed.commands) ? parsed.commands : [];
|
||||||
if (commandsArray.length > 0) {
|
if (commandsArray.length > 0) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'step',
|
type: 'step',
|
||||||
step: 3,
|
step: 3,
|
||||||
message: `Executing ${commandsArray.length} commands...`
|
message: `Executing ${commandsArray.length} commands...`
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [index, cmd] of commandsArray.entries()) {
|
for (const [index, cmd] of commandsArray.entries()) {
|
||||||
try {
|
try {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
@@ -681,14 +681,14 @@ export async function POST(request: NextRequest) {
|
|||||||
command: cmd,
|
command: cmd,
|
||||||
action: 'executing'
|
action: 'executing'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use provider runCommand
|
// Use provider runCommand
|
||||||
const result = await providerInstance.runCommand(cmd);
|
const result = await providerInstance.runCommand(cmd);
|
||||||
|
|
||||||
// Get command output from provider result
|
// Get command output from provider result
|
||||||
const stdout = result.stdout;
|
const stdout = result.stdout;
|
||||||
const stderr = result.stderr;
|
const stderr = result.stderr;
|
||||||
|
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'command-output',
|
type: 'command-output',
|
||||||
@@ -697,7 +697,7 @@ export async function POST(request: NextRequest) {
|
|||||||
stream: 'stdout'
|
stream: 'stdout'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'command-output',
|
type: 'command-output',
|
||||||
@@ -706,11 +706,11 @@ export async function POST(request: NextRequest) {
|
|||||||
stream: 'stderr'
|
stream: 'stderr'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.commandsExecuted) {
|
if (results.commandsExecuted) {
|
||||||
results.commandsExecuted.push(cmd);
|
results.commandsExecuted.push(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'command-complete',
|
type: 'command-complete',
|
||||||
command: cmd,
|
command: cmd,
|
||||||
@@ -729,7 +729,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send final results
|
// Send final results
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'complete',
|
type: 'complete',
|
||||||
@@ -738,7 +738,7 @@ export async function POST(request: NextRequest) {
|
|||||||
structure: parsed.structure,
|
structure: parsed.structure,
|
||||||
message: `Successfully applied ${results.filesCreated.length} files`
|
message: `Successfully applied ${results.filesCreated.length} files`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track applied files in conversation state
|
// Track applied files in conversation state
|
||||||
if (global.conversationState && results.filesCreated.length > 0) {
|
if (global.conversationState && results.filesCreated.length > 0) {
|
||||||
const messages = global.conversationState.context.messages;
|
const messages = global.conversationState.context.messages;
|
||||||
@@ -751,7 +751,7 @@ export async function POST(request: NextRequest) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track applied code in project evolution
|
// Track applied code in project evolution
|
||||||
if (global.conversationState.context.projectEvolution) {
|
if (global.conversationState.context.projectEvolution) {
|
||||||
global.conversationState.context.projectEvolution.majorChanges.push({
|
global.conversationState.context.projectEvolution.majorChanges.push({
|
||||||
@@ -760,10 +760,10 @@ export async function POST(request: NextRequest) {
|
|||||||
filesAffected: results.filesCreated || []
|
filesAffected: results.filesCreated || []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
global.conversationState.lastUpdated = Date.now();
|
global.conversationState.lastUpdated = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -773,7 +773,7 @@ export async function POST(request: NextRequest) {
|
|||||||
await writer.close();
|
await writer.close();
|
||||||
}
|
}
|
||||||
})(provider, request);
|
})(provider, request);
|
||||||
|
|
||||||
// Return the stream
|
// Return the stream
|
||||||
return new Response(stream.readable, {
|
return new Response(stream.readable, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
Reference in New Issue
Block a user