diff --git "a/app/generation/page.tsx" "b/app/generation/page.tsx" new file mode 100644--- /dev/null +++ "b/app/generation/page.tsx" @@ -0,0 +1,3958 @@ +'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 [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) => { + // Prevent duplicate sandbox creation + if (sandboxCreationRef.current) { + console.log('[createSandbox] Sandbox creation already in progress, skipping...'); + return null; + } + + sandboxCreationRef.current = true; + console.log('[createSandbox] Starting sandbox creation...'); + setLoading(true); + setShowLoadingBackground(true); + updateStatus('Creating 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({}) + }); + + 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 ( +
+