resolve installation issue on vercel sandbox + refine search feature with instruction capabilities
This commit is contained in:
@@ -550,6 +550,15 @@ export async function POST(request: NextRequest) {
|
|||||||
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
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
|
// Create directory if needed
|
||||||
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
|
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
|
||||||
if (dirPath) {
|
if (dirPath) {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandbox: any;
|
||||||
|
var activeSandboxProvider: any;
|
||||||
var existingFiles: Set<string>;
|
var existingFiles: Set<string>;
|
||||||
var sandboxState: SandboxState;
|
var sandboxState: SandboxState;
|
||||||
}
|
}
|
||||||
@@ -150,8 +151,11 @@ export async function POST(request: NextRequest) {
|
|||||||
global.existingFiles = new Set<string>();
|
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 no active sandbox, just return parsed results
|
||||||
if (!global.activeSandbox) {
|
if (!sandbox) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
results: {
|
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
|
// Apply to active sandbox
|
||||||
console.log('[apply-ai-code] Applying code to sandbox...');
|
console.log('[apply-ai-code] Applying code to sandbox...');
|
||||||
console.log('[apply-ai-code] Is edit mode:', isEdit);
|
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, '');
|
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}`);
|
console.log(`[apply-ai-code] Writing file using E2B files API: ${fullPath}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the correct E2B API - sandbox.files.write()
|
// Check if we're using provider pattern (v2) or direct sandbox (v1)
|
||||||
await global.activeSandbox.files.write(fullPath, fileContent);
|
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}`);
|
console.log(`[apply-ai-code] Successfully wrote file: ${fullPath}`);
|
||||||
|
|
||||||
// Update file cache
|
// Update file cache
|
||||||
@@ -432,10 +477,15 @@ function App() {
|
|||||||
export default App;`;
|
export default App;`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.writeFiles([{
|
// Use provider pattern if available
|
||||||
path: 'src/App.jsx',
|
if (sandbox.writeFile) {
|
||||||
content: Buffer.from(appContent)
|
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');
|
console.log('Auto-generated: src/App.jsx');
|
||||||
results.filesCreated.push('src/App.jsx (auto-generated)');
|
results.filesCreated.push('src/App.jsx (auto-generated)');
|
||||||
@@ -480,10 +530,15 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
await global.activeSandbox.writeFiles([{
|
// Use provider pattern if available
|
||||||
path: 'src/index.css',
|
if (sandbox.writeFile) {
|
||||||
content: Buffer.from(indexCssContent)
|
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');
|
console.log('Auto-generated: src/index.css');
|
||||||
results.filesCreated.push('src/index.css (with Tailwind)');
|
results.filesCreated.push('src/index.css (with Tailwind)');
|
||||||
@@ -502,15 +557,38 @@ body {
|
|||||||
const cmdName = commandParts[0];
|
const cmdName = commandParts[0];
|
||||||
const args = commandParts.slice(1);
|
const args = commandParts.slice(1);
|
||||||
|
|
||||||
// Execute command using Vercel Sandbox
|
// Execute command using sandbox
|
||||||
const result = await global.activeSandbox.runCommand({
|
let result;
|
||||||
cmd: cmdName,
|
if (sandbox.runCommand && typeof sandbox.runCommand === 'function') {
|
||||||
args
|
// 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}`);
|
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 (stdout) console.log(stdout);
|
||||||
if (stderr) console.log(`Errors: ${stderr}`);
|
if (stderr) console.log(`Errors: ${stderr}`);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandbox: any;
|
||||||
|
var activeSandboxProvider: any;
|
||||||
var lastViteRestartTime: number;
|
var lastViteRestartTime: number;
|
||||||
var viteRestartInProgress: boolean;
|
var viteRestartInProgress: boolean;
|
||||||
}
|
}
|
||||||
@@ -10,7 +11,10 @@ const RESTART_COOLDOWN_MS = 5000; // 5 second cooldown between restarts
|
|||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
if (!global.activeSandbox) {
|
// Check both v1 and v2 global references
|
||||||
|
const provider = global.activeSandbox || global.activeSandboxProvider;
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'No active sandbox'
|
error: 'No active sandbox'
|
||||||
@@ -40,45 +44,42 @@ export async function POST() {
|
|||||||
// Set the restart flag
|
// Set the restart flag
|
||||||
global.viteRestartInProgress = true;
|
global.viteRestartInProgress = true;
|
||||||
|
|
||||||
console.log('[restart-vite] Forcing Vite restart...');
|
console.log('[restart-vite] Using provider method to restart Vite...');
|
||||||
|
|
||||||
// Kill existing Vite processes
|
// Use the provider's restartViteServer method if available
|
||||||
try {
|
if (typeof provider.restartViteServer === 'function') {
|
||||||
await global.activeSandbox.runCommand({
|
await provider.restartViteServer();
|
||||||
cmd: 'pkill',
|
console.log('[restart-vite] Vite restarted via provider method');
|
||||||
args: ['-f', 'vite']
|
} else {
|
||||||
});
|
// Fallback to manual restart using provider's runCommand
|
||||||
console.log('[restart-vite] Killed existing Vite processes');
|
console.log('[restart-vite] Fallback to manual Vite restart...');
|
||||||
|
|
||||||
// Wait a moment for processes to terminate
|
// Kill existing Vite processes
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
try {
|
||||||
} catch {
|
await provider.runCommand('pkill -f vite');
|
||||||
console.log('[restart-vite] No existing Vite processes found');
|
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
|
// Update global state
|
||||||
global.lastViteRestartTime = Date.now();
|
global.lastViteRestartTime = Date.now();
|
||||||
global.viteRestartInProgress = false;
|
global.viteRestartInProgress = false;
|
||||||
|
|||||||
+109
-279
@@ -86,6 +86,7 @@ export default function AISandboxPage() {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [showLoadingBackground, setShowLoadingBackground] = useState(false);
|
const [showLoadingBackground, setShowLoadingBackground] = useState(false);
|
||||||
const [urlScreenshot, setUrlScreenshot] = useState<string | null>(null);
|
const [urlScreenshot, setUrlScreenshot] = useState<string | null>(null);
|
||||||
|
const [isScreenshotLoaded, setIsScreenshotLoaded] = useState(false);
|
||||||
const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false);
|
const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false);
|
||||||
const [screenshotError, setScreenshotError] = useState<string | null>(null);
|
const [screenshotError, setScreenshotError] = useState<string | null>(null);
|
||||||
const [isPreparingDesign, setIsPreparingDesign] = useState(false);
|
const [isPreparingDesign, setIsPreparingDesign] = useState(false);
|
||||||
@@ -93,6 +94,7 @@ export default function AISandboxPage() {
|
|||||||
const [targetUrl, setTargetUrl] = useState<string>('');
|
const [targetUrl, setTargetUrl] = useState<string>('');
|
||||||
const [sidebarScrolled, setSidebarScrolled] = useState(false);
|
const [sidebarScrolled, setSidebarScrolled] = useState(false);
|
||||||
const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null);
|
const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null);
|
||||||
|
const [isStartingNewGeneration, setIsStartingNewGeneration] = useState(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [sandboxFiles, setSandboxFiles] = useState<Record<string, string>>({});
|
const [sandboxFiles, setSandboxFiles] = useState<Record<string, string>>({});
|
||||||
const [hasInitialSubmission, setHasInitialSubmission] = useState<boolean>(false);
|
const [hasInitialSubmission, setHasInitialSubmission] = useState<boolean>(false);
|
||||||
@@ -566,25 +568,10 @@ export default function AISandboxPage() {
|
|||||||
// Fetch sandbox files after creation
|
// Fetch sandbox files after creation
|
||||||
setTimeout(fetchSandboxFiles, 1000);
|
setTimeout(fetchSandboxFiles, 1000);
|
||||||
|
|
||||||
// Restart Vite server to ensure it's running
|
// For Vercel sandboxes, Vite is already started during setupViteApp
|
||||||
setTimeout(async () => {
|
// No need to restart it immediately after creation
|
||||||
try {
|
// Only restart if there's an actual issue later
|
||||||
console.log('[createSandbox] Ensuring Vite server is running...');
|
console.log('[createSandbox] Sandbox ready with Vite server 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);
|
|
||||||
|
|
||||||
// Only add welcome message if not coming from home screen
|
// Only add welcome message if not coming from home screen
|
||||||
if (!fromHomeScreen) {
|
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);
|
setLoading(true);
|
||||||
log('Applying AI-generated code...');
|
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
|
// Use streaming endpoint for real-time feedback
|
||||||
|
const effectiveSandboxData = overrideSandboxData || sandboxData;
|
||||||
const response = await fetch('/api/apply-ai-code-stream', {
|
const response = await fetch('/api/apply-ai-code-stream', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -648,7 +636,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
|
|||||||
response: code,
|
response: code,
|
||||||
isEdit: isEdit,
|
isEdit: isEdit,
|
||||||
packages: pendingPackages,
|
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
|
const refreshDelay = appConfig.codeApplication.defaultRefreshDelay; // Allow Vite to process changes
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (iframeRef.current && sandboxData?.url) {
|
const currentSandboxData = effectiveSandboxData;
|
||||||
|
if (iframeRef.current && currentSandboxData?.url) {
|
||||||
console.log('[home] Refreshing iframe after code application...');
|
console.log('[home] Refreshing iframe after code application...');
|
||||||
|
|
||||||
// Method 1: Change src with timestamp
|
// 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;
|
iframeRef.current.src = urlWithTimestamp;
|
||||||
|
|
||||||
// Method 2: Force reload after a short delay
|
// 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
|
// 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
|
// Wait for Vite to process the file changes
|
||||||
// If packages were installed, wait longer for Vite to restart
|
// If packages were installed, wait longer for Vite to restart
|
||||||
const packagesInstalled = results?.packagesInstalled?.length > 0 || data.results?.packagesInstalled?.length > 0;
|
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`);
|
console.log(`[applyGeneratedCode] Packages installed: ${packagesInstalled}, refresh delay: ${refreshDelay}ms`);
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (iframeRef.current && sandboxData?.url) {
|
if (iframeRef.current && currentSandboxData?.url) {
|
||||||
console.log('[applyGeneratedCode] Starting iframe refresh sequence...');
|
console.log('[applyGeneratedCode] Starting iframe refresh sequence...');
|
||||||
console.log('[applyGeneratedCode] Current iframe src:', iframeRef.current.src);
|
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
|
// Method 1: Try direct navigation first
|
||||||
try {
|
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);
|
console.log('[applyGeneratedCode] Attempting direct navigation to:', urlWithTimestamp);
|
||||||
|
|
||||||
// Remove any existing onload handler
|
// 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();
|
iframeRef.current.remove();
|
||||||
|
|
||||||
// Add new iframe
|
// Add new iframe
|
||||||
newIframe.src = `${sandboxData.url}?t=${Date.now()}&recreated=true`;
|
newIframe.src = `${currentSandboxData.url}?t=${Date.now()}&recreated=true`;
|
||||||
parent?.appendChild(newIframe);
|
parent?.appendChild(newIframe);
|
||||||
|
|
||||||
// Update ref
|
// Update ref
|
||||||
@@ -1520,11 +1510,13 @@ Tip: I automatically detect and install npm packages from your code imports (lik
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (activeTab === 'preview') {
|
} 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 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 (
|
return (
|
||||||
<div className="relative w-full h-full bg-gray-900">
|
<div className="relative w-full h-full bg-gray-900">
|
||||||
{/* Screenshot as background when available */}
|
{/* Screenshot as background when available */}
|
||||||
@@ -1533,21 +1525,69 @@ Tip: I automatically detect and install npm packages from your code imports (lik
|
|||||||
<img
|
<img
|
||||||
src={urlScreenshot}
|
src={urlScreenshot}
|
||||||
alt="Website preview"
|
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 */}
|
{/* Loading overlay - only show when actively processing initial generation */}
|
||||||
{shouldShowLoadingOverlay && (
|
{shouldShowLoadingOverlay && (
|
||||||
<div className="absolute inset-0 bg-black/70 flex items-center justify-center backdrop-blur-sm">
|
<div className="absolute inset-0 bg-black/70 flex flex-col items-center justify-center backdrop-blur-sm">
|
||||||
<div className="text-center">
|
{/* 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" />
|
<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">
|
<p className="text-white text-lg font-medium">
|
||||||
{isCapturingScreenshot ? 'Analyzing website...' :
|
{isCapturingScreenshot ? 'Analyzing website...' :
|
||||||
isPreparingDesign ? 'Preparing design...' :
|
isPreparingDesign ? 'Preparing design...' :
|
||||||
generationProgress.isGenerating ? 'Generating code...' :
|
generationProgress.isGenerating ? 'Generating code...' :
|
||||||
'Loading...'}
|
'Loading...'}
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2049,10 +2089,16 @@ Tip: I automatically detect and install npm packages from your code imports (lik
|
|||||||
// setLeftPanelVisible(true);
|
// setLeftPanelVisible(true);
|
||||||
|
|
||||||
// Wait for sandbox creation if it's still in progress
|
// Wait for sandbox creation if it's still in progress
|
||||||
|
let activeSandboxData = sandboxData;
|
||||||
if (sandboxPromise) {
|
if (sandboxPromise) {
|
||||||
addChatMessage('Waiting for sandbox to be ready...', 'system');
|
addChatMessage('Waiting for sandbox to be ready...', 'system');
|
||||||
try {
|
try {
|
||||||
await sandboxPromise;
|
const newSandboxData = await sandboxPromise;
|
||||||
|
if (newSandboxData) {
|
||||||
|
activeSandboxData = newSandboxData;
|
||||||
|
// Also update the state for future use
|
||||||
|
setSandboxData(newSandboxData);
|
||||||
|
}
|
||||||
// Remove the waiting message
|
// Remove the waiting message
|
||||||
setChatMessages(prev => prev.filter(msg => msg.content !== 'Waiting for sandbox to be ready...'));
|
setChatMessages(prev => prev.filter(msg => msg.content !== 'Waiting for sandbox to be ready...'));
|
||||||
} catch {
|
} 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
|
// 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();
|
const data = await response.json();
|
||||||
if (data.success && data.screenshot) {
|
if (data.success && data.screenshot) {
|
||||||
|
setIsScreenshotLoaded(false); // Reset loaded state for new screenshot
|
||||||
setUrlScreenshot(data.screenshot);
|
setUrlScreenshot(data.screenshot);
|
||||||
// Set preparing design state
|
// Set preparing design state
|
||||||
setIsPreparingDesign(true);
|
setIsPreparingDesign(true);
|
||||||
@@ -2593,6 +2647,13 @@ Tip: I automatically detect and install npm packages from your code imports (lik
|
|||||||
|
|
||||||
setHomeScreenFading(true);
|
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
|
// Set loading background to ensure proper visual feedback
|
||||||
setShowLoadingBackground(true);
|
setShowLoadingBackground(true);
|
||||||
|
|
||||||
@@ -2622,6 +2683,11 @@ Tip: I automatically detect and install npm packages from your code imports (lik
|
|||||||
setShowHomeScreen(false);
|
setShowHomeScreen(false);
|
||||||
setHomeScreenFading(false);
|
setHomeScreenFading(false);
|
||||||
|
|
||||||
|
// Clear the starting flag after transition
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsStartingNewGeneration(false);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
// Wait for sandbox to be ready (if it's still creating)
|
// Wait for sandbox to be ready (if it's still creating)
|
||||||
await sandboxPromise;
|
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
|
// Clear preparing design state and switch to generation tab
|
||||||
setIsPreparingDesign(false);
|
setIsPreparingDesign(false);
|
||||||
|
setIsScreenshotLoaded(false); // Reset loaded state
|
||||||
setUrlScreenshot(null); // Clear screenshot when starting generation
|
setUrlScreenshot(null); // Clear screenshot when starting generation
|
||||||
setTargetUrl(''); // Clear target URL
|
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
|
// Clear screenshot and preparing design states to prevent them from showing on next run
|
||||||
|
setIsScreenshotLoaded(false); // Reset loaded state
|
||||||
setUrlScreenshot(null);
|
setUrlScreenshot(null);
|
||||||
setIsPreparingDesign(false);
|
setIsPreparingDesign(false);
|
||||||
setTargetUrl('');
|
setTargetUrl('');
|
||||||
setScreenshotError(null);
|
setScreenshotError(null);
|
||||||
setLoadingStage(null); // Clear loading stage
|
setLoadingStage(null); // Clear loading stage
|
||||||
|
setIsStartingNewGeneration(false); // Clear new generation flag
|
||||||
setShowLoadingBackground(false); // Clear loading background
|
setShowLoadingBackground(false); // Clear loading background
|
||||||
|
|
||||||
setTimeout(() => {
|
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');
|
addChatMessage(`Failed to clone website: ${error.message}`, 'system');
|
||||||
setUrlStatus([]);
|
setUrlStatus([]);
|
||||||
setIsPreparingDesign(false);
|
setIsPreparingDesign(false);
|
||||||
|
setIsStartingNewGeneration(false); // Clear new generation flag on error
|
||||||
|
setLoadingStage(null);
|
||||||
// Also clear generation progress on error
|
// Also clear generation progress on error
|
||||||
setGenerationProgress(prev => ({
|
setGenerationProgress(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -2998,247 +3069,6 @@ Focus on the key sections and content, making it clean and modern.`;
|
|||||||
return (
|
return (
|
||||||
<HeaderProvider>
|
<HeaderProvider>
|
||||||
<div className="font-sans bg-background text-foreground h-screen flex flex-col">
|
<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">
|
<div className="bg-white py-[15px] py-[8px] border-b border-border-faint flex items-center justify-between shadow-sm">
|
||||||
<HeaderBrandKit />
|
<HeaderBrandKit />
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
msg.content
|
<span className="text-body-input">{msg.content}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+333
-113
@@ -47,6 +47,8 @@ export default function HomePage() {
|
|||||||
const [hasSearched, setHasSearched] = useState<boolean>(false);
|
const [hasSearched, setHasSearched] = useState<boolean>(false);
|
||||||
const [isFadingOut, setIsFadingOut] = useState<boolean>(false);
|
const [isFadingOut, setIsFadingOut] = useState<boolean>(false);
|
||||||
const [showSelectMessage, setShowSelectMessage] = useState<boolean>(false);
|
const [showSelectMessage, setShowSelectMessage] = useState<boolean>(false);
|
||||||
|
const [showInstructionsForIndex, setShowInstructionsForIndex] = useState<number | null>(null);
|
||||||
|
const [additionalInstructions, setAdditionalInstructions] = useState<string>('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Simple URL validation
|
// Simple URL validation
|
||||||
@@ -170,10 +172,12 @@ export default function HomePage() {
|
|||||||
const performSearch = async (searchQuery: string) => {
|
const performSearch = async (searchQuery: string) => {
|
||||||
if (!searchQuery.trim() || isURL(searchQuery)) {
|
if (!searchQuery.trim() || isURL(searchQuery)) {
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
|
setShowSearchTiles(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
|
setShowSearchTiles(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/search', {
|
const response = await fetch('/api/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -184,6 +188,7 @@ export default function HomePage() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setSearchResults(data.results || []);
|
setSearchResults(data.results || []);
|
||||||
|
setShowSearchTiles(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search error:', 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">
|
<div className="p-16 flex gap-12 items-center w-full relative bg-white rounded-20">
|
||||||
{isURL(url) ? (
|
{/* Show different UI when search results are displayed */}
|
||||||
// Scrape icon for URLs
|
{hasSearched && searchResults.length > 0 && !isFadingOut ? (
|
||||||
<svg
|
<>
|
||||||
width="20"
|
{/* Selection mode icon */}
|
||||||
height="20"
|
<svg
|
||||||
viewBox="0 0 20 20"
|
width="20"
|
||||||
fill="none"
|
height="20"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 20 20"
|
||||||
className="opacity-40 flex-shrink-0"
|
fill="none"
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<rect x="3" y="3" width="14" height="14" rx="2" stroke="currentColor" strokeWidth="1.5"/>
|
className="opacity-40 flex-shrink-0"
|
||||||
<path d="M7 10L9 12L13 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
>
|
||||||
</svg>
|
<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
|
{isURL(url) ? (
|
||||||
width="20"
|
// Scrape icon for URLs
|
||||||
height="20"
|
<svg
|
||||||
viewBox="0 0 20 20"
|
width="20"
|
||||||
fill="none"
|
height="20"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 20 20"
|
||||||
className="opacity-40 flex-shrink-0"
|
fill="none"
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" strokeWidth="1.5"/>
|
className="opacity-40 flex-shrink-0"
|
||||||
<path d="M12.5 12.5L16.5 16.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
>
|
||||||
</svg>
|
<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>
|
</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" />
|
<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 ? (
|
{isSearching ? (
|
||||||
// Loading state with animated skeletons
|
// Loading state with animated scrolling skeletons
|
||||||
<div className="relative h-[250px] overflow-hidden">
|
<div className="relative h-[250px] overflow-hidden">
|
||||||
{/* Edge fade overlays */}
|
{/* 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 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="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">
|
<div className="carousel-container absolute left-0 flex gap-12 py-4">
|
||||||
{[0, 1, 2, 3, 4].map((index) => (
|
{/* Duplicate skeleton tiles for continuous scroll */}
|
||||||
|
{[...Array(10), ...Array(10)].map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={`loading-${index}`}
|
key={`loading-${index}`}
|
||||||
className="flex-shrink-0 w-[400px] h-[240px] rounded-24 overflow-hidden border-2 border-gray-200/30 bg-white relative"
|
className="flex-shrink-0 w-[400px] h-[240px] rounded-lg 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
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 skeleton-shimmer">
|
<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 className="absolute inset-0 bg-gradient-to-r from-gray-100 via-gray-50 to-gray-100 skeleton-gradient" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fake browser UI */}
|
{/* Fake browser UI - 5x bigger */}
|
||||||
<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="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-1">
|
<div className="flex gap-3">
|
||||||
<div className="w-2 h-2 rounded-full bg-gray-300" />
|
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" />
|
||||||
<div className="w-2 h-2 rounded-full bg-gray-300" />
|
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" style={{ animationDelay: '0.1s' }} />
|
||||||
<div className="w-2 h-2 rounded-full bg-gray-300" />
|
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" style={{ animationDelay: '0.2s' }} />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Content skeleton */}
|
{/* Content skeleton - positioned just below nav bar */}
|
||||||
<div className="p-4 mt-8">
|
<div className="absolute top-44 left-4 right-4">
|
||||||
<div className="h-6 bg-gray-200 rounded w-3/4 mb-3" />
|
<div className="h-3 bg-gray-200 rounded w-3/4 mb-2 animate-pulse" />
|
||||||
<div className="h-4 bg-gray-150 rounded w-full mb-2" />
|
<div className="h-3 bg-gray-150 rounded w-1/2 mb-2 animate-pulse" style={{ animationDelay: '0.2s' }} />
|
||||||
<div className="h-4 bg-gray-150 rounded w-5/6 mb-2" />
|
<div className="h-3 bg-gray-150 rounded w-2/3 animate-pulse" style={{ animationDelay: '0.3s' }} />
|
||||||
<div className="h-4 bg-gray-150 rounded w-4/6" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
) : searchResults.length > 0 ? (
|
) : searchResults.length > 0 ? (
|
||||||
// Actual results
|
// Actual results
|
||||||
@@ -496,11 +534,173 @@ export default function HomePage() {
|
|||||||
<div className="carousel-container absolute left-0 flex gap-12 py-4">
|
<div className="carousel-container absolute left-0 flex gap-12 py-4">
|
||||||
{/* Duplicate results for infinite scroll */}
|
{/* Duplicate results for infinite scroll */}
|
||||||
{[...searchResults, ...searchResults].map((result, index) => (
|
{[...searchResults, ...searchResults].map((result, index) => (
|
||||||
<button
|
<div
|
||||||
key={`${result.url}-${index}`}
|
key={`${result.url}-${index}`}
|
||||||
onClick={() => handleSubmit(result)}
|
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"
|
||||||
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"
|
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 ? (
|
{result.screenshot ? (
|
||||||
<img
|
<img
|
||||||
src={result.screenshot}
|
src={result.screenshot}
|
||||||
@@ -509,9 +709,29 @@ export default function HomePage() {
|
|||||||
loading="lazy"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,37 +57,12 @@ export default function SidebarInput({ onSubmit, disabled = false }: SidebarInpu
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div >
|
<div >
|
||||||
<div className="p-4 border-b border-gray-100">
|
<div className="p-4 border-b border-gray-100">
|
||||||
{/* URL Input */}
|
{/* link to home page with button */}
|
||||||
<div className="flex gap-3 items-center">
|
<a href="/">
|
||||||
<svg
|
<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">
|
||||||
width="20"
|
Generate a new website
|
||||||
height="20"
|
</button>
|
||||||
viewBox="0 0 20 20"
|
</a>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Options Section - Show when valid URL */}
|
{/* Options Section - Show when valid URL */}
|
||||||
|
|||||||
@@ -35,23 +35,13 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID;
|
sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID;
|
||||||
sandboxConfig.token = process.env.VERCEL_TOKEN;
|
sandboxConfig.token = process.env.VERCEL_TOKEN;
|
||||||
} else if (process.env.VERCEL_OIDC_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);
|
this.sandbox = await Sandbox.create(sandboxConfig);
|
||||||
|
|
||||||
const sandboxId = this.sandbox.sandboxId;
|
const sandboxId = this.sandbox.sandboxId;
|
||||||
sandboxId: sandboxId,
|
// Sandbox created successfully
|
||||||
status: this.sandbox.status
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the sandbox URL using the correct Vercel Sandbox API
|
// Get the sandbox URL using the correct Vercel Sandbox API
|
||||||
const sandboxUrl = this.sandbox.domain(5173);
|
const sandboxUrl = this.sandbox.domain(5173);
|
||||||
@@ -91,9 +81,33 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
env: {}
|
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 {
|
return {
|
||||||
stdout: result.stdout || '',
|
stdout: stdout,
|
||||||
stderr: result.stderr || '',
|
stderr: stderr,
|
||||||
exitCode: result.exitCode || 0,
|
exitCode: result.exitCode || 0,
|
||||||
success: result.exitCode === 0
|
success: result.exitCode === 0
|
||||||
};
|
};
|
||||||
@@ -115,21 +129,12 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
// Vercel sandbox default working directory is /vercel/sandbox
|
// Vercel sandbox default working directory is /vercel/sandbox
|
||||||
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
|
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
|
||||||
|
|
||||||
originalPath: path,
|
// Writing file to sandbox
|
||||||
fullPath: fullPath,
|
|
||||||
contentLength: content.length,
|
|
||||||
contentPreview: content.substring(0, 100) + (content.length > 100 ? '...' : ''),
|
|
||||||
sandboxId: this.sandbox.sandboxId,
|
|
||||||
sandboxStatus: this.sandbox.status
|
|
||||||
});
|
|
||||||
|
|
||||||
// Based on Vercel SDK docs, writeFiles expects path and Buffer content
|
// Based on Vercel SDK docs, writeFiles expects path and Buffer content
|
||||||
try {
|
try {
|
||||||
const buffer = Buffer.from(content, 'utf-8');
|
const buffer = Buffer.from(content, 'utf-8');
|
||||||
path: fullPath,
|
// Writing file with buffer
|
||||||
bufferLength: buffer.length,
|
|
||||||
isBuffer: Buffer.isBuffer(buffer)
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.sandbox.writeFiles([{
|
await this.sandbox.writeFiles([{
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
@@ -148,6 +153,7 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fallback to command-based approach if writeFiles fails
|
// Fallback to command-based approach if writeFiles fails
|
||||||
|
// Falling back to command-based file write
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
|
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
|
||||||
@@ -156,10 +162,7 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
cmd: 'mkdir',
|
cmd: 'mkdir',
|
||||||
args: ['-p', dir]
|
args: ['-p', dir]
|
||||||
});
|
});
|
||||||
exitCode: mkdirResult.exitCode,
|
// Directory created
|
||||||
stdout: mkdirResult.stdout,
|
|
||||||
stderr: mkdirResult.stderr
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write file using echo and redirection
|
// Write file using echo and redirection
|
||||||
@@ -175,10 +178,7 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
args: ['-c', `echo "${escapedContent}" > "${fullPath}"`]
|
args: ['-c', `echo "${escapedContent}" > "${fullPath}"`]
|
||||||
});
|
});
|
||||||
|
|
||||||
exitCode: writeResult.exitCode,
|
// File written
|
||||||
stdout: writeResult.stdout,
|
|
||||||
stderr: writeResult.stderr
|
|
||||||
});
|
|
||||||
|
|
||||||
if (writeResult.exitCode === 0) {
|
if (writeResult.exitCode === 0) {
|
||||||
this.existingFiles.add(path);
|
this.existingFiles.add(path);
|
||||||
@@ -201,11 +201,35 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
args: [fullPath]
|
args: [fullPath]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.exitCode !== 0) {
|
// Handle stdout and stderr - they might be functions in Vercel SDK
|
||||||
throw new Error(`Failed to read file: ${result.stderr}`);
|
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[]> {
|
async listFiles(directory: string = '/vercel/sandbox'): Promise<string[]> {
|
||||||
@@ -219,11 +243,24 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
cwd: '/'
|
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) {
|
if (result.exitCode !== 0) {
|
||||||
return [];
|
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> {
|
async installPackages(packages: string[]): Promise<CommandResult> {
|
||||||
@@ -233,6 +270,7 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
|
|
||||||
const flags = process.env.NPM_FLAGS || '';
|
const flags = process.env.NPM_FLAGS || '';
|
||||||
|
|
||||||
|
// Installing packages
|
||||||
|
|
||||||
// Build args array
|
// Build args array
|
||||||
const args = ['install'];
|
const args = ['install'];
|
||||||
@@ -247,14 +285,38 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
cwd: '/vercel/sandbox'
|
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
|
// Restart Vite if configured and successful
|
||||||
if (result.exitCode === 0 && process.env.AUTO_RESTART_VITE === 'true') {
|
if (result.exitCode === 0 && process.env.AUTO_RESTART_VITE === 'true') {
|
||||||
await this.restartViteServer();
|
await this.restartViteServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stdout: result.stdout || '',
|
stdout: stdout,
|
||||||
stderr: result.stderr || '',
|
stderr: stderr,
|
||||||
exitCode: result.exitCode || 0,
|
exitCode: result.exitCode || 0,
|
||||||
success: result.exitCode === 0
|
success: result.exitCode === 0
|
||||||
};
|
};
|
||||||
@@ -265,19 +327,14 @@ export class VercelProvider extends SandboxProvider {
|
|||||||
throw new Error('No active sandbox');
|
throw new Error('No active sandbox');
|
||||||
}
|
}
|
||||||
|
|
||||||
sandboxId: this.sandbox.sandboxId,
|
// Setting up Vite app for sandbox
|
||||||
status: this.sandbox.status
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create directory structure
|
// Create directory structure
|
||||||
const mkdirResult = await this.sandbox.runCommand({
|
const mkdirResult = await this.sandbox.runCommand({
|
||||||
cmd: 'mkdir',
|
cmd: 'mkdir',
|
||||||
args: ['-p', '/vercel/sandbox/src']
|
args: ['-p', '/vercel/sandbox/src']
|
||||||
});
|
});
|
||||||
exitCode: mkdirResult.exitCode,
|
// Directory structure created
|
||||||
stdout: mkdirResult.stdout,
|
|
||||||
stderr: mkdirResult.stderr
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create package.json
|
// Create package.json
|
||||||
const packageJson = {
|
const packageJson = {
|
||||||
@@ -413,6 +470,7 @@ body {
|
|||||||
|
|
||||||
await this.writeFile('src/index.css', indexCss);
|
await this.writeFile('src/index.css', indexCss);
|
||||||
|
|
||||||
|
// Installing npm dependencies
|
||||||
|
|
||||||
// Install dependencies
|
// Install dependencies
|
||||||
try {
|
try {
|
||||||
@@ -422,12 +480,10 @@ body {
|
|||||||
cwd: '/vercel/sandbox'
|
cwd: '/vercel/sandbox'
|
||||||
});
|
});
|
||||||
|
|
||||||
exitCode: installResult.exitCode,
|
// npm install completed
|
||||||
stdout: typeof installResult.stdout === 'function' ? 'function' : installResult.stdout,
|
|
||||||
stderr: typeof installResult.stderr === 'function' ? 'function' : installResult.stderr
|
|
||||||
});
|
|
||||||
|
|
||||||
if (installResult.exitCode === 0) {
|
if (installResult.exitCode === 0) {
|
||||||
|
// Dependencies installed successfully
|
||||||
} else {
|
} else {
|
||||||
console.warn('[VercelProvider] npm install had issues:', installResult.stderr);
|
console.warn('[VercelProvider] npm install had issues:', installResult.stderr);
|
||||||
}
|
}
|
||||||
@@ -445,6 +501,7 @@ body {
|
|||||||
cwd: '/vercel/sandbox'
|
cwd: '/vercel/sandbox'
|
||||||
});
|
});
|
||||||
if (altResult.exitCode === 0) {
|
if (altResult.exitCode === 0) {
|
||||||
|
// Alternative npm install succeeded
|
||||||
} else {
|
} else {
|
||||||
console.warn('[VercelProvider] Alternative npm install also had issues:', altResult.stderr);
|
console.warn('[VercelProvider] Alternative npm install also had issues:', altResult.stderr);
|
||||||
}
|
}
|
||||||
@@ -455,6 +512,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start Vite dev server
|
// Start Vite dev server
|
||||||
|
// Starting Vite dev server
|
||||||
|
|
||||||
// Kill any existing Vite processes
|
// Kill any existing Vite processes
|
||||||
await this.sandbox.runCommand({
|
await this.sandbox.runCommand({
|
||||||
@@ -470,6 +528,7 @@ body {
|
|||||||
cwd: '/vercel/sandbox'
|
cwd: '/vercel/sandbox'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Vite server started in background
|
||||||
|
|
||||||
// Wait for Vite to be ready
|
// Wait for Vite to be ready
|
||||||
await new Promise(resolve => setTimeout(resolve, 7000));
|
await new Promise(resolve => setTimeout(resolve, 7000));
|
||||||
@@ -490,6 +549,7 @@ body {
|
|||||||
throw new Error('No active sandbox');
|
throw new Error('No active sandbox');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restarting Vite server
|
||||||
|
|
||||||
// Kill existing Vite process
|
// Kill existing Vite process
|
||||||
await this.sandbox.runCommand({
|
await this.sandbox.runCommand({
|
||||||
@@ -508,6 +568,7 @@ body {
|
|||||||
cwd: '/vercel/sandbox'
|
cwd: '/vercel/sandbox'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Vite server started in background
|
||||||
|
|
||||||
// Wait for Vite to be ready
|
// Wait for Vite to be ready
|
||||||
await new Promise(resolve => setTimeout(resolve, 7000));
|
await new Promise(resolve => setTimeout(resolve, 7000));
|
||||||
|
|||||||
@@ -216,6 +216,59 @@
|
|||||||
transform: translateX(-50%);
|
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; }
|
.grecaptcha-badge { visibility: hidden; }
|
||||||
|
|||||||
Reference in New Issue
Block a user