resolve installation issue on vercel sandbox + refine search feature with instruction capabilities

This commit is contained in:
Developers Digest
2025-09-09 15:58:36 -04:00
parent 599e209888
commit 8687860a47
8 changed files with 760 additions and 533 deletions
+9
View File
@@ -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) {
+96 -18
View File
@@ -128,6 +128,7 @@ function parseAIResponse(response: string): ParsedResponse {
declare global {
var activeSandbox: any;
var activeSandboxProvider: any;
var existingFiles: Set<string>;
var sandboxState: SandboxState;
}
@@ -150,8 +151,11 @@ export async function POST(request: NextRequest) {
global.existingFiles = new Set<string>();
}
// 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}`);
+37 -36
View File
@@ -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;
+109 -279
View File
@@ -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<string | null>(null);
const [isScreenshotLoaded, setIsScreenshotLoaded] = useState(false);
const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false);
const [screenshotError, setScreenshotError] = useState<string | null>(null);
const [isPreparingDesign, setIsPreparingDesign] = useState(false);
@@ -93,6 +94,7 @@ export default function AISandboxPage() {
const [targetUrl, setTargetUrl] = useState<string>('');
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<Record<string, string>>({});
const [hasInitialSubmission, setHasInitialSubmission] = useState<boolean>(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
</div>
);
} 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 (
<div className="relative w-full h-full bg-gray-900">
{/* Screenshot as background when available */}
@@ -1533,21 +1525,69 @@ Tip: I automatically detect and install npm packages from your code imports (lik
<img
src={urlScreenshot}
alt="Website preview"
className="absolute inset-0 w-full h-full object-cover"
className="absolute inset-0 w-full h-full object-cover transition-opacity duration-700"
style={{
opacity: isScreenshotLoaded ? 1 : 0,
willChange: 'opacity'
}}
onLoad={() => setIsScreenshotLoaded(true)}
loading="eager"
/>
)}
{/* Loading overlay - only show when actively processing initial generation */}
{shouldShowLoadingOverlay && (
<div className="absolute inset-0 bg-black/70 flex items-center justify-center backdrop-blur-sm">
<div className="text-center">
<div className="absolute inset-0 bg-black/70 flex flex-col items-center justify-center backdrop-blur-sm">
{/* Large animated browser URL bar */}
<div className="w-full max-w-4xl mb-12 px-8 animate-fade-in-up" style={{ animationDelay: '0.2s' }}>
<div className="bg-gray-800/90 rounded-2xl p-6 backdrop-blur-sm border border-gray-700/50 shadow-2xl transform scale-100 animate-pulse-subtle">
<div className="flex items-center gap-4">
{/* Browser dots - bigger */}
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-red-500/70 animate-pulse" style={{ animationDelay: '0s' }} />
<div className="w-5 h-5 rounded-full bg-yellow-500/70 animate-pulse" style={{ animationDelay: '0.1s' }} />
<div className="w-5 h-5 rounded-full bg-green-500/70 animate-pulse" style={{ animationDelay: '0.2s' }} />
</div>
{/* URL bar - bigger */}
<div className="flex-1 bg-gray-900/50 rounded-lg px-6 py-3">
<p className="text-gray-300 text-xl truncate animate-text-shimmer">
{targetUrl || homeUrlInput.replace(/^https?:\/\//i, '') || 'example.com'}
</p>
</div>
</div>
</div>
</div>
{/* Loading animation with skeleton */}
<div className="text-center max-w-md">
{/* Animated skeleton lines */}
<div className="mb-6 space-y-3">
<div className="h-2 bg-gradient-to-r from-transparent via-white/20 to-transparent rounded animate-pulse"
style={{ animationDuration: '1.5s', animationDelay: '0s' }} />
<div className="h-2 bg-gradient-to-r from-transparent via-white/20 to-transparent rounded animate-pulse w-4/5 mx-auto"
style={{ animationDuration: '1.5s', animationDelay: '0.2s' }} />
<div className="h-2 bg-gradient-to-r from-transparent via-white/20 to-transparent rounded animate-pulse w-3/5 mx-auto"
style={{ animationDuration: '1.5s', animationDelay: '0.4s' }} />
</div>
{/* Spinner */}
<div className="w-12 h-12 border-3 border-white/30 border-t-white rounded-full animate-spin mx-auto mb-4" />
{/* Status text */}
<p className="text-white text-lg font-medium">
{isCapturingScreenshot ? 'Analyzing website...' :
isPreparingDesign ? 'Preparing design...' :
generationProgress.isGenerating ? 'Generating code...' :
'Loading...'}
</p>
{/* Subtle progress hint */}
<p className="text-white/60 text-sm mt-2">
{isCapturingScreenshot ? 'Taking a screenshot of the site' :
isPreparingDesign ? 'Understanding the layout and structure' :
generationProgress.isGenerating ? 'Writing React components' :
'Please wait...'}
</p>
</div>
</div>
)}
@@ -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 (
<HeaderProvider>
<div className="font-sans bg-background text-foreground h-screen flex flex-col">
{/* Home Screen Overlay */}
{showHomeScreen && (
<div className={`fixed inset-0 z-50 transition-opacity duration-500 ${homeScreenFading ? 'opacity-0' : 'opacity-100'}`}>
{/* Clean Background */}
<div className="absolute inset-0 bg-white overflow-hidden">
</div>
{/* Close button on hover */}
<button
onClick={() => {
setHomeScreenFading(true);
setTimeout(() => {
setShowHomeScreen(false);
setHomeScreenFading(false);
}, 500);
}}
className="absolute top-8 right-8 text-gray-500 hover:text-gray-700 transition-all duration-300 opacity-0 hover:opacity-100 bg-white/80 backdrop-blur-sm p-2 rounded-lg shadow-sm"
style={{ opacity: 0 }}
onMouseEnter={(e) => e.currentTarget.style.opacity = '0.8'}
onMouseLeave={(e) => e.currentTarget.style.opacity = '0'}
>
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Header */}
<div className="absolute top-0 left-0 right-0 z-20 px-6 py-4 flex items-center justify-between animate-[fadeIn_0.8s_ease-out]">
<HeaderBrandKit />
<a
href="https://github.com/mendableai/open-lovable"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-6 px-6 py-8 rounded-8 text-label-medium font-medium text-accent-black hover:bg-black-alpha-4 active:bg-black-alpha-6 transition-all duration-200"
>
<FiGithub style={{ width: '16px', height: '16px' }} />
<span>Use this Template</span>
</a>
</div>
{/* Main content */}
<div className="relative z-10 h-full flex items-center justify-center px-4">
<div className="text-center max-w-4xl min-w-[600px] mx-auto">
{/* Firecrawl-style Header */}
<div className="text-center">
<h1 className="text-[2.5rem] lg:text-[3.8rem] text-center text-[#36322F] font-semibold tracking-tight leading-[0.9] animate-[fadeIn_0.8s_ease-out]">
<span className="hidden md:inline">Open Lovable</span>
<span className="md:hidden">Open Lovable</span>
</h1>
<motion.p
className="text-base lg:text-lg max-w-lg mx-auto mt-2.5 text-zinc-500 text-center text-balance"
animate={{
opacity: showStyleSelector ? 0.7 : 1
}}
transition={{ duration: 0.3, ease: "easeOut" }}
>
Re-imagine any website, in seconds.
</motion.p>
</div>
<form onSubmit={handleHomeScreenSubmit} className="mt-5 max-w-3xl mx-auto">
<div className="w-full relative group">
<input
type="text"
value={homeUrlInput}
onChange={(e) => {
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
/>
<div
aria-hidden="true"
className={`absolute top-1/2 -translate-y-1/2 left-4 pointer-events-none text-sm text-opacity-50 text-start transition-opacity ${
homeUrlInput ? 'opacity-0' : 'opacity-100'
}`}
>
<span className="text-[#605A57]/50" style={{ fontFamily: 'monospace' }}>
https://firecrawl.dev
</span>
</div>
<button
type="submit"
disabled={!homeUrlInput.trim()}
className="absolute top-1/2 transform -translate-y-1/2 right-2 flex h-10 items-center justify-center rounded-md px-3 text-sm font-medium text-zinc-500 hover:text-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title={selectedStyle ? `Clone with ${selectedStyle} Style` : 'Clone Website'}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 10 4 15 9 20"></polyline>
<path d="M20 4v7a4 4 0 0 1-4 4H4"></path>
</svg>
</button>
</div>
{/* Style Selector - Slides out when valid domain is entered */}
{showStyleSelector && (
<div className="overflow-hidden mt-4">
<div className={`transition-all duration-500 ease-out transform ${
showStyleSelector ? 'translate-y-0 opacity-100' : '-translate-y-4 opacity-0'
}`}>
<div className="bg-white/80 backdrop-blur-sm border border-gray-200 rounded-xl p-4 shadow-sm">
<p className="text-sm text-gray-600 mb-3 font-medium">How do you want your site to look?</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{[
{ 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) => (
<button
key={style.name}
type="button"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
// Submit the form
const form = e.currentTarget.closest('form');
if (form) {
form.requestSubmit();
}
}
}}
onClick={() => {
if (selectedStyle === style.name) {
// Deselect if clicking the same style
setSelectedStyle(null);
// Keep only additional context, remove the style theme part
const currentAdditional = homeContextInput.replace(/^[^,]+theme\s*,?\s*/, '').trim();
setHomeContextInput(currentAdditional);
} else {
// Select new style
setSelectedStyle(style.name);
// Extract any additional context (everything after the style theme)
const currentAdditional = homeContextInput.replace(/^[^,]+theme\s*,?\s*/, '').trim();
setHomeContextInput(style.name.toLowerCase() + ' theme' + (currentAdditional ? ', ' + currentAdditional : ''));
}
}}
className={`p-3 rounded-lg border transition-all ${
selectedStyle === style.name
? 'border-orange-400 bg-orange-50 text-gray-900 shadow-sm'
: 'border-gray-200 bg-white hover:border-orange-200 hover:bg-orange-50/50 text-gray-700'
}`}
>
<div className="text-sm font-medium">{style.name}</div>
<div className="text-xs text-gray-500 mt-1">{style.description}</div>
</button>
))}
</div>
{/* Additional context input - part of the style selector */}
<div className="mt-4 mb-2">
<input
type="text"
value={(() => {
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"
/>
</div>
</div>
</div>
</div>
)}
</form>
{/* Model Selector */}
<div className="mt-6 flex items-center justify-center animate-[fadeIn_1s_ease-out]">
<select
value={aiModel}
onChange={(e) => {
const newModel = e.target.value;
setAiModel(newModel);
const params = new URLSearchParams(searchParams);
params.set('model', newModel);
if (sandboxData?.sandboxId) {
params.set('sandbox', sandboxData.sandboxId);
}
router.push(`/generation?${params.toString()}`);
}}
className="px-3 py-1.5 text-sm bg-white border border-gray-300 rounded-[10px] focus:outline-none focus:ring-2 focus:ring-[#36322F] focus:border-transparent"
style={{
boxShadow: '0 0 0 1px #e3e1de66, 0 1px 2px #5f4a2e14'
}}
>
{appConfig.ai.availableModels.map(model => (
<option key={model} value={model}>
{model.includes('claude') ? `Claude ${model.split('-')[2]}` :
model.includes('gpt') ? `GPT-${model.split('-')[1]}` : model}
</option>
))}
</select>
</div>
</div>
</div>
</div>
)}
<div className="bg-white py-[15px] py-[8px] border-b border-border-faint flex items-center justify-between shadow-sm">
<HeaderBrandKit />
<div className="flex items-center gap-2">
@@ -3444,7 +3274,7 @@ Focus on the key sections and content, making it clean and modern.`;
</div>
</div>
) : (
msg.content
<span className="text-body-input">{msg.content}</span>
)}
</div>
+333 -113
View File
@@ -47,6 +47,8 @@ export default function HomePage() {
const [hasSearched, setHasSearched] = useState<boolean>(false);
const [isFadingOut, setIsFadingOut] = useState<boolean>(false);
const [showSelectMessage, setShowSelectMessage] = useState<boolean>(false);
const [showInstructionsForIndex, setShowInstructionsForIndex] = useState<number | null>(null);
const [additionalInstructions, setAdditionalInstructions] = useState<string>('');
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() {
>
<div className="p-16 flex gap-12 items-center w-full relative bg-white rounded-20">
{isURL(url) ? (
// Scrape icon for URLs
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40 flex-shrink-0"
>
<rect x="3" y="3" width="14" height="14" rx="2" stroke="currentColor" strokeWidth="1.5"/>
<path d="M7 10L9 12L13 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
{/* Show different UI when search results are displayed */}
{hasSearched && searchResults.length > 0 && !isFadingOut ? (
<>
{/* Selection mode icon */}
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40 flex-shrink-0"
>
<rect x="2" y="4" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
<rect x="11" y="4" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
<rect x="2" y="11" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
<rect x="11" y="11" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
</svg>
{/* Selection message */}
<div className="flex-1 text-body-input text-accent-black">
Select which site to clone from the results below
</div>
{/* Search again button */}
<button
onClick={(e) => {
e.preventDefault();
setIsFadingOut(true);
setTimeout(() => {
setSearchResults([]);
setHasSearched(false);
setShowSearchTiles(false);
setIsFadingOut(false);
setUrl('');
}, 500);
}}
className="button relative rounded-10 px-12 py-8 text-label-medium font-medium flex items-center justify-center gap-6 bg-gray-100 hover:bg-gray-200 text-gray-700 active:scale-[0.995] transition-all"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-60"
>
<path d="M14 14L10 10M11 6.5C11 9 9 11 6.5 11C4 11 2 9 2 6.5C2 4 4 2 6.5 2C9 2 11 4 11 6.5Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
<span>Search Again</span>
</button>
</>
) : (
// Search icon for search terms
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40 flex-shrink-0"
>
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" strokeWidth="1.5"/>
<path d="M12.5 12.5L16.5 16.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
<>
{isURL(url) ? (
// Scrape icon for URLs
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40 flex-shrink-0"
>
<rect x="3" y="3" width="14" height="14" rx="2" stroke="currentColor" strokeWidth="1.5"/>
<path d="M7 10L9 12L13 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
) : (
// Search icon for search terms
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40 flex-shrink-0"
>
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" strokeWidth="1.5"/>
<path d="M12.5 12.5L16.5 16.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
)}
<input
className="flex-1 bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48 focus:outline-none focus:ring-0 focus:border-transparent"
placeholder="Enter URL or search term..."
type="text"
value={url}
disabled={isSearching}
onChange={(e) => {
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);
}
}}
/>
<div
onClick={(e) => {
e.preventDefault();
if (!isSearching) {
handleSubmit();
}
}}
className={isSearching ? 'pointer-events-none' : ''}
>
<HeroInputSubmitButton
dirty={url.length > 0}
buttonText={isURL(url) ? 'Scrape Site' : 'Search'}
disabled={isSearching}
/>
</div>
</>
)}
<input
className="flex-1 bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48 focus:outline-none focus:ring-0 focus:border-transparent"
placeholder="Enter URL or search term..."
type="text"
value={url}
disabled={isSearching}
onChange={(e) => {
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);
}}
/>
<div
onClick={(e) => {
e.preventDefault();
if (!isSearching) {
handleSubmit();
}
}}
className={isSearching ? 'pointer-events-none' : ''}
>
<HeroInputSubmitButton
dirty={url.length > 0}
buttonText={isURL(url) ? 'Scrape Site' : 'Search'}
disabled={isSearching}
/>
</div>
</div>
@@ -430,61 +487,42 @@ export default function HomePage() {
<div className="absolute inset-0 bg-gradient-to-b from-gray-50/50 to-white rounded-[50%] transform scale-x-150 -translate-y-24" />
{isSearching ? (
// Loading state with animated skeletons
// Loading state with animated scrolling skeletons
<div className="relative h-[250px] overflow-hidden">
{/* Edge fade overlays */}
<div className="absolute left-0 top-0 bottom-0 w-[120px] z-20 pointer-events-none" style={{background: 'linear-gradient(to right, white 0%, white 20%, transparent 100%)'}} />
<div className="absolute right-0 top-0 bottom-0 w-[120px] z-20 pointer-events-none" style={{background: 'linear-gradient(to left, white 0%, white 20%, transparent 100%)'}} />
<div className="flex gap-12 py-4 px-8">
{[0, 1, 2, 3, 4].map((index) => (
<div className="carousel-container absolute left-0 flex gap-12 py-4">
{/* Duplicate skeleton tiles for continuous scroll */}
{[...Array(10), ...Array(10)].map((_, index) => (
<div
key={`loading-${index}`}
className="flex-shrink-0 w-[400px] h-[240px] rounded-24 overflow-hidden border-2 border-gray-200/30 bg-white relative"
style={{
animation: `fadeIn 0.5s ease-out forwards`,
animationDelay: `${index * 100}ms`,
opacity: 0
}}
className="flex-shrink-0 w-[400px] h-[240px] rounded-lg overflow-hidden border-2 border-gray-200/30 bg-white relative"
>
<div className="absolute inset-0 skeleton-shimmer">
<div className="absolute inset-0 bg-gradient-to-r from-gray-100 via-gray-50 to-gray-100 skeleton-gradient" />
</div>
{/* Fake browser UI */}
<div className="absolute top-0 left-0 right-0 h-8 bg-gray-100 border-b border-gray-200/50 flex items-center px-3 gap-2">
<div className="flex gap-1">
<div className="w-2 h-2 rounded-full bg-gray-300" />
<div className="w-2 h-2 rounded-full bg-gray-300" />
<div className="w-2 h-2 rounded-full bg-gray-300" />
{/* Fake browser UI - 5x bigger */}
<div className="absolute top-0 left-0 right-0 h-40 bg-gray-100 border-b border-gray-200/50 flex items-center px-6 gap-4">
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" />
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" style={{ animationDelay: '0.1s' }} />
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" style={{ animationDelay: '0.2s' }} />
</div>
<div className="flex-1 h-4 bg-gray-200 rounded-sm mx-4" />
<div className="flex-1 h-8 bg-gray-200 rounded-md mx-6 animate-pulse" />
</div>
{/* Content skeleton */}
<div className="p-4 mt-8">
<div className="h-6 bg-gray-200 rounded w-3/4 mb-3" />
<div className="h-4 bg-gray-150 rounded w-full mb-2" />
<div className="h-4 bg-gray-150 rounded w-5/6 mb-2" />
<div className="h-4 bg-gray-150 rounded w-4/6" />
{/* Content skeleton - positioned just below nav bar */}
<div className="absolute top-44 left-4 right-4">
<div className="h-3 bg-gray-200 rounded w-3/4 mb-2 animate-pulse" />
<div className="h-3 bg-gray-150 rounded w-1/2 mb-2 animate-pulse" style={{ animationDelay: '0.2s' }} />
<div className="h-3 bg-gray-150 rounded w-2/3 animate-pulse" style={{ animationDelay: '0.3s' }} />
</div>
</div>
))}
</div>
{/* Loading text */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="bg-white/95 backdrop-blur-sm rounded-full px-6 py-3 shadow-lg border border-gray-200">
<div className="flex items-center gap-3">
<div className="flex gap-1">
<div className="w-2 h-2 bg-orange-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<div className="w-2 h-2 bg-orange-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<div className="w-2 h-2 bg-orange-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
<span className="text-sm font-medium text-gray-700">Searching for sites...</span>
</div>
</div>
</div>
</div>
) : searchResults.length > 0 ? (
// Actual results
@@ -496,11 +534,173 @@ export default function HomePage() {
<div className="carousel-container absolute left-0 flex gap-12 py-4">
{/* Duplicate results for infinite scroll */}
{[...searchResults, ...searchResults].map((result, index) => (
<button
<div
key={`${result.url}-${index}`}
onClick={() => handleSubmit(result)}
className="flex-shrink-0 w-[400px] h-[240px] rounded-24 overflow-hidden border-2 border-gray-200/50 hover:border-orange-500 transition-all duration-300 hover:shadow-2xl hover:scale-[1.02] cursor-pointer bg-white"
className="group flex-shrink-0 w-[400px] h-[240px] rounded-lg overflow-hidden border-2 border-gray-200/50 transition-all duration-300 hover:shadow-2xl bg-white relative"
onMouseLeave={() => {
if (showInstructionsForIndex === index) {
setShowInstructionsForIndex(null);
setAdditionalInstructions('');
}
}}
>
{/* Hover overlay with clone buttons or instructions input */}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/50 to-black/30 opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-10 flex flex-col items-center justify-center p-6">
{showInstructionsForIndex === index ? (
/* Instructions input view - matching main input style exactly */
<div className="w-full max-w-[380px]">
<div className="bg-white rounded-20" style={{
boxShadow: "0px 0px 44px 0px rgba(0, 0, 0, 0.02), 0px 88px 56px -20px rgba(0, 0, 0, 0.03), 0px 56px 56px -20px rgba(0, 0, 0, 0.02), 0px 32px 32px -20px rgba(0, 0, 0, 0.03), 0px 16px 24px -12px rgba(0, 0, 0, 0.03), 0px 0px 0px 1px rgba(0, 0, 0, 0.05)"
}}>
{/* Input area matching main search */}
<div className="p-16 flex gap-12 items-start w-full relative">
{/* Instructions icon */}
<div className="mt-2 flex-shrink-0">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40"
>
<path d="M5 5H15M5 10H15M5 15H10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</div>
<textarea
value={additionalInstructions}
onChange={(e) => setAdditionalInstructions(e.target.value)}
placeholder="Describe your customizations..."
className="flex-1 bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48 focus:outline-none focus:ring-0 focus:border-transparent resize-none min-h-[60px]"
autoFocus
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.stopPropagation();
setShowInstructionsForIndex(null);
setAdditionalInstructions('');
}
}}
/>
</div>
{/* Divider */}
<div className="border-t border-black-alpha-5" />
{/* Buttons area matching main style */}
<div className="p-10 flex justify-between items-center">
<button
onClick={(e) => {
e.stopPropagation();
setShowInstructionsForIndex(null);
setAdditionalInstructions('');
}}
className="button relative rounded-10 px-8 py-8 text-label-medium font-medium flex items-center justify-center bg-black-alpha-4 hover:bg-black-alpha-6 text-black-alpha-48 active:scale-[0.995] transition-all"
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 5L7 10L12 15" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
<button
onClick={(e) => {
e.stopPropagation();
if (additionalInstructions.trim()) {
sessionStorage.setItem('additionalInstructions', additionalInstructions);
handleSubmit(result);
}
}}
disabled={!additionalInstructions.trim()}
className={`
button relative rounded-10 px-8 py-8 text-label-medium font-medium
flex items-center justify-center gap-6
${additionalInstructions.trim()
? 'button-primary text-accent-white active:scale-[0.995]'
: 'bg-black-alpha-4 text-black-alpha-24 cursor-not-allowed'
}
`}
>
{additionalInstructions.trim() && <div className="button-background absolute inset-0 rounded-10 pointer-events-none" />}
<span className="px-6 relative">Apply & Clone</span>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="relative"
>
<path d="M11.6667 4.79163L16.875 9.99994M16.875 9.99994L11.6667 15.2083M16.875 9.99994H3.125" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"/>
</svg>
</button>
</div>
</div>
</div>
) : (
/* Default buttons view */
<>
<div className="text-white text-center mb-3">
<p className="text-base font-semibold mb-0.5">{result.title}</p>
<p className="text-[11px] opacity-80">Choose how to clone this site</p>
</div>
<div className="flex gap-3 justify-center">
{/* Instant Clone Button - Orange/Heat style */}
<button
onClick={(e) => {
e.stopPropagation();
handleSubmit(result);
}}
className="bg-orange-500 hover:bg-orange-600 flex items-center justify-center button relative text-label-medium button-primary group/button rounded-10 p-8 gap-2 text-white active:scale-[0.995]"
>
<div className="button-background absolute inset-0 rounded-10 pointer-events-none" />
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="relative"
>
<path d="M11.6667 4.79163L16.875 9.99994M16.875 9.99994L11.6667 15.2083M16.875 9.99994H3.125" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"/>
</svg>
<span className="px-6 relative">Instant Clone</span>
</button>
{/* Instructions Button - Gray style */}
<button
onClick={(e) => {
e.stopPropagation();
setShowInstructionsForIndex(index);
setAdditionalInstructions('');
}}
className="bg-gray-100 hover:bg-gray-200 flex items-center justify-center button relative text-label-medium rounded-10 p-8 gap-2 text-gray-700 active:scale-[0.995]"
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-60"
>
<path d="M5 5H15M5 10H15M5 15H10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M14 14L16 16L14 18" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span className="px-6">Add Instructions</span>
</button>
</div>
</>
)}
</div>
{result.screenshot ? (
<img
src={result.screenshot}
@@ -509,9 +709,29 @@ export default function HomePage() {
loading="lazy"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-gray-100 to-gray-50" />
<div className="w-full h-full bg-gradient-to-br from-gray-100 to-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 rounded-full bg-gray-200 mx-auto mb-3 flex items-center justify-center">
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-gray-400"
>
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="1.5"/>
<path d="M3 9H21" stroke="currentColor" strokeWidth="1.5"/>
<circle cx="6" cy="6" r="1" fill="currentColor"/>
<circle cx="9" cy="6" r="1" fill="currentColor"/>
<circle cx="12" cy="6" r="1" fill="currentColor"/>
</svg>
</div>
<p className="text-gray-500 text-sm font-medium">{result.title}</p>
</div>
</div>
)}
</button>
</div>
))}
</div>
</div>
+12 -37
View File
@@ -44,9 +44,9 @@ export default function SidebarInput({ onSubmit, disabled = false }: SidebarInpu
const handleSubmit = (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (!url.trim() || disabled) return;
onSubmit(url.trim(), selectedStyle, selectedModel, additionalInstructions || undefined);
// Reset form
setUrl("");
setAdditionalInstructions("");
@@ -57,37 +57,12 @@ export default function SidebarInput({ onSubmit, disabled = false }: SidebarInpu
<div className="w-full">
<div >
<div className="p-4 border-b border-gray-100">
{/* URL Input */}
<div className="flex gap-3 items-center">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="opacity-40 flex-shrink-0"
>
<rect x="3" y="3" width="14" height="14" rx="2" stroke="currentColor" strokeWidth="1.5"/>
<path d="M7 10L9 12L13 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<input
className="flex-1 bg-transparent text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none"
placeholder="Enter URL to scrape..."
type="text"
value={url}
disabled={disabled}
onChange={(e) => {
setUrl(e.target.value);
setIsValidUrl(validateUrl(e.target.value));
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSubmit();
}
}}
/>
</div>
{/* link to home page with button */}
<a href="/">
<button className="w-full px-3 py-2 text-xs font-medium text-gray-700 bg-white rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500">
Generate a new website
</button>
</a>
</div>
{/* Options Section - Show when valid URL */}
@@ -104,8 +79,8 @@ export default function SidebarInput({ onSubmit, disabled = false }: SidebarInpu
disabled={disabled}
className={`
py-2 px-2 rounded text-xs font-medium border transition-all text-center
${selectedStyle === style.id
? 'border-orange-500 bg-orange-50 text-orange-900'
${selectedStyle === style.id
? 'border-orange-500 bg-orange-50 text-orange-900'
: 'border-gray-200 hover:border-gray-300 bg-white text-gray-700'
}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
@@ -133,7 +108,7 @@ export default function SidebarInput({ onSubmit, disabled = false }: SidebarInpu
))}
</select>
</div>
{/* Additional Instructions */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-2">Additional Instructions (optional)</label>
@@ -155,7 +130,7 @@ export default function SidebarInput({ onSubmit, disabled = false }: SidebarInpu
className={`
w-full py-2.5 px-4 rounded-lg text-sm font-medium transition-all
${isValidUrl && !disabled
? 'bg-orange-500 hover:bg-orange-600 text-white'
? 'bg-orange-500 hover:bg-orange-600 text-white'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}
`}
+111 -50
View File
@@ -35,23 +35,13 @@ export class VercelProvider extends SandboxProvider {
sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID;
sandboxConfig.token = process.env.VERCEL_TOKEN;
} else if (process.env.VERCEL_OIDC_TOKEN) {
} else {
sandboxConfig.oidcToken = process.env.VERCEL_OIDC_TOKEN;
}
runtime: sandboxConfig.runtime,
timeout: sandboxConfig.timeout,
ports: sandboxConfig.ports,
hasTeamId: !!sandboxConfig.teamId,
hasProjectId: !!sandboxConfig.projectId,
hasToken: !!sandboxConfig.token
});
this.sandbox = await Sandbox.create(sandboxConfig);
const sandboxId = this.sandbox.sandboxId;
sandboxId: sandboxId,
status: this.sandbox.status
});
// Sandbox created successfully
// Get the sandbox URL using the correct Vercel Sandbox API
const sandboxUrl = this.sandbox.domain(5173);
@@ -91,9 +81,33 @@ export class VercelProvider extends SandboxProvider {
env: {}
});
// Handle stdout and stderr - they might be functions in Vercel SDK
let stdout = '';
let stderr = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
try {
if (typeof result.stderr === 'function') {
stderr = await result.stderr();
} else {
stderr = result.stderr || '';
}
} catch (e) {
stderr = '';
}
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
stdout: stdout,
stderr: stderr,
exitCode: result.exitCode || 0,
success: result.exitCode === 0
};
@@ -115,21 +129,12 @@ export class VercelProvider extends SandboxProvider {
// Vercel sandbox default working directory is /vercel/sandbox
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
originalPath: path,
fullPath: fullPath,
contentLength: content.length,
contentPreview: content.substring(0, 100) + (content.length > 100 ? '...' : ''),
sandboxId: this.sandbox.sandboxId,
sandboxStatus: this.sandbox.status
});
// Writing file to sandbox
// Based on Vercel SDK docs, writeFiles expects path and Buffer content
try {
const buffer = Buffer.from(content, 'utf-8');
path: fullPath,
bufferLength: buffer.length,
isBuffer: Buffer.isBuffer(buffer)
});
// Writing file with buffer
await this.sandbox.writeFiles([{
path: fullPath,
@@ -148,6 +153,7 @@ export class VercelProvider extends SandboxProvider {
});
// Fallback to command-based approach if writeFiles fails
// Falling back to command-based file write
// Ensure directory exists
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
@@ -156,10 +162,7 @@ export class VercelProvider extends SandboxProvider {
cmd: 'mkdir',
args: ['-p', dir]
});
exitCode: mkdirResult.exitCode,
stdout: mkdirResult.stdout,
stderr: mkdirResult.stderr
});
// Directory created
}
// Write file using echo and redirection
@@ -175,10 +178,7 @@ export class VercelProvider extends SandboxProvider {
args: ['-c', `echo "${escapedContent}" > "${fullPath}"`]
});
exitCode: writeResult.exitCode,
stdout: writeResult.stdout,
stderr: writeResult.stderr
});
// File written
if (writeResult.exitCode === 0) {
this.existingFiles.add(path);
@@ -201,11 +201,35 @@ export class VercelProvider extends SandboxProvider {
args: [fullPath]
});
if (result.exitCode !== 0) {
throw new Error(`Failed to read file: ${result.stderr}`);
// Handle stdout and stderr - they might be functions in Vercel SDK
let stdout = '';
let stderr = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
return result.stdout || '';
try {
if (typeof result.stderr === 'function') {
stderr = await result.stderr();
} else {
stderr = result.stderr || '';
}
} catch (e) {
stderr = '';
}
if (result.exitCode !== 0) {
throw new Error(`Failed to read file: ${stderr}`);
}
return stdout;
}
async listFiles(directory: string = '/vercel/sandbox'): Promise<string[]> {
@@ -219,11 +243,24 @@ export class VercelProvider extends SandboxProvider {
cwd: '/'
});
// Handle stdout - it might be a function in Vercel SDK
let stdout = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
if (result.exitCode !== 0) {
return [];
}
return (result.stdout || '').split('\n').filter((line: string) => line.trim() !== '');
return stdout.split('\n').filter((line: string) => line.trim() !== '');
}
async installPackages(packages: string[]): Promise<CommandResult> {
@@ -233,6 +270,7 @@ export class VercelProvider extends SandboxProvider {
const flags = process.env.NPM_FLAGS || '';
// Installing packages
// Build args array
const args = ['install'];
@@ -247,14 +285,38 @@ export class VercelProvider extends SandboxProvider {
cwd: '/vercel/sandbox'
});
// Handle stdout and stderr - they might be functions in Vercel SDK
let stdout = '';
let stderr = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
try {
if (typeof result.stderr === 'function') {
stderr = await result.stderr();
} else {
stderr = result.stderr || '';
}
} catch (e) {
stderr = '';
}
// Restart Vite if configured and successful
if (result.exitCode === 0 && process.env.AUTO_RESTART_VITE === 'true') {
await this.restartViteServer();
}
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
stdout: stdout,
stderr: stderr,
exitCode: result.exitCode || 0,
success: result.exitCode === 0
};
@@ -265,19 +327,14 @@ export class VercelProvider extends SandboxProvider {
throw new Error('No active sandbox');
}
sandboxId: this.sandbox.sandboxId,
status: this.sandbox.status
});
// Setting up Vite app for sandbox
// Create directory structure
const mkdirResult = await this.sandbox.runCommand({
cmd: 'mkdir',
args: ['-p', '/vercel/sandbox/src']
});
exitCode: mkdirResult.exitCode,
stdout: mkdirResult.stdout,
stderr: mkdirResult.stderr
});
// Directory structure created
// Create package.json
const packageJson = {
@@ -413,6 +470,7 @@ body {
await this.writeFile('src/index.css', indexCss);
// Installing npm dependencies
// Install dependencies
try {
@@ -422,12 +480,10 @@ body {
cwd: '/vercel/sandbox'
});
exitCode: installResult.exitCode,
stdout: typeof installResult.stdout === 'function' ? 'function' : installResult.stdout,
stderr: typeof installResult.stderr === 'function' ? 'function' : installResult.stderr
});
// npm install completed
if (installResult.exitCode === 0) {
// Dependencies installed successfully
} else {
console.warn('[VercelProvider] npm install had issues:', installResult.stderr);
}
@@ -445,6 +501,7 @@ body {
cwd: '/vercel/sandbox'
});
if (altResult.exitCode === 0) {
// Alternative npm install succeeded
} else {
console.warn('[VercelProvider] Alternative npm install also had issues:', altResult.stderr);
}
@@ -455,6 +512,7 @@ body {
}
// Start Vite dev server
// Starting Vite dev server
// Kill any existing Vite processes
await this.sandbox.runCommand({
@@ -470,6 +528,7 @@ body {
cwd: '/vercel/sandbox'
});
// Vite server started in background
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, 7000));
@@ -490,6 +549,7 @@ body {
throw new Error('No active sandbox');
}
// Restarting Vite server
// Kill existing Vite process
await this.sandbox.runCommand({
@@ -508,6 +568,7 @@ body {
cwd: '/vercel/sandbox'
});
// Vite server started in background
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, 7000));
+53
View File
@@ -216,6 +216,59 @@
transform: translateX(-50%);
}
}
/* Loading state animations */
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse-subtle {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.95;
}
}
@keyframes text-shimmer {
0% {
background-position: -200% center;
}
100% {
background-position: 200% center;
}
}
.animate-fade-in-up {
animation: fade-in-up 0.6s ease-out forwards;
opacity: 0;
}
.animate-pulse-subtle {
animation: pulse-subtle 2s ease-in-out infinite;
}
.animate-text-shimmer {
background: linear-gradient(
90deg,
rgb(156 163 175) 0%,
rgb(229 231 235) 50%,
rgb(156 163 175) 100%
);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: text-shimmer 3s linear infinite;
}
}
.grecaptcha-badge { visibility: hidden; }