added fast apply
if isEdit === true & Morph API Key is in .env: - Morph Fast Apply is activated.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { parseMorphEdits, applyMorphEditToFile } from '@/lib/morph-fast-apply';
|
||||
import { Sandbox } from '@e2b/code-interpreter';
|
||||
import type { SandboxState } from '@/types/sandbox';
|
||||
import type { ConversationState } from '@/types/conversation';
|
||||
@@ -278,6 +279,12 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Parse the AI response
|
||||
const parsed = parseAIResponse(response);
|
||||
const morphEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
|
||||
const morphEdits = morphEnabled ? parseMorphEdits(response) : [];
|
||||
console.log('[apply-ai-code-stream] Morph Fast Apply mode:', morphEnabled);
|
||||
if (morphEnabled) {
|
||||
console.log('[apply-ai-code-stream] Morph edits found:', morphEdits.length);
|
||||
}
|
||||
|
||||
// Log what was parsed
|
||||
console.log('[apply-ai-code-stream] Parsed result:');
|
||||
@@ -392,6 +399,14 @@ export async function POST(request: NextRequest) {
|
||||
message: 'Starting code application...',
|
||||
totalSteps: 3
|
||||
});
|
||||
if (morphEnabled) {
|
||||
await sendProgress({ type: 'info', message: 'Morph Fast Apply enabled' });
|
||||
await sendProgress({ type: 'info', message: `Parsed ${morphEdits.length} Morph edits` });
|
||||
if (morphEdits.length === 0) {
|
||||
console.warn('[apply-ai-code-stream] Morph enabled but no <edit> blocks found; falling back to full-file flow');
|
||||
await sendProgress({ type: 'warning', message: 'Morph enabled but no <edit> blocks found; falling back to full-file flow' });
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Install packages
|
||||
const packagesArray = Array.isArray(packages) ? packages : [];
|
||||
@@ -463,7 +478,7 @@ export async function POST(request: NextRequest) {
|
||||
if (data.type === 'success' && data.installedPackages) {
|
||||
results.packagesInstalled = data.installedPackages;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
@@ -496,11 +511,60 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// 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 => {
|
||||
let filteredFiles = filesArray.filter(file => {
|
||||
if (!file || typeof file !== 'object') return false;
|
||||
const fileName = (file.path || '').split('/').pop() || '';
|
||||
return !configFiles.includes(fileName);
|
||||
});
|
||||
|
||||
// If Morph is enabled and we have edits, apply them before file writes
|
||||
const morphUpdatedPaths = new Set<string>();
|
||||
if (morphEnabled && morphEdits.length > 0 && sandboxInstance) {
|
||||
await sendProgress({ type: 'info', message: `Applying ${morphEdits.length} fast edits via Morph...` });
|
||||
for (const [idx, edit] of morphEdits.entries()) {
|
||||
try {
|
||||
await sendProgress({ type: 'file-progress', current: idx + 1, total: morphEdits.length, fileName: edit.targetFile, action: 'morph-applying' });
|
||||
const result = await applyMorphEditToFile({
|
||||
sandbox: sandboxInstance,
|
||||
targetPath: edit.targetFile,
|
||||
instructions: edit.instructions,
|
||||
updateSnippet: edit.update
|
||||
});
|
||||
if (result.success && result.normalizedPath) {
|
||||
console.log('[apply-ai-code-stream] Morph updated', result.normalizedPath);
|
||||
morphUpdatedPaths.add(result.normalizedPath);
|
||||
if (results.filesUpdated) results.filesUpdated.push(result.normalizedPath);
|
||||
await sendProgress({ type: 'file-complete', fileName: result.normalizedPath, action: 'morph-updated' });
|
||||
} else {
|
||||
const msg = result.error || 'Unknown Morph error';
|
||||
console.error('[apply-ai-code-stream] Morph apply failed for', edit.targetFile, msg);
|
||||
if (results.errors) results.errors.push(`Morph apply failed for ${edit.targetFile}: ${msg}`);
|
||||
await sendProgress({ type: 'file-error', fileName: edit.targetFile, error: msg });
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message;
|
||||
console.error('[apply-ai-code-stream] Morph apply exception for', edit.targetFile, msg);
|
||||
if (results.errors) results.errors.push(`Morph apply exception for ${edit.targetFile}: ${msg}`);
|
||||
await sendProgress({ type: 'file-error', fileName: edit.targetFile, error: msg });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid overwriting Morph-updated files in the file write loop
|
||||
if (morphUpdatedPaths.size > 0) {
|
||||
filteredFiles = filteredFiles.filter(file => {
|
||||
if (!file?.path) return true;
|
||||
let normalizedPath = file.path.startsWith('/') ? file.path.slice(1) : file.path;
|
||||
const fileName = normalizedPath.split('/').pop() || '';
|
||||
if (!normalizedPath.startsWith('src/') &&
|
||||
!normalizedPath.startsWith('public/') &&
|
||||
normalizedPath !== 'index.html' &&
|
||||
!configFiles.includes(fileName)) {
|
||||
normalizedPath = 'src/' + normalizedPath;
|
||||
}
|
||||
return !morphUpdatedPaths.has(normalizedPath);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [index, file] of filteredFiles.entries()) {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { parseMorphEdits, applyMorphEditToFile } from '@/lib/morph-fast-apply';
|
||||
import type { SandboxState } from '@/types/sandbox';
|
||||
import type { ConversationState } from '@/types/conversation';
|
||||
|
||||
@@ -144,6 +145,12 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Parse the AI response
|
||||
const parsed = parseAIResponse(response);
|
||||
const morphEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
|
||||
const morphEdits = morphEnabled ? parseMorphEdits(response) : [];
|
||||
console.log('[apply-ai-code] Morph Fast Apply mode:', morphEnabled);
|
||||
if (morphEnabled) {
|
||||
console.log('[apply-ai-code] Morph edits found:', morphEdits.length);
|
||||
}
|
||||
|
||||
// Initialize existingFiles if not already
|
||||
if (!global.existingFiles) {
|
||||
@@ -172,6 +179,14 @@ export async function POST(request: NextRequest) {
|
||||
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));
|
||||
if (morphEnabled) {
|
||||
console.log('[apply-ai-code] Morph Fast Apply enabled');
|
||||
if (morphEdits.length > 0) {
|
||||
console.log('[apply-ai-code] Parsed Morph edits:', morphEdits.map(e => e.targetFile));
|
||||
} else {
|
||||
console.log('[apply-ai-code] No <edit> blocks found in response');
|
||||
}
|
||||
}
|
||||
|
||||
const results = {
|
||||
filesCreated: [] as string[],
|
||||
@@ -296,9 +311,46 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt Morph Fast Apply for edits before file creation
|
||||
const morphUpdatedPaths = new Set<string>();
|
||||
|
||||
if (morphEnabled && morphEdits.length > 0) {
|
||||
if (!global.activeSandbox) {
|
||||
console.warn('[apply-ai-code] Morph edits found but no active sandbox; skipping Morph application');
|
||||
} else {
|
||||
console.log(`[apply-ai-code] Applying ${morphEdits.length} fast edits via Morph...`);
|
||||
for (const edit of morphEdits) {
|
||||
try {
|
||||
const result = await applyMorphEditToFile({
|
||||
sandbox: global.activeSandbox,
|
||||
targetPath: edit.targetFile,
|
||||
instructions: edit.instructions,
|
||||
updateSnippet: edit.update
|
||||
});
|
||||
|
||||
if (result.success && result.normalizedPath) {
|
||||
morphUpdatedPaths.add(result.normalizedPath);
|
||||
results.filesUpdated.push(result.normalizedPath);
|
||||
console.log('[apply-ai-code] Morph applied to', result.normalizedPath);
|
||||
} else {
|
||||
const msg = result.error || 'Unknown Morph error';
|
||||
console.error('[apply-ai-code] Morph apply failed:', msg);
|
||||
results.errors.push(`Morph apply failed for ${edit.targetFile}: ${msg}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[apply-ai-code] Morph apply exception:', e);
|
||||
results.errors.push(`Morph apply exception for ${edit.targetFile}: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (morphEnabled && morphEdits.length === 0) {
|
||||
console.warn('[apply-ai-code] Morph enabled but no <edit> blocks found; falling back to full-file flow');
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
let 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`);
|
||||
@@ -306,6 +358,21 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Avoid overwriting files already updated by Morph
|
||||
if (morphUpdatedPaths.size > 0) {
|
||||
filteredFiles = filteredFiles.filter(file => {
|
||||
let normalizedPath = file.path.startsWith('/') ? file.path.slice(1) : file.path;
|
||||
const fileName = normalizedPath.split('/').pop() || '';
|
||||
if (!normalizedPath.startsWith('src/') &&
|
||||
!normalizedPath.startsWith('public/') &&
|
||||
normalizedPath !== 'index.html' &&
|
||||
!configFiles.includes(fileName)) {
|
||||
normalizedPath = 'src/' + normalizedPath;
|
||||
}
|
||||
return !morphUpdatedPaths.has(normalizedPath);
|
||||
});
|
||||
}
|
||||
|
||||
// Create or update files AFTER package installation
|
||||
for (const file of filteredFiles) {
|
||||
@@ -354,7 +421,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
} catch (writeError) {
|
||||
console.error(`[apply-ai-code] E2B file write error:`, writeError);
|
||||
throw writeError;
|
||||
throw writeError as Error;
|
||||
}
|
||||
|
||||
|
||||
@@ -491,7 +558,7 @@ with open(file_path, 'w') as f:
|
||||
print(f"Auto-generated: {file_path}")
|
||||
`);
|
||||
results.filesCreated.push('src/index.css (with Tailwind)');
|
||||
} catch (error) {
|
||||
} catch {
|
||||
results.errors.push('Failed to create index.css with Tailwind');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,7 +551,7 @@ Remember: You are a SURGEON making a precise incision, not an artist repainting
|
||||
}
|
||||
|
||||
// Build system prompt with conversation awareness
|
||||
const systemPrompt = `You are an expert React developer with perfect memory of the conversation. You maintain context across messages and remember scraped websites, generated components, and applied code. Generate clean, modern React code for Vite applications.
|
||||
let systemPrompt = `You are an expert React developer with perfect memory of the conversation. You maintain context across messages and remember scraped websites, generated components, and applied code. Generate clean, modern React code for Vite applications.
|
||||
${conversationContext}
|
||||
|
||||
🚨 CRITICAL RULES - YOUR MOST IMPORTANT INSTRUCTIONS:
|
||||
@@ -897,6 +897,24 @@ CRITICAL: When files are provided in the context:
|
||||
4. Do NOT ask to see files - they are already provided in the context above
|
||||
5. Make the requested change immediately`;
|
||||
|
||||
// If Morph Fast Apply is enabled (edit mode + MORPH_API_KEY), force <edit> block output
|
||||
const morphFastApplyEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
|
||||
if (morphFastApplyEnabled) {
|
||||
systemPrompt += `
|
||||
|
||||
MORPH FAST APPLY MODE (EDIT-ONLY):
|
||||
- Output edits as <edit> blocks, not full <file> blocks, for files that already exist.
|
||||
- Format for each edit:
|
||||
<edit target_file="src/components/Header.jsx">
|
||||
<instructions>Describe the minimal change, single sentence.</instructions>
|
||||
<update>Provide the SMALLEST code snippet necessary to perform the change.</update>
|
||||
</edit>
|
||||
- Only use <file> blocks when you must CREATE a brand-new file.
|
||||
- Prefer ONE edit block for a simple change; multiple edits only if absolutely needed for separate files.
|
||||
- Keep updates minimal and precise; do not rewrite entire files.
|
||||
`;
|
||||
}
|
||||
|
||||
// Build full prompt with context
|
||||
let fullPrompt = prompt;
|
||||
if (context) {
|
||||
@@ -1140,6 +1158,17 @@ CRITICAL: When files are provided in the context:
|
||||
}
|
||||
|
||||
if (contextParts.length > 0) {
|
||||
if (morphFastApplyEnabled) {
|
||||
contextParts.push('\nOUTPUT FORMAT (REQUIRED IN MORPH MODE):');
|
||||
contextParts.push('<edit target_file="src/components/Component.jsx">');
|
||||
contextParts.push('<instructions>Minimal, precise instruction.</instructions>');
|
||||
contextParts.push('<update>// Smallest necessary snippet</update>');
|
||||
contextParts.push('</edit>');
|
||||
contextParts.push('\nIf you need to create a NEW file, then and only then output a full file:');
|
||||
contextParts.push('<file path="src/components/NewComponent.jsx">');
|
||||
contextParts.push('// Full file content when creating new files');
|
||||
contextParts.push('</file>');
|
||||
}
|
||||
fullPrompt = `CONTEXT:\n${contextParts.join('\n')}\n\nUSER REQUEST:\n${prompt}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user