Leon4gr45's picture
Refactor: Consolidate AI providers to Blablador (1/5) (#6)
e2a4790 verified
raw
history blame
18.1 kB
import { NextRequest, NextResponse } from 'next/server';
import { streamText } from 'ai';
import type { SandboxState } from '@/types/sandbox';
import { selectFilesForEdit, getFileContents, formatFilesForAI } from '@/lib/context-selector';
import { executeSearchPlan, formatSearchResultsForAI, selectTargetFile } from '@/lib/file-search-executor';
import { FileManifest } from '@/types/file-manifest';
import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation';
import { appConfig } from '@/config/app.config';
import getProviderForModel from '@/lib/ai/provider-manager';
// Force dynamic route to enable streaming
export const dynamic = 'force-dynamic';
// Helper function to analyze user preferences from conversation history
function analyzeUserPreferences(messages: ConversationMessage[]): {
commonPatterns: string[];
preferredEditStyle: 'targeted' | 'comprehensive';
} {
const userMessages = messages.filter(m => m.role === 'user');
const patterns: string[] = [];
let targetedEditCount = 0;
let comprehensiveEditCount = 0;
userMessages.forEach(msg => {
const content = msg.content.toLowerCase();
if (content.match(/\b(update|change|fix|modify|edit|remove|delete)\s+(\w+\s+)?(\w+)\b/)) {
targetedEditCount++;
}
if (content.match(/\b(rebuild|recreate|redesign|overhaul|refactor)\b/)) {
comprehensiveEditCount++;
}
if (content.includes('hero')) patterns.push('hero section edits');
if (content.includes('header')) patterns.push('header modifications');
if (content.includes('color') || content.includes('style')) patterns.push('styling changes');
if (content.includes('button')) patterns.push('button updates');
if (content.includes('animation')) patterns.push('animation requests');
});
return {
commonPatterns: [...new Set(patterns)].slice(0, 3), // Top 3 unique patterns
preferredEditStyle: targetedEditCount > comprehensiveEditCount ? 'targeted' : 'comprehensive'
};
}
declare global {
var sandboxState: SandboxState;
var conversationState: ConversationState | null;
}
export async function POST(request: NextRequest) {
try {
const { prompt, context, isEdit = false } = await request.json();
console.log('[generate-ai-code-stream] Received request:');
console.log('[generate-ai-code-stream] - prompt:', prompt);
console.log('[generate-ai-code-stream] - isEdit:', isEdit);
console.log('[generate-ai-code-stream] - context.sandboxId:', context?.sandboxId);
console.log('[generate-ai-code-stream] - context.currentFiles:', context?.currentFiles ? Object.keys(context.currentFiles) : 'none');
console.log('[generate-ai-code-stream] - currentFiles count:', context?.currentFiles ? Object.keys(context.currentFiles).length : 0);
if (!global.conversationState) {
global.conversationState = {
conversationId: `conv-${Date.now()}`,
startedAt: Date.now(),
lastUpdated: Date.now(),
context: {
messages: [],
edits: [],
projectEvolution: { majorChanges: [] },
userPreferences: {}
}
};
}
const userMessage: ConversationMessage = {
id: `msg-${Date.now()}`,
role: 'user',
content: prompt,
timestamp: Date.now(),
metadata: {
sandboxId: context?.sandboxId
}
};
global.conversationState.context.messages.push(userMessage);
if (global.conversationState.context.messages.length > 20) {
global.conversationState.context.messages = global.conversationState.context.messages.slice(-15);
console.log('[generate-ai-code-stream] Trimmed conversation history to prevent context overflow');
}
if (global.conversationState.context.edits.length > 10) {
global.conversationState.context.edits = global.conversationState.context.edits.slice(-8);
}
if (context?.currentFiles && Object.keys(context.currentFiles).length > 0) {
const firstFile = Object.entries(context.currentFiles)[0];
console.log('[generate-ai-code-stream] - sample file:', firstFile[0]);
console.log('[generate-ai-code-stream] - sample content preview:',
typeof firstFile[1] === 'string' ? firstFile[1].substring(0, 100) + '...' : 'not a string');
}
if (!prompt) {
return NextResponse.json({
success: false,
error: 'Prompt is required'
}, { status: 400 });
}
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
const sendProgress = async (data: any) => {
const message = `data: ${JSON.stringify(data)}
`;
try {
await writer.write(encoder.encode(message));
if (data.type === 'stream' || data.type === 'conversation') {
await writer.write(encoder.encode(': keepalive\n\n'));
}
} catch (error) {
console.error('[generate-ai-code-stream] Error writing to stream:', error);
}
};
(async () => {
try {
await sendProgress({ type: 'status', message: 'Initializing AI...' });
let editContext = null;
let enhancedSystemPrompt = '';
if (isEdit) {
console.log('[generate-ai-code-stream] Edit mode detected - starting agentic search workflow');
const manifest: FileManifest | undefined = global.sandboxState?.fileCache?.manifest;
if (manifest) {
await sendProgress({ type: 'status', message: '🔍 Creating search plan...' });
const fileContents = global.sandboxState.fileCache?.files || {};
console.log('[generate-ai-code-stream] Files available for search:', Object.keys(fileContents).length);
try {
const intentResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/analyze-edit-intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, manifest })
});
if (intentResponse.ok) {
const { searchPlan } = await intentResponse.json();
console.log('[generate-ai-code-stream] Search plan received:', searchPlan);
await sendProgress({
type: 'status',
message: `🔎 Searching for: "${searchPlan.searchTerms.join('", "')}"`
});
const searchExecution = executeSearchPlan(searchPlan,
Object.fromEntries(
Object.entries(fileContents).map(([path, data]) => [
path.startsWith('/') ? path : `/home/user/app/${path}`,
data.content
])
)
);
console.log('[generate-ai-code-stream] Search execution:', {
success: searchExecution.success,
resultsCount: searchExecution.results.length,
filesSearched: searchExecution.filesSearched,
time: searchExecution.executionTime + 'ms'
});
if (searchExecution.success && searchExecution.results.length > 0) {
const target = selectTargetFile(searchExecution.results, searchPlan.editType);
if (target) {
await sendProgress({
type: 'status',
message: `✅ Found code in ${target.filePath.split('/').pop()} at line ${target.lineNumber}`
});
console.log('[generate-ai-code-stream] Target selected:', target);
enhancedSystemPrompt = `
${formatSearchResultsForAI(searchExecution.results)}
SURGICAL EDIT INSTRUCTIONS:
You have been given the EXACT location of the code to edit.
- File: ${target.filePath}
- Line: ${target.lineNumber}
- Reason: ${target.reason}
Make ONLY the change requested by the user. Do not modify any other code.
User request: "${prompt}"`;
editContext = {
primaryFiles: [target.filePath],
contextFiles: [],
systemPrompt: enhancedSystemPrompt,
editIntent: {
type: searchPlan.editType,
description: searchPlan.reasoning,
targetFiles: [target.filePath],
confidence: 0.95,
searchTerms: searchPlan.searchTerms
}
};
console.log('[generate-ai-code-stream] Surgical edit context created');
}
} else {
console.warn('[generate-ai-code-stream] Search found no results, falling back to broader context');
await sendProgress({
type: 'status',
message: '⚠️ Could not find exact match, using broader search...'
});
}
} else {
console.error('[generate-ai-code-stream] Failed to get search plan');
}
} catch (error) {
console.error('[generate-ai-code-stream] Error in agentic search workflow:', error);
await sendProgress({
type: 'status',
message: '⚠️ Search workflow error, falling back to keyword method...'
});
if (manifest) {
editContext = selectFilesForEdit(prompt, manifest);
}
}
} else {
console.warn('[generate-ai-code-stream] AI intent analysis failed, falling back to keyword method');
if (manifest) {
editContext = selectFilesForEdit(prompt, manifest);
} else {
console.log('[generate-ai-code-stream] No manifest available for fallback');
await sendProgress({
type: 'status',
message: '⚠️ No file manifest available, will use broad context'
});
}
}
if (editContext) {
enhancedSystemPrompt = editContext.systemPrompt;
await sendProgress({
type: 'status',
message: `Identified edit type: ${editContext.editIntent?.description || 'Code modification'}`
});
} else if (!manifest) {
console.log('[generate-ai-code-stream] WARNING: No manifest available for edit mode!');
if (global.activeSandbox) {
await sendProgress({ type: 'status', message: 'Fetching current files from sandbox...' });
try {
const filesResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/get-sandbox-files`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (filesResponse.ok) {
const filesData = await filesResponse.json();
if (filesData.success && filesData.manifest) {
console.log('[generate-ai-code-stream] Successfully fetched manifest from sandbox');
const manifest = filesData.manifest;
try {
const intentResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/analyze-edit-intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, manifest })
});
if (intentResponse.ok) {
const { searchPlan } = await intentResponse.json();
console.log('[generate-ai-code-stream] Search plan received (after fetch):', searchPlan);
let targetFiles: any[] = [];
if (!searchPlan || searchPlan.searchTerms.length === 0) {
console.warn('[generate-ai-code-stream] No target files after fetch, searching for relevant files');
const promptLower = prompt.toLowerCase();
const allFilePaths = Object.keys(manifest.files);
if (promptLower.includes('hero')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('hero'));
} else if (promptLower.includes('header')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('header'));
} else if (promptLower.includes('footer')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('footer'));
} else if (promptLower.includes('nav')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('nav'));
} else if (promptLower.includes('button')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('button'));
}
if (targetFiles.length > 0) {
console.log('[generate-ai-code-stream] Found target files by keyword search after fetch:', targetFiles);
}
}
const allFiles = Object.keys(manifest.files)
.filter(path => !targetFiles.includes(path));
editContext = {
primaryFiles: targetFiles,
contextFiles: allFiles,
systemPrompt: `...`, // Omitting for brevity
editIntent: {
type: searchPlan?.editType || 'UPDATE_COMPONENT',
targetFiles: targetFiles,
confidence: searchPlan ? 0.85 : 0.6,
description: searchPlan?.reasoning || 'Keyword-based file selection',
suggestedContext: []
}
};
enhancedSystemPrompt = editContext.systemPrompt;
await sendProgress({
type: 'status',
message: `Identified edit type: ${editContext.editIntent.description}`
});
}
} catch (error) {
console.error('[generate-ai-code-stream] Error analyzing intent after fetch:', error);
}
} else {
console.error('[generate-ai-code-stream] Failed to get manifest from sandbox files');
}
} else {
console.error('[generate-ai-code-stream] Failed to fetch sandbox files:', filesResponse.status);
}
} catch (error) {
console.error('[generate-ai-code-stream] Error fetching sandbox files:', error);
await sendProgress({
type: 'warning',
message: 'Could not analyze existing files for targeted edits. Proceeding with general edit mode.'
});
}
} else {
console.log('[generate-ai-code-stream] No active sandbox to fetch files from');
await sendProgress({
type: 'warning',
message: 'No existing files found. Consider generating initial code first.'
});
}
}
}
let conversationContext = '';
if (global.conversationState && global.conversationState.context.messages.length > 1) {
// Omitting for brevity
}
let systemPrompt = `...`; // Omitting for brevity
const morphFastApplyEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
if (morphFastApplyEnabled) {
// Omitting for brevity
}
let fullPrompt = prompt;
if (context) {
// Omitting for brevity
}
await sendProgress({ type: 'status', message: 'Planning application structure...' });
console.log('\n[generate-ai-code-stream] Starting streaming response...\n');
const { client, actualModel } = getProviderForModel('code');
console.log(`[generate-ai-code-stream] Using provider: blablador, model: ${actualModel}`);
const streamOptions: any = {
model: client(actualModel),
messages: [
{
role: 'system',
content: systemPrompt + `...` // Omitting for brevity
},
{
role: 'user',
content: fullPrompt + `...` // Omitting for brevity
}
],
maxTokens: 8192,
stopSequences: []
};
// ... rest of the streaming logic
} catch (error) {
console.error('[generate-ai-code-stream] Stream processing error:', error);
await sendProgress({
type: 'error',
error: (error as Error).message
});
} finally {
await writer.close();
}
})();
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Transfer-Encoding': 'chunked',
'Content-Encoding': 'none',
'X-Accel-Buffering': 'no',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
} catch (error) {
console.error('[generate-ai-code-stream] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}