diff --git a/.local b/.local new file mode 100644 index 00000000..e69de29b diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..b50a33be --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,144 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Package Management +- Use `pnpm` (preferred package manager) +- `pnpm install` - Install dependencies +- `pnpm run dev` - Start development server with Turbopack +- `pnpm run build` - Build for production +- `pnpm run start` - Start production server +- `pnpm run lint` - Run ESLint + +### Testing +- `pnpm run test:integration` - Run E2B integration tests +- `pnpm run test:api` - Test API endpoints +- `pnpm run test:code` - Test code execution functionality +- `pnpm run test:all` - Run all tests sequentially + +## Architecture Overview + +### Core Technology Stack +- **Next.js 15.4.3** with App Router architecture +- **React 19** with modern features +- **TypeScript** for type safety +- **Tailwind CSS** with Tailwind CSS v4.1.11 +- **E2B Code Interpreter** for sandbox execution +- **AI SDKs**: Anthropic, OpenAI, Groq integration + +### Project Structure +- `app/` - Next.js App Router pages and API routes +- `app/api/` - API endpoints for sandbox management, AI integration +- `components/` - Reusable React components +- `lib/` - Utility functions and shared logic +- `types/` - TypeScript type definitions +- `config/` - Application configuration +- `docs/` - Internal documentation + +### Key Components + +#### Sandbox Management (`app/api/`) +- `create-ai-sandbox/` - Creates E2B sandboxes for code execution +- `apply-ai-code/` & `apply-ai-code-stream/` - Apply AI-generated code with streaming +- `install-packages/` - Automatically install npm packages +- `get-sandbox-files/` - Retrieve sandbox file structure +- `run-command/` - Execute commands in sandbox +- `deploy-to-vercel/` - Deploy sandbox projects to live Vercel URLs +- `generate-component-library/` - Create comprehensive component libraries + +#### AI Integration +- Uses multiple AI providers (Anthropic, OpenAI, Groq) +- Default model: `moonshotai/kimi-k2-instruct` (Groq) +- Streaming responses with real-time feedback +- XML-based package detection system + +#### Package Detection System +The app uses XML tags in AI responses for automatic package management: +- `package-name` - Single package +- `pkg1, pkg2, pkg3` - Multiple packages +- `npm run build` - Execute commands +- See `docs/PACKAGE_DETECTION_GUIDE.md` for complete documentation + +#### Configuration (`config/app.config.ts`) +- E2B sandbox timeout: 15 minutes +- Vite development server on port 5173 +- AI model settings and temperature +- Package installation with `--legacy-peer-deps` + +### Core Workflows + +#### Code Generation Flow +1. User submits request via chat interface +2. AI analyzes request and generates code with XML tags +3. System parses XML for packages, files, and commands +4. Packages automatically installed in sandbox +5. Files created/updated with real-time streaming feedback +6. Commands executed with output streaming + +#### Deployment Flow +1. User clicks deploy button in sandbox toolbar +2. System collects all project files from E2B sandbox +3. Essential files auto-generated if missing (package.json, vite.config, etc.) +4. Files bundled and sent to Vercel deployment API +5. Live URL returned and auto-opened in new tab +6. User can share live website instantly + +#### Sandbox Lifecycle +- Sandboxes auto-created on first interaction +- 15-minute timeout with automatic cleanup +- Vite dev server runs on port 5173 +- File changes trigger automatic rebuilds + +### Environment Variables Required +```env +E2B_API_KEY=your_e2b_api_key +FIRECRAWL_API_KEY=your_firecrawl_api_key +ANTHROPIC_API_KEY=your_anthropic_api_key # Optional +OPENAI_API_KEY=your_openai_api_key # Optional +GROQ_API_KEY=your_groq_api_key # Optional +``` + +### Important Implementation Details + +#### File Import Paths +- Uses `@/*` alias for root-level imports +- All components use centralized icon imports from `@/lib/icons` +- Avoid direct icon library imports to prevent Turbopack chunk issues + +#### Hydration and State Management +- Client-side state initialization to prevent hydration errors +- Uses loading state during hydration for consistent rendering +- Session storage persistence for UI state (home screen visibility) + +#### Error Handling +- Comprehensive streaming error feedback +- Automatic truncation recovery (disabled by default) +- Vite error monitoring and reporting system + +#### Performance Considerations +- Uses Turbopack for fast development builds +- Streaming responses for real-time user feedback +- File caching for sandbox state management +- CSS rebuild delays (2000ms) for styling changes + +### Key Utilities + +#### `lib/file-parser.ts` +- Parses AI responses for file content extraction +- Handles XML tag parsing for packages and commands + +#### `lib/edit-intent-analyzer.ts` +- Analyzes user requests for edit intentions +- Determines file targets and edit types + +#### `components/CodeApplicationProgress.tsx` +- Real-time progress display for code application +- Shows package installation, file creation, and command execution status + +### Testing Strategy +- Integration tests for E2B sandbox functionality +- API endpoint testing for all routes +- Code execution validation +- Error handling verification \ No newline at end of file diff --git a/README.md b/README.md index 11facd13..38ea0d60 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,124 @@
-# Open Lovable +# Open Lovable - AI Website Generator -Chat with AI to build React apps instantly. +Clone any website and get a clean React app you can run and deploy. -Open Lovable Demo +## Features -
+- 🚀 **One-click website cloning** - Just paste a URL and get a React app +- 🎨 **AI-powered generation** - Uses Claude Sonnet 4 to recreate websites +- 🔧 **Live sandbox** - See your code running in real-time +- 📱 **Responsive design** - Works on desktop and mobile +- 🎯 **Multiple styles** - Choose from different design approaches +- 📦 **Export options** - Download as ZIP or deploy to Vercel -## Setup +## Quick Start -1. **Clone & Install** -```bash -git clone https://github.com/mendableai/open-lovable.git -cd open-lovable -npm install -``` +1. **Clone the repository** + ```bash + git clone https://github.com/your-username/open-lovable.git + cd open-lovable + ``` -2. **Add `.env.local`** -```env -# Required -E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev (Sandboxes) -FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) +2. **Install dependencies** + ```bash + npm install + # or + pnpm install + ``` -# Optional (need at least one AI provider) -ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com -OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) -GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended) -``` +3. **Set up environment variables** + ```bash + cp .env.example .env.local + # Add your Anthropic API key + ANTHROPIC_API_KEY=your_key_here + ``` + +4. **Run the development server** + ```bash + npm run dev + # or + pnpm dev + ``` + +5. **Open your browser** + Navigate to [http://localhost:3000](http://localhost:3000) + +## Usage + +1. **Enter a website URL** in the input field +2. **Choose a style** (Neobrutalist, Glassmorphism, etc.) +3. **Wait for AI generation** - this may take 30-60 seconds +4. **Preview your site** in the live sandbox +5. **Download or deploy** your generated React app + +## Deployment + +### Option 1: Download as ZIP +- Click the download button to get your project as a ZIP file +- Extract and run locally with `npm install && npm run dev` +- Deploy to any hosting service manually + +### Option 2: Deploy to Vercel (Requires Setup) +To enable one-click Vercel deployment: + +1. **Get your Vercel token:** + - Go to [vercel.com/account/tokens](https://vercel.com/account/tokens) + - Create a new token with deployment permissions + +2. **Add to environment:** + ```bash + # Add to your .env.local file + VERCEL_TOKEN=your_token_here + ``` + +3. **Redeploy your app** and the deploy button will work! + +### Option 3: Manual Vercel Deployment +1. Download your project as ZIP +2. Go to [vercel.com](https://vercel.com) +3. Create new project and upload the ZIP +4. Vercel will automatically detect it's a Vite/React app + +## Environment Variables -3. **Run** ```bash -npm run dev +# Required +ANTHROPIC_API_KEY=your_anthropic_api_key + +# Optional (for Vercel deployment) +VERCEL_TOKEN=your_vercel_token ``` -Open [http://localhost:3000](http://localhost:3000) +## Tech Stack + +- **Frontend:** Next.js 14, React, TypeScript, Tailwind CSS +- **AI:** Claude Sonnet 4 via Anthropic API +- **Sandbox:** E2B for live code execution +- **Deployment:** Vercel API integration +- **Styling:** Tailwind CSS with custom components + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request ## License -MIT \ No newline at end of file +MIT License - see [LICENSE](LICENSE) for details. + +## Support + +If you encounter issues: +1. Check the console for error messages +2. Ensure your API keys are properly set +3. Try refreshing the page +4. Open an issue on GitHub + +--- + +Built with ❤️ by the Open Lovable team \ No newline at end of file diff --git a/app/api/analyze-edit-intent/route.ts b/app/api/analyze-edit-intent/route.ts index 2284829d..0fc6c73d 100644 --- a/app/api/analyze-edit-intent/route.ts +++ b/app/api/analyze-edit-intent/route.ts @@ -93,19 +93,8 @@ export async function POST(request: NextRequest) { console.log('[analyze-edit-intent] File summary preview:', fileSummary.split('\n').slice(0, 5).join('\n')); // Select the appropriate AI model based on the request - let aiModel; - if (model.startsWith('anthropic/')) { - aiModel = anthropic(model.replace('anthropic/', '')); - } else if (model.startsWith('openai/')) { - if (model.includes('gpt-oss')) { - aiModel = groq(model); - } else { - aiModel = openai(model.replace('openai/', '')); - } - } else { - // Default to groq if model format is unclear - aiModel = groq(model); - } + // Force Groq as provider + const aiModel = groq(model); console.log('[analyze-edit-intent] Using AI model:', model); diff --git a/app/api/conversation-state/route.ts b/app/api/conversation-state/route.ts index 1a374686..94caeb0d 100644 --- a/app/api/conversation-state/route.ts +++ b/app/api/conversation-state/route.ts @@ -58,11 +58,13 @@ export async function POST(request: NextRequest) { case 'clear-old': // Clear old conversation data but keep recent context + // Make this action idempotent: if there's no active conversation, just return success if (!global.conversationState) { return NextResponse.json({ - success: false, - error: 'No active conversation to clear' - }, { status: 400 }); + success: true, + message: 'No active conversation to clear', + state: null + }); } // Keep only recent data diff --git a/app/api/create-ai-sandbox/route.ts b/app/api/create-ai-sandbox/route.ts index 257ce1db..c2a598dc 100644 --- a/app/api/create-ai-sandbox/route.ts +++ b/app/api/create-ai-sandbox/route.ts @@ -1,364 +1,21 @@ import { NextResponse } from 'next/server'; -import { Sandbox } from '@e2b/code-interpreter'; -import type { SandboxState } from '@/types/sandbox'; -import { appConfig } from '@/config/app.config'; - -// Store active sandbox globally -declare global { - var activeSandbox: any; - var sandboxData: any; - var existingFiles: Set; - var sandboxState: SandboxState; -} +import SandboxManager from '@/lib/sandbox-manager'; export async function POST() { - let sandbox: any = null; - try { - console.log('[create-ai-sandbox] Creating base sandbox...'); - - // Kill existing sandbox if any - if (global.activeSandbox) { - console.log('[create-ai-sandbox] Killing existing sandbox...'); - try { - await global.activeSandbox.kill(); - } catch (e) { - console.error('Failed to close existing sandbox:', e); - } - global.activeSandbox = null; - } - - // Clear existing files tracking - if (global.existingFiles) { - global.existingFiles.clear(); - } else { - global.existingFiles = new Set(); - } - - // Create base sandbox - we'll set up Vite ourselves for full control - console.log(`[create-ai-sandbox] Creating base E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`); - sandbox = await Sandbox.create({ - apiKey: process.env.E2B_API_KEY, - timeoutMs: appConfig.e2b.timeoutMs - }); - - const sandboxId = (sandbox as any).sandboxId || Date.now().toString(); - const host = (sandbox as any).getHost(appConfig.e2b.vitePort); - - console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`); - console.log(`[create-ai-sandbox] Sandbox host: ${host}`); - - // Set up a basic Vite React app using Python to write files - console.log('[create-ai-sandbox] Setting up Vite React app...'); - - // Write all files in a single Python script to avoid multiple executions - const setupScript = ` -import os -import json - -print('Setting up React app with Vite and Tailwind...') - -# Create directory structure -os.makedirs('/home/user/app/src', exist_ok=True) - -# Package.json -package_json = { - "name": "sandbox-app", - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite --host", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@vitejs/plugin-react": "^4.0.0", - "vite": "^4.3.9", - "tailwindcss": "^3.3.0", - "postcss": "^8.4.31", - "autoprefixer": "^10.4.16" - } -} - -with open('/home/user/app/package.json', 'w') as f: - json.dump(package_json, f, indent=2) -print('✓ package.json') - -# Vite config for E2B - with allowedHosts -vite_config = """import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -// E2B-compatible Vite configuration -export default defineConfig({ - plugins: [react()], - server: { - host: '0.0.0.0', - port: 5173, - strictPort: true, - hmr: false, - allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1'] - } -})""" - -with open('/home/user/app/vite.config.js', 'w') as f: - f.write(vite_config) -print('✓ vite.config.js') - -# Tailwind config - standard without custom design tokens -tailwind_config = """/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: {}, - }, - plugins: [], -}""" - -with open('/home/user/app/tailwind.config.js', 'w') as f: - f.write(tailwind_config) -print('✓ tailwind.config.js') - -# PostCSS config -postcss_config = """export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}""" - -with open('/home/user/app/postcss.config.js', 'w') as f: - f.write(postcss_config) -print('✓ postcss.config.js') - -# Index.html -index_html = """ - - - - - Sandbox App - - -
- - -""" - -with open('/home/user/app/index.html', 'w') as f: - f.write(index_html) -print('✓ index.html') - -# Main.jsx -main_jsx = """import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.jsx' -import './index.css' - -ReactDOM.createRoot(document.getElementById('root')).render( - - - , -)""" + const manager = SandboxManager.getInstance(); + const info = await manager.createSandbox(); -with open('/home/user/app/src/main.jsx', 'w') as f: - f.write(main_jsx) -print('✓ src/main.jsx') - -# App.jsx with explicit Tailwind test -app_jsx = """function App() { - return ( -
-
-

- Sandbox Ready
- Start building your React app with Vite and Tailwind CSS! -

-
-
- ) -} - -export default App""" - -with open('/home/user/app/src/App.jsx', 'w') as f: - f.write(app_jsx) -print('✓ src/App.jsx') - -# Index.css with explicit Tailwind directives -index_css = """@tailwind base; -@tailwind components; -@tailwind utilities; - -/* Force Tailwind to load */ -@layer base { - :root { - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; - } - - * { - margin: 0; - padding: 0; - box-sizing: border-box; - } -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; - background-color: rgb(17 24 39); -}""" - -with open('/home/user/app/src/index.css', 'w') as f: - f.write(index_css) -print('✓ src/index.css') - -print('\\nAll files created successfully!') -`; - - // Execute the setup script - await sandbox.runCode(setupScript); - - // Install dependencies - console.log('[create-ai-sandbox] Installing dependencies...'); - await sandbox.runCode(` -import subprocess -import sys - -print('Installing npm packages...') -result = subprocess.run( - ['npm', 'install'], - cwd='/home/user/app', - capture_output=True, - text=True -) - -if result.returncode == 0: - print('✓ Dependencies installed successfully') -else: - print(f'⚠ Warning: npm install had issues: {result.stderr}') - # Continue anyway as it might still work - `); - - // Start Vite dev server - console.log('[create-ai-sandbox] Starting Vite dev server...'); - await sandbox.runCode(` -import subprocess -import os -import time - -os.chdir('/home/user/app') - -# Kill any existing Vite processes -subprocess.run(['pkill', '-f', 'vite'], capture_output=True) -time.sleep(1) - -# Start Vite dev server -env = os.environ.copy() -env['FORCE_COLOR'] = '0' - -process = subprocess.Popen( - ['npm', 'run', 'dev'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env -) - -print(f'✓ Vite dev server started with PID: {process.pid}') -print('Waiting for server to be ready...') - `); - - // Wait for Vite to be fully ready - await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay)); - - // Force Tailwind CSS to rebuild by touching the CSS file - await sandbox.runCode(` -import os -import time - -# Touch the CSS file to trigger rebuild -css_file = '/home/user/app/src/index.css' -if os.path.exists(css_file): - os.utime(css_file, None) - print('✓ Triggered CSS rebuild') - -# Also ensure PostCSS processes it -time.sleep(2) -print('✓ Tailwind CSS should be loaded') - `); - - // Store sandbox globally - global.activeSandbox = sandbox; - global.sandboxData = { - sandboxId, - url: `https://${host}` - }; - - // Set extended timeout on the sandbox instance if method available - if (typeof sandbox.setTimeout === 'function') { - sandbox.setTimeout(appConfig.e2b.timeoutMs); - console.log(`[create-ai-sandbox] Set sandbox timeout to ${appConfig.e2b.timeoutMinutes} minutes`); - } - - // Initialize sandbox state - global.sandboxState = { - fileCache: { - files: {}, - lastSync: Date.now(), - sandboxId - }, - sandbox, - sandboxData: { - sandboxId, - url: `https://${host}` - } - }; - - // Track initial files - global.existingFiles.add('src/App.jsx'); - global.existingFiles.add('src/main.jsx'); - global.existingFiles.add('src/index.css'); - global.existingFiles.add('index.html'); - global.existingFiles.add('package.json'); - global.existingFiles.add('vite.config.js'); - global.existingFiles.add('tailwind.config.js'); - global.existingFiles.add('postcss.config.js'); - - console.log('[create-ai-sandbox] Sandbox ready at:', `https://${host}`); - return NextResponse.json({ success: true, - sandboxId, - url: `https://${host}`, + sandboxId: info.sandboxId, + url: info.url, message: 'Sandbox created and Vite React app initialized' }); - - } catch (error) { + } catch (error: any) { console.error('[create-ai-sandbox] Error:', error); - - // Clean up on error - if (sandbox) { - try { - await sandbox.kill(); - } catch (e) { - console.error('Failed to close sandbox on error:', e); - } - } - return NextResponse.json( - { - error: error instanceof Error ? error.message : 'Failed to create sandbox', - details: error instanceof Error ? error.stack : undefined - }, + { success: false, error: error?.message || 'Failed to create sandbox' }, { status: 500 } ); } diff --git a/app/api/deploy-to-vercel/route.ts b/app/api/deploy-to-vercel/route.ts new file mode 100644 index 00000000..a593ef47 --- /dev/null +++ b/app/api/deploy-to-vercel/route.ts @@ -0,0 +1,401 @@ +import { NextRequest, NextResponse } from 'next/server'; +import type { SandboxState } from '@/types/sandbox'; + +interface DeploymentRequest { + sandboxId?: string; + projectName?: string; + files?: Array<{ + path: string; + content: string; + }>; +} + +interface VercelFile { + file: string; + data: string; +} + +declare global { + // Active E2B sandbox handle (may be undefined on serverless cold starts) + // eslint-disable-next-line no-var + var activeSandbox: any | undefined; + // Server-side cache of files captured during generation + // Must match existing global declaration exactly (no union) + // eslint-disable-next-line no-var + var sandboxState: SandboxState; +} + +// Add a simple health check +export async function GET() { + return NextResponse.json({ + status: 'ok', + message: 'Deploy to Vercel endpoint is working', + timestamp: new Date().toISOString() + }); +} + +export async function POST(req: NextRequest) { + try { + const body: DeploymentRequest = await req.json(); + const { projectName = 'my-site', sandboxId, files: requestFiles } = body; + + console.log('[deploy-vercel] Starting deployment process...'); + console.log('[deploy-vercel] Request body:', { + projectName, + sandboxId, + filesCount: requestFiles?.length || 0 + }); + + // Get all files for deployment + const files: VercelFile[] = []; + + try { + if (requestFiles && requestFiles.length > 0) { + // Use files sent from frontend + console.log('[deploy-vercel] Using files from request'); + console.log('[deploy-vercel] First few files:', requestFiles.slice(0, 3)); + + for (const file of requestFiles) { + // Validate file structure + if (!file.path || !file.content) { + console.warn(`[deploy-vercel] Skipping invalid file:`, file); + continue; + } + + if (!shouldSkipFile(file.path)) { + files.push({ file: file.path, data: file.content }); + } else { + console.log(`[deploy-vercel] Skipping file: ${file.path}`); + } + } + + console.log(`[deploy-vercel] Processed ${files.length} valid files from request`); + } else { + // Fallback: try to get files from global state (for development) + console.log('[deploy-vercel] No files in request, checking global state...'); + + // Check if we're in development and have access to sandbox + if (typeof global !== 'undefined' && global.sandboxState?.fileCache?.files) { + const cachedFiles = global.sandboxState.fileCache.files; + console.log('[deploy-vercel] Using cached files from sandboxState'); + for (const [path, fileData] of Object.entries(cachedFiles)) { + if (!shouldSkipFile(path)) { + const content = (fileData as any).content as string; + files.push({ file: path, data: content }); + } + } + } else if (typeof global !== 'undefined' && global.activeSandbox) { + // Fallback to querying the live sandbox if available + console.log('[deploy-vercel] Cached files not found. Listing files from active sandbox'); + const result = await global.activeSandbox.filesystem.list('/home/user/app', { recursive: true }); + for (const item of result) { + if (item.type === 'file' && !shouldSkipFile(item.path)) { + try { + const content = await global.activeSandbox.filesystem.read(`/home/user/app/${item.path}`); + files.push({ file: item.path, data: content }); + } catch (readError) { + console.warn(`[deploy-vercel] Could not read file ${item.path}:`, readError); + } + } + } + } else { + console.warn('[deploy-vercel] No files provided and no global sandbox state available'); + return NextResponse.json({ + error: 'No files provided for deployment. Please ensure your project has been generated and try again.' + }, { status: 400 }); + } + } + + // Ensure we have essential files for a React/Vite project + await ensureEssentialFiles(files); + + console.log(`[deploy-vercel] Collected ${files.length} files for deployment`); + console.log('[deploy-vercel] File paths:', files.map(f => f.file)); + + // Validate that we have the minimum required files + if (files.length === 0) { + console.error('[deploy-vercel] No files available for deployment'); + return NextResponse.json({ + error: 'No files available for deployment. Please ensure your project has been generated.' + }, { status: 400 }); + } + + // Check for essential files + const hasPackageJson = files.some(f => f.file === 'package.json'); + const hasIndexHtml = files.some(f => f.file === 'index.html'); + const hasMainEntry = files.some(f => f.file.includes('main.') || f.file.includes('index.') || f.file.includes('App.')); + + if (!hasPackageJson || !hasIndexHtml || !hasMainEntry) { + console.warn('[deploy-vercel] Missing essential files, but continuing with generated defaults'); + } + + } catch (error) { + console.error('[deploy-vercel] Error collecting files:', error); + return NextResponse.json({ + error: 'Failed to collect project files for deployment' + }, { status: 500 }); + } + + // Deploy to Vercel using their API + const deployment = await deployToVercel(files, projectName); + + if (deployment.error) { + console.error('[deploy-vercel] Vercel deployment failed:', deployment.error); + return NextResponse.json({ + error: deployment.error + }, { status: 500 }); + } + + // Handle successful deployment + console.log('[deploy-vercel] Deployment successful:', deployment.url); + + return NextResponse.json({ + success: true, + url: deployment.url, + deploymentId: deployment.id, + message: `Successfully deployed to ${deployment.url}` + }); + + } catch (error: any) { + console.error('[deploy-vercel] Deployment failed:', error); + return NextResponse.json({ + error: `Deployment failed: ${error.message}`, + details: error.toString() + }, { status: 500 }); + } +} + +// Helper function to skip unnecessary files +function shouldSkipFile(filePath: string): boolean { + const skipPatterns = [ + 'node_modules/', + '.git/', + '.next/', + 'dist/', + 'build/', + '.env', + '.env.local', + '.DS_Store', + '*.log', + 'package-lock.json', // Vercel will regenerate this + 'yarn.lock', + 'pnpm-lock.yaml' + ]; + + return skipPatterns.some(pattern => { + if (pattern.endsWith('/')) { + return filePath.startsWith(pattern); + } else if (pattern.includes('*')) { + const regex = new RegExp(pattern.replace('*', '.*')); + return regex.test(filePath); + } else { + return filePath === pattern || filePath.endsWith('/' + pattern); + } + }); +} + +// Ensure essential files exist for deployment +async function ensureEssentialFiles(files: VercelFile[]): Promise { + const fileNames = files.map(f => f.file); + + // Ensure package.json exists + if (!fileNames.includes('package.json')) { + files.push({ + file: 'package.json', + data: JSON.stringify({ + name: 'deployed-site', + version: '1.0.0', + type: 'module', + scripts: { + dev: 'vite', + build: 'vite build', + preview: 'vite preview' + }, + dependencies: { + react: '^18.2.0', + 'react-dom': '^18.2.0' + }, + devDependencies: { + '@types/react': '^18.2.0', + '@types/react-dom': '^18.2.0', + '@vitejs/plugin-react': '^4.0.0', + autoprefixer: '^10.4.14', + postcss: '^8.4.24', + tailwindcss: '^3.3.0', + typescript: '^5.0.0', + vite: '^4.4.0' + } + }, null, 2) + }); + } + + // Ensure index.html exists + if (!fileNames.includes('index.html')) { + files.push({ + file: 'index.html', + data: ` + + + + + My Site + + +
+ + +` + }); + } + + // Ensure vite.config.js exists + if (!fileNames.some(f => f.startsWith('vite.config'))) { + files.push({ + file: 'vite.config.js', + data: `import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist' + } +})` + }); + } + + // Ensure main.tsx exists if no entry point found + const hasEntryPoint = fileNames.some(f => + f.includes('main.') || f.includes('index.') || f.includes('App.') + ); + + if (!hasEntryPoint) { + files.push({ + file: 'src/main.tsx', + data: `import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +)` + }); + + files.push({ + file: 'src/App.tsx', + data: `import React from 'react' + +function App() { + return ( +
+
+

+ Welcome to Your Site +

+

+ Your site has been deployed successfully! +

+
+
+ ) +} + +export default App` + }); + + files.push({ + file: 'src/index.css', + data: `@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +}` + }); + } +} + +// Deploy to Vercel using their API +async function deployToVercel(files: VercelFile[], projectName: string) { + try { + const deploymentPayload = { + name: projectName, + files, + projectSettings: { + framework: 'vite' + }, + target: 'production' + }; + + console.log(`[deploy-vercel] Deploying ${files.length} files to Vercel...`); + console.log('[deploy-vercel] Project name:', projectName); + console.log('[deploy-vercel] Deployment payload size:', JSON.stringify(deploymentPayload).length, 'bytes'); + + // Vercel deployment with authentication + const response = await fetch('https://api.vercel.com/v13/deployments', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.VERCEL_TOKEN}`, + }, + body: JSON.stringify(deploymentPayload) + }); + + console.log('[deploy-vercel] Vercel API response status:', response.status, response.statusText); + + if (!response.ok) { + let errorData = ''; + try { + errorData = await response.text(); + } catch (e) { + errorData = 'Could not read error response'; + } + + console.error('[deploy-vercel] Vercel API error response:', { + status: response.status, + statusText: response.statusText, + errorData + }); + + return { + error: `Vercel deployment failed: ${response.status} ${response.statusText}. ${errorData}` + }; + } + + let deployment; + try { + deployment = await response.json(); + console.log('[deploy-vercel] Vercel deployment response:', deployment); + } catch (e) { + console.error('[deploy-vercel] Failed to parse Vercel response:', e); + return { + error: 'Failed to parse Vercel deployment response' + }; + } + + // Construct the deployment URL + const deploymentUrl = `https://${deployment.url}`; + console.log('[deploy-vercel] Final deployment URL:', deploymentUrl); + + return { + success: true, + url: deploymentUrl, + id: deployment.id + }; + + } catch (error: any) { + console.error('[deploy-vercel] Error calling Vercel API:', error); + return { + error: `Failed to call Vercel API: ${error.message}` + }; + } +} \ No newline at end of file diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index 002723b3..7fc8d68a 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -162,15 +162,18 @@ export async function POST(request: NextRequest) { const manifest: FileManifest | undefined = global.sandboxState?.fileCache?.manifest; - if (manifest) { + if (manifest && global.sandboxState?.fileCache?.files) { await sendProgress({ type: 'status', message: '🔍 Creating search plan...' }); - const fileContents = global.sandboxState.fileCache.files; + const fileCacheSafe = global.sandboxState?.fileCache; + const fileContents = fileCacheSafe!.files; console.log('[generate-ai-code-stream] Files available for search:', Object.keys(fileContents).length); // STEP 1: Get search plan from AI try { - const intentResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/analyze-edit-intent`, { + const vercelUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : undefined; + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || vercelUrl || 'http://localhost:3000'; + const intentResponse = await fetch(`${baseUrl}/api/analyze-edit-intent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, manifest, model }) @@ -326,7 +329,7 @@ User request: "${prompt}"`; // For now, fall back to keyword search since we don't have file contents for search execution // This path happens when no manifest was initially available - let targetFiles = []; + let targetFiles: string[] = []; if (!searchPlan || searchPlan.searchTerms.length === 0) { console.warn('[generate-ai-code-stream] No target files after fetch, searching for relevant files'); @@ -948,16 +951,18 @@ CRITICAL: When files are provided in the context: } // Store files in cache - for (const [path, content] of Object.entries(filesData.files)) { - const normalizedPath = path.replace('/home/user/app/', ''); - global.sandboxState.fileCache.files[normalizedPath] = { - content: content as string, - lastModified: Date.now() - }; - } - - if (filesData.manifest) { - global.sandboxState.fileCache.manifest = filesData.manifest; + if (global.sandboxState?.fileCache) { + for (const [path, content] of Object.entries(filesData.files)) { + const normalizedPath = path.replace('/home/user/app/', ''); + global.sandboxState.fileCache.files[normalizedPath] = { + content: content as string, + lastModified: Date.now() + }; + } + + if (filesData.manifest) { + global.sandboxState.fileCache.manifest = filesData.manifest; + } // Now try to analyze edit intent with the fetched manifest if (!editContext) { @@ -988,8 +993,10 @@ CRITICAL: When files are provided in the context: } // Update variables - backendFiles = global.sandboxState.fileCache.files; - hasBackendFiles = Object.keys(backendFiles).length > 0; + if (global.sandboxState?.fileCache?.files) { + backendFiles = global.sandboxState.fileCache.files; + hasBackendFiles = Object.keys(backendFiles).length > 0; + } console.log('[generate-ai-code-stream] Updated backend cache with fetched files'); } } @@ -1147,11 +1154,9 @@ CRITICAL: When files are provided in the context: const packagesToInstall: string[] = []; // Determine which provider to use based on model - const isAnthropic = model.startsWith('anthropic/'); - const isOpenAI = model.startsWith('openai/gpt-5'); - const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : groq); - const actualModel = isAnthropic ? model.replace('anthropic/', '') : - (model === 'openai/gpt-5') ? 'gpt-5' : model; + // Force Groq as provider + const modelProvider = groq; + const actualModel = model; // Make streaming API call with appropriate provider const streamOptions: any = { @@ -1227,14 +1232,7 @@ It's better to have 3 complete files than 10 incomplete files.` streamOptions.temperature = 0.7; } - // Add reasoning effort for GPT-5 models - if (isOpenAI) { - streamOptions.experimental_providerMetadata = { - openai: { - reasoningEffort: 'high' - } - }; - } + // Using Groq; no OpenAI-specific provider metadata const result = await streamText(streamOptions); @@ -1588,7 +1586,7 @@ Provide the complete file content without any truncation. Include all necessary } const completionResult = await streamText({ - model: completionClient(modelMapping[model] || model), + model: completionClient(model), messages: [ { role: 'system', @@ -1596,8 +1594,7 @@ Provide the complete file content without any truncation. Include all necessary }, { role: 'user', content: completionPrompt } ], - temperature: isGPT5 ? undefined : appConfig.ai.defaultTemperature, - maxTokens: appConfig.ai.truncationRecoveryMaxTokens + temperature: appConfig.ai.defaultTemperature }); // Get the full text from the stream diff --git a/app/api/generate-component-library/route.ts b/app/api/generate-component-library/route.ts new file mode 100644 index 00000000..e4f7e0f0 --- /dev/null +++ b/app/api/generate-component-library/route.ts @@ -0,0 +1,530 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { appConfig } from '@/config/app.config'; +import { groq } from '@ai-sdk/groq'; +import { generateText } from 'ai'; + +interface ComponentLibraryRequest { + sandboxId: string; + designStyle?: string; + projectContext?: string; +} + +const COMPONENT_LIBRARY_PROMPT = `You are a React component library generator. Generate a comprehensive set of reusable UI components that match the project's design system. + +Generate 15 essential React components with TypeScript and Tailwind CSS: + +1. Button (primary, secondary, outline variants) +2. Input (with label, error states) +3. Card (with header, body, footer) +4. Modal (with backdrop, close button) +5. Badge (success, warning, error, info) +6. Alert (success, warning, error, info) +7. Avatar (with fallback initials) +8. Dropdown Menu (with items, separators) +9. Tabs (horizontal navigation) +10. Progress Bar (with percentage) +11. Loading Spinner +12. Tooltip (hover state) +13. Checkbox (with label) +14. Radio Button (with label) +15. Toggle Switch + +Requirements: +- Use TypeScript with proper interfaces +- Use Tailwind CSS for styling +- Include proper accessibility attributes +- Use React.forwardRef where appropriate +- Include JSDoc comments +- Make components composable and reusable +- Follow modern React patterns (hooks, functional components) +- Include proper prop validation + +Design context: {{DESIGN_CONTEXT}} +Project context: {{PROJECT_CONTEXT}} + +Format your response as XML with individual file tags: + + +// Button component code here + + + +// Input component code here + + +Continue for all 15 components... + +Make sure each component is production-ready and follows best practices.`; + +function createComponentShowcasePage(generatedFiles: Array<{ path: string; content: string }>): string { + const componentImports = generatedFiles + .map(f => { + const componentName = f.path.split('/').pop()?.replace('.tsx', ''); + if (!componentName) return ''; + const capitalizedName = componentName.charAt(0).toUpperCase() + componentName.slice(1); + return `import ${capitalizedName} from '../../components/ui/${componentName}';`; + }) + .filter(line => line !== '') + .join('\n'); + + const componentSections = generatedFiles + .map(f => { + const componentName = f.path.split('/').pop()?.replace('.tsx', ''); + if (!componentName) return ''; + const capitalizedName = componentName.charAt(0).toUpperCase() + componentName.slice(1); + + // Clean up the component code for display + const cleanCode = f.content + .replace(/^import.*$/gm, '') // Remove imports + .replace(/^export default.*$/gm, '') // Remove export + .replace(/^\s*$/gm, '') // Remove empty lines + .trim(); + + return ` + const ${componentName}Examples = { + basic: \`<${capitalizedName}>Example\`, + withProps: \`<${capitalizedName} variant="primary" size="lg">Example\`, + }; + + `; + }) + .filter(section => section !== '') + .join('\n'); + + return `import React, { useState } from 'react'; +${componentImports} + +// Copy to clipboard function +const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); +}; + +// Component Section wrapper +const ComponentSection: React.FC<{ + title: string; + component: React.ComponentType; + examples: { basic: string; withProps: string }; + code: string; +}> = ({ title, component: Component, examples, code }) => { + const [showCode, setShowCode] = useState(false); + const [activeExample, setActiveExample] = useState('basic'); + + return ( +
+
+

{title}

+
+ +
+ {/* Live Preview */} +
+

Preview

+
+
+ {activeExample === 'basic' && Example} + {activeExample === 'withProps' && ( +
+ Primary + Secondary + Small + Large +
+ )} +
+
+
+ + {/* Example Tabs */} +
+
+ + +
+ +
+ {examples[activeExample as keyof typeof examples]} +
+
+ + {/* Code Toggle */} +
+ + + +
+ + {/* Full Component Code */} + {showCode && ( +
+
{code}
+
+ )} +
+
+ ); +}; + +export default function ComponentLibraryPage() { + return ( +
+
+
+

Component Library

+

+ Browse and copy components for your project. All components are built with TypeScript and Tailwind CSS. +

+
+ +
+
+

How to use these components:

+
    +
  1. 1. Click "Copy Component" to copy the full component code
  2. +
  3. 2. Create a new file in your components/ui/ folder
  4. +
  5. 3. Paste the code and save
  6. +
  7. 4. Import and use: \`import Button from './components/ui/button'\`
  8. +
+
+
+ +
+ ${componentSections} +
+ +
+
+

Need more components?

+

Generate additional components by chatting with the AI!

+ +
+
+
+
+ ); +}`; +} + +function createAppWithRouter(generatedFiles: Array<{ path: string; content: string }>): string { + return `import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; +import ComponentLibraryPage from './pages/components'; +import './App.css'; + +function HomePage() { + return ( +
+ + +
+
+

Welcome to Your App

+

+ This is your main application. You now have ${generatedFiles.length} custom components ready to use! +

+ +
+

🎉 Component Library Generated!

+

+ We've created ${generatedFiles.length} professional components for your project. Each component includes: +

+
    +
  • TypeScript support with proper types
  • +
  • Tailwind CSS styling
  • +
  • Accessibility attributes
  • +
  • Multiple variants and sizes
  • +
  • Copy-paste ready code
  • +
+ + + 🧩 Browse Components → + +
+ +
+
+

🚀 Quick Start

+

Import and use components in your project:

+
+ import Button from './components/ui/button';
+
+ <Button variant="primary">Click me</Button> +
+
+ +
+

📋 Copy & Paste

+

Each component page includes:

+
    +
  • • Live preview with examples
  • +
  • • Full source code
  • +
  • • Usage examples
  • +
  • • One-click copy buttons
  • +
+
+
+
+
+
+ ); +} + +function App() { + return ( + +
+ + } /> + } /> + +
+
+ ); +} + +export default App;`; +} + +function createNavigationComponent(): string { + return `import React from 'react'; + +const ComponentLibraryNav: React.FC = () => { + return ( +
+
+
+ 🧩 +
+
+

+ Component Library Generated! + + Browse Components → + +

+
+
+
+ ); +}; + +export default ComponentLibraryNav;`; +} + +export async function POST(req: NextRequest) { + try { + const body: ComponentLibraryRequest = await req.json(); + const { sandboxId, designStyle = 'modern', projectContext = 'general web application' } = body; + + if (!sandboxId) { + return NextResponse.json({ error: 'Sandbox ID is required' }, { status: 400 }); + } + + // Get current sandbox files for context + let currentProjectContext = projectContext; + try { + if (global.activeSandbox) { + // Try to get some context from existing files + const files = await global.activeSandbox.filesystem.list('/home/user/app'); + const hasReact = files.some((f: any) => f.name === 'package.json' || f.name.includes('react')); + const hasTailwind = files.some((f: any) => f.name.includes('tailwind')); + + currentProjectContext += hasReact ? ' (React project)' : ''; + currentProjectContext += hasTailwind ? ' (with Tailwind CSS)' : ''; + } + } catch (e) { + // Ignore context gathering errors + } + + // Generate component library using AI + const prompt = COMPONENT_LIBRARY_PROMPT + .replace('{{DESIGN_CONTEXT}}', designStyle) + .replace('{{PROJECT_CONTEXT}}', currentProjectContext); + + const result = await generateText({ + model: groq(appConfig.ai.defaultModel), + prompt, + temperature: 0.3, + }); + + // Parse the AI response to extract files + const filePattern = /([\s\S]*?)<\/file>/g; + const generatedFiles: Array<{ path: string; content: string }> = []; + + let match; + while ((match = filePattern.exec(result.text)) !== null) { + const [, path, content] = match; + generatedFiles.push({ + path: path.trim(), + content: content.trim() + }); + } + + if (generatedFiles.length === 0) { + return NextResponse.json({ + error: 'No components were generated. Please try again.', + rawResponse: result.text + }, { status: 500 }); + } + + // Install react-router-dom for navigation + try { + if (global.activeSandbox) { + await global.activeSandbox.commands.run('npm install react-router-dom @types/react-router-dom', { + cwd: '/home/user/app', + timeout: 60 + }); + } + } catch (e) { + // Continue even if package installation fails + console.log('Note: Could not install react-router-dom automatically'); + } + + // Apply files to sandbox + const results = { + filesCreated: [] as string[], + errors: [] as string[] + }; + + for (const file of generatedFiles) { + try { + if (!global.activeSandbox) { + results.errors.push('No active sandbox available'); + continue; + } + + // Ensure directory exists + const dirPath = file.path.substring(0, file.path.lastIndexOf('/')); + if (dirPath) { + await global.activeSandbox.filesystem.makeDir(`/home/user/app/${dirPath}`, { recursive: true }); + } + + // Write the file + await global.activeSandbox.filesystem.write(`/home/user/app/${file.path}`, file.content); + results.filesCreated.push(file.path); + + } catch (error: any) { + console.error(`Error creating file ${file.path}:`, error); + results.errors.push(`Failed to create ${file.path}: ${error.message}`); + } + } + + // Also create an index file to export all components + const indexContent = generatedFiles + .map(f => { + const componentName = f.path.split('/').pop()?.replace('.tsx', ''); + if (!componentName) return ''; + const capitalizedName = componentName.charAt(0).toUpperCase() + componentName.slice(1); + return `export { default as ${capitalizedName} } from './${componentName}';`; + }) + .filter(line => line !== '') + .join('\n'); + + try { + await global.activeSandbox.filesystem.write('/home/user/app/components/ui/index.ts', indexContent); + results.filesCreated.push('components/ui/index.ts'); + } catch (error: any) { + results.errors.push(`Failed to create index file: ${error.message}`); + } + + // Create component library showcase page + const showcasePage = createComponentShowcasePage(generatedFiles); + try { + await global.activeSandbox.filesystem.makeDir('/home/user/app/src/pages', { recursive: true }); + await global.activeSandbox.filesystem.write('/home/user/app/src/pages/components.tsx', showcasePage); + results.filesCreated.push('src/pages/components.tsx'); + } catch (error: any) { + results.errors.push(`Failed to create showcase page: ${error.message}`); + } + + // Create or update main App.tsx to include routing to components page + const appContent = createAppWithRouter(generatedFiles); + try { + await global.activeSandbox.filesystem.write('/home/user/app/src/App.tsx', appContent); + results.filesCreated.push('src/App.tsx'); + } catch (error: any) { + // If App.tsx already exists, try to add navigation instead + try { + await global.activeSandbox.filesystem.write('/home/user/app/src/ComponentLibraryNav.tsx', createNavigationComponent()); + results.filesCreated.push('src/ComponentLibraryNav.tsx'); + } catch (navError: any) { + results.errors.push(`Failed to create navigation: ${navError.message}`); + } + } + + return NextResponse.json({ + success: true, + message: `Generated ${results.filesCreated.length} components`, + results, + componentsGenerated: generatedFiles.length + }); + + } catch (error: any) { + console.error('Component library generation error:', error); + return NextResponse.json({ + error: 'Failed to generate component library', + details: error.message + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/get-sandbox-files/route.ts b/app/api/get-sandbox-files/route.ts index d892046e..4ae4a0a9 100644 --- a/app/api/get-sandbox-files/route.ts +++ b/app/api/get-sandbox-files/route.ts @@ -1,4 +1,5 @@ -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; +import { Sandbox } from '@e2b/code-interpreter'; import { parseJavaScriptFile, buildComponentTree } from '@/lib/file-parser'; import { FileManifest, FileInfo, RouteInfo } from '@/types/file-manifest'; import type { SandboxState } from '@/types/sandbox'; @@ -7,8 +8,21 @@ declare global { var activeSandbox: any; } -export async function GET() { +export async function GET(request: NextRequest) { try { + const { searchParams } = new URL(request.url); + const sandboxId = searchParams.get('sandbox') || undefined; + + if (!global.activeSandbox && sandboxId) { + try { + console.log(`[get-sandbox-files] Attempting reconnect to sandbox ${sandboxId}...`); + const sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY }); + global.activeSandbox = sandbox; + } catch (e) { + console.error('[get-sandbox-files] Reconnect failed:', e); + } + } + if (!global.activeSandbox) { return NextResponse.json({ success: false, diff --git a/app/api/kill-sandbox/route.ts b/app/api/kill-sandbox/route.ts index 70d005af..57d3b6e7 100644 --- a/app/api/kill-sandbox/route.ts +++ b/app/api/kill-sandbox/route.ts @@ -1,47 +1,22 @@ import { NextResponse } from 'next/server'; - -declare global { - var activeSandbox: any; - var sandboxData: any; - var existingFiles: Set; -} +import SandboxManager from '@/lib/sandbox-manager'; export async function POST() { try { - console.log('[kill-sandbox] Killing active sandbox...'); - - let sandboxKilled = false; - - // Kill existing sandbox if any - if (global.activeSandbox) { - try { - await global.activeSandbox.close(); - sandboxKilled = true; - console.log('[kill-sandbox] Sandbox closed successfully'); - } catch (e) { - console.error('[kill-sandbox] Failed to close sandbox:', e); - } - global.activeSandbox = null; - global.sandboxData = null; - } - - // Clear existing files tracking - if (global.existingFiles) { - global.existingFiles.clear(); - } - + const manager = SandboxManager.getInstance(); + await manager.killSandbox(); + return NextResponse.json({ success: true, - sandboxKilled, + sandboxKilled: true, message: 'Sandbox cleaned up successfully' }); - - } catch (error) { + } catch (error: any) { console.error('[kill-sandbox] Error:', error); return NextResponse.json( { success: false, - error: (error as Error).message + error: error?.message || 'Failed to kill sandbox' }, { status: 500 } ); diff --git a/app/api/restart-vite/route.ts b/app/api/restart-vite/route.ts index ca6b4ba1..7fd14530 100644 --- a/app/api/restart-vite/route.ts +++ b/app/api/restart-vite/route.ts @@ -1,11 +1,25 @@ -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; +import { Sandbox } from '@e2b/code-interpreter'; declare global { var activeSandbox: any; } -export async function POST() { +export async function POST(request: NextRequest) { try { + const body = await request.json().catch(() => ({})); + const sandboxId = body?.sandboxId as string | undefined; + + if (!global.activeSandbox && sandboxId) { + try { + console.log(`[restart-vite] Attempting reconnect to sandbox ${sandboxId}...`); + const sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY }); + global.activeSandbox = sandbox; + } catch (e) { + console.error('[restart-vite] Reconnect failed:', e); + } + } + if (!global.activeSandbox) { return NextResponse.json({ success: false, diff --git a/app/api/sandbox-logs/route.ts b/app/api/sandbox-logs/route.ts index 84d02088..b38badc5 100644 --- a/app/api/sandbox-logs/route.ts +++ b/app/api/sandbox-logs/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { Sandbox } from '@e2b/code-interpreter'; declare global { var activeSandbox: any; @@ -6,6 +7,19 @@ declare global { export async function GET(request: NextRequest) { try { + const { searchParams } = new URL(request.url); + const sandboxId = searchParams.get('sandbox') || undefined; + + if (!global.activeSandbox && sandboxId) { + try { + console.log(`[sandbox-logs] Attempting reconnect to sandbox ${sandboxId}...`); + const sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY }); + global.activeSandbox = sandbox; + } catch (e) { + console.error('[sandbox-logs] Reconnect failed:', e); + } + } + if (!global.activeSandbox) { return NextResponse.json({ success: false, diff --git a/app/api/sandbox-status/route.ts b/app/api/sandbox-status/route.ts index 7f5e0b56..0fcdda70 100644 --- a/app/api/sandbox-status/route.ts +++ b/app/api/sandbox-status/route.ts @@ -1,54 +1,28 @@ -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; +import SandboxManager from '@/lib/sandbox-manager'; -declare global { - var activeSandbox: any; - var sandboxData: any; - var existingFiles: Set; -} - -export async function GET() { +export async function GET(request: NextRequest) { try { - // Check if sandbox exists - const sandboxExists = !!global.activeSandbox; - - let sandboxHealthy = false; - let sandboxInfo = null; - - if (sandboxExists && global.activeSandbox) { - try { - // Since Python isn't available in the Vite template, just check if sandbox exists - // The sandbox object existing is enough to confirm it's healthy - sandboxHealthy = true; - sandboxInfo = { - sandboxId: global.sandboxData?.sandboxId, - url: global.sandboxData?.url, - filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [], - lastHealthCheck: new Date().toISOString() - }; - } catch (error) { - console.error('[sandbox-status] Health check failed:', error); - sandboxHealthy = false; - } - } - + const manager = SandboxManager.getInstance(); + const status = await manager.getSandboxStatus(); + return NextResponse.json({ success: true, - active: sandboxExists, - healthy: sandboxHealthy, - sandboxData: sandboxInfo, - message: sandboxHealthy - ? 'Sandbox is active and healthy' - : sandboxExists - ? 'Sandbox exists but is not responding' + active: status.active, + healthy: status.healthy, + sandboxData: status.sandboxInfo, + message: status.healthy + ? 'Sandbox is active and healthy' + : status.active + ? 'Sandbox exists but is not responding' : 'No active sandbox' }); - - } catch (error) { + } catch (error: any) { console.error('[sandbox-status] Error:', error); return NextResponse.json({ success: false, active: false, - error: (error as Error).message + error: error?.message || 'Failed to get sandbox status' }, { status: 500 }); } } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 8c11a46a..33c1062e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,11 +1,12 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; +import { Analytics } from "@vercel/analytics/next"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Open Lovable", + title: "Devs", description: "Re-imagine any website in seconds with AI-powered website builder.", }; @@ -18,6 +19,7 @@ export default function RootLayout({ {children} + ); diff --git a/app/page.tsx b/app/page.tsx index 43b01d53..65e51f05 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, Suspense } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import { appConfig } from '@/config/app.config'; import { Button } from '@/components/ui/button'; @@ -12,9 +12,8 @@ import { FiFile, FiChevronRight, FiChevronDown, - FiGithub, BsFolderFill, - BsFolder2Open, + BsFolder2Open, SiJavascript, SiReact, SiCss3, @@ -22,6 +21,20 @@ import { } from '@/lib/icons'; import { motion, AnimatePresence } from 'framer-motion'; import CodeApplicationProgress, { type CodeApplicationState } from '@/components/CodeApplicationProgress'; +import AnimatedCodeBackground from '@/components/AnimatedCodeBackground'; +import { AISuggestions } from '@/components/AISuggestions'; +import { AISuggestion } from '@/types/suggestions'; +import { SuggestionGenerator } from '@/lib/suggestion-generator'; +// import DemoFlow from '@/components/DemoFlow'; + +// Wrap page content with Suspense to satisfy useSearchParams requirement +export default function AISandboxPage() { + return ( + + + + ); +} interface SandboxData { sandboxId: string; @@ -42,7 +55,7 @@ interface ChatMessage { }; } -export default function AISandboxPage() { +function AISandboxPageContent() { const [sandboxData, setSandboxData] = useState(null); const [loading, setLoading] = useState(false); const [status, setStatus] = useState({ text: 'Not connected', active: false }); @@ -60,14 +73,12 @@ export default function AISandboxPage() { const [aiEnabled] = useState(true); const searchParams = useSearchParams(); const router = useRouter(); - const [aiModel, setAiModel] = useState(() => { - const modelParam = searchParams.get('model'); - return appConfig.ai.availableModels.includes(modelParam || '') ? modelParam! : appConfig.ai.defaultModel; - }); + const [aiModel] = useState(appConfig.ai.defaultModel); const [urlOverlayVisible, setUrlOverlayVisible] = useState(false); const [urlInput, setUrlInput] = useState(''); const [urlStatus, setUrlStatus] = useState([]); - const [showHomeScreen, setShowHomeScreen] = useState(true); + const [showHomeScreen, setShowHomeScreen] = useState(true); + const [isClient, setIsClient] = useState(false); const [expandedFolders, setExpandedFolders] = useState>(new Set(['app', 'src', 'src/components'])); const [selectedFile, setSelectedFile] = useState(null); const [homeScreenFading, setHomeScreenFading] = useState(false); @@ -85,6 +96,8 @@ export default function AISandboxPage() { const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null); const [sandboxFiles, setSandboxFiles] = useState>({}); const [fileStructure, setFileStructure] = useState(''); + const [aiSuggestions, setAiSuggestions] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); const [conversationContext, setConversationContext] = useState<{ scrapedWebsites: Array<{ url: string; content: any; timestamp: Date }>; @@ -136,6 +149,9 @@ export default function AISandboxPage() { // Clear old conversation data on component mount and create/restore sandbox useEffect(() => { + // Mark as client-side to avoid hydration issues + setIsClient(true); + const initializePage = async () => { // Clear old conversation try { @@ -151,19 +167,42 @@ export default function AISandboxPage() { // Check if sandbox ID is in URL const sandboxIdParam = searchParams.get('sandbox'); - + if (sandboxIdParam) { - // Try to restore existing sandbox + // Try to restore existing sandbox by reconnecting on the server console.log('[home] Attempting to restore sandbox:', sandboxIdParam); setLoading(true); try { - // For now, just create a new sandbox - you could enhance this to actually restore - // the specific sandbox if your backend supports it - await createSandbox(true); + const res = await fetch(`/api/sandbox-status?sandbox=${encodeURIComponent(sandboxIdParam)}`); + const data = await res.json(); + + if (res.ok && data?.active && data?.sandboxData?.sandboxId) { + setSandboxData(data.sandboxData); + updateStatus('Sandbox active', true); + + // Update URL without navigating/remounting + const newParams = new URLSearchParams(searchParams.toString()); + newParams.set('sandbox', data.sandboxData.sandboxId); + newParams.set('model', aiModel); + if (typeof window !== 'undefined') { + const newUrl = `/?${newParams.toString()}`; + window.history.replaceState(null, '', newUrl); + } + + // Fade out loading background after restore + setTimeout(() => { + setShowLoadingBackground(false); + }, 1000); + } else { + // Fall back to creating a new sandbox if restore fails + await createSandbox(true); + } } catch (error) { console.error('[ai-sandbox] Failed to restore sandbox:', error); // Create new sandbox on error await createSandbox(true); + } finally { + setLoading(false); } } else { // Automatically create new sandbox @@ -175,6 +214,16 @@ export default function AISandboxPage() { initializePage(); }, []); // Run only on mount + // Handle client-side initialization after hydration + useEffect(() => { + if (isClient) { + const persisted = window.sessionStorage.getItem('ui.showHomeScreen'); + if (persisted !== null) { + setShowHomeScreen(persisted === 'true'); + } + } + }, [isClient]); + useEffect(() => { // Handle Escape key for home screen const handleKeyDown = (e: KeyboardEvent) => { @@ -182,6 +231,9 @@ export default function AISandboxPage() { setHomeScreenFading(true); setTimeout(() => { setShowHomeScreen(false); + if (typeof window !== 'undefined') { + window.sessionStorage.setItem('ui.showHomeScreen', 'false'); + } setHomeScreenFading(false); }, 500); } @@ -334,7 +386,7 @@ export default function AISandboxPage() { const checkSandboxStatus = async () => { try { - const response = await fetch('/api/sandbox-status'); + const response = await fetch(`/api/sandbox-status${sandboxData?.sandboxId ? `?sandbox=${sandboxData.sandboxId}` : ''}`); const data = await response.json(); if (data.active && data.healthy && data.sandboxData) { @@ -380,11 +432,14 @@ export default function AISandboxPage() { log(`Sandbox ID: ${data.sandboxId}`); log(`URL: ${data.url}`); - // Update URL with sandbox ID + // Update URL with sandbox ID without navigating/remounting const newParams = new URLSearchParams(searchParams.toString()); newParams.set('sandbox', data.sandboxId); newParams.set('model', aiModel); - router.push(`/?${newParams.toString()}`, { scroll: false }); + if (typeof window !== 'undefined') { + const newUrl = `/?${newParams.toString()}`; + window.history.replaceState(null, '', newUrl); + } // Fade out loading background after sandbox loads setTimeout(() => { @@ -405,7 +460,7 @@ export default function AISandboxPage() { const restartResponse = await fetch('/api/restart-vite', { method: 'POST', headers: { 'Content-Type': 'application/json' } - }); + , body: JSON.stringify({ sandboxId: data.sandboxId }) }); if (restartResponse.ok) { const restartData = await restartResponse.json(); @@ -425,9 +480,10 @@ export default function AISandboxPage() { Tip: I automatically detect and install npm packages from your code imports (like react-router-dom, axios, etc.)`, 'system'); } + // Reload cross-origin iframe by resetting its src with a cache-buster setTimeout(() => { if (iframeRef.current) { - iframeRef.current.src = data.url; + iframeRef.current.src = `${data.url}?t=${Date.now()}`; } }, 100); } else { @@ -516,7 +572,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik } else if (data.message.includes('Creating files') || data.message.includes('Applying')) { setCodeApplicationState({ stage: 'applying', - filesGenerated: results.filesCreated + filesGenerated: data.files || 0 }); } break; @@ -616,8 +672,8 @@ Tip: I automatically detect and install npm packages from your code imports (lik if (data.success) { const { results } = data; - - // Log package installation results without duplicate messages + + // Log package installation results without duplicate messages if (results.packagesInstalled?.length > 0) { log(`Packages installed: ${results.packagesInstalled.join(', ')}`); } @@ -677,22 +733,22 @@ Tip: I automatically detect and install npm packages from your code imports (lik log(data.explanation); } - if (data.autoCompleted) { + if (finalData.autoCompleted) { log('Auto-generating missing components...', 'command'); - if (data.autoCompletedComponents) { + if (finalData.autoCompletedComponents) { setTimeout(() => { log('Auto-generated missing components:', 'info'); - data.autoCompletedComponents.forEach((comp: string) => { + finalData.autoCompletedComponents.forEach((comp: string) => { log(` ${comp}`, 'command'); }); }, 1000); } - } else if (data.warning) { - log(data.warning, 'error'); + } else if (finalData.warning) { + log(finalData.warning, 'error'); - if (data.missingImports && data.missingImports.length > 0) { - const missingList = data.missingImports.join(', '); + if (finalData.missingImports && finalData.missingImports.length > 0) { + const missingList = finalData.missingImports.join(', '); addChatMessage( `Ask me to "create the missing components: ${missingList}" to fix these import errors.`, 'system' @@ -702,7 +758,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik log('Code applied successfully!'); console.log('[applyGeneratedCode] Response data:', data); - console.log('[applyGeneratedCode] Debug info:', data.debug); + console.log('[applyGeneratedCode] Debug info:', finalData.debug); console.log('[applyGeneratedCode] Current sandboxData:', sandboxData); console.log('[applyGeneratedCode] Current iframe element:', iframeRef.current); console.log('[applyGeneratedCode] Current iframe src:', iframeRef.current?.src); @@ -768,13 +824,9 @@ Tip: I automatically detect and install npm packages from your code imports (lik // Method 2: Force reload after a short delay setTimeout(() => { - try { - if (iframeRef.current?.contentWindow) { - iframeRef.current.contentWindow.location.reload(); - console.log('[home] Force reloaded iframe content'); - } - } catch (e) { - console.log('[home] Could not reload iframe (cross-origin):', e); + // Cross-origin: reset src instead of accessing contentWindow + if (iframeRef.current && sandboxData?.url) { + iframeRef.current.src = `${sandboxData.url}?t=${Date.now()}&forceReload=1`; } }, 1000); } @@ -907,7 +959,8 @@ Tip: I automatically detect and install npm packages from your code imports (lik const response = await fetch('/api/restart-vite', { method: 'POST', - headers: { 'Content-Type': 'application/json' } + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sandboxId: sandboxData?.sandboxId }) }); if (response.ok) { @@ -997,7 +1050,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik // Create a map of edited files const editedFiles = new Set( generationProgress.files - .filter(f => f.edited) + .filter(f => f.completed) .map(f => f.path) ); @@ -1010,7 +1063,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik if (!fileTree[dir]) fileTree[dir] = []; fileTree[dir].push({ name: fileName, - edited: file.edited || false + edited: file.completed || false }); }); @@ -1393,7 +1446,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik ref={iframeRef} src={sandboxData.url} className="w-full h-full border-none" - title="Open Lovable Sandbox" + title="Makerthrive Cloner Sandbox" allow="clipboard-write" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals" /> @@ -1634,8 +1687,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik ...updatedState.files[existingFileIndex], content: fileContent.trim(), type: fileType, - completed: true, - edited: true + completed: true }, ...updatedState.files.slice(existingFileIndex + 1) ]; @@ -1645,8 +1697,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik path: filePath, content: fileContent.trim(), type: fileType, - completed: true, - edited: false + completed: true }]; } @@ -1839,6 +1890,31 @@ Tip: I automatically detect and install npm packages from your code imports (lik thinkingDuration: undefined })); + // Generate AI suggestions after successful generation + if (generationProgress.files.length > 0 && conversationContext.scrapedWebsites.length > 0) { + try { + const generatedCode = { + files: generationProgress.files.map(f => ({ path: f.path, content: '' })), + components: generationProgress.files.filter(f => f.path.includes('components/')).map(f => f.path.split('/').pop() || '') + }; + + const suggestions = SuggestionGenerator.generateSuggestions( + conversationContext.scrapedWebsites, + generatedCode, + 6 + ); + + setAiSuggestions(suggestions); + setShowSuggestions(true); + + // Add a message about the suggestions + addChatMessage('💡 Here are some AI-powered suggestions to enhance your website!', 'system'); + } catch (error) { + console.error('Failed to generate suggestions:', error); + // Don't fail the whole generation if suggestions fail + } + } + setTimeout(() => { // Switch to preview but keep files for display setActiveTab('preview'); @@ -1914,6 +1990,161 @@ Tip: I automatically detect and install npm packages from your code imports (lik } }; + const generateComponentLibrary = async () => { + if (!sandboxData) { + addChatMessage('No active sandbox available. Create a sandbox first!', 'system'); + return; + } + + setLoading(true); + log('Generating component library...'); + addChatMessage('🎨 Generating a complete component library for your project...', 'system'); + + try { + const response = await fetch('/api/generate-component-library', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sandboxId: sandboxData.sandboxId, + designStyle: 'modern', + projectContext: 'React application with Tailwind CSS' + }) + }); + + const data = await response.json(); + + if (data.success) { + log(`Component library generated! Created ${data.componentsGenerated} components`); + addChatMessage( + `✅ Component library generated successfully!\n\n` + + `📦 Created ${data.componentsGenerated} components:\n` + + `${data.results.filesCreated.join(', ')}\n\n` + + `💡 Components are available in your components/ui/ folder.\n` + + `Import them like: import { Button } from '@/components/ui/button'`, + 'system' + ); + + // Refresh the iframe to show the new components + if (iframeRef.current && sandboxData?.url) { + setTimeout(() => { + if (iframeRef.current) { + const urlWithTimestamp = `${sandboxData.url}?t=${Date.now()}`; + iframeRef.current.src = urlWithTimestamp; + } + }, 2000); + } + + // Structure will be updated automatically through file monitoring + + } else { + throw new Error(data.error || 'Failed to generate component library'); + } + } catch (error: any) { + log(`Failed to generate component library: ${error.message}`, 'error'); + addChatMessage(`❌ Failed to generate component library: ${error.message}`, 'system'); + } finally { + setLoading(false); + } + }; + + const deployToVercel = async () => { + if (!sandboxData) { + addChatMessage('No active sandbox to deploy. Create a project first!', 'system'); + return; + } + + // Check if we have files to deploy + if (!generationProgress.files || generationProgress.files.length === 0) { + addChatMessage('❌ **No files to deploy!**\n\nPlease generate some code first, then try deploying again.', 'system'); + return; + } + + setLoading(true); + log('Deploying to Vercel...'); + addChatMessage('🚀 **Demo Deployment in Progress**\n\nDeploying to our Vercel account for demo purposes... This may take 30-60 seconds.\n\n⚠️ **Note:** This deployment will be on our account, not yours.', 'system'); + + try { + // Prepare files for deployment + const filesForDeployment = generationProgress.files + .filter(file => file.completed) // Only include completed files + .map(file => ({ + path: file.path, + content: file.content + })); + + console.log(`[deploy] Total files: ${generationProgress.files.length}`); + console.log(`[deploy] Completed files: ${generationProgress.files.filter(f => f.completed).length}`); + console.log(`[deploy] Files for deployment:`, filesForDeployment); + console.log(`[deploy] Sending ${filesForDeployment.length} files for deployment`); + + if (filesForDeployment.length === 0) { + addChatMessage('❌ **No completed files to deploy!**\n\nPlease wait for code generation to complete, then try deploying again.', 'system'); + setLoading(false); + return; + } + + const response = await fetch('/api/deploy-to-vercel', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sandboxId: sandboxData.sandboxId, + projectName: 'my-site-' + Date.now().toString().slice(-6), // Unique name + files: filesForDeployment + }) + }); + + const data = await response.json(); + + if (data.success) { + log(`Deployment successful! Live at: ${data.url}`); + addChatMessage( + `🎉 **Demo Deployment Successful!**\n\n` + + `🌐 **Live URL:** ${data.url}\n\n` + + `⚠️ **Important:** This deployment is on our Vercel account (demo purposes)\n` + + `📋 You can share this URL, but we control the deployment\n\n` + + `💡 **Want your own deployment?** Download as ZIP and deploy to your own Vercel account!`, + 'system' + ); + + // Auto-open the deployed site in a new tab after a short delay + setTimeout(() => { + window.open(data.url, '_blank'); + }, 1000); + + } else { + throw new Error(data.error || 'Deployment failed'); + } + } catch (error: any) { + log(`Deployment failed: ${error.message}`, 'error'); + + // Check if it's the authentication error + if (error.message.includes('authentication') || error.message.includes('token')) { + addChatMessage( + `🔐 **Vercel Authentication Required**\n\n` + + `To deploy to Vercel, you need to:\n\n` + + `1️⃣ **Get your Vercel token:**\n` + + ` • Go to [vercel.com/account/tokens](https://vercel.com/account/tokens)\n` + + ` • Create a new token\n\n` + + `2️⃣ **Add it to your environment:**\n` + + ` • Add \`VERCEL_TOKEN=your_token_here\` to your \`.env.local\` file\n\n` + + `3️⃣ **Redeploy:** Click the deploy button again\n\n` + + `💡 **Alternative:** Download your project as ZIP and deploy manually!`, + 'system' + ); + } else { + addChatMessage( + `❌ **Deployment Failed**\n\n` + + `Error: ${error.message}\n\n` + + `💡 **Try again:** The issue might be temporary\n` + + `🛠️ **Need help?** Make sure your project has valid React/HTML files`, + 'system' + ); + } + } finally { + setLoading(false); + } + }; + const reapplyLastGeneration = async () => { if (!conversationContext.lastGeneratedCode) { addChatMessage('No previous generation to re-apply', 'system'); @@ -1974,6 +2205,53 @@ Tip: I automatically detect and install npm packages from your code imports (lik type: 'system', timestamp: new Date() }]); + setAiSuggestions([]); + setShowSuggestions(false); + }; + + const handleSuggestionClick = (suggestion: AISuggestion) => { + // Set the suggestion prompt as the chat input + setAiChatInput(suggestion.prompt); + + // Add a message showing the suggestion was selected + addChatMessage(`Selected suggestion: ${suggestion.title}`, 'system'); + + // Focus the chat input + setTimeout(() => { + const chatInput = document.querySelector('textarea[placeholder=""]') as HTMLTextAreaElement; + if (chatInput) { + chatInput.focus(); + chatInput.setSelectionRange(chatInput.value.length, chatInput.value.length); + } + }, 100); + }; + + const regenerateSuggestions = () => { + if (conversationContext.scrapedWebsites.length > 0 && generationProgress.files.length > 0) { + try { + const generatedCode = { + files: generationProgress.files.map(f => ({ path: f.path, content: '' })), + components: generationProgress.files.filter(f => f.path.includes('components/')).map(f => f.path.split('/').pop() || '') + }; + + const suggestions = SuggestionGenerator.generateSuggestions( + conversationContext.scrapedWebsites, + generatedCode, + 6 + ); + + setAiSuggestions(suggestions); + addChatMessage('🔄 Generated fresh AI suggestions for your website!', 'system'); + } catch (error) { + console.error('Failed to regenerate suggestions:', error); + addChatMessage('❌ Failed to regenerate suggestions. Please try again.', 'system'); + } + } + }; + + const hideSuggestions = () => { + setShowSuggestions(false); + addChatMessage('Suggestions hidden. You can regenerate them anytime!', 'system'); }; @@ -2115,7 +2393,7 @@ Focus on the key sections and content, making it clean and modern while preservi prompt: recreatePrompt, model: aiModel, context: { - sandboxId: sandboxData?.id, + sandboxId: sandboxData?.sandboxId, structure: structureContent, conversationContext: conversationContext } @@ -2338,8 +2616,13 @@ Focus on the key sections and content, making it clean and modern while preservi e.preventDefault(); if (!homeUrlInput.trim()) return; - setHomeScreenFading(true); - + // Immediately switch off home screen to avoid flicker/remounts + setShowHomeScreen(false); + if (typeof window !== 'undefined') { + window.sessionStorage.setItem('ui.showHomeScreen', 'false'); + } + setHomeScreenFading(false); + // Clear messages and immediately show the cloning message setChatMessages([]); let displayUrl = homeUrlInput.trim(); @@ -2359,15 +2642,11 @@ Focus on the key sections and content, making it clean and modern while preservi captureUrlScreenshot(displayUrl); } - // Set loading stage immediately before hiding home screen + // Set loading stage and switch to preview immediately setLoadingStage('gathering'); - // Also ensure we're on preview tab to show the loading overlay setActiveTab('preview'); - + setTimeout(async () => { - setShowHomeScreen(false); - setHomeScreenFading(false); - // Wait for sandbox to be ready (if it's still creating) await sandboxPromise; @@ -2571,8 +2850,7 @@ Focus on the key sections and content, making it clean and modern.`; ...updatedState.files[existingFileIndex], content: fileContent.trim(), type: fileType, - completed: true, - edited: true + completed: true }, ...updatedState.files.slice(existingFileIndex + 1) ]; @@ -2582,8 +2860,7 @@ Focus on the key sections and content, making it clean and modern.`; path: filePath, content: fileContent.trim(), type: fileType, - completed: true, - edited: false + completed: true }]; } @@ -2687,6 +2964,31 @@ Focus on the key sections and content, making it clean and modern.`; setUrlStatus([]); setHomeContextInput(''); + // Generate AI suggestions after successful generation + if (generationProgress.files.length > 0 && conversationContext.scrapedWebsites.length > 0) { + try { + const generatedCode = { + files: generationProgress.files.map(f => ({ path: f.path, content: '' })), + components: generationProgress.files.filter(f => f.path.includes('components/')).map(f => f.path.split('/').pop() || '') + }; + + const suggestions = SuggestionGenerator.generateSuggestions( + conversationContext.scrapedWebsites, + generatedCode, + 6 + ); + + setAiSuggestions(suggestions); + setShowSuggestions(true); + + // Add a message about the suggestions + addChatMessage('💡 Here are some AI-powered suggestions to enhance your website!', 'system'); + } catch (error) { + console.error('Failed to generate suggestions:', error); + // Don't fail the whole generation if suggestions fail + } + } + // Clear generation progress and all screenshot/design states setGenerationProgress(prev => ({ ...prev, @@ -2725,298 +3027,154 @@ Focus on the key sections and content, making it clean and modern.`; return (
- {/* Home Screen Overlay */} - {showHomeScreen && ( -
- {/* Simple Sun Gradient Background */} -
- {/* Main Sun - Pulsing */} -
- - {/* Inner Sun Core - Brighter */} -
- - {/* Outer Glow - Subtle */} -
- - {/* Giant Glowing Orb - Center Bottom */} -
-
-
-
-
-
-
-
+ {/* Loading state while client initializes */} + {!isClient && ( +
+
+
+

Loading...

- - - {/* Close button on hover */} - + + {/* Header */} +
+ devs.dev { - setHomeScreenFading(true); - setTimeout(() => { - setShowHomeScreen(false); - setHomeScreenFading(false); - }, 500); + setShowHomeScreen(true); + router.push('/'); }} - 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'} - > - - - - - - {/* Header */} - - - {/* Main content */} -
-
- {/* Firecrawl-style Header */} -
-

- Open Lovable - Open Lovable -

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

+ Clone any website + Clone any website + _ +

+ + Prompt visual changes. Get a clean React app you can run and deploy. + +
+ +
+
+
+
+ + - +
+

+ FREE Service Temporarily Unavailable +

+

+ Due to high API costs and heavy usage, our website cloning service is temporarily suspended. + We're working on optimizing costs and will resume service soon. +

+

+ Thank you for your patience and understanding. +

+
+

+ Want your own version of this site? We offer white-label boilerplates with custom domain and branding. +

+

+ Email art@makerthrive.com for details. +

+
- - {/* Style Selector - Slides out when valid domain is entered */} - {showStyleSelector && ( -
-
-
-

How do you want your site to look?

-
- {[ - { name: 'Neobrutalist', description: 'Bold colors, thick borders' }, - { name: 'Glassmorphism', description: 'Frosted glass effects' }, - { name: 'Minimalist', description: 'Clean and simple' }, - { name: 'Dark Mode', description: 'Dark theme' }, - { name: 'Gradient', description: 'Colorful gradients' }, - { name: 'Retro', description: '80s/90s aesthetic' }, - { name: 'Modern', description: 'Contemporary design' }, - { name: 'Monochrome', description: 'Black and white' } - ].map((style) => ( - - ))} -
- - {/* Additional context input - part of the style selector */} -
- { - if (!selectedStyle) return homeContextInput; - // Extract additional context by removing the style theme part - const additional = homeContextInput.replace(new RegExp('^' + selectedStyle.toLowerCase() + ' theme\\s*,?\\s*', 'i'), ''); - return additional; - })()} - onChange={(e) => { - const additionalContext = e.target.value; - if (selectedStyle) { - setHomeContextInput(selectedStyle.toLowerCase() + ' theme' + (additionalContext.trim() ? ', ' + additionalContext : '')); - } else { - setHomeContextInput(additionalContext); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const form = e.currentTarget.closest('form'); - if (form) { - form.requestSubmit(); - } - } - }} - placeholder="Add more details: specific features, color preferences..." - className="w-full px-4 py-2 text-sm bg-white border border-gray-200 rounded-lg text-gray-900 placeholder-gray-500 focus:outline-none focus:border-orange-300 focus:ring-2 focus:ring-orange-100 transition-all duration-200" - /> -
-
-
-
- )} - - - {/* Model Selector */} -
-
+ + +
+ This domain and site are for sale. If interested, email art@makerthrive.com. +
+
+ + Buy Me A Coffee + +
+ + {/** Demo disabled for now **/} + {/* +
+ +
+ */}
+
)} + {isClient && !showHomeScreen && (
- Firecrawl { + setShowHomeScreen(true); + router.push('/'); + }} />
- {/* Model Selector - Left side */} - + {/* Model Selector removed - model is forced in config */} + + + {/* Manual Deploy Instructions Button */} + + + {/* Deploy to Vercel button - Commented out for now */} + {/* + + */} +
{status.text}
+ )}
{/* Center Panel - AI Chat (1/3 of remaining width) */} @@ -3291,9 +3513,33 @@ Focus on the key sections and content, making it clean and modern.`; )}
)} + + {/* AI Suggestions */} +
+ {/* Show suggestions button when hidden */} + {!showSuggestions && aiSuggestions.length > 0 && ( +
+ +
+ )} +