From 13a4c5e1de46b35700cd0ac306320ff18d3a0487 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:12:06 -0400 Subject: [PATCH] confirm build --- README.md | 67 +- app/api/analyze-edit-intent/route.ts | 2 +- app/api/create-zip/route.ts | 4 +- app/api/generate-ai-code-stream/route.ts | 25 +- app/api/get-sandbox-files/route.ts | 4 +- app/api/install-packages-v2/route.ts | 2 +- app/api/monitor-vite-logs/route.ts | 4 +- app/api/run-command-v2/route.ts | 2 +- app/api/sandbox-logs/route.ts | 8 +- app/api/scrape-screenshot/route.ts | 25 +- app/api/scrape-website/route.ts | 28 +- app/builder/page.tsx | 2 +- app/generation/GenerationClient.tsx | 20 + app/generation/page-content.tsx | 3525 +++++++++++++++++ app/generation/page.tsx | 25 +- app/{page.new.tsx => page.new.tsx.bak} | 0 app/page.tsx | 2 +- atoms/sheets.ts | 3 + components/HeroInput.tsx | 2 +- .../sections/ai-readiness/ControlPanel.tsx | 5 +- .../EndpointsSearch/EndpointsSearch.tsx | 1 - .../sections/endpoints/Extract/Extract.tsx | 1 - .../hero-input/Tabs/Mobile/Mobile.tsx | 3 +- .../sections/hero/Pixi/tickers/ascii.ts | 2 +- .../features/components/AnimatedRect.ts | 5 +- .../features/components/BlinkingContainer.ts | 9 +- components/app/generation/SidebarInput.tsx | 19 +- components/shared/buttons/slate-button.tsx | 6 +- .../header/Dropdown/Wrapper/Wrapper.tsx | 1 - components/shared/hero-flame.tsx | 2 +- components/shared/icons/animated-icons.tsx | 8 +- .../shared/preview/live-preview-frame.tsx | 16 +- components/ui/code.tsx | 17 + components/ui/motion/scramble-text.tsx | 48 + components/ui/shadcn/switch.tsx | 6 +- components/ui/spinner.tsx | 48 + eslint.config.mjs | 2 +- lib/app/utils.ts | 10 + package.json | 23 + .../templates/vercel/README.md | 2 +- pnpm-lock.yaml | 2590 ++++++++++-- public/compressor.json | 16 + 42 files changed, 6151 insertions(+), 439 deletions(-) create mode 100644 app/generation/GenerationClient.tsx create mode 100644 app/generation/page-content.tsx rename app/{page.new.tsx => page.new.tsx.bak} (100%) create mode 100644 atoms/sheets.ts create mode 100644 components/ui/code.tsx create mode 100644 components/ui/motion/scramble-text.tsx create mode 100644 components/ui/spinner.tsx create mode 100644 lib/app/utils.ts create mode 100644 public/compressor.json diff --git a/README.md b/README.md index a5c9800..8d0d904 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Chat with AI to build React apps instantly. An example app made by the [Firecrawl](https://firecrawl.dev/?ref=open-lovable-github) team. For a complete cloud solution, check out [Lovable.dev ❤️](https://lovable.dev/). +Supports both **E2B** and **Vercel** sandboxes for code execution. Choose your preferred sandbox provider in the setup below. + Open Lovable Demo @@ -12,7 +14,7 @@ Chat with AI to build React apps instantly. An example app made by the [Firecraw ```bash git clone https://github.com/mendableai/open-lovable.git cd open-lovable -npm install +pnpm install # or npm install / yarn install ``` 2. **Add `.env.local`** @@ -21,31 +23,72 @@ npm install # Required FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) -# Vercel Sandbox Authentication (choose one method) -# See: https://vercel.com/docs/vercel-sandbox#authentication +# ================================================================= +# SANDBOX PROVIDER - Choose ONE: E2B or Vercel +# ================================================================= -# Method 1: OIDC Token (recommended for development) +# Optional: Specify sandbox provider (defaults to 'e2b' if not set) +# SANDBOX_PROVIDER=e2b # or 'vercel' + +# ----------------------------------------------------------------- +# OPTION 1: E2B Sandbox +# ----------------------------------------------------------------- +# Get your API key from: https://e2b.dev +E2B_API_KEY=your_e2b_api_key + +# ----------------------------------------------------------------- +# OPTION 2: Vercel Sandbox +# ----------------------------------------------------------------- +# Method 1: OIDC Token (automatic setup) # Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically # VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull -# Method 2: Personal Access Token (for production or when OIDC unavailable) -# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID +# Method 2: Personal Access Token (manual setup) +# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID # VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID # VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard +# See: https://vercel.com/docs/vercel-sandbox#authentication -# Optional (need at least one AI provider) +# ================================================================= +# AI PROVIDERS - Add at least one +# ================================================================= ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com -OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) -GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey -GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended) +OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) +GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey +GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference) ``` 3. **Run** ```bash -npm run dev +pnpm dev # or npm run dev / yarn dev ``` -Open [http://localhost:3000](http://localhost:3000) +Open [http://localhost:3000](http://localhost:3000) + +## Sandbox Providers + +Open Lovable supports two sandbox providers for code execution: + +### 🔧 E2B +- **Full-featured development environment** with Node.js, npm, and pre-installed tools +- **Persistent file system** across interactions +- **Fast startup** and reliable performance +- **Advanced debugging** capabilities +- Perfect for complex applications and debugging + +### ⚡ Vercel +- **Vercel-hosted sandboxes** with automatic scaling +- **Integrated with Vercel ecosystem** for seamless deployment +- **Automatic OIDC authentication** (run `vercel link` then `vercel env pull`) +- Great for **production workflows** and Vercel users + +### Choosing Your Provider + +- **Use E2B** if you need full development capabilities and debugging features +- **Use Vercel** if you're already in the Vercel ecosystem or prefer their hosted solution +- **Default**: E2B (if no `SANDBOX_PROVIDER` is specified) + +You can switch providers anytime by updating the `SANDBOX_PROVIDER` environment variable. ## License diff --git a/app/api/analyze-edit-intent/route.ts b/app/api/analyze-edit-intent/route.ts index 5d7da65..07798a0 100644 --- a/app/api/analyze-edit-intent/route.ts +++ b/app/api/analyze-edit-intent/route.ts @@ -76,7 +76,7 @@ export async function POST(request: NextRequest) { // Create a summary of available files for the AI const validFiles = Object.entries(manifest.files as Record) - .filter(([path, _info]) => { + .filter(([path]) => { // Filter out invalid paths return path.includes('.') && !path.match(/\/\d+$/); }); diff --git a/app/api/create-zip/route.ts b/app/api/create-zip/route.ts index fae6c39..91418c4 100644 --- a/app/api/create-zip/route.ts +++ b/app/api/create-zip/route.ts @@ -1,10 +1,10 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextResponse } from 'next/server'; declare global { var activeSandbox: any; } -export async function POST(_request: NextRequest) { +export async function POST() { try { if (!global.activeSandbox) { return NextResponse.json({ diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index 417f5cf..5a327c4 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -194,7 +194,7 @@ export async function POST(request: NextRequest) { if (manifest) { await sendProgress({ type: 'status', message: '🔍 Creating search plan...' }); - const fileContents = global.sandboxState.fileCache.files; + const fileContents = global.sandboxState.fileCache?.files || {}; console.log('[generate-ai-code-stream] Files available for search:', Object.keys(fileContents).length); // STEP 1: Get search plan from AI @@ -244,7 +244,7 @@ export async function POST(request: NextRequest) { console.log('[generate-ai-code-stream] Target selected:', target); // Create surgical edit context with exact location - const normalizedPath = target.filePath.replace('/home/user/app/', ''); + // normalizedPath would be: target.filePath.replace('/home/user/app/', ''); // fileContent available but not used in current implementation // const fileContent = fileContents[normalizedPath]?.content || ''; @@ -356,7 +356,7 @@ User request: "${prompt}"`; // For now, fall back to keyword search since we don't have file contents for search execution // This path happens when no manifest was initially available - let targetFiles = []; + let targetFiles: any[] = []; if (!searchPlan || searchPlan.searchTerms.length === 0) { console.warn('[generate-ai-code-stream] No target files after fetch, searching for relevant files'); @@ -985,13 +985,15 @@ CRITICAL: When files are provided in the context: // Store files in cache for (const [path, content] of Object.entries(filesData.files)) { const normalizedPath = path.replace('/home/user/app/', ''); - global.sandboxState.fileCache.files[normalizedPath] = { - content: content as string, - lastModified: Date.now() - }; + if (global.sandboxState.fileCache) { + global.sandboxState.fileCache.files[normalizedPath] = { + content: content as string, + lastModified: Date.now() + }; + } } - if (filesData.manifest) { + if (filesData.manifest && global.sandboxState.fileCache) { global.sandboxState.fileCache.manifest = filesData.manifest; // Now try to analyze edit intent with the fetched manifest @@ -1023,7 +1025,7 @@ CRITICAL: When files are provided in the context: } // Update variables - backendFiles = global.sandboxState.fileCache.files; + backendFiles = global.sandboxState.fileCache?.files || {}; hasBackendFiles = Object.keys(backendFiles).length > 0; console.log('[generate-ai-code-stream] Updated backend cache with fetched files'); } @@ -1363,7 +1365,7 @@ It's better to have 3 complete files than 10 incomplete files.` let tagBuffer = ''; // Stream the response and parse for packages in real-time - for await (const textPart of result.textStream) { + for await (const textPart of result?.textStream || []) { const text = textPart || ''; generatedCode += text; currentFile += text; @@ -1729,8 +1731,7 @@ Provide the complete file content without any truncation. Include all necessary }, { role: 'user', content: completionPrompt } ], - temperature: isGPT5 ? undefined : appConfig.ai.defaultTemperature, - maxTokens: appConfig.ai.truncationRecoveryMaxTokens + temperature: model.startsWith('openai/gpt-5') ? undefined : appConfig.ai.defaultTemperature }); // Get the full text from the stream diff --git a/app/api/get-sandbox-files/route.ts b/app/api/get-sandbox-files/route.ts index 81ad76f..94df887 100644 --- a/app/api/get-sandbox-files/route.ts +++ b/app/api/get-sandbox-files/route.ts @@ -44,7 +44,7 @@ export async function GET() { throw new Error('Failed to list files'); } - const fileList = (await findResult.stdout()).split('\n').filter(f => f.trim()); + const fileList = (await findResult.stdout()).split('\n').filter((f: string) => f.trim()); console.log('[get-sandbox-files] Found', fileList.length, 'files'); // Read content of each file (limit to reasonable sizes) @@ -91,7 +91,7 @@ export async function GET() { let structure = ''; if (treeResult.exitCode === 0) { - const dirs = (await treeResult.stdout()).split('\n').filter(d => d.trim()); + const dirs = (await treeResult.stdout()).split('\n').filter((d: string) => d.trim()); structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines } diff --git a/app/api/install-packages-v2/route.ts b/app/api/install-packages-v2/route.ts index 01643f7..3797898 100644 --- a/app/api/install-packages-v2/route.ts +++ b/app/api/install-packages-v2/route.ts @@ -3,7 +3,7 @@ import { SandboxProvider } from '@/lib/sandbox/types'; import { sandboxManager } from '@/lib/sandbox/sandbox-manager'; declare global { - var activeSandboxProvider: SandboxProvider | null; + var activeSandboxProvider: any; } export async function POST(request: NextRequest) { diff --git a/app/api/monitor-vite-logs/route.ts b/app/api/monitor-vite-logs/route.ts index 2c6dc60..a74a010 100644 --- a/app/api/monitor-vite-logs/route.ts +++ b/app/api/monitor-vite-logs/route.ts @@ -41,7 +41,7 @@ export async function GET() { }); if (findResult.exitCode === 0) { - const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim()); + const logFiles = (await findResult.stdout()).split('\n').filter((f: string) => f.trim()); for (const logFile of logFiles.slice(0, 3)) { try { @@ -51,7 +51,7 @@ export async function GET() { }); if (grepResult.exitCode === 0) { - const errorLines = (await grepResult.stdout()).split('\n').filter(line => line.trim()); + const errorLines = (await grepResult.stdout()).split('\n').filter((line: string) => line.trim()); for (const line of errorLines) { // Extract package name from error line diff --git a/app/api/run-command-v2/route.ts b/app/api/run-command-v2/route.ts index cca755a..18cd15f 100644 --- a/app/api/run-command-v2/route.ts +++ b/app/api/run-command-v2/route.ts @@ -4,7 +4,7 @@ import { sandboxManager } from '@/lib/sandbox/sandbox-manager'; // Get active sandbox provider from global state declare global { - var activeSandboxProvider: SandboxProvider | null; + var activeSandboxProvider: any; } export async function POST(request: NextRequest) { diff --git a/app/api/sandbox-logs/route.ts b/app/api/sandbox-logs/route.ts index 8404da1..2a7f2fe 100644 --- a/app/api/sandbox-logs/route.ts +++ b/app/api/sandbox-logs/route.ts @@ -1,10 +1,10 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextResponse } from 'next/server'; declare global { var activeSandbox: any; } -export async function GET(_request: NextRequest) { +export async function GET() { try { if (!global.activeSandbox) { return NextResponse.json({ @@ -26,7 +26,7 @@ export async function GET(_request: NextRequest) { if (psResult.exitCode === 0) { const psOutput = await psResult.stdout(); - const viteProcesses = psOutput.split('\n').filter(line => + const viteProcesses = psOutput.split('\n').filter((line: string) => line.toLowerCase().includes('vite') || line.toLowerCase().includes('npm run dev') ); @@ -49,7 +49,7 @@ export async function GET(_request: NextRequest) { }); if (findResult.exitCode === 0) { - const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim()); + const logFiles = (await findResult.stdout()).split('\n').filter((f: string) => f.trim()); for (const logFile of logFiles.slice(0, 2)) { try { diff --git a/app/api/scrape-screenshot/route.ts b/app/api/scrape-screenshot/route.ts index cdd4ea2..bd681ac 100644 --- a/app/api/scrape-screenshot/route.ts +++ b/app/api/scrape-screenshot/route.ts @@ -51,17 +51,17 @@ export async function POST(req: NextRequest) { screenshot: scrapeResult.screenshot, metadata: scrapeResult.metadata || {} }); - } else if (scrapeResult?.data?.screenshot) { + } else if ((scrapeResult as any)?.data?.screenshot) { // Nested data structure return NextResponse.json({ success: true, - screenshot: scrapeResult.data.screenshot, - metadata: scrapeResult.data.metadata || {} + screenshot: (scrapeResult as any).data.screenshot, + metadata: (scrapeResult as any).data.metadata || {} }); - } else if (scrapeResult?.success === false) { + } else if ((scrapeResult as any)?.success === false) { // Explicit failure - console.error('[scrape-screenshot] Firecrawl API error:', scrapeResult.error); - throw new Error(scrapeResult.error || 'Failed to capture screenshot'); + console.error('[scrape-screenshot] Firecrawl API error:', (scrapeResult as any).error); + throw new Error((scrapeResult as any).error || 'Failed to capture screenshot'); } else { // No screenshot in response console.error('[scrape-screenshot] No screenshot in response. Full response:', JSON.stringify(scrapeResult, null, 2)); @@ -72,19 +72,10 @@ export async function POST(req: NextRequest) { console.error('[scrape-screenshot] Screenshot capture error:', error); console.error('[scrape-screenshot] Error stack:', error.stack); - // Provide fallback response for development - if (process.env.NODE_ENV === 'development') { - console.warn('[scrape-screenshot] Returning placeholder screenshot for development'); - return NextResponse.json({ - success: true, - screenshot: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', - metadata: { error: 'Screenshot capture failed, using placeholder' } - }); - } + // Provide fallback response for development - removed NODE_ENV check as it doesn't work in Next.js production builds return NextResponse.json({ - error: error.message || 'Failed to capture screenshot', - details: process.env.NODE_ENV === 'development' ? error.stack : undefined + error: error.message || 'Failed to capture screenshot' }, { status: 500 }); } } \ No newline at end of file diff --git a/app/api/scrape-website/route.ts b/app/api/scrape-website/route.ts index 2990b33..af461ea 100644 --- a/app/api/scrape-website/route.ts +++ b/app/api/scrape-website/route.ts @@ -49,23 +49,27 @@ export async function POST(request: NextRequest) { }); // Handle the response according to the latest SDK structure - if (!scrapeResult.success) { - throw new Error(scrapeResult.error || "Failed to scrape website"); + const result = scrapeResult as any; + if (result.success === false) { + throw new Error(result.error || "Failed to scrape website"); } + // The SDK may return data directly or nested + const data = result.data || result; + return NextResponse.json({ success: true, data: { - title: scrapeResult.data?.metadata?.title || "Untitled", - content: scrapeResult.data?.markdown || scrapeResult.data?.html || "", - description: scrapeResult.data?.metadata?.description || "", - markdown: scrapeResult.data?.markdown || "", - html: scrapeResult.data?.html || "", - metadata: scrapeResult.data?.metadata || {}, - screenshot: scrapeResult.data?.screenshot || null, - links: scrapeResult.data?.links || [], + title: data?.metadata?.title || "Untitled", + content: data?.markdown || data?.html || "", + description: data?.metadata?.description || "", + markdown: data?.markdown || "", + html: data?.html || "", + metadata: data?.metadata || {}, + screenshot: data?.screenshot || null, + links: data?.links || [], // Include raw data for flexibility - raw: scrapeResult.data + raw: data } }); @@ -94,7 +98,7 @@ export async function POST(request: NextRequest) { } // Optional: Add OPTIONS handler for CORS if needed -export async function OPTIONS(_request: NextRequest) { +export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: { diff --git a/app/builder/page.tsx b/app/builder/page.tsx index 56c4a2a..48fb724 100644 --- a/app/builder/page.tsx +++ b/app/builder/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; diff --git a/app/generation/GenerationClient.tsx b/app/generation/GenerationClient.tsx new file mode 100644 index 0000000..4a7e197 --- /dev/null +++ b/app/generation/GenerationClient.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { Suspense } from 'react'; +import dynamic from 'next/dynamic'; + +const AISandboxPage = dynamic( + () => import('./page-content'), + { + ssr: false, + loading: () =>
Loading...
+ } +); + +export default function GenerationClient() { + return ( + Loading...}> + + + ); +} \ No newline at end of file diff --git a/app/generation/page-content.tsx b/app/generation/page-content.tsx new file mode 100644 index 0000000..2907723 --- /dev/null +++ b/app/generation/page-content.tsx @@ -0,0 +1,3525 @@ +'use client'; + +import { useState, useEffect, useRef, Suspense } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +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, + 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'; + }; +} + +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); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [targetUrl, setTargetUrl] = useState(''); + const [sidebarScrolled, setSidebarScrolled] = useState(false); + const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null); + const [isStartingNewGeneration, setIsStartingNewGeneration] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [sandboxFiles, setSandboxFiles] = useState>({}); + const [hasInitialSubmission, setHasInitialSubmission] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + 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) { + // If we have a style but no details, set the style as context + 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); + } + + 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 + if (!sandboxData) { + 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'); + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + 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(); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + 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); + break; + + case 'error': + addChatMessage(`Error: ${data.message || data.error || 'Unknown error'}`, 'system'); + 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 waiting message + return remainingContent || 'Waiting for next file...'; + })()} + +
+
+ )} +
+ )} +
+
+ + {/* 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 && ( +
+ {/* Large animated browser URL bar */} +
+
+
+ {/* Browser dots - bigger */} +
+
+
+
+
+ {/* URL bar - bigger */} +
+

+ {targetUrl || homeUrlInput.replace(/^https?:\/\//i, '') || 'example.com'} +

+
+
+
+
+ + {/* Loading animation with skeleton */} +
+ {/* Animated skeleton lines */} +
+
+
+
+
+ + {/* Spinner */} +
+ + {/* 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 ( +
+