Spaces:
Build error
Build error
| 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 }); | |
| } | |
| } | |