From 8687860a473b916f7ec1d97fd24ac785bb4b1233 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:58:36 -0400 Subject: [PATCH] resolve installation issue on vercel sandbox + refine search feature with instruction capabilities --- app/api/apply-ai-code-stream/route.ts | 9 + app/api/apply-ai-code/route.ts | 114 +++++- app/api/restart-vite/route.ts | 73 ++-- app/generation/page.tsx | 388 +++++------------- app/page.tsx | 446 +++++++++++++++------ components/app/generation/SidebarInput.tsx | 49 +-- lib/sandbox/providers/vercel-provider.ts | 161 +++++--- styles/main.css | 53 +++ 8 files changed, 760 insertions(+), 533 deletions(-) diff --git a/app/api/apply-ai-code-stream/route.ts b/app/api/apply-ai-code-stream/route.ts index e94bca9..3812ac0 100644 --- a/app/api/apply-ai-code-stream/route.ts +++ b/app/api/apply-ai-code-stream/route.ts @@ -550,6 +550,15 @@ export async function POST(request: NextRequest) { fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, ''); } + // Fix common Tailwind CSS errors in CSS files + if (file.path.endsWith('.css')) { + // Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist) + fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl'); + // Replace any other non-existent shadow utilities + fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl'); + fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl'); + } + // Create directory if needed const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : ''; if (dirPath) { diff --git a/app/api/apply-ai-code/route.ts b/app/api/apply-ai-code/route.ts index e98cbbb..da2cb37 100644 --- a/app/api/apply-ai-code/route.ts +++ b/app/api/apply-ai-code/route.ts @@ -128,6 +128,7 @@ function parseAIResponse(response: string): ParsedResponse { declare global { var activeSandbox: any; + var activeSandboxProvider: any; var existingFiles: Set; var sandboxState: SandboxState; } @@ -150,8 +151,11 @@ export async function POST(request: NextRequest) { global.existingFiles = new Set(); } + // Get the active sandbox or provider + const sandbox = global.activeSandbox || global.activeSandboxProvider; + // If no active sandbox, just return parsed results - if (!global.activeSandbox) { + if (!sandbox) { return NextResponse.json({ success: true, results: { @@ -167,6 +171,30 @@ export async function POST(request: NextRequest) { }); } + // Verify sandbox is ready before applying code + console.log('[apply-ai-code] Verifying sandbox is ready...'); + + // For Vercel sandboxes, check if Vite is running + if (sandbox.constructor?.name === 'VercelProvider' || sandbox.getSandboxInfo?.()?.provider === 'vercel') { + console.log('[apply-ai-code] Detected Vercel sandbox, checking Vite status...'); + try { + // Check if Vite process is running + const checkResult = await sandbox.runCommand('pgrep -f vite'); + if (!checkResult || !checkResult.stdout) { + console.log('[apply-ai-code] Vite not running, starting it...'); + // Start Vite if not running + await sandbox.runCommand('sh -c "cd /vercel/sandbox && nohup npm run dev > /tmp/vite.log 2>&1 &"'); + // Wait for Vite to start + await new Promise(resolve => setTimeout(resolve, 5000)); + console.log('[apply-ai-code] Vite started, proceeding with code application'); + } else { + console.log('[apply-ai-code] Vite is already running'); + } + } catch (e) { + console.log('[apply-ai-code] Could not check Vite status, proceeding anyway:', e); + } + } + // Apply to active sandbox console.log('[apply-ai-code] Applying code to sandbox...'); console.log('[apply-ai-code] Is edit mode:', isEdit); @@ -336,11 +364,28 @@ export async function POST(request: NextRequest) { fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, ''); } + // Fix common Tailwind CSS errors in CSS files + if (file.path.endsWith('.css')) { + // Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist) + fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl'); + // Replace any other non-existent shadow utilities + fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl'); + fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl'); + } + console.log(`[apply-ai-code] Writing file using E2B files API: ${fullPath}`); try { - // Use the correct E2B API - sandbox.files.write() - await global.activeSandbox.files.write(fullPath, fileContent); + // Check if we're using provider pattern (v2) or direct sandbox (v1) + if (sandbox.writeFile) { + // V2: Provider pattern (Vercel/E2B provider) + await sandbox.writeFile(file.path, fileContent); + } else if (sandbox.files?.write) { + // V1: Direct E2B sandbox + await sandbox.files.write(fullPath, fileContent); + } else { + throw new Error('Unsupported sandbox type'); + } console.log(`[apply-ai-code] Successfully wrote file: ${fullPath}`); // Update file cache @@ -432,10 +477,15 @@ function App() { export default App;`; try { - await global.activeSandbox.writeFiles([{ - path: 'src/App.jsx', - content: Buffer.from(appContent) - }]); + // Use provider pattern if available + if (sandbox.writeFile) { + await sandbox.writeFile('src/App.jsx', appContent); + } else if (sandbox.writeFiles) { + await sandbox.writeFiles([{ + path: 'src/App.jsx', + content: Buffer.from(appContent) + }]); + } console.log('Auto-generated: src/App.jsx'); results.filesCreated.push('src/App.jsx (auto-generated)'); @@ -480,10 +530,15 @@ body { min-height: 100vh; }`; - await global.activeSandbox.writeFiles([{ - path: 'src/index.css', - content: Buffer.from(indexCssContent) - }]); + // Use provider pattern if available + if (sandbox.writeFile) { + await sandbox.writeFile('src/index.css', indexCssContent); + } else if (sandbox.writeFiles) { + await sandbox.writeFiles([{ + path: 'src/index.css', + content: Buffer.from(indexCssContent) + }]); + } console.log('Auto-generated: src/index.css'); results.filesCreated.push('src/index.css (with Tailwind)'); @@ -502,15 +557,38 @@ body { const cmdName = commandParts[0]; const args = commandParts.slice(1); - // Execute command using Vercel Sandbox - const result = await global.activeSandbox.runCommand({ - cmd: cmdName, - args - }); + // Execute command using sandbox + let result; + if (sandbox.runCommand && typeof sandbox.runCommand === 'function') { + // Check if this is a provider pattern sandbox + const testResult = await sandbox.runCommand(cmd); + if (testResult && typeof testResult === 'object' && 'stdout' in testResult) { + // Provider returns CommandResult directly + result = testResult; + } else { + // Direct sandbox - expects object with cmd and args + result = await sandbox.runCommand({ + cmd: cmdName, + args + }); + } + } console.log(`Executed: ${cmd}`); - const stdout = await result.stdout(); - const stderr = await result.stderr(); + + // Handle result based on type + let stdout = ''; + let stderr = ''; + + if (result) { + if (typeof result.stdout === 'string') { + stdout = result.stdout; + stderr = result.stderr || ''; + } else if (typeof result.stdout === 'function') { + stdout = await result.stdout(); + stderr = await result.stderr(); + } + } if (stdout) console.log(stdout); if (stderr) console.log(`Errors: ${stderr}`); diff --git a/app/api/restart-vite/route.ts b/app/api/restart-vite/route.ts index 9472287..6d97bbb 100644 --- a/app/api/restart-vite/route.ts +++ b/app/api/restart-vite/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; declare global { var activeSandbox: any; + var activeSandboxProvider: any; var lastViteRestartTime: number; var viteRestartInProgress: boolean; } @@ -10,7 +11,10 @@ const RESTART_COOLDOWN_MS = 5000; // 5 second cooldown between restarts export async function POST() { try { - if (!global.activeSandbox) { + // Check both v1 and v2 global references + const provider = global.activeSandbox || global.activeSandboxProvider; + + if (!provider) { return NextResponse.json({ success: false, error: 'No active sandbox' @@ -40,45 +44,42 @@ export async function POST() { // Set the restart flag global.viteRestartInProgress = true; - console.log('[restart-vite] Forcing Vite restart...'); + console.log('[restart-vite] Using provider method to restart Vite...'); - // Kill existing Vite processes - try { - await global.activeSandbox.runCommand({ - cmd: 'pkill', - args: ['-f', 'vite'] - }); - console.log('[restart-vite] Killed existing Vite processes'); + // Use the provider's restartViteServer method if available + if (typeof provider.restartViteServer === 'function') { + await provider.restartViteServer(); + console.log('[restart-vite] Vite restarted via provider method'); + } else { + // Fallback to manual restart using provider's runCommand + console.log('[restart-vite] Fallback to manual Vite restart...'); - // Wait a moment for processes to terminate - await new Promise(resolve => setTimeout(resolve, 2000)); - } catch { - console.log('[restart-vite] No existing Vite processes found'); + // Kill existing Vite processes + try { + await provider.runCommand('pkill -f vite'); + console.log('[restart-vite] Killed existing Vite processes'); + + // Wait a moment for processes to terminate + await new Promise(resolve => setTimeout(resolve, 2000)); + } catch { + console.log('[restart-vite] No existing Vite processes found'); + } + + // Clear any error tracking files + try { + await provider.runCommand('bash -c "echo \'{\\"errors\\": [], \\"lastChecked\\": '+ Date.now() +'}\' > /tmp/vite-errors.json"'); + } catch { + // Ignore if this fails + } + + // Start Vite dev server in background + await provider.runCommand('sh -c "nohup npm run dev > /tmp/vite.log 2>&1 &"'); + console.log('[restart-vite] Vite dev server restarted'); + + // Wait for Vite to start up + await new Promise(resolve => setTimeout(resolve, 3000)); } - // Clear any error tracking files - try { - await global.activeSandbox.runCommand({ - cmd: 'bash', - args: ['-c', 'echo \'{"errors": [], "lastChecked": '+ Date.now() +'}\' > /tmp/vite-errors.json'] - }); - } catch { - // Ignore if this fails - } - - // Start Vite dev server in detached mode - // Start Vite dev server in detached mode - await global.activeSandbox.runCommand({ - cmd: 'npm', - args: ['run', 'dev'], - detached: true - }); - - console.log('[restart-vite] Vite dev server restarted'); - - // Wait for Vite to start up - await new Promise(resolve => setTimeout(resolve, 3000)); - // Update global state global.lastViteRestartTime = Date.now(); global.viteRestartInProgress = false; diff --git a/app/generation/page.tsx b/app/generation/page.tsx index c6728fc..9ba4d4e 100644 --- a/app/generation/page.tsx +++ b/app/generation/page.tsx @@ -86,6 +86,7 @@ export default function AISandboxPage() { // eslint-disable-next-line @typescript-eslint/no-unused-vars 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); @@ -93,6 +94,7 @@ export default function AISandboxPage() { 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); @@ -566,25 +568,10 @@ export default function AISandboxPage() { // Fetch sandbox files after creation setTimeout(fetchSandboxFiles, 1000); - // Restart Vite server to ensure it's running - setTimeout(async () => { - try { - console.log('[createSandbox] Ensuring Vite server is running...'); - const restartResponse = await fetch('/api/restart-vite', { - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); - - if (restartResponse.ok) { - const restartData = await restartResponse.json(); - if (restartData.success) { - console.log('[createSandbox] Vite server started successfully'); - } - } - } catch (error) { - console.error('[createSandbox] Error starting Vite server:', error); - } - }, 2000); + // 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) { @@ -624,7 +611,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik } }; - const applyGeneratedCode = async (code: string, isEdit: boolean = false) => { + const applyGeneratedCode = async (code: string, isEdit: boolean = false, overrideSandboxData?: SandboxData) => { setLoading(true); log('Applying AI-generated code...'); @@ -641,6 +628,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik } // 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' }, @@ -648,7 +636,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik response: code, isEdit: isEdit, packages: pendingPackages, - sandboxId: sandboxData?.sandboxId // Pass the sandbox ID to ensure proper connection + sandboxId: effectiveSandboxData?.sandboxId // Pass the sandbox ID to ensure proper connection }) }); @@ -940,11 +928,12 @@ Tip: I automatically detect and install npm packages from your code imports (lik const refreshDelay = appConfig.codeApplication.defaultRefreshDelay; // Allow Vite to process changes setTimeout(() => { - if (iframeRef.current && sandboxData?.url) { + const currentSandboxData = effectiveSandboxData; + if (iframeRef.current && currentSandboxData?.url) { console.log('[home] Refreshing iframe after code application...'); // Method 1: Change src with timestamp - const urlWithTimestamp = `${sandboxData.url}?t=${Date.now()}&applied=true`; + const urlWithTimestamp = `${currentSandboxData.url}?t=${Date.now()}&applied=true`; iframeRef.current.src = urlWithTimestamp; // Method 2: Force reload after a short delay @@ -966,7 +955,8 @@ Tip: I automatically detect and install npm packages from your code imports (lik } // Give Vite HMR a moment to detect changes, then ensure refresh - if (iframeRef.current && sandboxData?.url) { + 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; @@ -974,14 +964,14 @@ Tip: I automatically detect and install npm packages from your code imports (lik console.log(`[applyGeneratedCode] Packages installed: ${packagesInstalled}, refresh delay: ${refreshDelay}ms`); setTimeout(async () => { - if (iframeRef.current && sandboxData?.url) { + 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:', sandboxData.url); + console.log('[applyGeneratedCode] Sandbox URL:', currentSandboxData.url); // Method 1: Try direct navigation first try { - const urlWithTimestamp = `${sandboxData.url}?t=${Date.now()}&force=true`; + const urlWithTimestamp = `${currentSandboxData.url}?t=${Date.now()}&force=true`; console.log('[applyGeneratedCode] Attempting direct navigation to:', urlWithTimestamp); // Remove any existing onload handler @@ -1027,7 +1017,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik iframeRef.current.remove(); // Add new iframe - newIframe.src = `${sandboxData.url}?t=${Date.now()}&recreated=true`; + newIframe.src = `${currentSandboxData.url}?t=${Date.now()}&recreated=true`; parent?.appendChild(newIframe); // Update ref @@ -1520,11 +1510,13 @@ Tip: I automatically detect and install npm packages from your code imports (lik ); } else if (activeTab === 'preview') { - // Only show loading state for initial generation, not for edits + // Show loading state for initial generation or when starting a new generation with existing sandbox const isInitialGeneration = !sandboxData?.url && (urlScreenshot || isCapturingScreenshot || isPreparingDesign || loadingStage); - const shouldShowLoadingOverlay = isInitialGeneration && (loading || generationProgress.isGenerating || isPreparingDesign || loadingStage || isCapturingScreenshot); + const isNewGenerationWithSandbox = isStartingNewGeneration && sandboxData?.url; + const shouldShowLoadingOverlay = (isInitialGeneration || isNewGenerationWithSandbox) && + (loading || generationProgress.isGenerating || isPreparingDesign || loadingStage || isCapturingScreenshot || isStartingNewGeneration); - if (isInitialGeneration) { + if (isInitialGeneration || isNewGenerationWithSandbox) { return (
{/* Screenshot as background when available */} @@ -1533,21 +1525,69 @@ Tip: I automatically detect and install npm packages from your code imports (lik 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...'} +

)} @@ -2049,10 +2089,16 @@ Tip: I automatically detect and install npm packages from your code imports (lik // setLeftPanelVisible(true); // Wait for sandbox creation if it's still in progress + let activeSandboxData = sandboxData; if (sandboxPromise) { addChatMessage('Waiting for sandbox to be ready...', 'system'); try { - await sandboxPromise; + const newSandboxData = await sandboxPromise; + if (newSandboxData) { + activeSandboxData = newSandboxData; + // Also update the state for future use + setSandboxData(newSandboxData); + } // Remove the waiting message setChatMessages(prev => prev.filter(msg => msg.content !== 'Waiting for sandbox to be ready...')); } catch { @@ -2061,9 +2107,16 @@ Tip: I automatically detect and install npm packages from your code imports (lik } } - if (sandboxData && generatedCode) { + if (activeSandboxData && generatedCode) { + // For new sandbox creations (especially Vercel), add a delay to ensure Vite is ready + if (sandboxCreating) { + console.log('[startGeneration] New sandbox created, waiting for services to be ready...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + // Use isEdit flag that was determined at the start - await applyGeneratedCode(generatedCode, isEdit); + // Pass the sandbox data from the promise if it's different from the state + await applyGeneratedCode(generatedCode, isEdit, activeSandboxData !== sandboxData ? activeSandboxData : undefined); } } @@ -2562,6 +2615,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik const data = await response.json(); if (data.success && data.screenshot) { + setIsScreenshotLoaded(false); // Reset loaded state for new screenshot setUrlScreenshot(data.screenshot); // Set preparing design state setIsPreparingDesign(true); @@ -2593,6 +2647,13 @@ Tip: I automatically detect and install npm packages from your code imports (lik setHomeScreenFading(true); + // Set immediate loading state for better UX + setIsStartingNewGeneration(true); + setLoadingStage('gathering'); + + // Immediately switch to preview tab to show loading + setActiveTab('preview'); + // Set loading background to ensure proper visual feedback setShowLoadingBackground(true); @@ -2622,6 +2683,11 @@ Tip: I automatically detect and install npm packages from your code imports (lik setShowHomeScreen(false); setHomeScreenFading(false); + // Clear the starting flag after transition + setTimeout(() => { + setIsStartingNewGeneration(false); + }, 1000); + // Wait for sandbox to be ready (if it's still creating) await sandboxPromise; @@ -2676,6 +2742,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik // Clear preparing design state and switch to generation tab setIsPreparingDesign(false); + setIsScreenshotLoaded(false); // Reset loaded state setUrlScreenshot(null); // Clear screenshot when starting generation setTargetUrl(''); // Clear target URL @@ -2967,11 +3034,13 @@ Focus on the key sections and content, making it clean and modern.`; })); // Clear screenshot and preparing design states to prevent them from showing on next run + setIsScreenshotLoaded(false); // Reset loaded state setUrlScreenshot(null); setIsPreparingDesign(false); setTargetUrl(''); setScreenshotError(null); setLoadingStage(null); // Clear loading stage + setIsStartingNewGeneration(false); // Clear new generation flag setShowLoadingBackground(false); // Clear loading background setTimeout(() => { @@ -2982,6 +3051,8 @@ Focus on the key sections and content, making it clean and modern.`; addChatMessage(`Failed to clone website: ${error.message}`, 'system'); setUrlStatus([]); setIsPreparingDesign(false); + setIsStartingNewGeneration(false); // Clear new generation flag on error + setLoadingStage(null); // Also clear generation progress on error setGenerationProgress(prev => ({ ...prev, @@ -2998,247 +3069,6 @@ Focus on the key sections and content, making it clean and modern.`; return (
- {/* Home Screen Overlay */} - {showHomeScreen && ( -
- {/* Clean Background */} -
-
- - - {/* Close button on hover */} - - - {/* Header */} - - - {/* Main content */} -
-
- {/* Firecrawl-style Header */} -
-

- Open Lovable - Open Lovable -

- - Re-imagine any website, in seconds. - -
- -
-
- { - const value = e.target.value; - setHomeUrlInput(value); - - // Check if it's a valid domain - const domainRegex = /^(https?:\/\/)?(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(\/?.*)?$/; - if (domainRegex.test(value) && value.length > 5) { - // Small delay to make the animation feel smoother - setTimeout(() => setShowStyleSelector(true), 100); - } else { - setShowStyleSelector(false); - setSelectedStyle(null); - } - }} - placeholder=" " - aria-placeholder="https://firecrawl.dev" - className="h-[3.25rem] w-full resize-none focus-visible:outline-none focus-visible:ring-orange-500 focus-visible:ring-2 rounded-[18px] text-sm text-[#36322F] px-4 pr-12 border-[.75px] border-border bg-white" - style={{ - boxShadow: '0 0 0 1px #e3e1de66, 0 1px 2px #5f4a2e14, 0 4px 6px #5f4a2e0a, 0 40px 40px -24px #684b2514', - filter: 'drop-shadow(rgba(249, 224, 184, 0.3) -0.731317px -0.731317px 35.6517px)' - }} - autoFocus - /> - - -
- - {/* Style Selector - Slides out when valid domain is entered */} - {showStyleSelector && ( -
-
-
-

How do you want your site to look?

-
- {[ - { name: 'Neobrutalist', description: 'Bold colors, thick borders' }, - { name: 'Glassmorphism', description: 'Frosted glass effects' }, - { name: 'Minimalist', description: 'Clean and simple' }, - { name: 'Dark Mode', description: 'Dark theme' }, - { name: 'Gradient', description: 'Colorful gradients' }, - { name: 'Retro', description: '80s/90s aesthetic' }, - { name: 'Modern', description: 'Contemporary design' }, - { name: 'Monochrome', description: 'Black and white' } - ].map((style) => ( - - ))} -
- - {/* Additional context input - part of the style selector */} -
- { - if (!selectedStyle) return homeContextInput; - // Extract additional context by removing the style theme part - const additional = homeContextInput.replace(new RegExp('^' + selectedStyle.toLowerCase() + ' theme\\s*,?\\s*', 'i'), ''); - return additional; - })()} - onChange={(e) => { - const additionalContext = e.target.value; - if (selectedStyle) { - setHomeContextInput(selectedStyle.toLowerCase() + ' theme' + (additionalContext.trim() ? ', ' + additionalContext : '')); - } else { - setHomeContextInput(additionalContext); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const form = e.currentTarget.closest('form'); - if (form) { - form.requestSubmit(); - } - } - }} - placeholder="Add more details: specific features, color preferences..." - className="w-full px-4 py-2 text-sm bg-white border border-gray-200 rounded-lg text-gray-900 placeholder-gray-500 focus:outline-none focus:border-orange-300 focus:ring-2 focus:ring-orange-100 transition-all duration-200" - /> -
-
-
-
- )} -
- - {/* Model Selector */} -
- -
-
-
-
- )} -
@@ -3444,7 +3274,7 @@ Focus on the key sections and content, making it clean and modern.`;
) : ( - msg.content + {msg.content} )}
diff --git a/app/page.tsx b/app/page.tsx index 9c883b1..31068c8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -47,6 +47,8 @@ export default function HomePage() { const [hasSearched, setHasSearched] = useState(false); const [isFadingOut, setIsFadingOut] = useState(false); const [showSelectMessage, setShowSelectMessage] = useState(false); + const [showInstructionsForIndex, setShowInstructionsForIndex] = useState(null); + const [additionalInstructions, setAdditionalInstructions] = useState(''); const router = useRouter(); // Simple URL validation @@ -170,10 +172,12 @@ export default function HomePage() { const performSearch = async (searchQuery: string) => { if (!searchQuery.trim() || isURL(searchQuery)) { setSearchResults([]); + setShowSearchTiles(false); return; } setIsSearching(true); + setShowSearchTiles(true); try { const response = await fetch('/api/search', { method: 'POST', @@ -184,6 +188,7 @@ export default function HomePage() { if (response.ok) { const data = await response.json(); setSearchResults(data.results || []); + setShowSearchTiles(true); } } catch (error) { console.error('Search error:', error); @@ -272,80 +277,132 @@ export default function HomePage() { >
- {isURL(url) ? ( - // Scrape icon for URLs - - - - + {/* Show different UI when search results are displayed */} + {hasSearched && searchResults.length > 0 && !isFadingOut ? ( + <> + {/* Selection mode icon */} + + + + + + + + {/* Selection message */} +
+ Select which site to clone from the results below +
+ + {/* Search again button */} + + ) : ( - // Search icon for search terms - - - - + <> + {isURL(url) ? ( + // Scrape icon for URLs + + + + + ) : ( + // Search icon for search terms + + + + + )} + { + const value = e.target.value; + setUrl(value); + setIsValidUrl(validateUrl(value)); + // Reset search state when input changes + if (value.trim() === "") { + setShowSearchTiles(false); + setHasSearched(false); + setSearchResults([]); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !isSearching) { + e.preventDefault(); + handleSubmit(); + } + }} + onFocus={() => { + if (url.trim() && !isURL(url) && searchResults.length > 0) { + setShowSearchTiles(true); + } + }} + /> +
{ + e.preventDefault(); + if (!isSearching) { + handleSubmit(); + } + }} + className={isSearching ? 'pointer-events-none' : ''} + > + 0} + buttonText={isURL(url) ? 'Scrape Site' : 'Search'} + disabled={isSearching} + /> +
+ )} - { - const value = e.target.value; - setUrl(value); - setIsValidUrl(validateUrl(value)); - // Reset search state when input changes - if (value.trim() === "") { - setShowSearchTiles(false); - setHasSearched(false); - setSearchResults([]); - } - }} - onKeyDown={(e) => { - if (e.key === "Enter" && !isSearching) { - e.preventDefault(); - handleSubmit(); - } - }} - onFocus={() => { - if (url.trim() && !isURL(url)) { - setShowSearchTiles(true); - } - }} - onBlur={() => { - setTimeout(() => setShowSearchTiles(false), 200); - }} - /> -
{ - e.preventDefault(); - if (!isSearching) { - handleSubmit(); - } - }} - className={isSearching ? 'pointer-events-none' : ''} - > - 0} - buttonText={isURL(url) ? 'Scrape Site' : 'Search'} - disabled={isSearching} - /> -
@@ -430,61 +487,42 @@ export default function HomePage() {
{isSearching ? ( - // Loading state with animated skeletons + // Loading state with animated scrolling skeletons
{/* Edge fade overlays */}
-
- {[0, 1, 2, 3, 4].map((index) => ( +
+ {/* Duplicate skeleton tiles for continuous scroll */} + {[...Array(10), ...Array(10)].map((_, index) => (
- {/* Fake browser UI */} -
-
-
-
-
+ {/* Fake browser UI - 5x bigger */} +
+
+
+
+
-
+
- {/* Content skeleton */} -
-
-
-
-
+ {/* Content skeleton - positioned just below nav bar */} +
+
+
+
))}
- - {/* Loading text */} -
-
-
-
-
-
-
-
- Searching for sites... -
-
-
) : searchResults.length > 0 ? ( // Actual results @@ -496,11 +534,173 @@ export default function HomePage() {
{/* Duplicate results for infinite scroll */} {[...searchResults, ...searchResults].map((result, index) => ( -