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 }); } }