'use client'; import { useState, useEffect, useRef, Suspense } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import Image from 'next/image'; import { appConfig } from '@/config/app.config'; import HeroInput from '@/components/HeroInput'; import SidebarInput from '@/components/app/generation/SidebarInput'; import HeaderBrandKit from '@/components/shared/header/BrandKit/BrandKit'; import { HeaderProvider } from '@/components/shared/header/HeaderContext'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; // Import icons from centralized module to avoid Turbopack chunk issues import { FiFile, FiChevronRight, FiChevronDown, FiGithub, BsFolderFill, BsFolder2Open, SiJavascript, SiReact, SiCss3, SiJson } from '@/lib/icons'; import { motion } from 'framer-motion'; import CodeApplicationProgress, { type CodeApplicationState } from '@/components/CodeApplicationProgress'; interface SandboxData { sandboxId: string; url: string; [key: string]: any; } interface ChatMessage { content: string; type: 'user' | 'ai' | 'system' | 'file-update' | 'command' | 'error'; timestamp: Date; metadata?: { scrapedUrl?: string; scrapedContent?: any; generatedCode?: string; appliedFiles?: string[]; commandType?: 'input' | 'output' | 'error' | 'success'; brandingData?: any; sourceUrl?: string; }; } interface ScrapeData { success: boolean; content?: string; url?: string; title?: string; source?: string; screenshot?: string; structured?: any; metadata?: any; message?: string; error?: string; } function AISandboxPage() { const [mode, setMode] = useState<'website' | 'paper'>('website'); const [sandboxData, setSandboxData] = useState(null); const [loading, setLoading] = useState(false); const [status, setStatus] = useState({ text: 'Not connected', active: false }); const [responseArea, setResponseArea] = useState([]); const [structureContent, setStructureContent] = useState('No sandbox created yet'); const [promptInput, setPromptInput] = useState(''); const [chatMessages, setChatMessages] = useState([ { content: 'Welcome! I can help you generate code with full context of your sandbox files and structure. Just start chatting - I\'ll automatically create a sandbox for you if needed!\n\nTip: If you see package errors like "react-router-dom not found", just type "npm install" or "check packages" to automatically install missing packages.', type: 'system', timestamp: new Date() } ]); const [aiChatInput, setAiChatInput] = useState(''); const [aiEnabled] = useState(true); const searchParams = useSearchParams(); const router = useRouter(); const [aiModel, setAiModel] = useState(() => { const modelParam = searchParams.get('model'); return appConfig.ai.availableModels.includes(modelParam || '') ? modelParam! : appConfig.ai.defaultModel; }); const [urlOverlayVisible, setUrlOverlayVisible] = useState(false); const [urlInput, setUrlInput] = useState(''); const [urlStatus, setUrlStatus] = useState([]); const [showHomeScreen, setShowHomeScreen] = useState(true); const [expandedFolders, setExpandedFolders] = useState>(new Set(['app', 'src', 'src/components'])); const [selectedFile, setSelectedFile] = useState(null); const [homeScreenFading, setHomeScreenFading] = useState(false); const [homeUrlInput, setHomeUrlInput] = useState(''); const [homeContextInput, setHomeContextInput] = useState(''); const [activeTab, setActiveTab] = useState<'generation' | 'preview'>('preview'); const [showStyleSelector, setShowStyleSelector] = useState(false); const [selectedStyle, setSelectedStyle] = useState(null); const [showLoadingBackground, setShowLoadingBackground] = useState(false); const [urlScreenshot, setUrlScreenshot] = useState(null); const [isScreenshotLoaded, setIsScreenshotLoaded] = useState(false); const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false); const [screenshotError, setScreenshotError] = useState(null); const [isPreparingDesign, setIsPreparingDesign] = useState(false); const [targetUrl, setTargetUrl] = useState(''); const [sidebarScrolled, setSidebarScrolled] = useState(false); const [screenshotCollapsed, setScreenshotCollapsed] = useState(false); const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null); const [isStartingNewGeneration, setIsStartingNewGeneration] = useState(false); const [sandboxFiles, setSandboxFiles] = useState>({}); const [hasInitialSubmission, setHasInitialSubmission] = useState(false); const [fileStructure, setFileStructure] = useState(''); const [conversationContext, setConversationContext] = useState<{ scrapedWebsites: Array<{ url: string; content: any; timestamp: Date }>; generatedComponents: Array<{ name: string; path: string; content: string }>; appliedCode: Array<{ files: string[]; timestamp: Date }>; currentProject: string; lastGeneratedCode?: string; }>({ scrapedWebsites: [], generatedComponents: [], appliedCode: [], currentProject: '', lastGeneratedCode: undefined }); const iframeRef = useRef(null); const chatMessagesRef = useRef(null); const codeDisplayRef = useRef(null); const [codeApplicationState, setCodeApplicationState] = useState({ stage: null }); const [generationProgress, setGenerationProgress] = useState<{ isGenerating: boolean; status: string; components: Array<{ name: string; path: string; completed: boolean }>; currentComponent: number; streamedCode: string; isStreaming: boolean; isThinking: boolean; thinkingText?: string; thinkingDuration?: number; currentFile?: { path: string; content: string; type: string }; files: Array<{ path: string; content: string; type: string; completed: boolean; edited?: boolean }>; lastProcessedPosition: number; isEdit?: boolean; }>({ isGenerating: false, status: '', components: [], currentComponent: 0, streamedCode: '', isStreaming: false, isThinking: false, files: [], lastProcessedPosition: 0 }); // Store flag to trigger generation after component mounts const [shouldAutoGenerate, setShouldAutoGenerate] = useState(false); // Clear old conversation data on component mount and create/restore sandbox useEffect(() => { let isMounted = true; let sandboxCreated = false; // Track if sandbox was created in this effect const initializePage = async () => { // Prevent double execution in React StrictMode if (sandboxCreated) return; // First check URL parameters (from home page navigation) const urlParam = searchParams.get('url'); const templateParam = searchParams.get('template'); const detailsParam = searchParams.get('details'); // Then check session storage as fallback const storedUrl = urlParam || sessionStorage.getItem('targetUrl'); const storedStyle = templateParam || sessionStorage.getItem('selectedStyle'); const storedModel = sessionStorage.getItem('selectedModel'); const storedInstructions = sessionStorage.getItem('additionalInstructions'); if (storedUrl) { // Mark that we have an initial submission since we're loading with a URL setHasInitialSubmission(true); // Clear sessionStorage after reading sessionStorage.removeItem('targetUrl'); sessionStorage.removeItem('selectedStyle'); sessionStorage.removeItem('selectedModel'); sessionStorage.removeItem('additionalInstructions'); // Note: Don't clear siteMarkdown here, it will be cleared when used // Set the values in the component state setHomeUrlInput(storedUrl); setSelectedStyle(storedStyle || 'modern'); // Add details to context if provided if (detailsParam) { setHomeContextInput(detailsParam); } else if (storedStyle && !urlParam) { // Only apply stored style if no screenshot URL is provided // This prevents unwanted style inheritance when using screenshot search const styleNames: Record = { '1': 'Glassmorphism', '2': 'Neumorphism', '3': 'Brutalism', '4': 'Minimalist', '5': 'Dark Mode', '6': 'Gradient Rich', '7': '3D Depth', '8': 'Retro Wave', 'modern': 'Modern clean and minimalist', 'playful': 'Fun colorful and playful', 'professional': 'Corporate professional and sleek', 'artistic': 'Creative artistic and unique' }; const styleName = styleNames[storedStyle] || storedStyle; let contextString = `${styleName} style design`; // Add additional instructions if provided if (storedInstructions) { contextString += `. ${storedInstructions}`; } setHomeContextInput(contextString); } else if (storedInstructions && !urlParam) { // Apply only instructions if no style but instructions are provided // and no screenshot URL is provided setHomeContextInput(storedInstructions); } if (storedModel) { setAiModel(storedModel); } // Skip the home screen and go directly to builder setShowHomeScreen(false); setHomeScreenFading(false); // Set flag to auto-trigger generation after component updates setShouldAutoGenerate(true); // Also set autoStart flag for the effect sessionStorage.setItem('autoStart', 'true'); } // Clear old conversation try { await fetch('/api/conversation-state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'clear-old' }) }); console.log('[home] Cleared old conversation data on mount'); } catch (error) { console.error('[ai-sandbox] Failed to clear old conversation:', error); if (isMounted) { addChatMessage('Failed to clear old conversation data.', 'error'); } } if (!isMounted) return; // Check if sandbox ID is in URL const sandboxIdParam = searchParams.get('sandbox'); setLoading(true); try { if (sandboxIdParam) { console.log('[home] Attempting to restore sandbox:', sandboxIdParam); // For now, just create a new sandbox - you could enhance this to actually restore // the specific sandbox if your backend supports it sandboxCreated = true; await createSandbox(true); } else { console.log('[home] No sandbox in URL, creating new sandbox automatically...'); sandboxCreated = true; await createSandbox(true); } // If we have a URL from the home page, mark for automatic start if (storedUrl && isMounted) { // We'll trigger the generation after the component is fully mounted // and the startGeneration function is defined sessionStorage.setItem('autoStart', 'true'); } } catch (error) { console.error('[ai-sandbox] Failed to create or restore sandbox:', error); if (isMounted) { addChatMessage('Failed to create or restore sandbox.', 'error'); } } finally { if (isMounted) { setLoading(false); } } }; initializePage(); return () => { isMounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Run only on mount useEffect(() => { // Handle Escape key for home screen const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && showHomeScreen) { setHomeScreenFading(true); setTimeout(() => { setShowHomeScreen(false); setHomeScreenFading(false); }, 500); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [showHomeScreen]); // Start capturing screenshot if URL is provided on mount (from home screen) useEffect(() => { if (!showHomeScreen && homeUrlInput && !urlScreenshot && !isCapturingScreenshot) { let screenshotUrl = homeUrlInput.trim(); if (!screenshotUrl.match(/^https?:\/\//i)) { screenshotUrl = 'https://' + screenshotUrl; } captureUrlScreenshot(screenshotUrl); } }, [showHomeScreen, homeUrlInput]); // eslint-disable-line react-hooks/exhaustive-deps // Auto-start generation if flagged useEffect(() => { const autoStart = sessionStorage.getItem('autoStart'); if (autoStart === 'true' && !showHomeScreen && homeUrlInput) { sessionStorage.removeItem('autoStart'); // Small delay to ensure everything is ready setTimeout(() => { console.log('[generation] Auto-starting generation for URL:', homeUrlInput); startGeneration(); }, 1000); } }, [showHomeScreen, homeUrlInput]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { // Only check sandbox status on mount if we don't already have sandboxData // AND we're not auto-starting a new generation (which would create a new sandbox) const autoStart = sessionStorage.getItem('autoStart'); if (!sandboxData && autoStart !== 'true') { checkSandboxStatus(); } }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (chatMessagesRef.current) { chatMessagesRef.current.scrollTop = chatMessagesRef.current.scrollHeight; } }, [chatMessages]); // Auto-trigger generation when flag is set (from home page navigation) useEffect(() => { if (shouldAutoGenerate && homeUrlInput && !showHomeScreen) { // Reset the flag setShouldAutoGenerate(false); // Trigger generation after a short delay to ensure everything is set up const timer = setTimeout(() => { console.log('[generation] Auto-triggering generation from URL params'); startGeneration(); }, 1000); return () => clearTimeout(timer); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldAutoGenerate, homeUrlInput, showHomeScreen]); const updateStatus = (text: string, active: boolean) => { setStatus({ text, active }); }; const log = (message: string, type: 'info' | 'error' | 'command' = 'info') => { setResponseArea(prev => [...prev, `[${type}] ${message}`]); }; const addChatMessage = (content: string, type: ChatMessage['type'], metadata?: ChatMessage['metadata']) => { setChatMessages(prev => { // Skip duplicate consecutive system messages if (type === 'system' && prev.length > 0) { const lastMessage = prev[prev.length - 1]; if (lastMessage.type === 'system' && lastMessage.content === content) { return prev; // Skip duplicate } } return [...prev, { content, type, timestamp: new Date(), metadata }]; }); }; const checkAndInstallPackages = async () => { // This function is only called when user explicitly requests it // Don't show error if no sandbox - it's likely being created if (!sandboxData) { console.log('[checkAndInstallPackages] No sandbox data available yet'); return; } // Vite error checking removed - handled by template setup addChatMessage('Checking packages... Sandbox is ready with Vite configuration.', 'system'); }; const handleSurfaceError = (_errors: any[]) => { // Function kept for compatibility but Vite errors are now handled by template // Focus the input const textarea = document.querySelector('textarea') as HTMLTextAreaElement; if (textarea) { textarea.focus(); } }; const installPackages = async (packages: string[]) => { if (!sandboxData) { addChatMessage('No active sandbox. Create a sandbox first!', 'system'); return; } try { const response = await fetch('/api/install-packages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ packages }) }); if (!response.ok) { throw new Error(`Failed to install packages: ${response.statusText}`); } const reader = response.body?.getReader(); const decoder = new TextDecoder(); while (reader) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); switch (data.type) { case 'command': // Don't show npm install commands - they're handled by info messages if (!data.command.includes('npm install')) { addChatMessage(data.command, 'command', { commandType: 'input' }); } break; case 'output': addChatMessage(data.message, 'command', { commandType: 'output' }); break; case 'error': if (data.message && data.message !== 'undefined') { addChatMessage(data.message, 'command', { commandType: 'error' }); } break; case 'warning': addChatMessage(data.message, 'command', { commandType: 'output' }); break; case 'success': addChatMessage(`${data.message}`, 'system'); break; case 'status': addChatMessage(data.message, 'system'); break; } } catch (e) { console.error('Failed to parse SSE data:', e); } } } } } catch (error: any) { addChatMessage(`Failed to install packages: ${error.message}`, 'system'); } }; const checkSandboxStatus = async () => { try { const response = await fetch('/api/sandbox-status'); const data = await response.json(); if (data.active && data.healthy && data.sandboxData) { console.log('[checkSandboxStatus] Setting sandboxData from API:', data.sandboxData); setSandboxData(data.sandboxData); updateStatus('Sandbox active', true); } else if (data.active && !data.healthy) { // Sandbox exists but not responding updateStatus('Sandbox not responding', false); // Keep existing sandboxData if we have it - don't clear it } else { // Only clear sandboxData if we don't already have it or if we're explicitly checking from a fresh state // This prevents clearing sandboxData during normal operation when it should persist if (!sandboxData) { console.log('[checkSandboxStatus] No existing sandboxData, clearing state'); setSandboxData(null); updateStatus('No sandbox', false); } else { // Keep existing sandboxData and just update status console.log('[checkSandboxStatus] Keeping existing sandboxData, sandbox inactive but data preserved'); updateStatus('Sandbox status unknown', false); } } } catch (error) { console.error('Failed to check sandbox status:', error); // Only clear on error if we don't have existing sandboxData if (!sandboxData) { setSandboxData(null); updateStatus('Error', false); } else { updateStatus('Status check failed', false); } } }; const sandboxCreationRef = useRef(false); const createSandbox = async (fromHomeScreen = false, runtimeOverride?: string) => { // Prevent duplicate sandbox creation if (sandboxCreationRef.current) { console.log('[createSandbox] Sandbox creation already in progress, skipping...'); return null; } const runtime = runtimeOverride || (mode === 'paper' ? 'python' : 'react'); sandboxCreationRef.current = true; console.log(`[createSandbox] Starting sandbox creation for ${runtime}...`); setLoading(true); setShowLoadingBackground(true); updateStatus(`Creating ${runtime} sandbox...`, false); setResponseArea([]); setScreenshotError(null); try { const response = await fetch('/api/create-ai-sandbox-v2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ runtime }) }); const data = await response.json(); console.log('[createSandbox] Response data:', data); if (data.success) { sandboxCreationRef.current = false; // Reset the ref on success console.log('[createSandbox] Setting sandboxData from creation:', data); setSandboxData(data); updateStatus('Sandbox active', true); log('Sandbox created successfully!'); log(`Sandbox ID: ${data.sandboxId}`); log(`URL: ${data.url}`); // Update URL with sandbox ID const newParams = new URLSearchParams(searchParams.toString()); newParams.set('sandbox', data.sandboxId); newParams.set('model', aiModel); router.push(`/generation?${newParams.toString()}`, { scroll: false }); // Fade out loading background after sandbox loads setTimeout(() => { setShowLoadingBackground(false); }, 3000); if (data.structure) { displayStructure(data.structure); } // Fetch sandbox files after creation setTimeout(fetchSandboxFiles, 1000); // For Vercel sandboxes, Vite is already started during setupViteApp // No need to restart it immediately after creation // Only restart if there's an actual issue later console.log('[createSandbox] Sandbox ready with Vite server running'); // Only add welcome message if not coming from home screen if (!fromHomeScreen) { addChatMessage(`Sandbox created! ID: ${data.sandboxId}. I now have context of your sandbox and can help you build your app. Just ask me to create components and I'll automatically apply them! Tip: I automatically detect and install npm packages from your code imports (like react-router-dom, axios, etc.)`, 'system'); } setTimeout(() => { if (iframeRef.current) { iframeRef.current.src = data.url; } }, 100); // Return the sandbox data so it can be used immediately return data; } else { throw new Error(data.error || 'Unknown error'); } } catch (error: any) { console.error('[createSandbox] Error:', error); updateStatus('Error', false); log(`Failed to create sandbox: ${error.message}`, 'error'); addChatMessage(`Failed to create sandbox: ${error.message}`, 'system'); throw error; } finally { setLoading(false); sandboxCreationRef.current = false; // Reset the ref } }; const displayStructure = (structure: any) => { if (typeof structure === 'object') { setStructureContent(JSON.stringify(structure, null, 2)); } else { setStructureContent(structure || 'No structure available'); } }; const applyGeneratedCode = async (code: string, isEdit: boolean = false, overrideSandboxData?: SandboxData) => { setLoading(true); log('Applying AI-generated code...'); try { // Show progress component instead of individual messages setCodeApplicationState({ stage: 'analyzing' }); // Get pending packages from tool calls const pendingPackages = ((window as any).pendingPackages || []).filter((pkg: any) => pkg && typeof pkg === 'string'); if (pendingPackages.length > 0) { console.log('[applyGeneratedCode] Sending packages from tool calls:', pendingPackages); // Clear pending packages after use (window as any).pendingPackages = []; } // Use streaming endpoint for real-time feedback const effectiveSandboxData = overrideSandboxData || sandboxData; const response = await fetch('/api/apply-ai-code-stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ response: code, isEdit: isEdit, packages: pendingPackages, sandboxId: effectiveSandboxData?.sandboxId // Pass the sandbox ID to ensure proper connection }) }); if (!response.ok) { throw new Error(`Failed to apply code: ${response.statusText}`); } // Handle streaming response const reader = response.body?.getReader(); const decoder = new TextDecoder(); let finalData: any = null; while (reader) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); switch (data.type) { case 'start': // Don't add as chat message, just update state setCodeApplicationState({ stage: 'analyzing' }); break; case 'step': // Update progress state based on step if (data.message.includes('Installing') && data.packages) { setCodeApplicationState({ stage: 'installing', packages: data.packages }); } else if (data.message.includes('Creating files') || data.message.includes('Applying')) { setCodeApplicationState({ stage: 'applying', filesGenerated: [] // Files will be populated when complete }); } break; case 'package-progress': // Handle package installation progress if (data.installedPackages) { setCodeApplicationState(prev => ({ ...prev, installedPackages: data.installedPackages })); } break; case 'command': // Don't show npm install commands - they're handled by info messages if (data.command && !data.command.includes('npm install')) { addChatMessage(data.command, 'command', { commandType: 'input' }); } break; case 'success': if (data.installedPackages) { setCodeApplicationState(prev => ({ ...prev, installedPackages: data.installedPackages })); } break; case 'file-progress': // Skip file progress messages, they're noisy break; case 'file-complete': // Could add individual file completion messages if desired break; case 'command-progress': addChatMessage(`${data.action} command: ${data.command}`, 'command', { commandType: 'input' }); break; case 'command-output': addChatMessage(data.output, 'command', { commandType: data.stream === 'stderr' ? 'error' : 'output' }); break; case 'command-complete': if (data.success) { addChatMessage(`Command completed successfully`, 'system'); } else { addChatMessage(`Command failed with exit code ${data.exitCode}`, 'system'); } break; case 'complete': finalData = data; setCodeApplicationState({ stage: 'complete' }); // Clear the state after a delay setTimeout(() => { setCodeApplicationState({ stage: null }); }, 3000); // Reset loading state when complete setLoading(false); break; case 'error': addChatMessage(`Error: ${data.message || data.error || 'Unknown error'}`, 'system'); // Reset loading state on error setLoading(false); break; case 'warning': addChatMessage(`${data.message}`, 'system'); break; case 'info': // Show info messages, especially for package installation if (data.message) { addChatMessage(data.message, 'system'); } break; } } catch { // Ignore parse errors } } } } // Process final data if (finalData && finalData.type === 'complete') { const data: any = { success: true, results: finalData.results, explanation: finalData.explanation, structure: finalData.structure, message: finalData.message, autoCompleted: finalData.autoCompleted, autoCompletedComponents: finalData.autoCompletedComponents, warning: finalData.warning, missingImports: finalData.missingImports, debug: finalData.debug }; if (data.success) { const { results } = data; // Log package installation results without duplicate messages if (results.packagesInstalled?.length > 0) { log(`Packages installed: ${results.packagesInstalled.join(', ')}`); } if (results.filesCreated?.length > 0) { log('Files created:'); results.filesCreated.forEach((file: string) => { log(` ${file}`, 'command'); }); // Verify files were actually created by refreshing the sandbox if needed if (sandboxData?.sandboxId && results.filesCreated.length > 0) { // Small delay to ensure files are written setTimeout(() => { // Force refresh the iframe to show new files if (iframeRef.current) { iframeRef.current.src = iframeRef.current.src; } }, 1000); } } if (results.filesUpdated?.length > 0) { log('Files updated:'); results.filesUpdated.forEach((file: string) => { log(` ${file}`, 'command'); }); } // Update conversation context with applied code setConversationContext(prev => ({ ...prev, appliedCode: [...prev.appliedCode, { files: [...(results.filesCreated || []), ...(results.filesUpdated || [])], timestamp: new Date() }] })); if (results.commandsExecuted?.length > 0) { log('Commands executed:'); results.commandsExecuted.forEach((cmd: string) => { log(` $ ${cmd}`, 'command'); }); } if (results.errors?.length > 0) { results.errors.forEach((err: string) => { log(err, 'error'); }); } if (data.structure) { displayStructure(data.structure); } if (data.explanation) { log(data.explanation); } if (data.autoCompleted) { log('Auto-generating missing components...', 'command'); if (data.autoCompletedComponents) { setTimeout(() => { log('Auto-generated missing components:', 'info'); data.autoCompletedComponents.forEach((comp: string) => { log(` ${comp}`, 'command'); }); }, 1000); } } else if (data.warning) { log(data.warning, 'error'); if (data.missingImports && data.missingImports.length > 0) { const missingList = data.missingImports.join(', '); addChatMessage( `Ask me to "create the missing components: ${missingList}" to fix these import errors.`, 'system' ); } } log('Code applied successfully!'); console.log('[applyGeneratedCode] Response data:', data); console.log('[applyGeneratedCode] Debug info:', data.debug); console.log('[applyGeneratedCode] Current sandboxData:', sandboxData); console.log('[applyGeneratedCode] Current iframe element:', iframeRef.current); console.log('[applyGeneratedCode] Current iframe src:', iframeRef.current?.src); // Set applying code state for edits to show loading overlay // Removed overlay - changes apply directly if (results.filesCreated?.length > 0) { setConversationContext(prev => ({ ...prev, appliedCode: [...prev.appliedCode, { files: results.filesCreated, timestamp: new Date() }] })); // Update the chat message to show success // Only show file list if not in edit mode if (isEdit) { addChatMessage(`Edit applied successfully!`, 'system'); } else { // Check if this is part of a generation flow (has recent AI recreation message) const recentMessages = chatMessages.slice(-5); const isPartOfGeneration = recentMessages.some(m => m.content.includes('AI recreation generated') || m.content.includes('Code generated') ); // Don't show files if part of generation flow to avoid duplication if (isPartOfGeneration) { addChatMessage(`Applied ${results.filesCreated.length} files successfully!`, 'system'); } else { addChatMessage(`Applied ${results.filesCreated.length} files successfully!`, 'system', { appliedFiles: results.filesCreated }); } } // If there are failed packages, add a message about checking for errors if (results.packagesFailed?.length > 0) { addChatMessage(`⚠️ Some packages failed to install. Check the error banner above for details.`, 'system'); } // Fetch updated file structure await fetchSandboxFiles(); // Skip automatic package check - it's not needed here and can cause false "no sandbox" messages // Packages are already installed during the apply-ai-code-stream process // Test build to ensure everything compiles correctly // Skip build test for now - it's causing errors with undefined activeSandbox // The build test was trying to access global.activeSandbox from the frontend, // but that's only available in the backend API routes console.log('[build-test] Skipping build test - would need API endpoint'); // Force iframe refresh after applying code const refreshDelay = appConfig.codeApplication.defaultRefreshDelay; // Allow Vite to process changes setTimeout(() => { const currentSandboxData = effectiveSandboxData; if (iframeRef.current && currentSandboxData?.url) { console.log('[home] Refreshing iframe after code application...'); // Method 1: Change src with timestamp const urlWithTimestamp = `${currentSandboxData.url}?t=${Date.now()}&applied=true`; iframeRef.current.src = urlWithTimestamp; // Method 2: Force reload after a short delay setTimeout(() => { try { if (iframeRef.current?.contentWindow) { iframeRef.current.contentWindow.location.reload(); console.log('[home] Force reloaded iframe content'); } } catch (e) { console.log('[home] Could not reload iframe (cross-origin):', e); } // Reload completed }, 1000); } }, refreshDelay); // Vite error checking removed - handled by template setup } // Give Vite HMR a moment to detect changes, then ensure refresh const currentSandboxData = effectiveSandboxData; if (iframeRef.current && currentSandboxData?.url) { // Wait for Vite to process the file changes // If packages were installed, wait longer for Vite to restart const packagesInstalled = results?.packagesInstalled?.length > 0 || data.results?.packagesInstalled?.length > 0; const refreshDelay = packagesInstalled ? appConfig.codeApplication.packageInstallRefreshDelay : appConfig.codeApplication.defaultRefreshDelay; console.log(`[applyGeneratedCode] Packages installed: ${packagesInstalled}, refresh delay: ${refreshDelay}ms`); setTimeout(async () => { if (iframeRef.current && currentSandboxData?.url) { console.log('[applyGeneratedCode] Starting iframe refresh sequence...'); console.log('[applyGeneratedCode] Current iframe src:', iframeRef.current.src); console.log('[applyGeneratedCode] Sandbox URL:', currentSandboxData.url); // Method 1: Try direct navigation first try { const urlWithTimestamp = `${currentSandboxData.url}?t=${Date.now()}&force=true`; console.log('[applyGeneratedCode] Attempting direct navigation to:', urlWithTimestamp); // Remove any existing onload handler iframeRef.current.onload = null; // Navigate directly iframeRef.current.src = urlWithTimestamp; // Wait a bit and check if it loaded await new Promise(resolve => setTimeout(resolve, 2000)); // Try to access the iframe content to verify it loaded try { const iframeDoc = iframeRef.current.contentDocument || iframeRef.current.contentWindow?.document; if (iframeDoc && iframeDoc.readyState === 'complete') { console.log('[applyGeneratedCode] Iframe loaded successfully'); return; } } catch { console.log('[applyGeneratedCode] Cannot access iframe content (CORS), assuming loaded'); return; } } catch (e) { console.error('[applyGeneratedCode] Direct navigation failed:', e); } // Method 2: Force complete iframe recreation if direct navigation failed console.log('[applyGeneratedCode] Falling back to iframe recreation...'); const parent = iframeRef.current.parentElement; const newIframe = document.createElement('iframe'); // Copy attributes newIframe.className = iframeRef.current.className; newIframe.title = iframeRef.current.title; newIframe.allow = iframeRef.current.allow; // Copy sandbox attributes const sandboxValue = iframeRef.current.getAttribute('sandbox'); if (sandboxValue) { newIframe.setAttribute('sandbox', sandboxValue); } // Remove old iframe iframeRef.current.remove(); // Add new iframe newIframe.src = `${currentSandboxData.url}?t=${Date.now()}&recreated=true`; parent?.appendChild(newIframe); // Update ref (iframeRef as any).current = newIframe; console.log('[applyGeneratedCode] Iframe recreated with new content'); } else { console.error('[applyGeneratedCode] No iframe or sandbox URL available for refresh'); } }, refreshDelay); // Dynamic delay based on whether packages were installed } } else { throw new Error(finalData?.error || 'Failed to apply code'); } } else { // If no final data was received, still close loading addChatMessage('Code application may have partially succeeded. Check the preview.', 'system'); } } catch (error: any) { log(`Failed to apply code: ${error.message}`, 'error'); } finally { setLoading(false); // Clear isEdit flag after applying code setGenerationProgress(prev => ({ ...prev, isEdit: false })); } }; const fetchSandboxFiles = async () => { if (!sandboxData) return; try { const response = await fetch('/api/get-sandbox-files', { method: 'GET', headers: { 'Content-Type': 'application/json', } }); if (response.ok) { const data = await response.json(); if (data.success) { setSandboxFiles(data.files || {}); setFileStructure(data.structure || ''); console.log('[fetchSandboxFiles] Updated file list:', Object.keys(data.files || {}).length, 'files'); } } } catch (error) { console.error('[fetchSandboxFiles] Error fetching files:', error); } }; // const restartViteServer = async () => { // try { // addChatMessage('Restarting Vite dev server...', 'system'); // // const response = await fetch('/api/restart-vite', { // method: 'POST', // headers: { 'Content-Type': 'application/json' } // }); // // if (response.ok) { // const data = await response.json(); // if (data.success) { // addChatMessage('✓ Vite dev server restarted successfully!', 'system'); // // // Refresh the iframe after a short delay // setTimeout(() => { // if (iframeRef.current && sandboxData?.url) { // iframeRef.current.src = `${sandboxData.url}?t=${Date.now()}`; // } // }, 2000); // } else { // addChatMessage(`Failed to restart Vite: ${data.error}`, 'error'); // } // } else { // addChatMessage('Failed to restart Vite server', 'error'); // } // } catch (error) { // console.error('[restartViteServer] Error:', error); // addChatMessage(`Error restarting Vite: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error'); // } // }; // const applyCode = async () => { // const code = promptInput.trim(); // if (!code) { // log('Please enter some code first', 'error'); // addChatMessage('No code to apply. Please generate code first.', 'system'); // return; // } // // // Prevent double clicks // if (loading) { // console.log('[applyCode] Already loading, skipping...'); // return; // } // // // Determine if this is an edit based on whether we have applied code before // const isEdit = conversationContext.appliedCode.length > 0; // await applyGeneratedCode(code, isEdit); // }; const renderMainContent = () => { if (activeTab === 'generation' && (generationProgress.isGenerating || generationProgress.files.length > 0)) { return ( /* Generation Tab Content */
{/* File Explorer - Hide during edits */} {!generationProgress.isEdit && (
Explorer
{/* File Tree */}
{/* Root app folder */}
toggleFolder('app')} > {expandedFolders.has('app') ? ( ) : ( )} {expandedFolders.has('app') ? ( ) : ( )} app
{expandedFolders.has('app') && (
{/* Group files by directory */} {(() => { const fileTree: { [key: string]: Array<{ name: string; edited?: boolean }> } = {}; // Create a map of edited files // const editedFiles = new Set( // generationProgress.files // .filter(f => f.edited) // .map(f => f.path) // ); // Process all files from generation progress generationProgress.files.forEach(file => { const parts = file.path.split('/'); const dir = parts.length > 1 ? parts.slice(0, -1).join('/') : ''; const fileName = parts[parts.length - 1]; if (!fileTree[dir]) fileTree[dir] = []; fileTree[dir].push({ name: fileName, edited: file.edited || false }); }); return Object.entries(fileTree).map(([dir, files]) => (
{dir && (
toggleFolder(dir)} > {expandedFolders.has(dir) ? ( ) : ( )} {expandedFolders.has(dir) ? ( ) : ( )} {dir.split('/').pop()}
)} {(!dir || expandedFolders.has(dir)) && (
{files.sort((a, b) => a.name.localeCompare(b.name)).map(fileInfo => { const fullPath = dir ? `${dir}/${fileInfo.name}` : fileInfo.name; const isSelected = selectedFile === fullPath; return (
handleFileClick(fullPath)} > {getFileIcon(fileInfo.name)} {fileInfo.name} {fileInfo.edited && ( )}
); })}
)}
)); })()}
)}
)} {/* Code Content */}
{/* Thinking Mode Display - Only show during active generation */} {generationProgress.isGenerating && (generationProgress.isThinking || generationProgress.thinkingText) && (
{generationProgress.isThinking ? ( <>
AI is thinking... ) : ( <> Thought for {generationProgress.thinkingDuration || 0} seconds )}
{generationProgress.thinkingText && (
                      {generationProgress.thinkingText}
                    
)}
)} {/* Live Code Display */}
{/* Show selected file if one is selected */} {selectedFile ? (
{getFileIcon(selectedFile)} {selectedFile}
{ const ext = selectedFile.split('.').pop()?.toLowerCase(); if (ext === 'css') return 'css'; if (ext === 'json') return 'json'; if (ext === 'html') return 'html'; return 'jsx'; })()} style={vscDarkPlus} customStyle={{ margin: 0, padding: '1rem', fontSize: '0.875rem', background: 'transparent', }} showLineNumbers={true} > {(() => { // Find the file content from generated files const file = generationProgress.files.find(f => f.path === selectedFile); return file?.content || '// File content will appear here'; })()}
) : /* If no files parsed yet, show loading or raw stream */ generationProgress.files.length === 0 && !generationProgress.currentFile ? ( generationProgress.isThinking ? ( // Beautiful loading state while thinking

AI is analyzing your request

{generationProgress.status || 'Preparing to generate code...'}

) : (
Streaming code...
{generationProgress.streamedCode || 'Starting code generation...'}
) ) : (
{/* Show current file being generated */} {generationProgress.currentFile && (
{generationProgress.currentFile.path} {generationProgress.currentFile.type === 'javascript' ? 'JSX' : generationProgress.currentFile.type.toUpperCase()}
{generationProgress.currentFile.content}
)} {/* Show completed files */} {generationProgress.files.map((file, idx) => (
{file.path}
{file.type === 'javascript' ? 'JSX' : file.type.toUpperCase()}
{file.content}
))} {/* Show remaining raw stream if there's content after the last file */} {!generationProgress.currentFile && generationProgress.streamedCode.length > 0 && (
Processing...
{(() => { // Show only the tail of the stream after the last file const lastFileEnd = generationProgress.files.length > 0 ? generationProgress.streamedCode.lastIndexOf('') + 7 : 0; let remainingContent = generationProgress.streamedCode.slice(lastFileEnd).trim(); // Remove explanation tags and content remainingContent = remainingContent.replace(/[\s\S]*?<\/explanation>/g, '').trim(); // If only whitespace or nothing left, show loading message // Use "Loading sandbox..." instead of "Waiting for next file..." for better UX return remainingContent || 'Loading sandbox...'; })()}
)}
)}
{/* Progress indicator */} {generationProgress.components.length > 0 && (
)}
); } else if (activeTab === 'preview') { // Show loading state for initial generation or when starting a new generation with existing sandbox const isInitialGeneration = !sandboxData?.url && (urlScreenshot || isCapturingScreenshot || isPreparingDesign || loadingStage); const isNewGenerationWithSandbox = isStartingNewGeneration && sandboxData?.url; const shouldShowLoadingOverlay = (isInitialGeneration || isNewGenerationWithSandbox) && (loading || generationProgress.isGenerating || isPreparingDesign || loadingStage || isCapturingScreenshot || isStartingNewGeneration); if (isInitialGeneration || isNewGenerationWithSandbox) { return (
{/* Screenshot as background when available */} {urlScreenshot && ( /* eslint-disable-next-line @next/next/no-img-element */ Website preview setIsScreenshotLoaded(true)} loading="eager" /> )} {/* Loading overlay - only show when actively processing initial generation */} {shouldShowLoadingOverlay && (
{/* Loading animation with skeleton */}
{/* Animated skeleton lines */}
{/* Status text */}

{isCapturingScreenshot ? 'Analyzing website...' : isPreparingDesign ? 'Preparing design...' : generationProgress.isGenerating ? 'Generating code...' : 'Loading...'}

{/* Subtle progress hint */}

{isCapturingScreenshot ? 'Taking a screenshot of the site' : isPreparingDesign ? 'Understanding the layout and structure' : generationProgress.isGenerating ? 'Writing React components' : 'Please wait...'}

)}
); } // Show sandbox iframe - keep showing during edits, only hide during initial loading if (sandboxData?.url) { return (