Spaces:
Build error
Build error
Deploy to clean space
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .cursor/mcp.json +8 -0
- .env.example +46 -0
- .gitignore +60 -0
- Dockerfile +26 -0
- LICENSE +21 -0
- PROJECT_README.md +67 -0
- README.md +6 -6
- app/api/analyze-edit-intent/route.ts +190 -0
- app/api/apply-ai-code-stream/route.ts +799 -0
- app/api/apply-ai-code/route.ts +800 -0
- app/api/check-vite-errors/route.ts +12 -0
- app/api/clear-vite-errors-cache/route.ts +26 -0
- app/api/conversation-state/route.ts +160 -0
- app/api/create-ai-sandbox-v2/route.ts +103 -0
- app/api/create-ai-sandbox/route.ts +384 -0
- app/api/create-zip/route.ts +70 -0
- app/api/detect-and-install-packages/route.ts +189 -0
- app/api/extract-brand-styles/route.ts +72 -0
- app/api/generate-ai-code-stream/route.ts +1896 -0
- app/api/get-sandbox-files/route.ts +208 -0
- app/api/install-packages-v2/route.ts +48 -0
- app/api/install-packages/route.ts +259 -0
- app/api/kill-sandbox/route.ts +49 -0
- app/api/monitor-vite-logs/route.ts +121 -0
- app/api/report-vite-error/route.ts +62 -0
- app/api/restart-vite/route.ts +103 -0
- app/api/run-command-v2/route.ts +50 -0
- app/api/run-command/route.ts +63 -0
- app/api/sandbox-logs/route.ts +89 -0
- app/api/sandbox-status/route.ts +57 -0
- app/api/scrape-screenshot/route.ts +81 -0
- app/api/scrape-url-enhanced/route.ts +127 -0
- app/api/scrape-website/route.ts +110 -0
- app/api/search/route.ts +51 -0
- app/api/text-generation/route.ts +32 -0
- app/builder/page.tsx +286 -0
- app/favicon.ico +0 -0
- app/fonts/GeistMonoVF.woff +0 -0
- app/fonts/GeistVF.woff +0 -0
- app/generation/page.tsx +0 -0
- app/globals.css +1 -0
- app/landing.tsx +90 -0
- app/layout.tsx +45 -0
- app/page.tsx +897 -0
- app/text-generation/page.tsx +11 -0
- atoms/sheets.ts +3 -0
- bun.lock +0 -0
- colors.json +182 -0
- components/CodeApplicationProgress.tsx +59 -0
- components/FirecrawlIcon.tsx +15 -0
.cursor/mcp.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mcpServers": {
|
| 3 |
+
"dev3000": {
|
| 4 |
+
"type": "http",
|
| 5 |
+
"url": "http://localhost:3684/mcp"
|
| 6 |
+
}
|
| 7 |
+
}
|
| 8 |
+
}
|
.env.example
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Required
|
| 2 |
+
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
|
| 3 |
+
|
| 4 |
+
# =================================================================================
|
| 5 |
+
# SANDBOX PROVIDER - Choose Option 1 OR 2
|
| 6 |
+
# =================================================================================
|
| 7 |
+
|
| 8 |
+
# Option 1: Vercel Sandbox (recommended - default)
|
| 9 |
+
# Set SANDBOX_PROVIDER=vercel and choose authentication method below
|
| 10 |
+
SANDBOX_PROVIDER=vercel
|
| 11 |
+
|
| 12 |
+
# Vercel Authentication - Choose method a OR b
|
| 13 |
+
# Method a: OIDC Token (recommended for development)
|
| 14 |
+
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
|
| 15 |
+
VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
|
| 16 |
+
|
| 17 |
+
# Method b: Personal Access Token (for production or when OIDC unavailable)
|
| 18 |
+
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID
|
| 19 |
+
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
|
| 20 |
+
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
|
| 21 |
+
|
| 22 |
+
# Get yours at https://console.groq.com
|
| 23 |
+
GROQ_API_KEY=your_groq_api_key_here
|
| 24 |
+
|
| 25 |
+
=======
|
| 26 |
+
# Option 2: E2B Sandbox
|
| 27 |
+
# Set SANDBOX_PROVIDER=e2b and configure E2B_API_KEY below
|
| 28 |
+
# SANDBOX_PROVIDER=e2b
|
| 29 |
+
# E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev
|
| 30 |
+
|
| 31 |
+
# =================================================================================
|
| 32 |
+
# AI PROVIDERS - Need at least one
|
| 33 |
+
# =================================================================================
|
| 34 |
+
|
| 35 |
+
# Vercel AI Gateway (recommended - provides access to multiple models)
|
| 36 |
+
AI_GATEWAY_API_KEY=your_ai_gateway_api_key # Get from https://vercel.com/dashboard/ai-gateway/api-keys
|
| 37 |
+
|
| 38 |
+
# Individual provider keys (used when AI_GATEWAY_API_KEY is not set)
|
| 39 |
+
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
|
| 40 |
+
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
|
| 41 |
+
GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey
|
| 42 |
+
GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended)
|
| 43 |
+
|
| 44 |
+
# Optional Morph Fast Apply
|
| 45 |
+
# Get yours at https://morphllm.com/
|
| 46 |
+
MORPH_API_KEY=your_fast_apply_key
|
.gitignore
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
**/node_modules/
|
| 6 |
+
/.pnp
|
| 7 |
+
.pnp.*
|
| 8 |
+
.yarn/*
|
| 9 |
+
!.yarn/patches
|
| 10 |
+
!.yarn/plugins
|
| 11 |
+
!.yarn/releases
|
| 12 |
+
!.yarn/versions
|
| 13 |
+
|
| 14 |
+
# testing
|
| 15 |
+
/coverage
|
| 16 |
+
|
| 17 |
+
# next.js
|
| 18 |
+
/.next/
|
| 19 |
+
/out/
|
| 20 |
+
|
| 21 |
+
# production
|
| 22 |
+
/build
|
| 23 |
+
|
| 24 |
+
# misc
|
| 25 |
+
.DS_Store
|
| 26 |
+
*.pem
|
| 27 |
+
|
| 28 |
+
# debug
|
| 29 |
+
npm-debug.log*
|
| 30 |
+
yarn-debug.log*
|
| 31 |
+
yarn-error.log*
|
| 32 |
+
.pnpm-debug.log*
|
| 33 |
+
|
| 34 |
+
# env files (can opt-in for committing if needed)
|
| 35 |
+
.env*
|
| 36 |
+
.env.local
|
| 37 |
+
!.env.example
|
| 38 |
+
|
| 39 |
+
# vercel
|
| 40 |
+
.vercel
|
| 41 |
+
|
| 42 |
+
# typescript
|
| 43 |
+
*.tsbuildinfo
|
| 44 |
+
next-env.d.ts
|
| 45 |
+
|
| 46 |
+
# E2B template builds
|
| 47 |
+
*.tar.gz
|
| 48 |
+
e2b-template-*
|
| 49 |
+
|
| 50 |
+
# IDE
|
| 51 |
+
.vscode/
|
| 52 |
+
.idea/
|
| 53 |
+
|
| 54 |
+
# Temporary files
|
| 55 |
+
*.tmp
|
| 56 |
+
*.temp
|
| 57 |
+
repomix-output.txt
|
| 58 |
+
bun.lockb
|
| 59 |
+
.env*.local
|
| 60 |
+
\n# Log files\n*.log
|
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Node.js runtime as a parent image
|
| 2 |
+
FROM node:18-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory in the container
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy package.json and pnpm-lock.yaml to leverage Docker cache
|
| 8 |
+
COPY package.json pnpm-lock.yaml ./
|
| 9 |
+
|
| 10 |
+
# Install pnpm
|
| 11 |
+
RUN npm install -g pnpm
|
| 12 |
+
|
| 13 |
+
# Install dependencies
|
| 14 |
+
RUN pnpm install
|
| 15 |
+
|
| 16 |
+
# Copy the rest of the application's code
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
# Build the Next.js application
|
| 20 |
+
RUN pnpm build
|
| 21 |
+
|
| 22 |
+
# Expose the port the app runs on
|
| 23 |
+
EXPOSE 3000
|
| 24 |
+
|
| 25 |
+
# Command to run the application
|
| 26 |
+
CMD ["pnpm", "start"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
PROJECT_README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Open Lovable
|
| 2 |
+
|
| 3 |
+
Chat with AI to build React apps instantly. An example app made by the [Firecrawl](https://firecrawl.dev/?ref=open-lovable-github) team. For a complete cloud solution, check out [Lovable.dev](https://lovable.dev/) ❤️.
|
| 4 |
+
|
| 5 |
+
<img src="https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmZtaHFleGRsMTNlaWNydGdianI4NGQ4dHhyZjB0d2VkcjRyeXBucCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZFVLWMa6dVskQX0qu1/giphy.gif" alt="Open Lovable Demo" width="100%"/>
|
| 6 |
+
|
| 7 |
+
## Setup
|
| 8 |
+
|
| 9 |
+
1. **Clone & Install**
|
| 10 |
+
```bash
|
| 11 |
+
git clone https://github.com/firecrawl/open-lovable.git
|
| 12 |
+
cd open-lovable
|
| 13 |
+
pnpm install # or npm install / yarn install
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
2. **Add `.env.local`**
|
| 17 |
+
|
| 18 |
+
```env
|
| 19 |
+
# =================================================================
|
| 20 |
+
# REQUIRED
|
| 21 |
+
# =================================================================
|
| 22 |
+
FIRECRAWL_API_KEY=your_firecrawl_api_key # https://firecrawl.dev
|
| 23 |
+
|
| 24 |
+
# =================================================================
|
| 25 |
+
# AI PROVIDER - Choose your LLM
|
| 26 |
+
# =================================================================
|
| 27 |
+
GEMINI_API_KEY=your_gemini_api_key # https://aistudio.google.com/app/apikey
|
| 28 |
+
ANTHROPIC_API_KEY=your_anthropic_api_key # https://console.anthropic.com
|
| 29 |
+
OPENAI_API_KEY=your_openai_api_key # https://platform.openai.com
|
| 30 |
+
GROQ_API_KEY=your_groq_api_key # https://console.groq.com
|
| 31 |
+
|
| 32 |
+
# =================================================================
|
| 33 |
+
# FAST APPLY (Optional - for faster edits)
|
| 34 |
+
# =================================================================
|
| 35 |
+
MORPH_API_KEY=your_morphllm_api_key # https://morphllm.com/dashboard
|
| 36 |
+
|
| 37 |
+
# =================================================================
|
| 38 |
+
# SANDBOX PROVIDER - Choose ONE: Vercel (default) or E2B
|
| 39 |
+
# =================================================================
|
| 40 |
+
SANDBOX_PROVIDER=vercel # or 'e2b'
|
| 41 |
+
|
| 42 |
+
# Option 1: Vercel Sandbox (default)
|
| 43 |
+
# Choose one authentication method:
|
| 44 |
+
|
| 45 |
+
# Method A: OIDC Token (recommended for development)
|
| 46 |
+
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
|
| 47 |
+
VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
|
| 48 |
+
|
| 49 |
+
# Method B: Personal Access Token (for production or when OIDC unavailable)
|
| 50 |
+
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID
|
| 51 |
+
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
|
| 52 |
+
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
|
| 53 |
+
|
| 54 |
+
# Option 2: E2B Sandbox
|
| 55 |
+
# E2B_API_KEY=your_e2b_api_key # https://e2b.dev
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
3. **Run**
|
| 59 |
+
```bash
|
| 60 |
+
pnpm dev # or npm run dev / yarn dev
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
Open [http://localhost:3000](http://localhost:3000)
|
| 64 |
+
|
| 65 |
+
## License
|
| 66 |
+
|
| 67 |
+
MIT
|
README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
-
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
# Trigger rebuild
|
| 3 |
+
title: Open Lovable
|
| 4 |
+
emoji: ❤️
|
| 5 |
+
colorFrom: pink
|
| 6 |
+
colorTo: blue
|
| 7 |
sdk: docker
|
| 8 |
+
app_port: 3000
|
| 9 |
pinned: false
|
| 10 |
---
|
|
|
|
|
|
app/api/analyze-edit-intent/route.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
import { createGroq } from '@ai-sdk/groq';
|
| 3 |
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
| 4 |
+
import { createOpenAI } from '@ai-sdk/openai';
|
| 5 |
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
| 6 |
+
import { generateObject } from 'ai';
|
| 7 |
+
import { z } from 'zod';
|
| 8 |
+
// import type { FileManifest } from '@/types/file-manifest'; // Type is used implicitly through manifest parameter
|
| 9 |
+
|
| 10 |
+
// Check if we're using Vercel AI Gateway
|
| 11 |
+
const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY;
|
| 12 |
+
const aiGatewayBaseURL = 'https://ai-gateway.vercel.sh/v1';
|
| 13 |
+
|
| 14 |
+
const groq = createGroq({
|
| 15 |
+
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GROQ_API_KEY,
|
| 16 |
+
baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined,
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
const anthropic = createAnthropic({
|
| 20 |
+
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.ANTHROPIC_API_KEY,
|
| 21 |
+
baseURL: isUsingAIGateway ? aiGatewayBaseURL : (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1'),
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
const openai = createOpenAI({
|
| 25 |
+
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.OPENAI_API_KEY,
|
| 26 |
+
baseURL: isUsingAIGateway ? aiGatewayBaseURL : process.env.OPENAI_BASE_URL,
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
const googleGenerativeAI = createGoogleGenerativeAI({
|
| 30 |
+
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GEMINI_API_KEY,
|
| 31 |
+
baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined,
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
// Schema for the AI's search plan - not file selection!
|
| 35 |
+
const searchPlanSchema = z.object({
|
| 36 |
+
editType: z.enum([
|
| 37 |
+
'UPDATE_COMPONENT',
|
| 38 |
+
'ADD_FEATURE',
|
| 39 |
+
'FIX_ISSUE',
|
| 40 |
+
'UPDATE_STYLE',
|
| 41 |
+
'REFACTOR',
|
| 42 |
+
'ADD_DEPENDENCY',
|
| 43 |
+
'REMOVE_ELEMENT'
|
| 44 |
+
]).describe('The type of edit being requested'),
|
| 45 |
+
|
| 46 |
+
reasoning: z.string().describe('Explanation of the search strategy'),
|
| 47 |
+
|
| 48 |
+
searchTerms: z.array(z.string()).describe('Specific text to search for (case-insensitive). Be VERY specific - exact button text, class names, etc.'),
|
| 49 |
+
|
| 50 |
+
regexPatterns: z.array(z.string()).optional().describe('Regex patterns for finding code structures (e.g., "className=[\\"\\\'].*header.*[\\"\\\']")'),
|
| 51 |
+
|
| 52 |
+
fileTypesToSearch: z.array(z.string()).default(['.jsx', '.tsx', '.js', '.ts']).describe('File extensions to search'),
|
| 53 |
+
|
| 54 |
+
expectedMatches: z.number().min(1).max(10).default(1).describe('Expected number of matches (helps validate search worked)'),
|
| 55 |
+
|
| 56 |
+
fallbackSearch: z.object({
|
| 57 |
+
terms: z.array(z.string()),
|
| 58 |
+
patterns: z.array(z.string()).optional()
|
| 59 |
+
}).optional().describe('Backup search if primary fails')
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
export async function POST(request: NextRequest) {
|
| 63 |
+
try {
|
| 64 |
+
const { prompt, manifest, model = 'openai/gpt-oss-20b' } = await request.json();
|
| 65 |
+
|
| 66 |
+
console.log('[analyze-edit-intent] Request received');
|
| 67 |
+
console.log('[analyze-edit-intent] Prompt:', prompt);
|
| 68 |
+
console.log('[analyze-edit-intent] Model:', model);
|
| 69 |
+
console.log('[analyze-edit-intent] Manifest files count:', manifest?.files ? Object.keys(manifest.files).length : 0);
|
| 70 |
+
|
| 71 |
+
if (!prompt || !manifest) {
|
| 72 |
+
return NextResponse.json({
|
| 73 |
+
error: 'prompt and manifest are required'
|
| 74 |
+
}, { status: 400 });
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Create a summary of available files for the AI
|
| 78 |
+
const validFiles = Object.entries(manifest.files as Record<string, any>)
|
| 79 |
+
.filter(([path]) => {
|
| 80 |
+
// Filter out invalid paths
|
| 81 |
+
return path.includes('.') && !path.match(/\/\d+$/);
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
const fileSummary = validFiles
|
| 85 |
+
.map(([path, info]: [string, any]) => {
|
| 86 |
+
const componentName = info.componentInfo?.name || path.split('/').pop();
|
| 87 |
+
// const hasImports = info.imports?.length > 0; // Kept for future use
|
| 88 |
+
const childComponents = info.componentInfo?.childComponents?.join(', ') || 'none';
|
| 89 |
+
return `- ${path} (${componentName}, renders: ${childComponents})`;
|
| 90 |
+
})
|
| 91 |
+
.join('\n');
|
| 92 |
+
|
| 93 |
+
console.log('[analyze-edit-intent] Valid files found:', validFiles.length);
|
| 94 |
+
|
| 95 |
+
if (validFiles.length === 0) {
|
| 96 |
+
console.error('[analyze-edit-intent] No valid files found in manifest');
|
| 97 |
+
return NextResponse.json({
|
| 98 |
+
success: false,
|
| 99 |
+
error: 'No valid files found in manifest'
|
| 100 |
+
}, { status: 400 });
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
console.log('[analyze-edit-intent] Analyzing prompt:', prompt);
|
| 104 |
+
console.log('[analyze-edit-intent] File summary preview:', fileSummary.split('\n').slice(0, 5).join('\n'));
|
| 105 |
+
|
| 106 |
+
// Select the appropriate AI model based on the request
|
| 107 |
+
let aiModel;
|
| 108 |
+
if (model.startsWith('anthropic/')) {
|
| 109 |
+
aiModel = anthropic(model.replace('anthropic/', ''));
|
| 110 |
+
} else if (model.startsWith('openai/')) {
|
| 111 |
+
if (model.includes('gpt-oss')) {
|
| 112 |
+
aiModel = groq(model);
|
| 113 |
+
} else {
|
| 114 |
+
aiModel = openai(model.replace('openai/', ''));
|
| 115 |
+
}
|
| 116 |
+
} else if (model.startsWith('google/')) {
|
| 117 |
+
aiModel = googleGenerativeAI(model.replace('google/', ''));
|
| 118 |
+
} else {
|
| 119 |
+
// Default to groq if model format is unclear
|
| 120 |
+
aiModel = groq(model);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
console.log('[analyze-edit-intent] Using AI model:', model);
|
| 124 |
+
|
| 125 |
+
// Use AI to create a search plan
|
| 126 |
+
const result = await generateObject({
|
| 127 |
+
model: aiModel,
|
| 128 |
+
schema: searchPlanSchema,
|
| 129 |
+
messages: [
|
| 130 |
+
{
|
| 131 |
+
role: 'system',
|
| 132 |
+
content: `You are an expert at planning code searches. Your job is to create a search strategy to find the exact code that needs to be edited.
|
| 133 |
+
|
| 134 |
+
DO NOT GUESS which files to edit. Instead, provide specific search terms that will locate the code.
|
| 135 |
+
|
| 136 |
+
SEARCH STRATEGY RULES:
|
| 137 |
+
1. For text changes (e.g., "change 'Start Deploying' to 'Go Now'"):
|
| 138 |
+
- Search for the EXACT text: "Start Deploying"
|
| 139 |
+
|
| 140 |
+
2. For style changes (e.g., "make header black"):
|
| 141 |
+
- Search for component names: "Header", "<header"
|
| 142 |
+
- Search for class names: "header", "navbar"
|
| 143 |
+
- Search for className attributes containing relevant words
|
| 144 |
+
|
| 145 |
+
3. For removing elements (e.g., "remove the deploy button"):
|
| 146 |
+
- Search for the button text or aria-label
|
| 147 |
+
- Search for relevant IDs or data-testids
|
| 148 |
+
|
| 149 |
+
4. For navigation/header issues:
|
| 150 |
+
- Search for: "navigation", "nav", "Header", "navbar"
|
| 151 |
+
- Look for Link components or href attributes
|
| 152 |
+
|
| 153 |
+
5. Be SPECIFIC:
|
| 154 |
+
- Use exact capitalization for user-visible text
|
| 155 |
+
- Include multiple search terms for redundancy
|
| 156 |
+
- Add regex patterns for structural searches
|
| 157 |
+
|
| 158 |
+
Current project structure for context:
|
| 159 |
+
${fileSummary}`
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
role: 'user',
|
| 163 |
+
content: `User request: "${prompt}"
|
| 164 |
+
|
| 165 |
+
Create a search plan to find the exact code that needs to be modified. Include specific search terms and patterns.`
|
| 166 |
+
}
|
| 167 |
+
]
|
| 168 |
+
});
|
| 169 |
+
|
| 170 |
+
console.log('[analyze-edit-intent] Search plan created:', {
|
| 171 |
+
editType: result.object.editType,
|
| 172 |
+
searchTerms: result.object.searchTerms,
|
| 173 |
+
patterns: result.object.regexPatterns?.length || 0,
|
| 174 |
+
reasoning: result.object.reasoning
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
// Return the search plan, not file matches
|
| 178 |
+
return NextResponse.json({
|
| 179 |
+
success: true,
|
| 180 |
+
searchPlan: result.object
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
} catch (error) {
|
| 184 |
+
console.error('[analyze-edit-intent] Error:', error);
|
| 185 |
+
return NextResponse.json({
|
| 186 |
+
success: false,
|
| 187 |
+
error: (error as Error).message
|
| 188 |
+
}, { status: 500 });
|
| 189 |
+
}
|
| 190 |
+
}
|
app/api/apply-ai-code-stream/route.ts
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
import { parseMorphEdits, applyMorphEditToFile } from '@/lib/morph-fast-apply';
|
| 3 |
+
// Sandbox import not needed - using global sandbox from sandbox-manager
|
| 4 |
+
import type { SandboxState } from '@/types/sandbox';
|
| 5 |
+
import type { ConversationState } from '@/types/conversation';
|
| 6 |
+
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
| 7 |
+
|
| 8 |
+
declare global {
|
| 9 |
+
var conversationState: ConversationState | null;
|
| 10 |
+
var activeSandboxProvider: any;
|
| 11 |
+
var existingFiles: Set<string>;
|
| 12 |
+
var sandboxState: SandboxState;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
interface ParsedResponse {
|
| 16 |
+
explanation: string;
|
| 17 |
+
template: string;
|
| 18 |
+
files: Array<{ path: string; content: string }>;
|
| 19 |
+
packages: string[];
|
| 20 |
+
commands: string[];
|
| 21 |
+
structure: string | null;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
function parseAIResponse(response: string): ParsedResponse {
|
| 25 |
+
const sections = {
|
| 26 |
+
files: [] as Array<{ path: string; content: string }>,
|
| 27 |
+
commands: [] as string[],
|
| 28 |
+
packages: [] as string[],
|
| 29 |
+
structure: null as string | null,
|
| 30 |
+
explanation: '',
|
| 31 |
+
template: ''
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
// Function to extract packages from import statements
|
| 35 |
+
function extractPackagesFromCode(content: string): string[] {
|
| 36 |
+
const packages: string[] = [];
|
| 37 |
+
// Match ES6 imports
|
| 38 |
+
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
|
| 39 |
+
let importMatch;
|
| 40 |
+
|
| 41 |
+
while ((importMatch = importRegex.exec(content)) !== null) {
|
| 42 |
+
const importPath = importMatch[1];
|
| 43 |
+
// Skip relative imports and built-in React
|
| 44 |
+
if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
|
| 45 |
+
importPath !== 'react' && importPath !== 'react-dom' &&
|
| 46 |
+
!importPath.startsWith('@/')) {
|
| 47 |
+
// Extract package name (handle scoped packages like @heroicons/react)
|
| 48 |
+
const packageName = importPath.startsWith('@')
|
| 49 |
+
? importPath.split('/').slice(0, 2).join('/')
|
| 50 |
+
: importPath.split('/')[0];
|
| 51 |
+
|
| 52 |
+
if (!packages.includes(packageName)) {
|
| 53 |
+
packages.push(packageName);
|
| 54 |
+
|
| 55 |
+
// Log important packages for debugging
|
| 56 |
+
if (packageName === 'react-router-dom' || packageName.includes('router') || packageName.includes('icon')) {
|
| 57 |
+
console.log(`[apply-ai-code-stream] Detected package from imports: ${packageName}`);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return packages;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Parse file sections - handle duplicates and prefer complete versions
|
| 67 |
+
const fileMap = new Map<string, { content: string; isComplete: boolean }>();
|
| 68 |
+
|
| 69 |
+
// First pass: Find all file declarations
|
| 70 |
+
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
|
| 71 |
+
let match;
|
| 72 |
+
while ((match = fileRegex.exec(response)) !== null) {
|
| 73 |
+
const filePath = match[1];
|
| 74 |
+
const content = match[2].trim();
|
| 75 |
+
const hasClosingTag = response.substring(match.index, match.index + match[0].length).includes('</file>');
|
| 76 |
+
|
| 77 |
+
// Check if this file already exists in our map
|
| 78 |
+
const existing = fileMap.get(filePath);
|
| 79 |
+
|
| 80 |
+
// Decide whether to keep this version
|
| 81 |
+
let shouldReplace = false;
|
| 82 |
+
if (!existing) {
|
| 83 |
+
shouldReplace = true; // First occurrence
|
| 84 |
+
} else if (!existing.isComplete && hasClosingTag) {
|
| 85 |
+
shouldReplace = true; // Replace incomplete with complete
|
| 86 |
+
console.log(`[apply-ai-code-stream] Replacing incomplete ${filePath} with complete version`);
|
| 87 |
+
} else if (existing.isComplete && hasClosingTag && content.length > existing.content.length) {
|
| 88 |
+
shouldReplace = true; // Replace with longer complete version
|
| 89 |
+
console.log(`[apply-ai-code-stream] Replacing ${filePath} with longer complete version`);
|
| 90 |
+
} else if (!existing.isComplete && !hasClosingTag && content.length > existing.content.length) {
|
| 91 |
+
shouldReplace = true; // Both incomplete, keep longer one
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if (shouldReplace) {
|
| 95 |
+
// Additional validation: reject obviously broken content
|
| 96 |
+
if (content.includes('...') && !content.includes('...props') && !content.includes('...rest')) {
|
| 97 |
+
console.warn(`[apply-ai-code-stream] Warning: ${filePath} contains ellipsis, may be truncated`);
|
| 98 |
+
// Still use it if it's the only version we have
|
| 99 |
+
if (!existing) {
|
| 100 |
+
fileMap.set(filePath, { content, isComplete: hasClosingTag });
|
| 101 |
+
}
|
| 102 |
+
} else {
|
| 103 |
+
fileMap.set(filePath, { content, isComplete: hasClosingTag });
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Convert map to array for sections.files
|
| 109 |
+
for (const [path, { content, isComplete }] of fileMap.entries()) {
|
| 110 |
+
if (!isComplete) {
|
| 111 |
+
console.log(`[apply-ai-code-stream] Warning: File ${path} appears to be truncated (no closing tag)`);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
sections.files.push({
|
| 115 |
+
path,
|
| 116 |
+
content
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
// Extract packages from file content
|
| 120 |
+
const filePackages = extractPackagesFromCode(content);
|
| 121 |
+
for (const pkg of filePackages) {
|
| 122 |
+
if (!sections.packages.includes(pkg)) {
|
| 123 |
+
sections.packages.push(pkg);
|
| 124 |
+
console.log(`[apply-ai-code-stream] 📦 Package detected from imports: ${pkg}`);
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Also parse markdown code blocks with file paths
|
| 130 |
+
const markdownFileRegex = /```(?:file )?path="([^"]+)"\n([\s\S]*?)```/g;
|
| 131 |
+
while ((match = markdownFileRegex.exec(response)) !== null) {
|
| 132 |
+
const filePath = match[1];
|
| 133 |
+
const content = match[2].trim();
|
| 134 |
+
sections.files.push({
|
| 135 |
+
path: filePath,
|
| 136 |
+
content: content
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
// Extract packages from file content
|
| 140 |
+
const filePackages = extractPackagesFromCode(content);
|
| 141 |
+
for (const pkg of filePackages) {
|
| 142 |
+
if (!sections.packages.includes(pkg)) {
|
| 143 |
+
sections.packages.push(pkg);
|
| 144 |
+
console.log(`[apply-ai-code-stream] 📦 Package detected from imports: ${pkg}`);
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// Parse plain text format like "Generated Files: Header.jsx, index.css"
|
| 150 |
+
const generatedFilesMatch = response.match(/Generated Files?:\s*([^\n]+)/i);
|
| 151 |
+
if (generatedFilesMatch) {
|
| 152 |
+
// Split by comma first, then trim whitespace, to preserve filenames with dots
|
| 153 |
+
const filesList = generatedFilesMatch[1]
|
| 154 |
+
.split(',')
|
| 155 |
+
.map(f => f.trim())
|
| 156 |
+
.filter(f => f.endsWith('.jsx') || f.endsWith('.js') || f.endsWith('.tsx') || f.endsWith('.ts') || f.endsWith('.css') || f.endsWith('.json') || f.endsWith('.html'));
|
| 157 |
+
console.log(`[apply-ai-code-stream] Detected generated files from plain text: ${filesList.join(', ')}`);
|
| 158 |
+
|
| 159 |
+
// Try to extract the actual file content if it follows
|
| 160 |
+
for (const fileName of filesList) {
|
| 161 |
+
// Look for the file content after the file name
|
| 162 |
+
const fileContentRegex = new RegExp(`${fileName}[\\s\\S]*?(?:import[\\s\\S]+?)(?=Generated Files:|Applying code|$)`, 'i');
|
| 163 |
+
const fileContentMatch = response.match(fileContentRegex);
|
| 164 |
+
if (fileContentMatch) {
|
| 165 |
+
// Extract just the code part (starting from import statements)
|
| 166 |
+
const codeMatch = fileContentMatch[0].match(/^(import[\s\S]+)$/m);
|
| 167 |
+
if (codeMatch) {
|
| 168 |
+
const filePath = fileName.includes('/') ? fileName : `src/components/${fileName}`;
|
| 169 |
+
sections.files.push({
|
| 170 |
+
path: filePath,
|
| 171 |
+
content: codeMatch[1].trim()
|
| 172 |
+
});
|
| 173 |
+
console.log(`[apply-ai-code-stream] Extracted content for ${filePath}`);
|
| 174 |
+
|
| 175 |
+
// Extract packages from this file
|
| 176 |
+
const filePackages = extractPackagesFromCode(codeMatch[1]);
|
| 177 |
+
for (const pkg of filePackages) {
|
| 178 |
+
if (!sections.packages.includes(pkg)) {
|
| 179 |
+
sections.packages.push(pkg);
|
| 180 |
+
console.log(`[apply-ai-code-stream] Package detected from imports: ${pkg}`);
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// Also try to parse if the response contains raw JSX/JS code blocks
|
| 189 |
+
const codeBlockRegex = /```(?:jsx?|tsx?|javascript|typescript)?\n([\s\S]*?)```/g;
|
| 190 |
+
while ((match = codeBlockRegex.exec(response)) !== null) {
|
| 191 |
+
const content = match[1].trim();
|
| 192 |
+
// Try to detect the file name from comments or context
|
| 193 |
+
const fileNameMatch = content.match(/\/\/\s*(?:File:|Component:)\s*([^\n]+)/);
|
| 194 |
+
if (fileNameMatch) {
|
| 195 |
+
const fileName = fileNameMatch[1].trim();
|
| 196 |
+
const filePath = fileName.includes('/') ? fileName : `src/components/${fileName}`;
|
| 197 |
+
|
| 198 |
+
// Don't add duplicate files
|
| 199 |
+
if (!sections.files.some(f => f.path === filePath)) {
|
| 200 |
+
sections.files.push({
|
| 201 |
+
path: filePath,
|
| 202 |
+
content: content
|
| 203 |
+
});
|
| 204 |
+
|
| 205 |
+
// Extract packages
|
| 206 |
+
const filePackages = extractPackagesFromCode(content);
|
| 207 |
+
for (const pkg of filePackages) {
|
| 208 |
+
if (!sections.packages.includes(pkg)) {
|
| 209 |
+
sections.packages.push(pkg);
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
// Parse commands
|
| 217 |
+
const cmdRegex = /<command>(.*?)<\/command>/g;
|
| 218 |
+
while ((match = cmdRegex.exec(response)) !== null) {
|
| 219 |
+
sections.commands.push(match[1].trim());
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// Parse packages - support both <package> and <packages> tags
|
| 223 |
+
const pkgRegex = /<package>(.*?)<\/package>/g;
|
| 224 |
+
while ((match = pkgRegex.exec(response)) !== null) {
|
| 225 |
+
sections.packages.push(match[1].trim());
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
// Also parse <packages> tag with multiple packages
|
| 229 |
+
const packagesRegex = /<packages>([\s\S]*?)<\/packages>/;
|
| 230 |
+
const packagesMatch = response.match(packagesRegex);
|
| 231 |
+
if (packagesMatch) {
|
| 232 |
+
const packagesContent = packagesMatch[1].trim();
|
| 233 |
+
// Split by newlines or commas
|
| 234 |
+
const packagesList = packagesContent.split(/[\n,]+/)
|
| 235 |
+
.map(pkg => pkg.trim())
|
| 236 |
+
.filter(pkg => pkg.length > 0);
|
| 237 |
+
sections.packages.push(...packagesList);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// Parse structure
|
| 241 |
+
const structureMatch = /<structure>([\s\S]*?)<\/structure>/;
|
| 242 |
+
const structResult = response.match(structureMatch);
|
| 243 |
+
if (structResult) {
|
| 244 |
+
sections.structure = structResult[1].trim();
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// Parse explanation
|
| 248 |
+
const explanationMatch = /<explanation>([\s\S]*?)<\/explanation>/;
|
| 249 |
+
const explResult = response.match(explanationMatch);
|
| 250 |
+
if (explResult) {
|
| 251 |
+
sections.explanation = explResult[1].trim();
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
// Parse template
|
| 255 |
+
const templateMatch = /<template>(.*?)<\/template>/;
|
| 256 |
+
const templResult = response.match(templateMatch);
|
| 257 |
+
if (templResult) {
|
| 258 |
+
sections.template = templResult[1].trim();
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
return sections;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
export async function POST(request: NextRequest) {
|
| 265 |
+
try {
|
| 266 |
+
const { response, isEdit = false, packages = [], sandboxId } = await request.json();
|
| 267 |
+
|
| 268 |
+
if (!response) {
|
| 269 |
+
return NextResponse.json({
|
| 270 |
+
error: 'response is required'
|
| 271 |
+
}, { status: 400 });
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
// Debug log the response
|
| 275 |
+
console.log('[apply-ai-code-stream] Received response to parse:');
|
| 276 |
+
console.log('[apply-ai-code-stream] Response length:', response.length);
|
| 277 |
+
console.log('[apply-ai-code-stream] Response preview:', response.substring(0, 500));
|
| 278 |
+
console.log('[apply-ai-code-stream] isEdit:', isEdit);
|
| 279 |
+
console.log('[apply-ai-code-stream] packages:', packages);
|
| 280 |
+
|
| 281 |
+
// Parse the AI response
|
| 282 |
+
const parsed = parseAIResponse(response);
|
| 283 |
+
const morphEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
|
| 284 |
+
const morphEdits = morphEnabled ? parseMorphEdits(response) : [];
|
| 285 |
+
console.log('[apply-ai-code-stream] Morph Fast Apply mode:', morphEnabled);
|
| 286 |
+
if (morphEnabled) {
|
| 287 |
+
console.log('[apply-ai-code-stream] Morph edits found:', morphEdits.length);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
// Log what was parsed
|
| 291 |
+
console.log('[apply-ai-code-stream] Parsed result:');
|
| 292 |
+
console.log('[apply-ai-code-stream] Files found:', parsed.files.length);
|
| 293 |
+
if (parsed.files.length > 0) {
|
| 294 |
+
parsed.files.forEach(f => {
|
| 295 |
+
console.log(`[apply-ai-code-stream] - ${f.path} (${f.content.length} chars)`);
|
| 296 |
+
});
|
| 297 |
+
}
|
| 298 |
+
console.log('[apply-ai-code-stream] Packages found:', parsed.packages);
|
| 299 |
+
|
| 300 |
+
// Initialize existingFiles if not already
|
| 301 |
+
if (!global.existingFiles) {
|
| 302 |
+
global.existingFiles = new Set<string>();
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
// Try to get provider from sandbox manager first
|
| 306 |
+
let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
|
| 307 |
+
|
| 308 |
+
// Fall back to global state if not found in manager
|
| 309 |
+
if (!provider) {
|
| 310 |
+
provider = global.activeSandboxProvider;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
// If we have a sandboxId but no provider, try to get or create one
|
| 314 |
+
if (!provider && sandboxId) {
|
| 315 |
+
console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`);
|
| 316 |
+
|
| 317 |
+
try {
|
| 318 |
+
provider = await sandboxManager.getOrCreateProvider(sandboxId);
|
| 319 |
+
|
| 320 |
+
// If we got a new provider (not reconnected), we need to create a new sandbox
|
| 321 |
+
if (!provider.getSandboxInfo()) {
|
| 322 |
+
console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`);
|
| 323 |
+
await provider.createSandbox();
|
| 324 |
+
await provider.setupViteApp();
|
| 325 |
+
sandboxManager.registerSandbox(sandboxId, provider);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
// Update legacy global state
|
| 329 |
+
global.activeSandboxProvider = provider;
|
| 330 |
+
console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`);
|
| 331 |
+
} catch (providerError) {
|
| 332 |
+
console.error(`[apply-ai-code-stream] Failed to get or create provider for sandbox ${sandboxId}:`, providerError);
|
| 333 |
+
return NextResponse.json({
|
| 334 |
+
success: false,
|
| 335 |
+
error: `Failed to create sandbox provider for ${sandboxId}. The sandbox may have expired.`,
|
| 336 |
+
results: {
|
| 337 |
+
filesCreated: [],
|
| 338 |
+
packagesInstalled: [],
|
| 339 |
+
commandsExecuted: [],
|
| 340 |
+
errors: [`Sandbox provider creation failed: ${(providerError as Error).message}`]
|
| 341 |
+
},
|
| 342 |
+
explanation: parsed.explanation,
|
| 343 |
+
structure: parsed.structure,
|
| 344 |
+
parsedFiles: parsed.files,
|
| 345 |
+
message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox reconnection failed.`
|
| 346 |
+
}, { status: 500 });
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// If we still don't have a provider, create a new one
|
| 351 |
+
if (!provider) {
|
| 352 |
+
console.log(`[apply-ai-code-stream] No active provider found, creating new sandbox...`);
|
| 353 |
+
try {
|
| 354 |
+
const { SandboxFactory } = await import('@/lib/sandbox/factory');
|
| 355 |
+
provider = SandboxFactory.create();
|
| 356 |
+
const sandboxInfo = await provider.createSandbox();
|
| 357 |
+
await provider.setupViteApp();
|
| 358 |
+
|
| 359 |
+
// Register with sandbox manager
|
| 360 |
+
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
|
| 361 |
+
|
| 362 |
+
// Store in legacy global state
|
| 363 |
+
global.activeSandboxProvider = provider;
|
| 364 |
+
global.sandboxData = {
|
| 365 |
+
sandboxId: sandboxInfo.sandboxId,
|
| 366 |
+
url: sandboxInfo.url
|
| 367 |
+
};
|
| 368 |
+
|
| 369 |
+
console.log(`[apply-ai-code-stream] Created new sandbox successfully`);
|
| 370 |
+
} catch (createError) {
|
| 371 |
+
console.error(`[apply-ai-code-stream] Failed to create new sandbox:`, createError);
|
| 372 |
+
return NextResponse.json({
|
| 373 |
+
success: false,
|
| 374 |
+
error: `Failed to create new sandbox: ${createError instanceof Error ? createError.message : 'Unknown error'}`,
|
| 375 |
+
results: {
|
| 376 |
+
filesCreated: [],
|
| 377 |
+
packagesInstalled: [],
|
| 378 |
+
commandsExecuted: [],
|
| 379 |
+
errors: [`Sandbox creation failed: ${createError instanceof Error ? createError.message : 'Unknown error'}`]
|
| 380 |
+
},
|
| 381 |
+
explanation: parsed.explanation,
|
| 382 |
+
structure: parsed.structure,
|
| 383 |
+
parsedFiles: parsed.files,
|
| 384 |
+
message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox creation failed.`
|
| 385 |
+
}, { status: 500 });
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
// Create a response stream for real-time updates
|
| 390 |
+
const encoder = new TextEncoder();
|
| 391 |
+
const stream = new TransformStream();
|
| 392 |
+
const writer = stream.writable.getWriter();
|
| 393 |
+
|
| 394 |
+
// Function to send progress updates
|
| 395 |
+
const sendProgress = async (data: any) => {
|
| 396 |
+
const message = `data: ${JSON.stringify(data)}\n\n`;
|
| 397 |
+
await writer.write(encoder.encode(message));
|
| 398 |
+
};
|
| 399 |
+
|
| 400 |
+
// Start processing in background (pass provider and request to the async function)
|
| 401 |
+
(async (providerInstance, req) => {
|
| 402 |
+
const results = {
|
| 403 |
+
filesCreated: [] as string[],
|
| 404 |
+
filesUpdated: [] as string[],
|
| 405 |
+
packagesInstalled: [] as string[],
|
| 406 |
+
packagesAlreadyInstalled: [] as string[],
|
| 407 |
+
packagesFailed: [] as string[],
|
| 408 |
+
commandsExecuted: [] as string[],
|
| 409 |
+
errors: [] as string[]
|
| 410 |
+
};
|
| 411 |
+
|
| 412 |
+
try {
|
| 413 |
+
await sendProgress({
|
| 414 |
+
type: 'start',
|
| 415 |
+
message: 'Starting code application...',
|
| 416 |
+
totalSteps: 3
|
| 417 |
+
});
|
| 418 |
+
if (morphEnabled) {
|
| 419 |
+
await sendProgress({ type: 'info', message: 'Morph Fast Apply enabled' });
|
| 420 |
+
await sendProgress({ type: 'info', message: `Parsed ${morphEdits.length} Morph edits` });
|
| 421 |
+
if (morphEdits.length === 0) {
|
| 422 |
+
console.warn('[apply-ai-code-stream] Morph enabled but no <edit> blocks found; falling back to full-file flow');
|
| 423 |
+
await sendProgress({ type: 'warning', message: 'Morph enabled but no <edit> blocks found; falling back to full-file flow' });
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
// Step 1: Install packages
|
| 428 |
+
const packagesArray = Array.isArray(packages) ? packages : [];
|
| 429 |
+
const parsedPackages = Array.isArray(parsed.packages) ? parsed.packages : [];
|
| 430 |
+
|
| 431 |
+
// Combine and deduplicate packages
|
| 432 |
+
const allPackages = [...packagesArray.filter(pkg => pkg && typeof pkg === 'string'), ...parsedPackages];
|
| 433 |
+
|
| 434 |
+
// Use Set to remove duplicates, then filter out pre-installed packages
|
| 435 |
+
const uniquePackages = [...new Set(allPackages)]
|
| 436 |
+
.filter(pkg => pkg && typeof pkg === 'string' && pkg.trim() !== '') // Remove empty strings
|
| 437 |
+
.filter(pkg => pkg !== 'react' && pkg !== 'react-dom'); // Filter pre-installed
|
| 438 |
+
|
| 439 |
+
// Log if we found duplicates
|
| 440 |
+
if (allPackages.length !== uniquePackages.length) {
|
| 441 |
+
console.log(`[apply-ai-code-stream] Removed ${allPackages.length - uniquePackages.length} duplicate packages`);
|
| 442 |
+
console.log(`[apply-ai-code-stream] Original packages:`, allPackages);
|
| 443 |
+
console.log(`[apply-ai-code-stream] Deduplicated packages:`, uniquePackages);
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
if (uniquePackages.length > 0) {
|
| 447 |
+
await sendProgress({
|
| 448 |
+
type: 'step',
|
| 449 |
+
step: 1,
|
| 450 |
+
message: `Installing ${uniquePackages.length} packages...`,
|
| 451 |
+
packages: uniquePackages
|
| 452 |
+
});
|
| 453 |
+
|
| 454 |
+
// Use streaming package installation
|
| 455 |
+
try {
|
| 456 |
+
// Construct the API URL properly for both dev and production
|
| 457 |
+
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
| 458 |
+
const host = req.headers.get('host') || 'localhost:3000';
|
| 459 |
+
const apiUrl = `${protocol}://${host}/api/install-packages`;
|
| 460 |
+
|
| 461 |
+
const installResponse = await fetch(apiUrl, {
|
| 462 |
+
method: 'POST',
|
| 463 |
+
headers: { 'Content-Type': 'application/json' },
|
| 464 |
+
body: JSON.stringify({
|
| 465 |
+
packages: uniquePackages,
|
| 466 |
+
sandboxId: sandboxId || providerInstance.getSandboxInfo()?.sandboxId
|
| 467 |
+
})
|
| 468 |
+
});
|
| 469 |
+
|
| 470 |
+
if (installResponse.ok && installResponse.body) {
|
| 471 |
+
const reader = installResponse.body.getReader();
|
| 472 |
+
const decoder = new TextDecoder();
|
| 473 |
+
|
| 474 |
+
while (true) {
|
| 475 |
+
const { done, value } = await reader.read();
|
| 476 |
+
if (done) break;
|
| 477 |
+
|
| 478 |
+
const chunk = decoder.decode(value);
|
| 479 |
+
if (!chunk) continue;
|
| 480 |
+
const lines = chunk.split('\n');
|
| 481 |
+
|
| 482 |
+
for (const line of lines) {
|
| 483 |
+
if (line.startsWith('data: ')) {
|
| 484 |
+
try {
|
| 485 |
+
const data = JSON.parse(line.slice(6));
|
| 486 |
+
|
| 487 |
+
// Forward package installation progress
|
| 488 |
+
await sendProgress({
|
| 489 |
+
type: 'package-progress',
|
| 490 |
+
...data
|
| 491 |
+
});
|
| 492 |
+
|
| 493 |
+
// Track results
|
| 494 |
+
if (data.type === 'success' && data.installedPackages) {
|
| 495 |
+
results.packagesInstalled = data.installedPackages;
|
| 496 |
+
}
|
| 497 |
+
} catch (parseError) {
|
| 498 |
+
console.debug('Error parsing terminal output:', parseError);
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
} catch (error) {
|
| 505 |
+
console.error('[apply-ai-code-stream] Error installing packages:', error);
|
| 506 |
+
await sendProgress({
|
| 507 |
+
type: 'warning',
|
| 508 |
+
message: `Package installation skipped (${(error as Error).message}). Continuing with file creation...`
|
| 509 |
+
});
|
| 510 |
+
results.errors.push(`Package installation failed: ${(error as Error).message}`);
|
| 511 |
+
}
|
| 512 |
+
} else {
|
| 513 |
+
await sendProgress({
|
| 514 |
+
type: 'step',
|
| 515 |
+
step: 1,
|
| 516 |
+
message: 'No additional packages to install, skipping...'
|
| 517 |
+
});
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
// Step 2: Create/update files
|
| 521 |
+
const filesArray = Array.isArray(parsed.files) ? parsed.files : [];
|
| 522 |
+
await sendProgress({
|
| 523 |
+
type: 'step',
|
| 524 |
+
step: 2,
|
| 525 |
+
message: `Creating ${filesArray.length} files...`
|
| 526 |
+
});
|
| 527 |
+
|
| 528 |
+
// Filter out config files that shouldn't be created
|
| 529 |
+
const configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js'];
|
| 530 |
+
let filteredFiles = filesArray.filter(file => {
|
| 531 |
+
if (!file || typeof file !== 'object') return false;
|
| 532 |
+
const fileName = (file.path || '').split('/').pop() || '';
|
| 533 |
+
return !configFiles.includes(fileName);
|
| 534 |
+
});
|
| 535 |
+
|
| 536 |
+
// If Morph is enabled and we have edits, apply them before file writes
|
| 537 |
+
const morphUpdatedPaths = new Set<string>();
|
| 538 |
+
if (morphEnabled && morphEdits.length > 0) {
|
| 539 |
+
const morphSandbox = (global as any).activeSandbox || providerInstance;
|
| 540 |
+
if (!morphSandbox) {
|
| 541 |
+
console.warn('[apply-ai-code-stream] No sandbox available to apply Morph edits');
|
| 542 |
+
await sendProgress({ type: 'warning', message: 'No sandbox available to apply Morph edits' });
|
| 543 |
+
} else {
|
| 544 |
+
await sendProgress({ type: 'info', message: `Applying ${morphEdits.length} fast edits via Morph...` });
|
| 545 |
+
for (const [idx, edit] of morphEdits.entries()) {
|
| 546 |
+
try {
|
| 547 |
+
await sendProgress({ type: 'file-progress', current: idx + 1, total: morphEdits.length, fileName: edit.targetFile, action: 'morph-applying' });
|
| 548 |
+
const result = await applyMorphEditToFile({
|
| 549 |
+
sandbox: morphSandbox,
|
| 550 |
+
targetPath: edit.targetFile,
|
| 551 |
+
instructions: edit.instructions,
|
| 552 |
+
updateSnippet: edit.update
|
| 553 |
+
});
|
| 554 |
+
if (result.success && result.normalizedPath) {
|
| 555 |
+
console.log('[apply-ai-code-stream] Morph updated', result.normalizedPath);
|
| 556 |
+
morphUpdatedPaths.add(result.normalizedPath);
|
| 557 |
+
if (results.filesUpdated) results.filesUpdated.push(result.normalizedPath);
|
| 558 |
+
await sendProgress({ type: 'file-complete', fileName: result.normalizedPath, action: 'morph-updated' });
|
| 559 |
+
} else {
|
| 560 |
+
const msg = result.error || 'Unknown Morph error';
|
| 561 |
+
console.error('[apply-ai-code-stream] Morph apply failed for', edit.targetFile, msg);
|
| 562 |
+
if (results.errors) results.errors.push(`Morph apply failed for ${edit.targetFile}: ${msg}`);
|
| 563 |
+
await sendProgress({ type: 'file-error', fileName: edit.targetFile, error: msg });
|
| 564 |
+
}
|
| 565 |
+
} catch (err) {
|
| 566 |
+
const msg = (err as Error).message;
|
| 567 |
+
console.error('[apply-ai-code-stream] Morph apply exception for', edit.targetFile, msg);
|
| 568 |
+
if (results.errors) results.errors.push(`Morph apply exception for ${edit.targetFile}: ${msg}`);
|
| 569 |
+
await sendProgress({ type: 'file-error', fileName: edit.targetFile, error: msg });
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
}
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
// Avoid overwriting Morph-updated files in the file write loop
|
| 576 |
+
if (morphUpdatedPaths.size > 0) {
|
| 577 |
+
filteredFiles = filteredFiles.filter(file => {
|
| 578 |
+
if (!file?.path) return true;
|
| 579 |
+
let normalizedPath = file.path.startsWith('/') ? file.path.slice(1) : file.path;
|
| 580 |
+
const fileName = normalizedPath.split('/').pop() || '';
|
| 581 |
+
if (!normalizedPath.startsWith('src/') &&
|
| 582 |
+
!normalizedPath.startsWith('public/') &&
|
| 583 |
+
normalizedPath !== 'index.html' &&
|
| 584 |
+
!configFiles.includes(fileName)) {
|
| 585 |
+
normalizedPath = 'src/' + normalizedPath;
|
| 586 |
+
}
|
| 587 |
+
return !morphUpdatedPaths.has(normalizedPath);
|
| 588 |
+
});
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
for (const [index, file] of filteredFiles.entries()) {
|
| 592 |
+
try {
|
| 593 |
+
// Send progress for each file
|
| 594 |
+
await sendProgress({
|
| 595 |
+
type: 'file-progress',
|
| 596 |
+
current: index + 1,
|
| 597 |
+
total: filteredFiles.length,
|
| 598 |
+
fileName: file.path,
|
| 599 |
+
action: 'creating'
|
| 600 |
+
});
|
| 601 |
+
|
| 602 |
+
// Normalize the file path
|
| 603 |
+
let normalizedPath = file.path;
|
| 604 |
+
if (normalizedPath.startsWith('/')) {
|
| 605 |
+
normalizedPath = normalizedPath.substring(1);
|
| 606 |
+
}
|
| 607 |
+
if (!normalizedPath.startsWith('src/') &&
|
| 608 |
+
!normalizedPath.startsWith('public/') &&
|
| 609 |
+
normalizedPath !== 'index.html' &&
|
| 610 |
+
!configFiles.includes(normalizedPath.split('/').pop() || '')) {
|
| 611 |
+
normalizedPath = 'src/' + normalizedPath;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
const isUpdate = global.existingFiles.has(normalizedPath);
|
| 615 |
+
|
| 616 |
+
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
|
| 617 |
+
let fileContent = file.content;
|
| 618 |
+
if (file.path.endsWith('.jsx') || file.path.endsWith('.js') || file.path.endsWith('.tsx') || file.path.endsWith('.ts')) {
|
| 619 |
+
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
// Fix common Tailwind CSS errors in CSS files
|
| 623 |
+
if (file.path.endsWith('.css')) {
|
| 624 |
+
// Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
|
| 625 |
+
fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl');
|
| 626 |
+
// Replace any other non-existent shadow utilities
|
| 627 |
+
fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl');
|
| 628 |
+
fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
// Create directory if needed
|
| 632 |
+
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
|
| 633 |
+
if (dirPath) {
|
| 634 |
+
await providerInstance.runCommand(`mkdir -p ${dirPath}`);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
// Write the file using provider
|
| 638 |
+
await providerInstance.writeFile(normalizedPath, fileContent);
|
| 639 |
+
|
| 640 |
+
// Update file cache
|
| 641 |
+
if (global.sandboxState?.fileCache) {
|
| 642 |
+
global.sandboxState.fileCache.files[normalizedPath] = {
|
| 643 |
+
content: fileContent,
|
| 644 |
+
lastModified: Date.now()
|
| 645 |
+
};
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
if (isUpdate) {
|
| 649 |
+
if (results.filesUpdated) results.filesUpdated.push(normalizedPath);
|
| 650 |
+
} else {
|
| 651 |
+
if (results.filesCreated) results.filesCreated.push(normalizedPath);
|
| 652 |
+
if (global.existingFiles) global.existingFiles.add(normalizedPath);
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
await sendProgress({
|
| 656 |
+
type: 'file-complete',
|
| 657 |
+
fileName: normalizedPath,
|
| 658 |
+
action: isUpdate ? 'updated' : 'created'
|
| 659 |
+
});
|
| 660 |
+
} catch (error) {
|
| 661 |
+
if (results.errors) {
|
| 662 |
+
results.errors.push(`Failed to create ${file.path}: ${(error as Error).message}`);
|
| 663 |
+
}
|
| 664 |
+
await sendProgress({
|
| 665 |
+
type: 'file-error',
|
| 666 |
+
fileName: file.path,
|
| 667 |
+
error: (error as Error).message
|
| 668 |
+
});
|
| 669 |
+
}
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
// Step 3: Execute commands
|
| 673 |
+
const commandsArray = Array.isArray(parsed.commands) ? parsed.commands : [];
|
| 674 |
+
if (commandsArray.length > 0) {
|
| 675 |
+
await sendProgress({
|
| 676 |
+
type: 'step',
|
| 677 |
+
step: 3,
|
| 678 |
+
message: `Executing ${commandsArray.length} commands...`
|
| 679 |
+
});
|
| 680 |
+
|
| 681 |
+
for (const [index, cmd] of commandsArray.entries()) {
|
| 682 |
+
try {
|
| 683 |
+
await sendProgress({
|
| 684 |
+
type: 'command-progress',
|
| 685 |
+
current: index + 1,
|
| 686 |
+
total: parsed.commands.length,
|
| 687 |
+
command: cmd,
|
| 688 |
+
action: 'executing'
|
| 689 |
+
});
|
| 690 |
+
|
| 691 |
+
// Use provider runCommand
|
| 692 |
+
const result = await providerInstance.runCommand(cmd);
|
| 693 |
+
|
| 694 |
+
// Get command output from provider result
|
| 695 |
+
const stdout = result.stdout;
|
| 696 |
+
const stderr = result.stderr;
|
| 697 |
+
|
| 698 |
+
if (stdout) {
|
| 699 |
+
await sendProgress({
|
| 700 |
+
type: 'command-output',
|
| 701 |
+
command: cmd,
|
| 702 |
+
output: stdout,
|
| 703 |
+
stream: 'stdout'
|
| 704 |
+
});
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
if (stderr) {
|
| 708 |
+
await sendProgress({
|
| 709 |
+
type: 'command-output',
|
| 710 |
+
command: cmd,
|
| 711 |
+
output: stderr,
|
| 712 |
+
stream: 'stderr'
|
| 713 |
+
});
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
if (results.commandsExecuted) {
|
| 717 |
+
results.commandsExecuted.push(cmd);
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
await sendProgress({
|
| 721 |
+
type: 'command-complete',
|
| 722 |
+
command: cmd,
|
| 723 |
+
exitCode: result.exitCode,
|
| 724 |
+
success: result.exitCode === 0
|
| 725 |
+
});
|
| 726 |
+
} catch (error) {
|
| 727 |
+
if (results.errors) {
|
| 728 |
+
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
|
| 729 |
+
}
|
| 730 |
+
await sendProgress({
|
| 731 |
+
type: 'command-error',
|
| 732 |
+
command: cmd,
|
| 733 |
+
error: (error as Error).message
|
| 734 |
+
});
|
| 735 |
+
}
|
| 736 |
+
}
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
// Send final results
|
| 740 |
+
await sendProgress({
|
| 741 |
+
type: 'complete',
|
| 742 |
+
results,
|
| 743 |
+
explanation: parsed.explanation,
|
| 744 |
+
structure: parsed.structure,
|
| 745 |
+
message: `Successfully applied ${results.filesCreated.length} files`
|
| 746 |
+
});
|
| 747 |
+
|
| 748 |
+
// Track applied files in conversation state
|
| 749 |
+
if (global.conversationState && results.filesCreated.length > 0) {
|
| 750 |
+
const messages = global.conversationState.context.messages;
|
| 751 |
+
if (messages.length > 0) {
|
| 752 |
+
const lastMessage = messages[messages.length - 1];
|
| 753 |
+
if (lastMessage.role === 'user') {
|
| 754 |
+
lastMessage.metadata = {
|
| 755 |
+
...lastMessage.metadata,
|
| 756 |
+
editedFiles: results.filesCreated
|
| 757 |
+
};
|
| 758 |
+
}
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
// Track applied code in project evolution
|
| 762 |
+
if (global.conversationState.context.projectEvolution) {
|
| 763 |
+
global.conversationState.context.projectEvolution.majorChanges.push({
|
| 764 |
+
timestamp: Date.now(),
|
| 765 |
+
description: parsed.explanation || 'Code applied',
|
| 766 |
+
filesAffected: results.filesCreated || []
|
| 767 |
+
});
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
global.conversationState.lastUpdated = Date.now();
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
} catch (error) {
|
| 774 |
+
await sendProgress({
|
| 775 |
+
type: 'error',
|
| 776 |
+
error: (error as Error).message
|
| 777 |
+
});
|
| 778 |
+
} finally {
|
| 779 |
+
await writer.close();
|
| 780 |
+
}
|
| 781 |
+
})(provider, request);
|
| 782 |
+
|
| 783 |
+
// Return the stream
|
| 784 |
+
return new Response(stream.readable, {
|
| 785 |
+
headers: {
|
| 786 |
+
'Content-Type': 'text/event-stream',
|
| 787 |
+
'Cache-Control': 'no-cache',
|
| 788 |
+
'Connection': 'keep-alive',
|
| 789 |
+
},
|
| 790 |
+
});
|
| 791 |
+
|
| 792 |
+
} catch (error) {
|
| 793 |
+
console.error('Apply AI code stream error:', error);
|
| 794 |
+
return NextResponse.json(
|
| 795 |
+
{ error: error instanceof Error ? error.message : 'Failed to parse AI code' },
|
| 796 |
+
{ status: 500 }
|
| 797 |
+
);
|
| 798 |
+
}
|
| 799 |
+
}
|
app/api/apply-ai-code/route.ts
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
import { parseMorphEdits, applyMorphEditToFile } from '@/lib/morph-fast-apply';
|
| 3 |
+
import type { SandboxState } from '@/types/sandbox';
|
| 4 |
+
import type { ConversationState } from '@/types/conversation';
|
| 5 |
+
|
| 6 |
+
declare global {
|
| 7 |
+
var conversationState: ConversationState | null;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
interface ParsedResponse {
|
| 11 |
+
explanation: string;
|
| 12 |
+
template: string;
|
| 13 |
+
files: Array<{ path: string; content: string }>;
|
| 14 |
+
packages: string[];
|
| 15 |
+
commands: string[];
|
| 16 |
+
structure: string | null;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function parseAIResponse(response: string): ParsedResponse {
|
| 20 |
+
const sections = {
|
| 21 |
+
files: [] as Array<{ path: string; content: string }>,
|
| 22 |
+
commands: [] as string[],
|
| 23 |
+
packages: [] as string[],
|
| 24 |
+
structure: null as string | null,
|
| 25 |
+
explanation: '',
|
| 26 |
+
template: ''
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
// Parse file sections - handle duplicates and prefer complete versions
|
| 30 |
+
const fileMap = new Map<string, { content: string; isComplete: boolean }>();
|
| 31 |
+
|
| 32 |
+
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
|
| 33 |
+
let match;
|
| 34 |
+
while ((match = fileRegex.exec(response)) !== null) {
|
| 35 |
+
const filePath = match[1];
|
| 36 |
+
const content = match[2].trim();
|
| 37 |
+
const hasClosingTag = response.substring(match.index, match.index + match[0].length).includes('</file>');
|
| 38 |
+
|
| 39 |
+
// Check if this file already exists in our map
|
| 40 |
+
const existing = fileMap.get(filePath);
|
| 41 |
+
|
| 42 |
+
// Decide whether to keep this version
|
| 43 |
+
let shouldReplace = false;
|
| 44 |
+
if (!existing) {
|
| 45 |
+
shouldReplace = true; // First occurrence
|
| 46 |
+
} else if (!existing.isComplete && hasClosingTag) {
|
| 47 |
+
shouldReplace = true; // Replace incomplete with complete
|
| 48 |
+
console.log(`[parseAIResponse] Replacing incomplete ${filePath} with complete version`);
|
| 49 |
+
} else if (existing.isComplete && hasClosingTag && content.length > existing.content.length) {
|
| 50 |
+
shouldReplace = true; // Replace with longer complete version
|
| 51 |
+
console.log(`[parseAIResponse] Replacing ${filePath} with longer complete version`);
|
| 52 |
+
} else if (!existing.isComplete && !hasClosingTag && content.length > existing.content.length) {
|
| 53 |
+
shouldReplace = true; // Both incomplete, keep longer one
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (shouldReplace) {
|
| 57 |
+
// Additional validation: reject obviously broken content
|
| 58 |
+
if (content.includes('...') && !content.includes('...props') && !content.includes('...rest')) {
|
| 59 |
+
console.warn(`[parseAIResponse] Warning: ${filePath} contains ellipsis, may be truncated`);
|
| 60 |
+
// Still use it if it's the only version we have
|
| 61 |
+
if (!existing) {
|
| 62 |
+
fileMap.set(filePath, { content, isComplete: hasClosingTag });
|
| 63 |
+
}
|
| 64 |
+
} else {
|
| 65 |
+
fileMap.set(filePath, { content, isComplete: hasClosingTag });
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Convert map to array for sections.files
|
| 71 |
+
for (const [path, { content, isComplete }] of fileMap.entries()) {
|
| 72 |
+
if (!isComplete) {
|
| 73 |
+
console.log(`[parseAIResponse] Warning: File ${path} appears to be truncated (no closing tag)`);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
sections.files.push({
|
| 77 |
+
path,
|
| 78 |
+
content
|
| 79 |
+
});
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Parse commands
|
| 83 |
+
const cmdRegex = /<command>(.*?)<\/command>/g;
|
| 84 |
+
while ((match = cmdRegex.exec(response)) !== null) {
|
| 85 |
+
sections.commands.push(match[1].trim());
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Parse packages - support both <package> and <packages> tags
|
| 89 |
+
const pkgRegex = /<package>(.*?)<\/package>/g;
|
| 90 |
+
while ((match = pkgRegex.exec(response)) !== null) {
|
| 91 |
+
sections.packages.push(match[1].trim());
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Also parse <packages> tag with multiple packages
|
| 95 |
+
const packagesRegex = /<packages>([\s\S]*?)<\/packages>/;
|
| 96 |
+
const packagesMatch = response.match(packagesRegex);
|
| 97 |
+
if (packagesMatch) {
|
| 98 |
+
const packagesContent = packagesMatch[1].trim();
|
| 99 |
+
// Split by newlines or commas
|
| 100 |
+
const packagesList = packagesContent.split(/[\n,]+/)
|
| 101 |
+
.map(pkg => pkg.trim())
|
| 102 |
+
.filter(pkg => pkg.length > 0);
|
| 103 |
+
sections.packages.push(...packagesList);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Parse structure
|
| 107 |
+
const structureMatch = /<structure>([\s\S]*?)<\/structure>/;
|
| 108 |
+
const structResult = response.match(structureMatch);
|
| 109 |
+
if (structResult) {
|
| 110 |
+
sections.structure = structResult[1].trim();
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// Parse explanation
|
| 114 |
+
const explanationMatch = /<explanation>([\s\S]*?)<\/explanation>/;
|
| 115 |
+
const explResult = response.match(explanationMatch);
|
| 116 |
+
if (explResult) {
|
| 117 |
+
sections.explanation = explResult[1].trim();
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// Parse template
|
| 121 |
+
const templateMatch = /<template>(.*?)<\/template>/;
|
| 122 |
+
const templResult = response.match(templateMatch);
|
| 123 |
+
if (templResult) {
|
| 124 |
+
sections.template = templResult[1].trim();
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
return sections;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
declare global {
|
| 131 |
+
var activeSandbox: any;
|
| 132 |
+
var activeSandboxProvider: any;
|
| 133 |
+
var existingFiles: Set<string>;
|
| 134 |
+
var sandboxState: SandboxState;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
export async function POST(request: NextRequest) {
|
| 138 |
+
try {
|
| 139 |
+
const { response, isEdit = false, packages = [] } = await request.json();
|
| 140 |
+
|
| 141 |
+
if (!response) {
|
| 142 |
+
return NextResponse.json({
|
| 143 |
+
error: 'response is required'
|
| 144 |
+
}, { status: 400 });
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Parse the AI response
|
| 148 |
+
const parsed = parseAIResponse(response);
|
| 149 |
+
const morphEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
|
| 150 |
+
const morphEdits = morphEnabled ? parseMorphEdits(response) : [];
|
| 151 |
+
console.log('[apply-ai-code] Morph Fast Apply mode:', morphEnabled);
|
| 152 |
+
if (morphEnabled) {
|
| 153 |
+
console.log('[apply-ai-code] Morph edits found:', morphEdits.length);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// Initialize existingFiles if not already
|
| 157 |
+
if (!global.existingFiles) {
|
| 158 |
+
global.existingFiles = new Set<string>();
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// Get the active sandbox or provider
|
| 162 |
+
const sandbox = global.activeSandbox || global.activeSandboxProvider;
|
| 163 |
+
|
| 164 |
+
// If no active sandbox, just return parsed results
|
| 165 |
+
if (!sandbox) {
|
| 166 |
+
return NextResponse.json({
|
| 167 |
+
success: true,
|
| 168 |
+
results: {
|
| 169 |
+
filesCreated: parsed.files.map(f => f.path),
|
| 170 |
+
packagesInstalled: parsed.packages,
|
| 171 |
+
commandsExecuted: parsed.commands,
|
| 172 |
+
errors: []
|
| 173 |
+
},
|
| 174 |
+
explanation: parsed.explanation,
|
| 175 |
+
structure: parsed.structure,
|
| 176 |
+
parsedFiles: parsed.files,
|
| 177 |
+
message: `Parsed ${parsed.files.length} files successfully. Create a sandbox to apply them.`
|
| 178 |
+
});
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Verify sandbox is ready before applying code
|
| 182 |
+
console.log('[apply-ai-code] Verifying sandbox is ready...');
|
| 183 |
+
|
| 184 |
+
// For Vercel sandboxes, check if Vite is running
|
| 185 |
+
if (sandbox.constructor?.name === 'VercelProvider' || sandbox.getSandboxInfo?.()?.provider === 'vercel') {
|
| 186 |
+
console.log('[apply-ai-code] Detected Vercel sandbox, checking Vite status...');
|
| 187 |
+
try {
|
| 188 |
+
// Check if Vite process is running
|
| 189 |
+
const checkResult = await sandbox.runCommand('pgrep -f vite');
|
| 190 |
+
if (!checkResult || !checkResult.stdout) {
|
| 191 |
+
console.log('[apply-ai-code] Vite not running, starting it...');
|
| 192 |
+
// Start Vite if not running
|
| 193 |
+
await sandbox.runCommand('sh -c "cd /vercel/sandbox && nohup npm run dev > /tmp/vite.log 2>&1 &"');
|
| 194 |
+
// Wait for Vite to start
|
| 195 |
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
| 196 |
+
console.log('[apply-ai-code] Vite started, proceeding with code application');
|
| 197 |
+
} else {
|
| 198 |
+
console.log('[apply-ai-code] Vite is already running');
|
| 199 |
+
}
|
| 200 |
+
} catch (e) {
|
| 201 |
+
console.log('[apply-ai-code] Could not check Vite status, proceeding anyway:', e);
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// Apply to active sandbox
|
| 206 |
+
console.log('[apply-ai-code] Applying code to sandbox...');
|
| 207 |
+
console.log('[apply-ai-code] Is edit mode:', isEdit);
|
| 208 |
+
console.log('[apply-ai-code] Files to write:', parsed.files.map(f => f.path));
|
| 209 |
+
console.log('[apply-ai-code] Existing files:', Array.from(global.existingFiles));
|
| 210 |
+
if (morphEnabled) {
|
| 211 |
+
console.log('[apply-ai-code] Morph Fast Apply enabled');
|
| 212 |
+
if (morphEdits.length > 0) {
|
| 213 |
+
console.log('[apply-ai-code] Parsed Morph edits:', morphEdits.map(e => e.targetFile));
|
| 214 |
+
} else {
|
| 215 |
+
console.log('[apply-ai-code] No <edit> blocks found in response');
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
const results = {
|
| 220 |
+
filesCreated: [] as string[],
|
| 221 |
+
filesUpdated: [] as string[],
|
| 222 |
+
packagesInstalled: [] as string[],
|
| 223 |
+
packagesAlreadyInstalled: [] as string[],
|
| 224 |
+
packagesFailed: [] as string[],
|
| 225 |
+
commandsExecuted: [] as string[],
|
| 226 |
+
errors: [] as string[]
|
| 227 |
+
};
|
| 228 |
+
|
| 229 |
+
// Combine packages from tool calls and parsed XML tags
|
| 230 |
+
const allPackages = [...packages.filter((pkg: any) => pkg && typeof pkg === 'string'), ...parsed.packages];
|
| 231 |
+
const uniquePackages = [...new Set(allPackages)]; // Remove duplicates
|
| 232 |
+
|
| 233 |
+
if (uniquePackages.length > 0) {
|
| 234 |
+
console.log('[apply-ai-code] Installing packages from XML tags and tool calls:', uniquePackages);
|
| 235 |
+
|
| 236 |
+
try {
|
| 237 |
+
const installResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/install-packages`, {
|
| 238 |
+
method: 'POST',
|
| 239 |
+
headers: { 'Content-Type': 'application/json' },
|
| 240 |
+
body: JSON.stringify({ packages: uniquePackages })
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
if (installResponse.ok) {
|
| 244 |
+
const installResult = await installResponse.json();
|
| 245 |
+
console.log('[apply-ai-code] Package installation result:', installResult);
|
| 246 |
+
|
| 247 |
+
if (installResult.installed && installResult.installed.length > 0) {
|
| 248 |
+
results.packagesInstalled = installResult.installed;
|
| 249 |
+
}
|
| 250 |
+
if (installResult.failed && installResult.failed.length > 0) {
|
| 251 |
+
results.packagesFailed = installResult.failed;
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
} catch (error) {
|
| 255 |
+
console.error('[apply-ai-code] Error installing packages:', error);
|
| 256 |
+
}
|
| 257 |
+
} else {
|
| 258 |
+
// Fallback to detecting packages from code
|
| 259 |
+
console.log('[apply-ai-code] No packages provided, detecting from generated code...');
|
| 260 |
+
console.log('[apply-ai-code] Number of files to scan:', parsed.files.length);
|
| 261 |
+
|
| 262 |
+
// Filter out config files first
|
| 263 |
+
const configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js'];
|
| 264 |
+
const filteredFilesForDetection = parsed.files.filter(file => {
|
| 265 |
+
const fileName = file.path.split('/').pop() || '';
|
| 266 |
+
return !configFiles.includes(fileName);
|
| 267 |
+
});
|
| 268 |
+
|
| 269 |
+
// Build files object for package detection
|
| 270 |
+
const filesForPackageDetection: Record<string, string> = {};
|
| 271 |
+
for (const file of filteredFilesForDetection) {
|
| 272 |
+
filesForPackageDetection[file.path] = file.content;
|
| 273 |
+
// Log if heroicons is found
|
| 274 |
+
if (file.content.includes('heroicons')) {
|
| 275 |
+
console.log(`[apply-ai-code] Found heroicons import in ${file.path}`);
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
try {
|
| 280 |
+
console.log('[apply-ai-code] Calling detect-and-install-packages...');
|
| 281 |
+
const packageResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/detect-and-install-packages`, {
|
| 282 |
+
method: 'POST',
|
| 283 |
+
headers: { 'Content-Type': 'application/json' },
|
| 284 |
+
body: JSON.stringify({ files: filesForPackageDetection })
|
| 285 |
+
});
|
| 286 |
+
|
| 287 |
+
console.log('[apply-ai-code] Package detection response status:', packageResponse.status);
|
| 288 |
+
|
| 289 |
+
if (packageResponse.ok) {
|
| 290 |
+
const packageResult = await packageResponse.json();
|
| 291 |
+
console.log('[apply-ai-code] Package installation result:', JSON.stringify(packageResult, null, 2));
|
| 292 |
+
|
| 293 |
+
if (packageResult.packagesInstalled && packageResult.packagesInstalled.length > 0) {
|
| 294 |
+
results.packagesInstalled = packageResult.packagesInstalled;
|
| 295 |
+
console.log(`[apply-ai-code] Installed packages: ${packageResult.packagesInstalled.join(', ')}`);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
if (packageResult.packagesAlreadyInstalled && packageResult.packagesAlreadyInstalled.length > 0) {
|
| 299 |
+
results.packagesAlreadyInstalled = packageResult.packagesAlreadyInstalled;
|
| 300 |
+
console.log(`[apply-ai-code] Already installed: ${packageResult.packagesAlreadyInstalled.join(', ')}`);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
if (packageResult.packagesFailed && packageResult.packagesFailed.length > 0) {
|
| 304 |
+
results.packagesFailed = packageResult.packagesFailed;
|
| 305 |
+
console.error(`[apply-ai-code] Failed to install packages: ${packageResult.packagesFailed.join(', ')}`);
|
| 306 |
+
results.errors.push(`Failed to install packages: ${packageResult.packagesFailed.join(', ')}`);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// Force Vite restart after package installation
|
| 310 |
+
if (results.packagesInstalled.length > 0) {
|
| 311 |
+
console.log('[apply-ai-code] Packages were installed, forcing Vite restart...');
|
| 312 |
+
|
| 313 |
+
try {
|
| 314 |
+
// Call the restart-vite endpoint
|
| 315 |
+
const restartResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/restart-vite`, {
|
| 316 |
+
method: 'POST',
|
| 317 |
+
headers: { 'Content-Type': 'application/json' }
|
| 318 |
+
});
|
| 319 |
+
|
| 320 |
+
if (restartResponse.ok) {
|
| 321 |
+
const restartResult = await restartResponse.json();
|
| 322 |
+
console.log('[apply-ai-code] Vite restart result:', restartResult.message);
|
| 323 |
+
} else {
|
| 324 |
+
console.error('[apply-ai-code] Failed to restart Vite:', await restartResponse.text());
|
| 325 |
+
}
|
| 326 |
+
} catch (e) {
|
| 327 |
+
console.error('[apply-ai-code] Error calling restart-vite:', e);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// Additional delay to ensure files can be written after restart
|
| 331 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 332 |
+
}
|
| 333 |
+
} else {
|
| 334 |
+
console.error('[apply-ai-code] Package detection/installation failed:', await packageResponse.text());
|
| 335 |
+
}
|
| 336 |
+
} catch (error) {
|
| 337 |
+
console.error('[apply-ai-code] Error detecting/installing packages:', error);
|
| 338 |
+
// Continue with file writing even if package installation fails
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
// Attempt Morph Fast Apply for edits before file creation
|
| 343 |
+
const morphUpdatedPaths = new Set<string>();
|
| 344 |
+
|
| 345 |
+
if (morphEnabled && morphEdits.length > 0) {
|
| 346 |
+
if (!global.activeSandbox) {
|
| 347 |
+
console.warn('[apply-ai-code] Morph edits found but no active sandbox; skipping Morph application');
|
| 348 |
+
} else {
|
| 349 |
+
console.log(`[apply-ai-code] Applying ${morphEdits.length} fast edits via Morph...`);
|
| 350 |
+
for (const edit of morphEdits) {
|
| 351 |
+
try {
|
| 352 |
+
const result = await applyMorphEditToFile({
|
| 353 |
+
sandbox: global.activeSandbox,
|
| 354 |
+
targetPath: edit.targetFile,
|
| 355 |
+
instructions: edit.instructions,
|
| 356 |
+
updateSnippet: edit.update
|
| 357 |
+
});
|
| 358 |
+
|
| 359 |
+
if (result.success && result.normalizedPath) {
|
| 360 |
+
morphUpdatedPaths.add(result.normalizedPath);
|
| 361 |
+
results.filesUpdated.push(result.normalizedPath);
|
| 362 |
+
console.log('[apply-ai-code] Morph applied to', result.normalizedPath);
|
| 363 |
+
} else {
|
| 364 |
+
const msg = result.error || 'Unknown Morph error';
|
| 365 |
+
console.error('[apply-ai-code] Morph apply failed:', msg);
|
| 366 |
+
results.errors.push(`Morph apply failed for ${edit.targetFile}: ${msg}`);
|
| 367 |
+
}
|
| 368 |
+
} catch (e) {
|
| 369 |
+
console.error('[apply-ai-code] Morph apply exception:', e);
|
| 370 |
+
results.errors.push(`Morph apply exception for ${edit.targetFile}: ${(e as Error).message}`);
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
if (morphEnabled && morphEdits.length === 0) {
|
| 376 |
+
console.warn('[apply-ai-code] Morph enabled but no <edit> blocks found; falling back to full-file flow');
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
// Filter out config files that shouldn't be created
|
| 380 |
+
const configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js'];
|
| 381 |
+
let filteredFiles = parsed.files.filter(file => {
|
| 382 |
+
const fileName = file.path.split('/').pop() || '';
|
| 383 |
+
if (configFiles.includes(fileName)) {
|
| 384 |
+
console.warn(`[apply-ai-code] Skipping config file: ${file.path} - already exists in template`);
|
| 385 |
+
return false;
|
| 386 |
+
}
|
| 387 |
+
return true;
|
| 388 |
+
});
|
| 389 |
+
|
| 390 |
+
// Avoid overwriting files already updated by Morph
|
| 391 |
+
if (morphUpdatedPaths.size > 0) {
|
| 392 |
+
filteredFiles = filteredFiles.filter(file => {
|
| 393 |
+
let normalizedPath = file.path.startsWith('/') ? file.path.slice(1) : file.path;
|
| 394 |
+
const fileName = normalizedPath.split('/').pop() || '';
|
| 395 |
+
if (!normalizedPath.startsWith('src/') &&
|
| 396 |
+
!normalizedPath.startsWith('public/') &&
|
| 397 |
+
normalizedPath !== 'index.html' &&
|
| 398 |
+
!configFiles.includes(fileName)) {
|
| 399 |
+
normalizedPath = 'src/' + normalizedPath;
|
| 400 |
+
}
|
| 401 |
+
return !morphUpdatedPaths.has(normalizedPath);
|
| 402 |
+
});
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
// Create or update files AFTER package installation
|
| 406 |
+
for (const file of filteredFiles) {
|
| 407 |
+
try {
|
| 408 |
+
// Normalize the file path
|
| 409 |
+
let normalizedPath = file.path;
|
| 410 |
+
// Remove leading slash if present
|
| 411 |
+
if (normalizedPath.startsWith('/')) {
|
| 412 |
+
normalizedPath = normalizedPath.substring(1);
|
| 413 |
+
}
|
| 414 |
+
// Ensure src/ prefix for component files
|
| 415 |
+
if (!normalizedPath.startsWith('src/') &&
|
| 416 |
+
!normalizedPath.startsWith('public/') &&
|
| 417 |
+
normalizedPath !== 'index.html' &&
|
| 418 |
+
normalizedPath !== 'package.json' &&
|
| 419 |
+
normalizedPath !== 'vite.config.js' &&
|
| 420 |
+
normalizedPath !== 'tailwind.config.js' &&
|
| 421 |
+
normalizedPath !== 'postcss.config.js') {
|
| 422 |
+
normalizedPath = 'src/' + normalizedPath;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
const fullPath = `/home/user/app/${normalizedPath}`;
|
| 426 |
+
const isUpdate = global.existingFiles.has(normalizedPath);
|
| 427 |
+
|
| 428 |
+
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
|
| 429 |
+
let fileContent = file.content;
|
| 430 |
+
if (file.path.endsWith('.jsx') || file.path.endsWith('.js') || file.path.endsWith('.tsx') || file.path.endsWith('.ts')) {
|
| 431 |
+
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
// Fix common Tailwind CSS errors in CSS files
|
| 435 |
+
if (file.path.endsWith('.css')) {
|
| 436 |
+
// Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
|
| 437 |
+
fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl');
|
| 438 |
+
// Replace any other non-existent shadow utilities
|
| 439 |
+
fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl');
|
| 440 |
+
fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
console.log(`[apply-ai-code] Writing file using E2B files API: ${fullPath}`);
|
| 444 |
+
|
| 445 |
+
try {
|
| 446 |
+
// Check if we're using provider pattern (v2) or direct sandbox (v1)
|
| 447 |
+
if (sandbox.writeFile) {
|
| 448 |
+
// V2: Provider pattern (Vercel/E2B provider)
|
| 449 |
+
await sandbox.writeFile(file.path, fileContent);
|
| 450 |
+
} else if (sandbox.files?.write) {
|
| 451 |
+
// V1: Direct E2B sandbox
|
| 452 |
+
await sandbox.files.write(fullPath, fileContent);
|
| 453 |
+
} else {
|
| 454 |
+
throw new Error('Unsupported sandbox type');
|
| 455 |
+
}
|
| 456 |
+
console.log(`[apply-ai-code] Successfully wrote file: ${fullPath}`);
|
| 457 |
+
|
| 458 |
+
// Update file cache
|
| 459 |
+
if (global.sandboxState?.fileCache) {
|
| 460 |
+
global.sandboxState.fileCache.files[normalizedPath] = {
|
| 461 |
+
content: fileContent,
|
| 462 |
+
lastModified: Date.now()
|
| 463 |
+
};
|
| 464 |
+
console.log(`[apply-ai-code] Updated file cache for: ${normalizedPath}`);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
} catch (writeError) {
|
| 468 |
+
console.error(`[apply-ai-code] E2B file write error:`, writeError);
|
| 469 |
+
throw writeError as Error;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
if (isUpdate) {
|
| 474 |
+
results.filesUpdated.push(normalizedPath);
|
| 475 |
+
} else {
|
| 476 |
+
results.filesCreated.push(normalizedPath);
|
| 477 |
+
global.existingFiles.add(normalizedPath);
|
| 478 |
+
}
|
| 479 |
+
} catch (error) {
|
| 480 |
+
results.errors.push(`Failed to create ${file.path}: ${(error as Error).message}`);
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
// Only create App.jsx if it's not an edit and doesn't exist
|
| 485 |
+
const appFileInParsed = parsed.files.some(f => {
|
| 486 |
+
const normalized = f.path.replace(/^\//, '').replace(/^src\//, '');
|
| 487 |
+
return normalized === 'App.jsx' || normalized === 'App.tsx';
|
| 488 |
+
});
|
| 489 |
+
|
| 490 |
+
const appFileExists = global.existingFiles.has('src/App.jsx') ||
|
| 491 |
+
global.existingFiles.has('src/App.tsx') ||
|
| 492 |
+
global.existingFiles.has('App.jsx') ||
|
| 493 |
+
global.existingFiles.has('App.tsx');
|
| 494 |
+
|
| 495 |
+
if (!isEdit && !appFileInParsed && !appFileExists && parsed.files.length > 0) {
|
| 496 |
+
// Find all component files
|
| 497 |
+
const componentFiles = parsed.files.filter(f =>
|
| 498 |
+
(f.path.endsWith('.jsx') || f.path.endsWith('.tsx')) &&
|
| 499 |
+
f.path.includes('component')
|
| 500 |
+
);
|
| 501 |
+
|
| 502 |
+
// Generate imports for components
|
| 503 |
+
const imports = componentFiles
|
| 504 |
+
.filter(f => !f.path.includes('App.') && !f.path.includes('main.') && !f.path.includes('index.'))
|
| 505 |
+
.map(f => {
|
| 506 |
+
const pathParts = f.path.split('/');
|
| 507 |
+
const fileName = pathParts[pathParts.length - 1];
|
| 508 |
+
const componentName = fileName.replace(/\.(jsx|tsx)$/, '');
|
| 509 |
+
// Fix import path - components are in src/components/
|
| 510 |
+
const importPath = f.path.startsWith('src/')
|
| 511 |
+
? f.path.replace('src/', './').replace(/\.(jsx|tsx)$/, '')
|
| 512 |
+
: './' + f.path.replace(/\.(jsx|tsx)$/, '');
|
| 513 |
+
return `import ${componentName} from '${importPath}';`;
|
| 514 |
+
})
|
| 515 |
+
.join('\n');
|
| 516 |
+
|
| 517 |
+
// Find the main component
|
| 518 |
+
const mainComponent = componentFiles.find(f => {
|
| 519 |
+
const name = f.path.toLowerCase();
|
| 520 |
+
return name.includes('header') ||
|
| 521 |
+
name.includes('hero') ||
|
| 522 |
+
name.includes('layout') ||
|
| 523 |
+
name.includes('main') ||
|
| 524 |
+
name.includes('home');
|
| 525 |
+
}) || componentFiles[0];
|
| 526 |
+
|
| 527 |
+
const mainComponentName = mainComponent
|
| 528 |
+
? mainComponent.path.split('/').pop()?.replace(/\.(jsx|tsx)$/, '')
|
| 529 |
+
: null;
|
| 530 |
+
|
| 531 |
+
// Create App.jsx with better structure
|
| 532 |
+
const appContent = `import React from 'react';
|
| 533 |
+
${imports}
|
| 534 |
+
|
| 535 |
+
function App() {
|
| 536 |
+
return (
|
| 537 |
+
<div className="min-h-screen bg-gray-900 text-white p-8">
|
| 538 |
+
${mainComponentName ? `<${mainComponentName} />` : '<div className="text-center">\n <h1 className="text-4xl font-bold mb-4">Welcome to your React App</h1>\n <p className="text-gray-400">Your components have been created but need to be added here.</p>\n </div>'}
|
| 539 |
+
{/* Generated components: ${componentFiles.map(f => f.path).join(', ')} */}
|
| 540 |
+
</div>
|
| 541 |
+
);
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
export default App;`;
|
| 545 |
+
|
| 546 |
+
try {
|
| 547 |
+
// Use provider pattern if available
|
| 548 |
+
if (sandbox.writeFile) {
|
| 549 |
+
await sandbox.writeFile('src/App.jsx', appContent);
|
| 550 |
+
} else if (sandbox.writeFiles) {
|
| 551 |
+
await sandbox.writeFiles([{
|
| 552 |
+
path: 'src/App.jsx',
|
| 553 |
+
content: Buffer.from(appContent)
|
| 554 |
+
}]);
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
console.log('Auto-generated: src/App.jsx');
|
| 558 |
+
results.filesCreated.push('src/App.jsx (auto-generated)');
|
| 559 |
+
} catch (error) {
|
| 560 |
+
results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`);
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
// Don't auto-generate App.css - we're using Tailwind CSS
|
| 564 |
+
|
| 565 |
+
// Only create index.css if it doesn't exist
|
| 566 |
+
const indexCssInParsed = parsed.files.some(f => {
|
| 567 |
+
const normalized = f.path.replace(/^\//, '').replace(/^src\//, '');
|
| 568 |
+
return normalized === 'index.css' || f.path === 'src/index.css';
|
| 569 |
+
});
|
| 570 |
+
|
| 571 |
+
const indexCssExists = global.existingFiles.has('src/index.css') ||
|
| 572 |
+
global.existingFiles.has('index.css');
|
| 573 |
+
|
| 574 |
+
if (!isEdit && !indexCssInParsed && !indexCssExists) {
|
| 575 |
+
try {
|
| 576 |
+
const indexCssContent = `@tailwind base;
|
| 577 |
+
@tailwind components;
|
| 578 |
+
@tailwind utilities;
|
| 579 |
+
|
| 580 |
+
:root {
|
| 581 |
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
| 582 |
+
line-height: 1.5;
|
| 583 |
+
font-weight: 400;
|
| 584 |
+
color-scheme: dark;
|
| 585 |
+
|
| 586 |
+
color: rgba(255, 255, 255, 0.87);
|
| 587 |
+
background-color: #0a0a0a;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
* {
|
| 591 |
+
box-sizing: border-box;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
body {
|
| 595 |
+
margin: 0;
|
| 596 |
+
min-width: 320px;
|
| 597 |
+
min-height: 100vh;
|
| 598 |
+
}`;
|
| 599 |
+
|
| 600 |
+
// Use provider pattern if available
|
| 601 |
+
if (sandbox.writeFile) {
|
| 602 |
+
await sandbox.writeFile('src/index.css', indexCssContent);
|
| 603 |
+
} else if (sandbox.writeFiles) {
|
| 604 |
+
await sandbox.writeFiles([{
|
| 605 |
+
path: 'src/index.css',
|
| 606 |
+
content: Buffer.from(indexCssContent)
|
| 607 |
+
}]);
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
console.log('Auto-generated: src/index.css');
|
| 611 |
+
results.filesCreated.push('src/index.css (with Tailwind)');
|
| 612 |
+
} catch (error) {
|
| 613 |
+
console.error('Failed to create index.css:', error);
|
| 614 |
+
results.errors.push('Failed to create index.css with Tailwind');
|
| 615 |
+
}
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
// Execute commands
|
| 620 |
+
for (const cmd of parsed.commands) {
|
| 621 |
+
try {
|
| 622 |
+
// Parse command and arguments
|
| 623 |
+
const commandParts = cmd.trim().split(/\s+/);
|
| 624 |
+
const cmdName = commandParts[0];
|
| 625 |
+
const args = commandParts.slice(1);
|
| 626 |
+
|
| 627 |
+
// Execute command using sandbox
|
| 628 |
+
let result;
|
| 629 |
+
if (sandbox.runCommand && typeof sandbox.runCommand === 'function') {
|
| 630 |
+
// Check if this is a provider pattern sandbox
|
| 631 |
+
const testResult = await sandbox.runCommand(cmd);
|
| 632 |
+
if (testResult && typeof testResult === 'object' && 'stdout' in testResult) {
|
| 633 |
+
// Provider returns CommandResult directly
|
| 634 |
+
result = testResult;
|
| 635 |
+
} else {
|
| 636 |
+
// Direct sandbox - expects object with cmd and args
|
| 637 |
+
result = await sandbox.runCommand({
|
| 638 |
+
cmd: cmdName,
|
| 639 |
+
args
|
| 640 |
+
});
|
| 641 |
+
}
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
console.log(`Executed: ${cmd}`);
|
| 645 |
+
|
| 646 |
+
// Handle result based on type
|
| 647 |
+
let stdout = '';
|
| 648 |
+
let stderr = '';
|
| 649 |
+
|
| 650 |
+
if (result) {
|
| 651 |
+
if (typeof result.stdout === 'string') {
|
| 652 |
+
stdout = result.stdout;
|
| 653 |
+
stderr = result.stderr || '';
|
| 654 |
+
} else if (typeof result.stdout === 'function') {
|
| 655 |
+
stdout = await result.stdout();
|
| 656 |
+
stderr = await result.stderr();
|
| 657 |
+
}
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
if (stdout) console.log(stdout);
|
| 661 |
+
if (stderr) console.log(`Errors: ${stderr}`);
|
| 662 |
+
|
| 663 |
+
results.commandsExecuted.push(cmd);
|
| 664 |
+
} catch (error) {
|
| 665 |
+
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
|
| 666 |
+
}
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
// Check for missing imports in App.jsx
|
| 670 |
+
const missingImports: string[] = [];
|
| 671 |
+
const appFile = parsed.files.find(f =>
|
| 672 |
+
f.path === 'src/App.jsx' || f.path === 'App.jsx'
|
| 673 |
+
);
|
| 674 |
+
|
| 675 |
+
if (appFile) {
|
| 676 |
+
// Extract imports from App.jsx
|
| 677 |
+
const importRegex = /import\s+(?:\w+|\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/g;
|
| 678 |
+
let match;
|
| 679 |
+
const imports: string[] = [];
|
| 680 |
+
|
| 681 |
+
while ((match = importRegex.exec(appFile.content)) !== null) {
|
| 682 |
+
const importPath = match[1];
|
| 683 |
+
if (importPath.startsWith('./') || importPath.startsWith('../')) {
|
| 684 |
+
imports.push(importPath);
|
| 685 |
+
}
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
// Check if all imported files exist
|
| 689 |
+
for (const imp of imports) {
|
| 690 |
+
// Skip CSS imports for this check
|
| 691 |
+
if (imp.endsWith('.css')) continue;
|
| 692 |
+
|
| 693 |
+
// Convert import path to expected file paths
|
| 694 |
+
const basePath = imp.replace('./', 'src/');
|
| 695 |
+
const possiblePaths = [
|
| 696 |
+
basePath + '.jsx',
|
| 697 |
+
basePath + '.js',
|
| 698 |
+
basePath + '/index.jsx',
|
| 699 |
+
basePath + '/index.js'
|
| 700 |
+
];
|
| 701 |
+
|
| 702 |
+
const fileExists = parsed.files.some(f =>
|
| 703 |
+
possiblePaths.some(path => f.path === path)
|
| 704 |
+
);
|
| 705 |
+
|
| 706 |
+
if (!fileExists) {
|
| 707 |
+
missingImports.push(imp);
|
| 708 |
+
}
|
| 709 |
+
}
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
// Prepare response
|
| 713 |
+
const responseData: any = {
|
| 714 |
+
success: true,
|
| 715 |
+
results,
|
| 716 |
+
explanation: parsed.explanation,
|
| 717 |
+
structure: parsed.structure,
|
| 718 |
+
message: `Applied ${results.filesCreated.length} files successfully`
|
| 719 |
+
};
|
| 720 |
+
|
| 721 |
+
// Handle missing imports automatically
|
| 722 |
+
if (missingImports.length > 0) {
|
| 723 |
+
console.warn('[apply-ai-code] Missing imports detected:', missingImports);
|
| 724 |
+
|
| 725 |
+
// Automatically generate missing components
|
| 726 |
+
try {
|
| 727 |
+
console.log('[apply-ai-code] Auto-generating missing components...');
|
| 728 |
+
|
| 729 |
+
const autoCompleteResponse = await fetch(
|
| 730 |
+
`${request.nextUrl.origin}/api/auto-complete-components`,
|
| 731 |
+
{
|
| 732 |
+
method: 'POST',
|
| 733 |
+
headers: { 'Content-Type': 'application/json' },
|
| 734 |
+
body: JSON.stringify({
|
| 735 |
+
missingImports,
|
| 736 |
+
model: 'claude-sonnet-4-20250514'
|
| 737 |
+
})
|
| 738 |
+
}
|
| 739 |
+
);
|
| 740 |
+
|
| 741 |
+
const autoCompleteData = await autoCompleteResponse.json();
|
| 742 |
+
|
| 743 |
+
if (autoCompleteData.success) {
|
| 744 |
+
responseData.autoCompleted = true;
|
| 745 |
+
responseData.autoCompletedComponents = autoCompleteData.components;
|
| 746 |
+
responseData.message = `Applied ${results.filesCreated.length} files + auto-generated ${autoCompleteData.files} missing components`;
|
| 747 |
+
|
| 748 |
+
// Add auto-completed files to results
|
| 749 |
+
results.filesCreated.push(...autoCompleteData.components);
|
| 750 |
+
} else {
|
| 751 |
+
// If auto-complete fails, still warn the user
|
| 752 |
+
responseData.warning = `Missing ${missingImports.length} imported components: ${missingImports.join(', ')}`;
|
| 753 |
+
responseData.missingImports = missingImports;
|
| 754 |
+
}
|
| 755 |
+
} catch (error) {
|
| 756 |
+
console.error('[apply-ai-code] Auto-complete failed:', error);
|
| 757 |
+
responseData.warning = `Missing ${missingImports.length} imported components: ${missingImports.join(', ')}`;
|
| 758 |
+
responseData.missingImports = missingImports;
|
| 759 |
+
}
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
// Track applied files in conversation state
|
| 763 |
+
if (global.conversationState && results.filesCreated.length > 0) {
|
| 764 |
+
// Update the last message metadata with edited files
|
| 765 |
+
const messages = global.conversationState.context.messages;
|
| 766 |
+
if (messages.length > 0) {
|
| 767 |
+
const lastMessage = messages[messages.length - 1];
|
| 768 |
+
if (lastMessage.role === 'user') {
|
| 769 |
+
lastMessage.metadata = {
|
| 770 |
+
...lastMessage.metadata,
|
| 771 |
+
editedFiles: results.filesCreated
|
| 772 |
+
};
|
| 773 |
+
}
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
// Track applied code in project evolution
|
| 777 |
+
if (global.conversationState.context.projectEvolution) {
|
| 778 |
+
global.conversationState.context.projectEvolution.majorChanges.push({
|
| 779 |
+
timestamp: Date.now(),
|
| 780 |
+
description: parsed.explanation || 'Code applied',
|
| 781 |
+
filesAffected: results.filesCreated
|
| 782 |
+
});
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
// Update last updated timestamp
|
| 786 |
+
global.conversationState.lastUpdated = Date.now();
|
| 787 |
+
|
| 788 |
+
console.log('[apply-ai-code] Updated conversation state with applied files:', results.filesCreated);
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
return NextResponse.json(responseData);
|
| 792 |
+
|
| 793 |
+
} catch (error) {
|
| 794 |
+
console.error('Apply AI code error:', error);
|
| 795 |
+
return NextResponse.json(
|
| 796 |
+
{ error: error instanceof Error ? error.message : 'Failed to parse AI code' },
|
| 797 |
+
{ status: 500 }
|
| 798 |
+
);
|
| 799 |
+
}
|
| 800 |
+
}
|
app/api/check-vite-errors/route.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
// Stub endpoint to prevent 404 errors
|
| 4 |
+
// This endpoint is being called but the source is unknown
|
| 5 |
+
// Returns empty errors array to satisfy any calling code
|
| 6 |
+
export async function GET() {
|
| 7 |
+
return NextResponse.json({
|
| 8 |
+
success: true,
|
| 9 |
+
errors: [],
|
| 10 |
+
message: 'No Vite errors detected'
|
| 11 |
+
});
|
| 12 |
+
}
|
app/api/clear-vite-errors-cache/route.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
var viteErrorsCache: { errors: any[], timestamp: number } | null;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export async function POST() {
|
| 8 |
+
try {
|
| 9 |
+
// Clear the cache
|
| 10 |
+
global.viteErrorsCache = null;
|
| 11 |
+
|
| 12 |
+
console.log('[clear-vite-errors-cache] Cache cleared');
|
| 13 |
+
|
| 14 |
+
return NextResponse.json({
|
| 15 |
+
success: true,
|
| 16 |
+
message: 'Vite errors cache cleared'
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
} catch (error) {
|
| 20 |
+
console.error('[clear-vite-errors-cache] Error:', error);
|
| 21 |
+
return NextResponse.json({
|
| 22 |
+
success: false,
|
| 23 |
+
error: (error as Error).message
|
| 24 |
+
}, { status: 500 });
|
| 25 |
+
}
|
| 26 |
+
}
|
app/api/conversation-state/route.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
import type { ConversationState } from '@/types/conversation';
|
| 3 |
+
|
| 4 |
+
declare global {
|
| 5 |
+
var conversationState: ConversationState | null;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
// GET: Retrieve current conversation state
|
| 9 |
+
export async function GET() {
|
| 10 |
+
try {
|
| 11 |
+
if (!global.conversationState) {
|
| 12 |
+
return NextResponse.json({
|
| 13 |
+
success: true,
|
| 14 |
+
state: null,
|
| 15 |
+
message: 'No active conversation'
|
| 16 |
+
});
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
return NextResponse.json({
|
| 20 |
+
success: true,
|
| 21 |
+
state: global.conversationState
|
| 22 |
+
});
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.error('[conversation-state] Error getting state:', error);
|
| 25 |
+
return NextResponse.json({
|
| 26 |
+
success: false,
|
| 27 |
+
error: (error as Error).message
|
| 28 |
+
}, { status: 500 });
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// POST: Reset or update conversation state
|
| 33 |
+
export async function POST(request: NextRequest) {
|
| 34 |
+
try {
|
| 35 |
+
const { action, data } = await request.json();
|
| 36 |
+
|
| 37 |
+
switch (action) {
|
| 38 |
+
case 'reset':
|
| 39 |
+
global.conversationState = {
|
| 40 |
+
conversationId: `conv-${Date.now()}`,
|
| 41 |
+
startedAt: Date.now(),
|
| 42 |
+
lastUpdated: Date.now(),
|
| 43 |
+
context: {
|
| 44 |
+
messages: [],
|
| 45 |
+
edits: [],
|
| 46 |
+
projectEvolution: { majorChanges: [] },
|
| 47 |
+
userPreferences: {}
|
| 48 |
+
}
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
console.log('[conversation-state] Reset conversation state');
|
| 52 |
+
|
| 53 |
+
return NextResponse.json({
|
| 54 |
+
success: true,
|
| 55 |
+
message: 'Conversation state reset',
|
| 56 |
+
state: global.conversationState
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
case 'clear-old':
|
| 60 |
+
// Clear old conversation data but keep recent context
|
| 61 |
+
if (!global.conversationState) {
|
| 62 |
+
// Initialize conversation state if it doesn't exist
|
| 63 |
+
global.conversationState = {
|
| 64 |
+
conversationId: `conv-${Date.now()}`,
|
| 65 |
+
startedAt: Date.now(),
|
| 66 |
+
lastUpdated: Date.now(),
|
| 67 |
+
context: {
|
| 68 |
+
messages: [],
|
| 69 |
+
edits: [],
|
| 70 |
+
projectEvolution: { majorChanges: [] },
|
| 71 |
+
userPreferences: {}
|
| 72 |
+
}
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
console.log('[conversation-state] Initialized new conversation state for clear-old');
|
| 76 |
+
|
| 77 |
+
return NextResponse.json({
|
| 78 |
+
success: true,
|
| 79 |
+
message: 'New conversation state initialized',
|
| 80 |
+
state: global.conversationState
|
| 81 |
+
});
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Keep only recent data
|
| 85 |
+
global.conversationState.context.messages = global.conversationState.context.messages.slice(-5);
|
| 86 |
+
global.conversationState.context.edits = global.conversationState.context.edits.slice(-3);
|
| 87 |
+
global.conversationState.context.projectEvolution.majorChanges =
|
| 88 |
+
global.conversationState.context.projectEvolution.majorChanges.slice(-2);
|
| 89 |
+
|
| 90 |
+
console.log('[conversation-state] Cleared old conversation data');
|
| 91 |
+
|
| 92 |
+
return NextResponse.json({
|
| 93 |
+
success: true,
|
| 94 |
+
message: 'Old conversation data cleared',
|
| 95 |
+
state: global.conversationState
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
case 'update':
|
| 99 |
+
if (!global.conversationState) {
|
| 100 |
+
return NextResponse.json({
|
| 101 |
+
success: false,
|
| 102 |
+
error: 'No active conversation to update'
|
| 103 |
+
}, { status: 400 });
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Update specific fields if provided
|
| 107 |
+
if (data) {
|
| 108 |
+
if (data.currentTopic) {
|
| 109 |
+
global.conversationState.context.currentTopic = data.currentTopic;
|
| 110 |
+
}
|
| 111 |
+
if (data.userPreferences) {
|
| 112 |
+
global.conversationState.context.userPreferences = {
|
| 113 |
+
...global.conversationState.context.userPreferences,
|
| 114 |
+
...data.userPreferences
|
| 115 |
+
};
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
global.conversationState.lastUpdated = Date.now();
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
return NextResponse.json({
|
| 122 |
+
success: true,
|
| 123 |
+
message: 'Conversation state updated',
|
| 124 |
+
state: global.conversationState
|
| 125 |
+
});
|
| 126 |
+
|
| 127 |
+
default:
|
| 128 |
+
return NextResponse.json({
|
| 129 |
+
success: false,
|
| 130 |
+
error: 'Invalid action. Use "reset" or "update"'
|
| 131 |
+
}, { status: 400 });
|
| 132 |
+
}
|
| 133 |
+
} catch (error) {
|
| 134 |
+
console.error('[conversation-state] Error:', error);
|
| 135 |
+
return NextResponse.json({
|
| 136 |
+
success: false,
|
| 137 |
+
error: (error as Error).message
|
| 138 |
+
}, { status: 500 });
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// DELETE: Clear conversation state
|
| 143 |
+
export async function DELETE() {
|
| 144 |
+
try {
|
| 145 |
+
global.conversationState = null;
|
| 146 |
+
|
| 147 |
+
console.log('[conversation-state] Cleared conversation state');
|
| 148 |
+
|
| 149 |
+
return NextResponse.json({
|
| 150 |
+
success: true,
|
| 151 |
+
message: 'Conversation state cleared'
|
| 152 |
+
});
|
| 153 |
+
} catch (error) {
|
| 154 |
+
console.error('[conversation-state] Error clearing state:', error);
|
| 155 |
+
return NextResponse.json({
|
| 156 |
+
success: false,
|
| 157 |
+
error: (error as Error).message
|
| 158 |
+
}, { status: 500 });
|
| 159 |
+
}
|
| 160 |
+
}
|
app/api/create-ai-sandbox-v2/route.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
import { SandboxFactory } from '@/lib/sandbox/factory';
|
| 3 |
+
// SandboxProvider type is used through SandboxFactory
|
| 4 |
+
import type { SandboxState } from '@/types/sandbox';
|
| 5 |
+
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
| 6 |
+
|
| 7 |
+
// Store active sandbox globally
|
| 8 |
+
declare global {
|
| 9 |
+
var activeSandboxProvider: any;
|
| 10 |
+
var sandboxData: any;
|
| 11 |
+
var existingFiles: Set<string>;
|
| 12 |
+
var sandboxState: SandboxState;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export async function POST() {
|
| 16 |
+
try {
|
| 17 |
+
console.log('[create-ai-sandbox-v2] Creating sandbox...');
|
| 18 |
+
|
| 19 |
+
// Clean up all existing sandboxes
|
| 20 |
+
console.log('[create-ai-sandbox-v2] Cleaning up existing sandboxes...');
|
| 21 |
+
await sandboxManager.terminateAll();
|
| 22 |
+
|
| 23 |
+
// Also clean up legacy global state
|
| 24 |
+
if (global.activeSandboxProvider) {
|
| 25 |
+
try {
|
| 26 |
+
await global.activeSandboxProvider.terminate();
|
| 27 |
+
} catch (e) {
|
| 28 |
+
console.error('Failed to terminate legacy global sandbox:', e);
|
| 29 |
+
}
|
| 30 |
+
global.activeSandboxProvider = null;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Clear existing files tracking
|
| 34 |
+
if (global.existingFiles) {
|
| 35 |
+
global.existingFiles.clear();
|
| 36 |
+
} else {
|
| 37 |
+
global.existingFiles = new Set<string>();
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Create new sandbox using factory
|
| 41 |
+
const provider = SandboxFactory.create();
|
| 42 |
+
const sandboxInfo = await provider.createSandbox();
|
| 43 |
+
|
| 44 |
+
console.log('[create-ai-sandbox-v2] Setting up Vite React app...');
|
| 45 |
+
await provider.setupViteApp();
|
| 46 |
+
|
| 47 |
+
// Register with sandbox manager
|
| 48 |
+
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
|
| 49 |
+
|
| 50 |
+
// Also store in legacy global state for backward compatibility
|
| 51 |
+
global.activeSandboxProvider = provider;
|
| 52 |
+
global.sandboxData = {
|
| 53 |
+
sandboxId: sandboxInfo.sandboxId,
|
| 54 |
+
url: sandboxInfo.url
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
// Initialize sandbox state
|
| 58 |
+
global.sandboxState = {
|
| 59 |
+
fileCache: {
|
| 60 |
+
files: {},
|
| 61 |
+
lastSync: Date.now(),
|
| 62 |
+
sandboxId: sandboxInfo.sandboxId
|
| 63 |
+
},
|
| 64 |
+
sandbox: provider, // Store the provider instead of raw sandbox
|
| 65 |
+
sandboxData: {
|
| 66 |
+
sandboxId: sandboxInfo.sandboxId,
|
| 67 |
+
url: sandboxInfo.url
|
| 68 |
+
}
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
console.log('[create-ai-sandbox-v2] Sandbox ready at:', sandboxInfo.url);
|
| 72 |
+
|
| 73 |
+
return NextResponse.json({
|
| 74 |
+
success: true,
|
| 75 |
+
sandboxId: sandboxInfo.sandboxId,
|
| 76 |
+
url: sandboxInfo.url,
|
| 77 |
+
provider: sandboxInfo.provider,
|
| 78 |
+
message: 'Sandbox created and Vite React app initialized'
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
} catch (error) {
|
| 82 |
+
console.error('[create-ai-sandbox-v2] Error:', error);
|
| 83 |
+
|
| 84 |
+
// Clean up on error
|
| 85 |
+
await sandboxManager.terminateAll();
|
| 86 |
+
if (global.activeSandboxProvider) {
|
| 87 |
+
try {
|
| 88 |
+
await global.activeSandboxProvider.terminate();
|
| 89 |
+
} catch (e) {
|
| 90 |
+
console.error('Failed to terminate sandbox on error:', e);
|
| 91 |
+
}
|
| 92 |
+
global.activeSandboxProvider = null;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
return NextResponse.json(
|
| 96 |
+
{
|
| 97 |
+
error: error instanceof Error ? error.message : 'Failed to create sandbox',
|
| 98 |
+
details: error instanceof Error ? error.stack : undefined
|
| 99 |
+
},
|
| 100 |
+
{ status: 500 }
|
| 101 |
+
);
|
| 102 |
+
}
|
| 103 |
+
}
|
app/api/create-ai-sandbox/route.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
import { Sandbox } from '@vercel/sandbox';
|
| 3 |
+
import type { SandboxState } from '@/types/sandbox';
|
| 4 |
+
import { appConfig } from '@/config/app.config';
|
| 5 |
+
|
| 6 |
+
// Store active sandbox globally
|
| 7 |
+
declare global {
|
| 8 |
+
var activeSandbox: any;
|
| 9 |
+
var sandboxData: any;
|
| 10 |
+
var existingFiles: Set<string>;
|
| 11 |
+
var sandboxState: SandboxState;
|
| 12 |
+
var sandboxCreationInProgress: boolean;
|
| 13 |
+
var sandboxCreationPromise: Promise<any> | null;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export async function POST() {
|
| 17 |
+
// Check if sandbox creation is already in progress
|
| 18 |
+
if (global.sandboxCreationInProgress && global.sandboxCreationPromise) {
|
| 19 |
+
console.log('[create-ai-sandbox] Sandbox creation already in progress, waiting for existing creation...');
|
| 20 |
+
try {
|
| 21 |
+
const existingResult = await global.sandboxCreationPromise;
|
| 22 |
+
console.log('[create-ai-sandbox] Returning existing sandbox creation result');
|
| 23 |
+
return NextResponse.json(existingResult);
|
| 24 |
+
} catch (error) {
|
| 25 |
+
console.error('[create-ai-sandbox] Existing sandbox creation failed:', error);
|
| 26 |
+
// Continue with new creation if the existing one failed
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Check if we already have an active sandbox
|
| 31 |
+
if (global.activeSandbox && global.sandboxData) {
|
| 32 |
+
console.log('[create-ai-sandbox] Returning existing active sandbox');
|
| 33 |
+
return NextResponse.json({
|
| 34 |
+
success: true,
|
| 35 |
+
sandboxId: global.sandboxData.sandboxId,
|
| 36 |
+
url: global.sandboxData.url
|
| 37 |
+
});
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Set the creation flag
|
| 41 |
+
global.sandboxCreationInProgress = true;
|
| 42 |
+
|
| 43 |
+
// Create the promise that other requests can await
|
| 44 |
+
global.sandboxCreationPromise = createSandboxInternal();
|
| 45 |
+
|
| 46 |
+
try {
|
| 47 |
+
const result = await global.sandboxCreationPromise;
|
| 48 |
+
return NextResponse.json(result);
|
| 49 |
+
} catch (error) {
|
| 50 |
+
console.error('[create-ai-sandbox] Sandbox creation failed:', error);
|
| 51 |
+
return NextResponse.json(
|
| 52 |
+
{
|
| 53 |
+
error: error instanceof Error ? error.message : 'Failed to create sandbox',
|
| 54 |
+
details: error instanceof Error ? error.stack : undefined
|
| 55 |
+
},
|
| 56 |
+
{ status: 500 }
|
| 57 |
+
);
|
| 58 |
+
} finally {
|
| 59 |
+
global.sandboxCreationInProgress = false;
|
| 60 |
+
global.sandboxCreationPromise = null;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
async function createSandboxInternal() {
|
| 65 |
+
let sandbox: any = null;
|
| 66 |
+
|
| 67 |
+
try {
|
| 68 |
+
console.log('[create-ai-sandbox] Creating Vercel sandbox...');
|
| 69 |
+
|
| 70 |
+
// Kill existing sandbox if any
|
| 71 |
+
if (global.activeSandbox) {
|
| 72 |
+
console.log('[create-ai-sandbox] Stopping existing sandbox...');
|
| 73 |
+
try {
|
| 74 |
+
await global.activeSandbox.stop();
|
| 75 |
+
} catch (e) {
|
| 76 |
+
console.error('Failed to stop existing sandbox:', e);
|
| 77 |
+
}
|
| 78 |
+
global.activeSandbox = null;
|
| 79 |
+
global.sandboxData = null;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Clear existing files tracking
|
| 83 |
+
if (global.existingFiles) {
|
| 84 |
+
global.existingFiles.clear();
|
| 85 |
+
} else {
|
| 86 |
+
global.existingFiles = new Set<string>();
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Create Vercel sandbox with flexible authentication
|
| 90 |
+
console.log(`[create-ai-sandbox] Creating Vercel sandbox with ${appConfig.vercelSandbox.timeoutMinutes} minute timeout...`);
|
| 91 |
+
|
| 92 |
+
// Prepare sandbox configuration
|
| 93 |
+
const sandboxConfig: any = {
|
| 94 |
+
timeout: appConfig.vercelSandbox.timeoutMs,
|
| 95 |
+
runtime: appConfig.vercelSandbox.runtime,
|
| 96 |
+
ports: [appConfig.vercelSandbox.devPort]
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
// Add authentication parameters if using personal access token
|
| 100 |
+
if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
|
| 101 |
+
console.log('[create-ai-sandbox] Using personal access token authentication');
|
| 102 |
+
sandboxConfig.teamId = process.env.VERCEL_TEAM_ID;
|
| 103 |
+
sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID;
|
| 104 |
+
sandboxConfig.token = process.env.VERCEL_TOKEN;
|
| 105 |
+
} else if (process.env.VERCEL_OIDC_TOKEN) {
|
| 106 |
+
console.log('[create-ai-sandbox] Using OIDC token authentication');
|
| 107 |
+
} else {
|
| 108 |
+
console.log('[create-ai-sandbox] No authentication found - relying on default Vercel authentication');
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
sandbox = await Sandbox.create(sandboxConfig);
|
| 112 |
+
|
| 113 |
+
const sandboxId = sandbox.sandboxId;
|
| 114 |
+
console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`);
|
| 115 |
+
|
| 116 |
+
// Set up a basic Vite React app
|
| 117 |
+
console.log('[create-ai-sandbox] Setting up Vite React app...');
|
| 118 |
+
|
| 119 |
+
// First, change to the working directory
|
| 120 |
+
await sandbox.runCommand('pwd');
|
| 121 |
+
// workDir is defined in appConfig - not needed here
|
| 122 |
+
|
| 123 |
+
// Get the sandbox URL using the correct Vercel Sandbox API
|
| 124 |
+
const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort);
|
| 125 |
+
|
| 126 |
+
// Extract the hostname from the sandbox URL for Vite config
|
| 127 |
+
const sandboxHostname = new URL(sandboxUrl).hostname;
|
| 128 |
+
console.log(`[create-ai-sandbox] Sandbox hostname: ${sandboxHostname}`);
|
| 129 |
+
|
| 130 |
+
// Create the Vite config content with the proper hostname (using string concatenation)
|
| 131 |
+
const viteConfigContent = `import { defineConfig } from 'vite'
|
| 132 |
+
import react from '@vitejs/plugin-react'
|
| 133 |
+
|
| 134 |
+
// Vercel Sandbox compatible Vite configuration
|
| 135 |
+
export default defineConfig({
|
| 136 |
+
plugins: [react()],
|
| 137 |
+
server: {
|
| 138 |
+
host: '0.0.0.0',
|
| 139 |
+
port: ${appConfig.vercelSandbox.devPort},
|
| 140 |
+
strictPort: true,
|
| 141 |
+
hmr: true,
|
| 142 |
+
allowedHosts: [
|
| 143 |
+
'localhost',
|
| 144 |
+
'127.0.0.1',
|
| 145 |
+
'` + sandboxHostname + `', // Allow the Vercel Sandbox domain
|
| 146 |
+
'.vercel.run', // Allow all Vercel sandbox domains
|
| 147 |
+
'.vercel-sandbox.dev' // Fallback pattern
|
| 148 |
+
]
|
| 149 |
+
}
|
| 150 |
+
})`;
|
| 151 |
+
|
| 152 |
+
// Create the project files (now we have the sandbox hostname)
|
| 153 |
+
const projectFiles = [
|
| 154 |
+
{
|
| 155 |
+
path: 'package.json',
|
| 156 |
+
content: Buffer.from(JSON.stringify({
|
| 157 |
+
"name": "sandbox-app",
|
| 158 |
+
"version": "1.0.0",
|
| 159 |
+
"type": "module",
|
| 160 |
+
"scripts": {
|
| 161 |
+
"dev": "vite --host --port 3000",
|
| 162 |
+
"build": "vite build",
|
| 163 |
+
"preview": "vite preview"
|
| 164 |
+
},
|
| 165 |
+
"dependencies": {
|
| 166 |
+
"react": "^18.2.0",
|
| 167 |
+
"react-dom": "^18.2.0"
|
| 168 |
+
},
|
| 169 |
+
"devDependencies": {
|
| 170 |
+
"@vitejs/plugin-react": "^4.0.0",
|
| 171 |
+
"vite": "^4.3.9",
|
| 172 |
+
"tailwindcss": "^3.3.0",
|
| 173 |
+
"postcss": "^8.4.31",
|
| 174 |
+
"autoprefixer": "^10.4.16"
|
| 175 |
+
}
|
| 176 |
+
}, null, 2))
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
path: 'vite.config.js',
|
| 180 |
+
content: Buffer.from(viteConfigContent)
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
path: 'tailwind.config.js',
|
| 184 |
+
content: Buffer.from(`/** @type {import('tailwindcss').Config} */
|
| 185 |
+
export default {
|
| 186 |
+
content: [
|
| 187 |
+
"./index.html",
|
| 188 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 189 |
+
],
|
| 190 |
+
theme: {
|
| 191 |
+
extend: {},
|
| 192 |
+
},
|
| 193 |
+
plugins: [],
|
| 194 |
+
}`)
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
path: 'postcss.config.js',
|
| 198 |
+
content: Buffer.from(`export default {
|
| 199 |
+
plugins: {
|
| 200 |
+
tailwindcss: {},
|
| 201 |
+
autoprefixer: {},
|
| 202 |
+
},
|
| 203 |
+
}`)
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
path: 'index.html',
|
| 207 |
+
content: Buffer.from(`<!DOCTYPE html>
|
| 208 |
+
<html lang="en">
|
| 209 |
+
<head>
|
| 210 |
+
<meta charset="UTF-8" />
|
| 211 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 212 |
+
<title>Sandbox App</title>
|
| 213 |
+
</head>
|
| 214 |
+
<body>
|
| 215 |
+
<div id="root"></div>
|
| 216 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 217 |
+
</body>
|
| 218 |
+
</html>`)
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
path: 'src/main.jsx',
|
| 222 |
+
content: Buffer.from(`import React from 'react'
|
| 223 |
+
import ReactDOM from 'react-dom/client'
|
| 224 |
+
import App from './App.jsx'
|
| 225 |
+
import './index.css'
|
| 226 |
+
|
| 227 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 228 |
+
<React.StrictMode>
|
| 229 |
+
<App />
|
| 230 |
+
</React.StrictMode>,
|
| 231 |
+
)`)
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
path: 'src/App.jsx',
|
| 235 |
+
content: Buffer.from(`function App() {
|
| 236 |
+
return (
|
| 237 |
+
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
|
| 238 |
+
<div className="text-center max-w-2xl">
|
| 239 |
+
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-blue-500 to-purple-600 bg-clip-text text-transparent">
|
| 240 |
+
Sandbox Ready
|
| 241 |
+
</h1>
|
| 242 |
+
<p className="text-lg text-gray-400">
|
| 243 |
+
Start building your React app with Vite and Tailwind CSS!
|
| 244 |
+
</p>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
)
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
export default App`)
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
path: 'src/index.css',
|
| 254 |
+
content: Buffer.from(`@tailwind base;
|
| 255 |
+
@tailwind components;
|
| 256 |
+
@tailwind utilities;
|
| 257 |
+
|
| 258 |
+
/* Force Tailwind to load */
|
| 259 |
+
@layer base {
|
| 260 |
+
:root {
|
| 261 |
+
font-synthesis: none;
|
| 262 |
+
text-rendering: optimizeLegibility;
|
| 263 |
+
-webkit-font-smoothing: antialiased;
|
| 264 |
+
-moz-osx-font-smoothing: grayscale;
|
| 265 |
+
-webkit-text-size-adjust: 100%;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
* {
|
| 269 |
+
margin: 0;
|
| 270 |
+
padding: 0;
|
| 271 |
+
box-sizing: border-box;
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
body {
|
| 276 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
| 277 |
+
background-color: rgb(17 24 39);
|
| 278 |
+
}`)
|
| 279 |
+
}
|
| 280 |
+
];
|
| 281 |
+
|
| 282 |
+
// Create directory structure first
|
| 283 |
+
await sandbox.runCommand({
|
| 284 |
+
cmd: 'mkdir',
|
| 285 |
+
args: ['-p', 'src']
|
| 286 |
+
});
|
| 287 |
+
|
| 288 |
+
// Write all files
|
| 289 |
+
await sandbox.writeFiles(projectFiles);
|
| 290 |
+
console.log('[create-ai-sandbox] ✓ Project files created');
|
| 291 |
+
|
| 292 |
+
// Install dependencies
|
| 293 |
+
console.log('[create-ai-sandbox] Installing dependencies...');
|
| 294 |
+
const installResult = await sandbox.runCommand({
|
| 295 |
+
cmd: 'npm',
|
| 296 |
+
args: ['install', '--loglevel', 'info']
|
| 297 |
+
});
|
| 298 |
+
if (installResult.exitCode === 0) {
|
| 299 |
+
console.log('[create-ai-sandbox] ✓ Dependencies installed successfully');
|
| 300 |
+
} else {
|
| 301 |
+
console.log('[create-ai-sandbox] ⚠ Warning: npm install had issues but continuing...');
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
// Start Vite dev server in detached mode
|
| 305 |
+
console.log('[create-ai-sandbox] Starting Vite dev server...');
|
| 306 |
+
const viteProcess = await sandbox.runCommand({
|
| 307 |
+
cmd: 'npm',
|
| 308 |
+
args: ['run', 'dev'],
|
| 309 |
+
detached: true
|
| 310 |
+
});
|
| 311 |
+
|
| 312 |
+
console.log('[create-ai-sandbox] ✓ Vite dev server started');
|
| 313 |
+
|
| 314 |
+
// Wait for Vite to be fully ready
|
| 315 |
+
await new Promise(resolve => setTimeout(resolve, appConfig.vercelSandbox.devServerStartupDelay));
|
| 316 |
+
|
| 317 |
+
// Store sandbox globally
|
| 318 |
+
global.activeSandbox = sandbox;
|
| 319 |
+
global.sandboxData = {
|
| 320 |
+
sandboxId,
|
| 321 |
+
url: sandboxUrl,
|
| 322 |
+
viteProcess
|
| 323 |
+
};
|
| 324 |
+
|
| 325 |
+
// Initialize sandbox state
|
| 326 |
+
global.sandboxState = {
|
| 327 |
+
fileCache: {
|
| 328 |
+
files: {},
|
| 329 |
+
lastSync: Date.now(),
|
| 330 |
+
sandboxId
|
| 331 |
+
},
|
| 332 |
+
sandbox,
|
| 333 |
+
sandboxData: {
|
| 334 |
+
sandboxId,
|
| 335 |
+
url: sandboxUrl
|
| 336 |
+
}
|
| 337 |
+
};
|
| 338 |
+
|
| 339 |
+
// Track initial files
|
| 340 |
+
global.existingFiles.add('src/App.jsx');
|
| 341 |
+
global.existingFiles.add('src/main.jsx');
|
| 342 |
+
global.existingFiles.add('src/index.css');
|
| 343 |
+
global.existingFiles.add('index.html');
|
| 344 |
+
global.existingFiles.add('package.json');
|
| 345 |
+
global.existingFiles.add('vite.config.js');
|
| 346 |
+
global.existingFiles.add('tailwind.config.js');
|
| 347 |
+
global.existingFiles.add('postcss.config.js');
|
| 348 |
+
|
| 349 |
+
console.log('[create-ai-sandbox] Sandbox ready at:', sandboxUrl);
|
| 350 |
+
|
| 351 |
+
const result = {
|
| 352 |
+
success: true,
|
| 353 |
+
sandboxId,
|
| 354 |
+
url: sandboxUrl,
|
| 355 |
+
message: 'Vercel sandbox created and Vite React app initialized'
|
| 356 |
+
};
|
| 357 |
+
|
| 358 |
+
// Store the result for reuse
|
| 359 |
+
global.sandboxData = {
|
| 360 |
+
...global.sandboxData,
|
| 361 |
+
...result
|
| 362 |
+
};
|
| 363 |
+
|
| 364 |
+
return result;
|
| 365 |
+
|
| 366 |
+
} catch (error) {
|
| 367 |
+
console.error('[create-ai-sandbox] Error:', error);
|
| 368 |
+
|
| 369 |
+
// Clean up on error
|
| 370 |
+
if (sandbox) {
|
| 371 |
+
try {
|
| 372 |
+
await sandbox.stop();
|
| 373 |
+
} catch (e) {
|
| 374 |
+
console.error('Failed to stop sandbox on error:', e);
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
// Clear global state on error
|
| 379 |
+
global.activeSandbox = null;
|
| 380 |
+
global.sandboxData = null;
|
| 381 |
+
|
| 382 |
+
throw error; // Throw to be caught by the outer handler
|
| 383 |
+
}
|
| 384 |
+
}
|
app/api/create-zip/route.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
var activeSandbox: any;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export async function POST() {
|
| 8 |
+
try {
|
| 9 |
+
if (!global.activeSandbox) {
|
| 10 |
+
return NextResponse.json({
|
| 11 |
+
success: false,
|
| 12 |
+
error: 'No active sandbox'
|
| 13 |
+
}, { status: 400 });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
console.log('[create-zip] Creating project zip...');
|
| 17 |
+
|
| 18 |
+
// Create zip file in sandbox using standard commands
|
| 19 |
+
const zipResult = await global.activeSandbox.runCommand({
|
| 20 |
+
cmd: 'bash',
|
| 21 |
+
args: ['-c', `zip -r /tmp/project.zip . -x "node_modules/*" ".git/*" ".next/*" "dist/*" "build/*" "*.log"`]
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
if (zipResult.exitCode !== 0) {
|
| 25 |
+
const error = await zipResult.stderr();
|
| 26 |
+
throw new Error(`Failed to create zip: ${error}`);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const sizeResult = await global.activeSandbox.runCommand({
|
| 30 |
+
cmd: 'bash',
|
| 31 |
+
args: ['-c', `ls -la /tmp/project.zip | awk '{print $5}'`]
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
const fileSize = await sizeResult.stdout();
|
| 35 |
+
console.log(`[create-zip] Created project.zip (${fileSize.trim()} bytes)`);
|
| 36 |
+
|
| 37 |
+
// Read the zip file and convert to base64
|
| 38 |
+
const readResult = await global.activeSandbox.runCommand({
|
| 39 |
+
cmd: 'base64',
|
| 40 |
+
args: ['/tmp/project.zip']
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
if (readResult.exitCode !== 0) {
|
| 44 |
+
const error = await readResult.stderr();
|
| 45 |
+
throw new Error(`Failed to read zip file: ${error}`);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const base64Content = (await readResult.stdout()).trim();
|
| 49 |
+
|
| 50 |
+
// Create a data URL for download
|
| 51 |
+
const dataUrl = `data:application/zip;base64,${base64Content}`;
|
| 52 |
+
|
| 53 |
+
return NextResponse.json({
|
| 54 |
+
success: true,
|
| 55 |
+
dataUrl,
|
| 56 |
+
fileName: 'vercel-sandbox-project.zip',
|
| 57 |
+
message: 'Zip file created successfully'
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
} catch (error) {
|
| 61 |
+
console.error('[create-zip] Error:', error);
|
| 62 |
+
return NextResponse.json(
|
| 63 |
+
{
|
| 64 |
+
success: false,
|
| 65 |
+
error: (error as Error).message
|
| 66 |
+
},
|
| 67 |
+
{ status: 500 }
|
| 68 |
+
);
|
| 69 |
+
}
|
| 70 |
+
}
|
app/api/detect-and-install-packages/route.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
var activeSandbox: any;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export async function POST(request: NextRequest) {
|
| 8 |
+
try {
|
| 9 |
+
const { files } = await request.json();
|
| 10 |
+
|
| 11 |
+
if (!files || typeof files !== 'object') {
|
| 12 |
+
return NextResponse.json({
|
| 13 |
+
success: false,
|
| 14 |
+
error: 'Files object is required'
|
| 15 |
+
}, { status: 400 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
if (!global.activeSandbox) {
|
| 19 |
+
return NextResponse.json({
|
| 20 |
+
success: false,
|
| 21 |
+
error: 'No active sandbox'
|
| 22 |
+
}, { status: 404 });
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
console.log('[detect-and-install-packages] Processing files:', Object.keys(files));
|
| 26 |
+
|
| 27 |
+
// Extract all import statements from the files
|
| 28 |
+
const imports = new Set<string>();
|
| 29 |
+
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s*,?\s*)*(?:from\s+)?['"]([^'"]+)['"]/g;
|
| 30 |
+
const requireRegex = /require\s*\(['"]([^'"]+)['"]\)/g;
|
| 31 |
+
|
| 32 |
+
for (const [filePath, content] of Object.entries(files)) {
|
| 33 |
+
if (typeof content !== 'string') continue;
|
| 34 |
+
|
| 35 |
+
// Skip non-JS/JSX/TS/TSX files
|
| 36 |
+
if (!filePath.match(/\.(jsx?|tsx?)$/)) continue;
|
| 37 |
+
|
| 38 |
+
// Find ES6 imports
|
| 39 |
+
let match;
|
| 40 |
+
while ((match = importRegex.exec(content)) !== null) {
|
| 41 |
+
imports.add(match[1]);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Find CommonJS requires
|
| 45 |
+
while ((match = requireRegex.exec(content)) !== null) {
|
| 46 |
+
imports.add(match[1]);
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
console.log('[detect-and-install-packages] Found imports:', Array.from(imports));
|
| 51 |
+
|
| 52 |
+
// Log specific heroicons imports
|
| 53 |
+
const heroiconImports = Array.from(imports).filter(imp => imp.includes('heroicons'));
|
| 54 |
+
if (heroiconImports.length > 0) {
|
| 55 |
+
console.log('[detect-and-install-packages] Heroicon imports:', heroiconImports);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Filter out relative imports and built-in modules
|
| 59 |
+
const packages = Array.from(imports).filter(imp => {
|
| 60 |
+
// Skip relative imports
|
| 61 |
+
if (imp.startsWith('.') || imp.startsWith('/')) return false;
|
| 62 |
+
|
| 63 |
+
// Skip built-in Node modules
|
| 64 |
+
const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process'];
|
| 65 |
+
if (builtins.includes(imp)) return false;
|
| 66 |
+
|
| 67 |
+
return true;
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
// Extract just the package names (without subpaths)
|
| 71 |
+
const packageNames = packages.map(pkg => {
|
| 72 |
+
if (pkg.startsWith('@')) {
|
| 73 |
+
// Scoped package: @scope/package or @scope/package/subpath
|
| 74 |
+
const parts = pkg.split('/');
|
| 75 |
+
return parts.slice(0, 2).join('/');
|
| 76 |
+
} else {
|
| 77 |
+
// Regular package: package or package/subpath
|
| 78 |
+
return pkg.split('/')[0];
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
// Remove duplicates
|
| 83 |
+
const uniquePackages = [...new Set(packageNames)];
|
| 84 |
+
|
| 85 |
+
console.log('[detect-and-install-packages] Packages to install:', uniquePackages);
|
| 86 |
+
|
| 87 |
+
if (uniquePackages.length === 0) {
|
| 88 |
+
return NextResponse.json({
|
| 89 |
+
success: true,
|
| 90 |
+
packagesInstalled: [],
|
| 91 |
+
message: 'No new packages to install'
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Check which packages are already installed
|
| 96 |
+
const installed: string[] = [];
|
| 97 |
+
const missing: string[] = [];
|
| 98 |
+
|
| 99 |
+
for (const packageName of uniquePackages) {
|
| 100 |
+
try {
|
| 101 |
+
const checkResult = await global.activeSandbox.runCommand({
|
| 102 |
+
cmd: 'test',
|
| 103 |
+
args: ['-d', `node_modules/${packageName}`]
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
if (checkResult.exitCode === 0) {
|
| 107 |
+
installed.push(packageName);
|
| 108 |
+
} else {
|
| 109 |
+
missing.push(packageName);
|
| 110 |
+
}
|
| 111 |
+
} catch (checkError) {
|
| 112 |
+
// If test command fails, assume package is missing
|
| 113 |
+
console.debug(`Package check failed for ${packageName}:`, checkError);
|
| 114 |
+
missing.push(packageName);
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
console.log('[detect-and-install-packages] Package status:', { installed, missing });
|
| 119 |
+
|
| 120 |
+
if (missing.length === 0) {
|
| 121 |
+
return NextResponse.json({
|
| 122 |
+
success: true,
|
| 123 |
+
packagesInstalled: [],
|
| 124 |
+
packagesAlreadyInstalled: installed,
|
| 125 |
+
message: 'All packages already installed'
|
| 126 |
+
});
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Install missing packages
|
| 130 |
+
console.log('[detect-and-install-packages] Installing packages:', missing);
|
| 131 |
+
|
| 132 |
+
const installResult = await global.activeSandbox.runCommand({
|
| 133 |
+
cmd: 'npm',
|
| 134 |
+
args: ['install', '--save', ...missing]
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
const stdout = await installResult.stdout();
|
| 138 |
+
const stderr = await installResult.stderr();
|
| 139 |
+
|
| 140 |
+
console.log('[detect-and-install-packages] Install stdout:', stdout);
|
| 141 |
+
if (stderr) {
|
| 142 |
+
console.log('[detect-and-install-packages] Install stderr:', stderr);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Verify installation
|
| 146 |
+
const finalInstalled: string[] = [];
|
| 147 |
+
const failed: string[] = [];
|
| 148 |
+
|
| 149 |
+
for (const packageName of missing) {
|
| 150 |
+
try {
|
| 151 |
+
const verifyResult = await global.activeSandbox.runCommand({
|
| 152 |
+
cmd: 'test',
|
| 153 |
+
args: ['-d', `node_modules/${packageName}`]
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
if (verifyResult.exitCode === 0) {
|
| 157 |
+
finalInstalled.push(packageName);
|
| 158 |
+
console.log(`✓ Verified installation of ${packageName}`);
|
| 159 |
+
} else {
|
| 160 |
+
failed.push(packageName);
|
| 161 |
+
console.log(`✗ Failed to verify installation of ${packageName}`);
|
| 162 |
+
}
|
| 163 |
+
} catch (error) {
|
| 164 |
+
failed.push(packageName);
|
| 165 |
+
console.log(`✗ Error verifying ${packageName}:`, error);
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
if (failed.length > 0) {
|
| 170 |
+
console.error('[detect-and-install-packages] Failed to install:', failed);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
return NextResponse.json({
|
| 174 |
+
success: true,
|
| 175 |
+
packagesInstalled: finalInstalled,
|
| 176 |
+
packagesFailed: failed,
|
| 177 |
+
packagesAlreadyInstalled: installed,
|
| 178 |
+
message: `Installed ${finalInstalled.length} packages`,
|
| 179 |
+
logs: stdout
|
| 180 |
+
});
|
| 181 |
+
|
| 182 |
+
} catch (error) {
|
| 183 |
+
console.error('[detect-and-install-packages] Error:', error);
|
| 184 |
+
return NextResponse.json({
|
| 185 |
+
success: false,
|
| 186 |
+
error: (error as Error).message
|
| 187 |
+
}, { status: 500 });
|
| 188 |
+
}
|
| 189 |
+
}
|
app/api/extract-brand-styles/route.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
export async function POST(request: NextRequest) {
|
| 4 |
+
try {
|
| 5 |
+
const body = await request.json();
|
| 6 |
+
const url = body.url;
|
| 7 |
+
const prompt = body.prompt;
|
| 8 |
+
|
| 9 |
+
console.log('[extract-brand-styles] Extracting brand styles for:', url);
|
| 10 |
+
console.log('[extract-brand-styles] User prompt:', prompt);
|
| 11 |
+
|
| 12 |
+
// Call Firecrawl API to extract branding information
|
| 13 |
+
const FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY;
|
| 14 |
+
|
| 15 |
+
if (!FIRECRAWL_API_KEY) {
|
| 16 |
+
console.error('[extract-brand-styles] No Firecrawl API key found');
|
| 17 |
+
throw new Error('Firecrawl API key not configured');
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
console.log('[extract-brand-styles] Calling Firecrawl branding API for:', url);
|
| 21 |
+
|
| 22 |
+
const firecrawlResponse = await fetch('https://api.firecrawl.dev/v2/scrape', {
|
| 23 |
+
method: 'POST',
|
| 24 |
+
headers: {
|
| 25 |
+
'Authorization': `Bearer ${FIRECRAWL_API_KEY}`,
|
| 26 |
+
'Content-Type': 'application/json',
|
| 27 |
+
},
|
| 28 |
+
body: JSON.stringify({
|
| 29 |
+
url: url,
|
| 30 |
+
formats: ['branding'],
|
| 31 |
+
}),
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
if (!firecrawlResponse.ok) {
|
| 35 |
+
const errorText = await firecrawlResponse.text();
|
| 36 |
+
console.error('[extract-brand-styles] Firecrawl API error:', firecrawlResponse.status, errorText);
|
| 37 |
+
throw new Error(`Firecrawl API returned ${firecrawlResponse.status}`);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
const firecrawlData = await firecrawlResponse.json();
|
| 41 |
+
console.log('[extract-brand-styles] Firecrawl response received successfully');
|
| 42 |
+
|
| 43 |
+
// Extract branding data from response
|
| 44 |
+
const brandingData = firecrawlData.data?.branding || firecrawlData.branding;
|
| 45 |
+
|
| 46 |
+
if (!brandingData) {
|
| 47 |
+
console.error('[extract-brand-styles] No branding data in Firecrawl response');
|
| 48 |
+
console.log('[extract-brand-styles] Response structure:', JSON.stringify(firecrawlData, null, 2));
|
| 49 |
+
throw new Error('No branding data in Firecrawl response');
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
console.log('[extract-brand-styles] Successfully extracted branding data');
|
| 53 |
+
|
| 54 |
+
// Return the branding data
|
| 55 |
+
return NextResponse.json({
|
| 56 |
+
success: true,
|
| 57 |
+
url,
|
| 58 |
+
styleName: brandingData.name || url,
|
| 59 |
+
guidelines: brandingData,
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
} catch (error) {
|
| 63 |
+
console.error('[extract-brand-styles] Error occurred:', error);
|
| 64 |
+
return NextResponse.json(
|
| 65 |
+
{
|
| 66 |
+
success: false,
|
| 67 |
+
error: error instanceof Error ? error.message : 'Failed to extract brand styles'
|
| 68 |
+
},
|
| 69 |
+
{ status: 500 }
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
}
|
app/api/generate-ai-code-stream/route.ts
ADDED
|
@@ -0,0 +1,1896 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
import { createGroq } from '@ai-sdk/groq';
|
| 3 |
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
| 4 |
+
import { createOpenAI } from '@ai-sdk/openai';
|
| 5 |
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
| 6 |
+
import { streamText } from 'ai';
|
| 7 |
+
import type { SandboxState } from '@/types/sandbox';
|
| 8 |
+
import { selectFilesForEdit, getFileContents, formatFilesForAI } from '@/lib/context-selector';
|
| 9 |
+
import { executeSearchPlan, formatSearchResultsForAI, selectTargetFile } from '@/lib/file-search-executor';
|
| 10 |
+
import { FileManifest } from '@/types/file-manifest';
|
| 11 |
+
import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation';
|
| 12 |
+
import { appConfig } from '@/config/app.config';
|
| 13 |
+
|
| 14 |
+
// Force dynamic route to enable streaming
|
| 15 |
+
export const dynamic = 'force-dynamic';
|
| 16 |
+
|
| 17 |
+
// Check if we're using Vercel AI Gateway
|
| 18 |
+
const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY;
|
| 19 |
+
const aiGatewayBaseURL = 'https://ai-gateway.vercel.sh/v1';
|
| 20 |
+
|
| 21 |
+
console.log('[generate-ai-code-stream] AI Gateway config:', {
|
| 22 |
+
isUsingAIGateway,
|
| 23 |
+
hasGroqKey: !!process.env.GROQ_API_KEY,
|
| 24 |
+
hasAIGatewayKey: !!process.env.AI_GATEWAY_API_KEY
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
const groq = createGroq({
|
| 28 |
+
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GROQ_API_KEY,
|
| 29 |
+
baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined,
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
const anthropic = createAnthropic({
|
| 33 |
+
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.ANTHROPIC_API_KEY,
|
| 34 |
+
baseURL: isUsingAIGateway ? aiGatewayBaseURL : (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1'),
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
const googleGenerativeAI = createGoogleGenerativeAI({
|
| 38 |
+
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GEMINI_API_KEY,
|
| 39 |
+
baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined,
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
const openai = createOpenAI({
|
| 43 |
+
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.OPENAI_API_KEY,
|
| 44 |
+
baseURL: isUsingAIGateway ? aiGatewayBaseURL : process.env.OPENAI_BASE_URL,
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
// Helper function to analyze user preferences from conversation history
|
| 48 |
+
function analyzeUserPreferences(messages: ConversationMessage[]): {
|
| 49 |
+
commonPatterns: string[];
|
| 50 |
+
preferredEditStyle: 'targeted' | 'comprehensive';
|
| 51 |
+
} {
|
| 52 |
+
const userMessages = messages.filter(m => m.role === 'user');
|
| 53 |
+
const patterns: string[] = [];
|
| 54 |
+
|
| 55 |
+
// Count edit-related keywords
|
| 56 |
+
let targetedEditCount = 0;
|
| 57 |
+
let comprehensiveEditCount = 0;
|
| 58 |
+
|
| 59 |
+
userMessages.forEach(msg => {
|
| 60 |
+
const content = msg.content.toLowerCase();
|
| 61 |
+
|
| 62 |
+
// Check for targeted edit patterns
|
| 63 |
+
if (content.match(/\b(update|change|fix|modify|edit|remove|delete)\s+(\w+\s+)?(\w+)\b/)) {
|
| 64 |
+
targetedEditCount++;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Check for comprehensive edit patterns
|
| 68 |
+
if (content.match(/\b(rebuild|recreate|redesign|overhaul|refactor)\b/)) {
|
| 69 |
+
comprehensiveEditCount++;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Extract common request patterns
|
| 73 |
+
if (content.includes('hero')) patterns.push('hero section edits');
|
| 74 |
+
if (content.includes('header')) patterns.push('header modifications');
|
| 75 |
+
if (content.includes('color') || content.includes('style')) patterns.push('styling changes');
|
| 76 |
+
if (content.includes('button')) patterns.push('button updates');
|
| 77 |
+
if (content.includes('animation')) patterns.push('animation requests');
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
return {
|
| 81 |
+
commonPatterns: [...new Set(patterns)].slice(0, 3), // Top 3 unique patterns
|
| 82 |
+
preferredEditStyle: targetedEditCount > comprehensiveEditCount ? 'targeted' : 'comprehensive'
|
| 83 |
+
};
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
declare global {
|
| 87 |
+
var sandboxState: SandboxState;
|
| 88 |
+
var conversationState: ConversationState | null;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
export async function POST(request: NextRequest) {
|
| 92 |
+
try {
|
| 93 |
+
const { prompt, model = 'openai/gpt-oss-20b', context, isEdit = false } = await request.json();
|
| 94 |
+
|
| 95 |
+
console.log('[generate-ai-code-stream] Received request:');
|
| 96 |
+
console.log('[generate-ai-code-stream] - prompt:', prompt);
|
| 97 |
+
console.log('[generate-ai-code-stream] - isEdit:', isEdit);
|
| 98 |
+
console.log('[generate-ai-code-stream] - context.sandboxId:', context?.sandboxId);
|
| 99 |
+
console.log('[generate-ai-code-stream] - context.currentFiles:', context?.currentFiles ? Object.keys(context.currentFiles) : 'none');
|
| 100 |
+
console.log('[generate-ai-code-stream] - currentFiles count:', context?.currentFiles ? Object.keys(context.currentFiles).length : 0);
|
| 101 |
+
|
| 102 |
+
// Initialize conversation state if not exists
|
| 103 |
+
if (!global.conversationState) {
|
| 104 |
+
global.conversationState = {
|
| 105 |
+
conversationId: `conv-${Date.now()}`,
|
| 106 |
+
startedAt: Date.now(),
|
| 107 |
+
lastUpdated: Date.now(),
|
| 108 |
+
context: {
|
| 109 |
+
messages: [],
|
| 110 |
+
edits: [],
|
| 111 |
+
projectEvolution: { majorChanges: [] },
|
| 112 |
+
userPreferences: {}
|
| 113 |
+
}
|
| 114 |
+
};
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// Add user message to conversation history
|
| 118 |
+
const userMessage: ConversationMessage = {
|
| 119 |
+
id: `msg-${Date.now()}`,
|
| 120 |
+
role: 'user',
|
| 121 |
+
content: prompt,
|
| 122 |
+
timestamp: Date.now(),
|
| 123 |
+
metadata: {
|
| 124 |
+
sandboxId: context?.sandboxId
|
| 125 |
+
}
|
| 126 |
+
};
|
| 127 |
+
global.conversationState.context.messages.push(userMessage);
|
| 128 |
+
|
| 129 |
+
// Clean up old messages to prevent unbounded growth
|
| 130 |
+
if (global.conversationState.context.messages.length > 20) {
|
| 131 |
+
// Keep only the last 15 messages
|
| 132 |
+
global.conversationState.context.messages = global.conversationState.context.messages.slice(-15);
|
| 133 |
+
console.log('[generate-ai-code-stream] Trimmed conversation history to prevent context overflow');
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Clean up old edits
|
| 137 |
+
if (global.conversationState.context.edits.length > 10) {
|
| 138 |
+
global.conversationState.context.edits = global.conversationState.context.edits.slice(-8);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Debug: Show a sample of actual file content
|
| 142 |
+
if (context?.currentFiles && Object.keys(context.currentFiles).length > 0) {
|
| 143 |
+
const firstFile = Object.entries(context.currentFiles)[0];
|
| 144 |
+
console.log('[generate-ai-code-stream] - sample file:', firstFile[0]);
|
| 145 |
+
console.log('[generate-ai-code-stream] - sample content preview:',
|
| 146 |
+
typeof firstFile[1] === 'string' ? firstFile[1].substring(0, 100) + '...' : 'not a string');
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
if (!prompt) {
|
| 150 |
+
return NextResponse.json({
|
| 151 |
+
success: false,
|
| 152 |
+
error: 'Prompt is required'
|
| 153 |
+
}, { status: 400 });
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// Create a stream for real-time updates
|
| 157 |
+
const encoder = new TextEncoder();
|
| 158 |
+
const stream = new TransformStream();
|
| 159 |
+
const writer = stream.writable.getWriter();
|
| 160 |
+
|
| 161 |
+
// Function to send progress updates with flushing
|
| 162 |
+
const sendProgress = async (data: any) => {
|
| 163 |
+
const message = `data: ${JSON.stringify(data)}\n\n`;
|
| 164 |
+
try {
|
| 165 |
+
await writer.write(encoder.encode(message));
|
| 166 |
+
// Force flush by writing a keep-alive comment
|
| 167 |
+
if (data.type === 'stream' || data.type === 'conversation') {
|
| 168 |
+
await writer.write(encoder.encode(': keepalive\n\n'));
|
| 169 |
+
}
|
| 170 |
+
} catch (error) {
|
| 171 |
+
console.error('[generate-ai-code-stream] Error writing to stream:', error);
|
| 172 |
+
}
|
| 173 |
+
};
|
| 174 |
+
|
| 175 |
+
// Start processing in background
|
| 176 |
+
(async () => {
|
| 177 |
+
try {
|
| 178 |
+
// Send initial status
|
| 179 |
+
await sendProgress({ type: 'status', message: 'Initializing AI...' });
|
| 180 |
+
|
| 181 |
+
// No keep-alive needed - sandbox provisioned for 10 minutes
|
| 182 |
+
|
| 183 |
+
// Check if we have a file manifest for edit mode
|
| 184 |
+
let editContext = null;
|
| 185 |
+
let enhancedSystemPrompt = '';
|
| 186 |
+
|
| 187 |
+
if (isEdit) {
|
| 188 |
+
console.log('[generate-ai-code-stream] Edit mode detected - starting agentic search workflow');
|
| 189 |
+
console.log('[generate-ai-code-stream] Has fileCache:', !!global.sandboxState?.fileCache);
|
| 190 |
+
console.log('[generate-ai-code-stream] Has manifest:', !!global.sandboxState?.fileCache?.manifest);
|
| 191 |
+
|
| 192 |
+
const manifest: FileManifest | undefined = global.sandboxState?.fileCache?.manifest;
|
| 193 |
+
|
| 194 |
+
if (manifest) {
|
| 195 |
+
await sendProgress({ type: 'status', message: '🔍 Creating search plan...' });
|
| 196 |
+
|
| 197 |
+
const fileContents = global.sandboxState.fileCache?.files || {};
|
| 198 |
+
console.log('[generate-ai-code-stream] Files available for search:', Object.keys(fileContents).length);
|
| 199 |
+
|
| 200 |
+
// STEP 1: Get search plan from AI
|
| 201 |
+
try {
|
| 202 |
+
const intentResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/analyze-edit-intent`, {
|
| 203 |
+
method: 'POST',
|
| 204 |
+
headers: { 'Content-Type': 'application/json' },
|
| 205 |
+
body: JSON.stringify({ prompt, manifest, model })
|
| 206 |
+
});
|
| 207 |
+
|
| 208 |
+
if (intentResponse.ok) {
|
| 209 |
+
const { searchPlan } = await intentResponse.json();
|
| 210 |
+
console.log('[generate-ai-code-stream] Search plan received:', searchPlan);
|
| 211 |
+
|
| 212 |
+
await sendProgress({
|
| 213 |
+
type: 'status',
|
| 214 |
+
message: `🔎 Searching for: "${searchPlan.searchTerms.join('", "')}"`
|
| 215 |
+
});
|
| 216 |
+
|
| 217 |
+
// STEP 2: Execute the search plan
|
| 218 |
+
const searchExecution = executeSearchPlan(searchPlan,
|
| 219 |
+
Object.fromEntries(
|
| 220 |
+
Object.entries(fileContents).map(([path, data]) => [
|
| 221 |
+
path.startsWith('/') ? path : `/home/user/app/${path}`,
|
| 222 |
+
data.content
|
| 223 |
+
])
|
| 224 |
+
)
|
| 225 |
+
);
|
| 226 |
+
|
| 227 |
+
console.log('[generate-ai-code-stream] Search execution:', {
|
| 228 |
+
success: searchExecution.success,
|
| 229 |
+
resultsCount: searchExecution.results.length,
|
| 230 |
+
filesSearched: searchExecution.filesSearched,
|
| 231 |
+
time: searchExecution.executionTime + 'ms'
|
| 232 |
+
});
|
| 233 |
+
|
| 234 |
+
if (searchExecution.success && searchExecution.results.length > 0) {
|
| 235 |
+
// STEP 3: Select the best target file
|
| 236 |
+
const target = selectTargetFile(searchExecution.results, searchPlan.editType);
|
| 237 |
+
|
| 238 |
+
if (target) {
|
| 239 |
+
await sendProgress({
|
| 240 |
+
type: 'status',
|
| 241 |
+
message: `✅ Found code in ${target.filePath.split('/').pop()} at line ${target.lineNumber}`
|
| 242 |
+
});
|
| 243 |
+
|
| 244 |
+
console.log('[generate-ai-code-stream] Target selected:', target);
|
| 245 |
+
|
| 246 |
+
// Create surgical edit context with exact location
|
| 247 |
+
// normalizedPath would be: target.filePath.replace('/home/user/app/', '');
|
| 248 |
+
// fileContent available but not used in current implementation
|
| 249 |
+
// const fileContent = fileContents[normalizedPath]?.content || '';
|
| 250 |
+
|
| 251 |
+
// Build enhanced context with search results
|
| 252 |
+
enhancedSystemPrompt = `
|
| 253 |
+
${formatSearchResultsForAI(searchExecution.results)}
|
| 254 |
+
|
| 255 |
+
SURGICAL EDIT INSTRUCTIONS:
|
| 256 |
+
You have been given the EXACT location of the code to edit.
|
| 257 |
+
- File: ${target.filePath}
|
| 258 |
+
- Line: ${target.lineNumber}
|
| 259 |
+
- Reason: ${target.reason}
|
| 260 |
+
|
| 261 |
+
Make ONLY the change requested by the user. Do not modify any other code.
|
| 262 |
+
User request: "${prompt}"`;
|
| 263 |
+
|
| 264 |
+
// Set up edit context with just this one file
|
| 265 |
+
editContext = {
|
| 266 |
+
primaryFiles: [target.filePath],
|
| 267 |
+
contextFiles: [],
|
| 268 |
+
systemPrompt: enhancedSystemPrompt,
|
| 269 |
+
editIntent: {
|
| 270 |
+
type: searchPlan.editType,
|
| 271 |
+
description: searchPlan.reasoning,
|
| 272 |
+
targetFiles: [target.filePath],
|
| 273 |
+
confidence: 0.95, // High confidence since we found exact location
|
| 274 |
+
searchTerms: searchPlan.searchTerms
|
| 275 |
+
}
|
| 276 |
+
};
|
| 277 |
+
|
| 278 |
+
console.log('[generate-ai-code-stream] Surgical edit context created');
|
| 279 |
+
}
|
| 280 |
+
} else {
|
| 281 |
+
// Search failed - fall back to old behavior but inform user
|
| 282 |
+
console.warn('[generate-ai-code-stream] Search found no results, falling back to broader context');
|
| 283 |
+
await sendProgress({
|
| 284 |
+
type: 'status',
|
| 285 |
+
message: '⚠️ Could not find exact match, using broader search...'
|
| 286 |
+
});
|
| 287 |
+
}
|
| 288 |
+
} else {
|
| 289 |
+
console.error('[generate-ai-code-stream] Failed to get search plan');
|
| 290 |
+
}
|
| 291 |
+
} catch (error) {
|
| 292 |
+
console.error('[generate-ai-code-stream] Error in agentic search workflow:', error);
|
| 293 |
+
await sendProgress({
|
| 294 |
+
type: 'status',
|
| 295 |
+
message: '⚠️ Search workflow error, falling back to keyword method...'
|
| 296 |
+
});
|
| 297 |
+
// Fall back to old method on any error if we have a manifest
|
| 298 |
+
if (manifest) {
|
| 299 |
+
editContext = selectFilesForEdit(prompt, manifest);
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
} else {
|
| 303 |
+
// Fall back to old method if AI analysis fails
|
| 304 |
+
console.warn('[generate-ai-code-stream] AI intent analysis failed, falling back to keyword method');
|
| 305 |
+
if (manifest) {
|
| 306 |
+
editContext = selectFilesForEdit(prompt, manifest);
|
| 307 |
+
} else {
|
| 308 |
+
console.log('[generate-ai-code-stream] No manifest available for fallback');
|
| 309 |
+
await sendProgress({
|
| 310 |
+
type: 'status',
|
| 311 |
+
message: '⚠️ No file manifest available, will use broad context'
|
| 312 |
+
});
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// If we got an edit context from any method, use its system prompt
|
| 317 |
+
if (editContext) {
|
| 318 |
+
enhancedSystemPrompt = editContext.systemPrompt;
|
| 319 |
+
|
| 320 |
+
await sendProgress({
|
| 321 |
+
type: 'status',
|
| 322 |
+
message: `Identified edit type: ${editContext.editIntent?.description || 'Code modification'}`
|
| 323 |
+
});
|
| 324 |
+
} else if (!manifest) {
|
| 325 |
+
console.log('[generate-ai-code-stream] WARNING: No manifest available for edit mode!');
|
| 326 |
+
|
| 327 |
+
// Try to fetch files from sandbox if we have one
|
| 328 |
+
if (global.activeSandbox) {
|
| 329 |
+
await sendProgress({ type: 'status', message: 'Fetching current files from sandbox...' });
|
| 330 |
+
|
| 331 |
+
try {
|
| 332 |
+
// Fetch files directly from sandbox
|
| 333 |
+
const filesResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/get-sandbox-files`, {
|
| 334 |
+
method: 'GET',
|
| 335 |
+
headers: { 'Content-Type': 'application/json' }
|
| 336 |
+
});
|
| 337 |
+
|
| 338 |
+
if (filesResponse.ok) {
|
| 339 |
+
const filesData = await filesResponse.json();
|
| 340 |
+
|
| 341 |
+
if (filesData.success && filesData.manifest) {
|
| 342 |
+
console.log('[generate-ai-code-stream] Successfully fetched manifest from sandbox');
|
| 343 |
+
const manifest = filesData.manifest;
|
| 344 |
+
|
| 345 |
+
// Now try to analyze edit intent with the fetched manifest
|
| 346 |
+
try {
|
| 347 |
+
const intentResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/analyze-edit-intent`, {
|
| 348 |
+
method: 'POST',
|
| 349 |
+
headers: { 'Content-Type': 'application/json' },
|
| 350 |
+
body: JSON.stringify({ prompt, manifest, model })
|
| 351 |
+
});
|
| 352 |
+
|
| 353 |
+
if (intentResponse.ok) {
|
| 354 |
+
const { searchPlan } = await intentResponse.json();
|
| 355 |
+
console.log('[generate-ai-code-stream] Search plan received (after fetch):', searchPlan);
|
| 356 |
+
|
| 357 |
+
// For now, fall back to keyword search since we don't have file contents for search execution
|
| 358 |
+
// This path happens when no manifest was initially available
|
| 359 |
+
let targetFiles: any[] = [];
|
| 360 |
+
if (!searchPlan || searchPlan.searchTerms.length === 0) {
|
| 361 |
+
console.warn('[generate-ai-code-stream] No target files after fetch, searching for relevant files');
|
| 362 |
+
|
| 363 |
+
const promptLower = prompt.toLowerCase();
|
| 364 |
+
const allFilePaths = Object.keys(manifest.files);
|
| 365 |
+
|
| 366 |
+
// Look for component names mentioned in the prompt
|
| 367 |
+
if (promptLower.includes('hero')) {
|
| 368 |
+
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('hero'));
|
| 369 |
+
} else if (promptLower.includes('header')) {
|
| 370 |
+
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('header'));
|
| 371 |
+
} else if (promptLower.includes('footer')) {
|
| 372 |
+
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('footer'));
|
| 373 |
+
} else if (promptLower.includes('nav')) {
|
| 374 |
+
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('nav'));
|
| 375 |
+
} else if (promptLower.includes('button')) {
|
| 376 |
+
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('button'));
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
if (targetFiles.length > 0) {
|
| 380 |
+
console.log('[generate-ai-code-stream] Found target files by keyword search after fetch:', targetFiles);
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
const allFiles = Object.keys(manifest.files)
|
| 385 |
+
.filter(path => !targetFiles.includes(path));
|
| 386 |
+
|
| 387 |
+
editContext = {
|
| 388 |
+
primaryFiles: targetFiles,
|
| 389 |
+
contextFiles: allFiles,
|
| 390 |
+
systemPrompt: `
|
| 391 |
+
You are an expert senior software engineer performing a surgical, context-aware code modification. Your primary directive is **precision and preservation**.
|
| 392 |
+
|
| 393 |
+
Think of yourself as a surgeon making a precise incision, not a construction worker demolishing a wall.
|
| 394 |
+
|
| 395 |
+
## Search-Based Edit
|
| 396 |
+
Search Terms: ${searchPlan?.searchTerms?.join(', ') || 'keyword-based'}
|
| 397 |
+
Edit Type: ${searchPlan?.editType || 'UPDATE_COMPONENT'}
|
| 398 |
+
Reasoning: ${searchPlan?.reasoning || 'Modifying based on user request'}
|
| 399 |
+
|
| 400 |
+
Files to Edit: ${targetFiles.join(', ') || 'To be determined'}
|
| 401 |
+
User Request: "${prompt}"
|
| 402 |
+
|
| 403 |
+
## Your Mandatory Thought Process (Execute Internally):
|
| 404 |
+
Before writing ANY code, you MUST follow these steps:
|
| 405 |
+
|
| 406 |
+
1. **Understand Intent:**
|
| 407 |
+
- What is the user's core goal? (adding feature, fixing bug, changing style?)
|
| 408 |
+
- Does the conversation history provide extra clues?
|
| 409 |
+
|
| 410 |
+
2. **Locate the Code:**
|
| 411 |
+
- First examine the Primary Files provided
|
| 412 |
+
- Check the "ALL PROJECT FILES" list to find the EXACT file name
|
| 413 |
+
- "nav" might be Navigation.tsx, NavBar.tsx, Nav.tsx, or Header.tsx
|
| 414 |
+
- DO NOT create a new file if a similar one exists!
|
| 415 |
+
|
| 416 |
+
3. **Plan the Changes (Mental Diff):**
|
| 417 |
+
- What is the *minimal* set of changes required?
|
| 418 |
+
- Which exact lines need to be added, modified, or deleted?
|
| 419 |
+
- Will this require new packages?
|
| 420 |
+
|
| 421 |
+
4. **Verify Preservation:**
|
| 422 |
+
- What existing code, props, state, and logic must NOT be touched?
|
| 423 |
+
- How can I make my change without disrupting surrounding code?
|
| 424 |
+
|
| 425 |
+
5. **Construct the Final Code:**
|
| 426 |
+
- Only after completing steps above, generate the final code
|
| 427 |
+
- Provide the ENTIRE file content with modifications integrated
|
| 428 |
+
|
| 429 |
+
## Critical Rules & Constraints:
|
| 430 |
+
|
| 431 |
+
**PRESERVATION IS KEY:** You MUST NOT rewrite entire components or files. Integrate your changes into the existing code. Preserve all existing logic, props, state, and comments not directly related to the user's request.
|
| 432 |
+
|
| 433 |
+
**MINIMALISM:** Only output files you have actually changed. If a file doesn't need modification, don't include it.
|
| 434 |
+
|
| 435 |
+
**COMPLETENESS:** Each file must be COMPLETE from first line to last:
|
| 436 |
+
- NEVER TRUNCATE - Include EVERY line
|
| 437 |
+
- NO ellipsis (...) to skip content
|
| 438 |
+
- ALL imports, functions, JSX, and closing tags must be present
|
| 439 |
+
- The file MUST be runnable
|
| 440 |
+
|
| 441 |
+
**SURGICAL PRECISION:**
|
| 442 |
+
- Change ONLY what's explicitly requested
|
| 443 |
+
- If user says "change background to green", change ONLY the background class
|
| 444 |
+
- 99% of the original code should remain untouched
|
| 445 |
+
- NO refactoring, reformatting, or "improvements" unless requested
|
| 446 |
+
|
| 447 |
+
**NO CONVERSATION:** Your output must contain ONLY the code. No explanations or apologies.
|
| 448 |
+
|
| 449 |
+
## EXAMPLES:
|
| 450 |
+
|
| 451 |
+
### CORRECT APPROACH for "change hero background to blue":
|
| 452 |
+
<thinking>
|
| 453 |
+
I need to change the background color of the Hero component. Looking at the file, I see the main div has 'bg-gray-900'. I will change ONLY this to 'bg-blue-500' and leave everything else exactly as is.
|
| 454 |
+
</thinking>
|
| 455 |
+
|
| 456 |
+
Then return the EXACT same file with only 'bg-gray-900' changed to 'bg-blue-500'.
|
| 457 |
+
|
| 458 |
+
### WRONG APPROACH (DO NOT DO THIS):
|
| 459 |
+
- Rewriting the Hero component from scratch
|
| 460 |
+
- Changing the structure or reorganizing imports
|
| 461 |
+
- Adding or removing unrelated code
|
| 462 |
+
- Reformatting or "cleaning up" the code
|
| 463 |
+
|
| 464 |
+
Remember: You are a SURGEON making a precise incision, not an artist repainting the canvas!`,
|
| 465 |
+
editIntent: {
|
| 466 |
+
type: searchPlan?.editType || 'UPDATE_COMPONENT',
|
| 467 |
+
targetFiles: targetFiles,
|
| 468 |
+
confidence: searchPlan ? 0.85 : 0.6,
|
| 469 |
+
description: searchPlan?.reasoning || 'Keyword-based file selection',
|
| 470 |
+
suggestedContext: []
|
| 471 |
+
}
|
| 472 |
+
};
|
| 473 |
+
|
| 474 |
+
enhancedSystemPrompt = editContext.systemPrompt;
|
| 475 |
+
|
| 476 |
+
await sendProgress({
|
| 477 |
+
type: 'status',
|
| 478 |
+
message: `Identified edit type: ${editContext.editIntent.description}`
|
| 479 |
+
});
|
| 480 |
+
}
|
| 481 |
+
} catch (error) {
|
| 482 |
+
console.error('[generate-ai-code-stream] Error analyzing intent after fetch:', error);
|
| 483 |
+
}
|
| 484 |
+
} else {
|
| 485 |
+
console.error('[generate-ai-code-stream] Failed to get manifest from sandbox files');
|
| 486 |
+
}
|
| 487 |
+
} else {
|
| 488 |
+
console.error('[generate-ai-code-stream] Failed to fetch sandbox files:', filesResponse.status);
|
| 489 |
+
}
|
| 490 |
+
} catch (error) {
|
| 491 |
+
console.error('[generate-ai-code-stream] Error fetching sandbox files:', error);
|
| 492 |
+
await sendProgress({
|
| 493 |
+
type: 'warning',
|
| 494 |
+
message: 'Could not analyze existing files for targeted edits. Proceeding with general edit mode.'
|
| 495 |
+
});
|
| 496 |
+
}
|
| 497 |
+
} else {
|
| 498 |
+
console.log('[generate-ai-code-stream] No active sandbox to fetch files from');
|
| 499 |
+
await sendProgress({
|
| 500 |
+
type: 'warning',
|
| 501 |
+
message: 'No existing files found. Consider generating initial code first.'
|
| 502 |
+
});
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
// Build conversation context for system prompt
|
| 508 |
+
let conversationContext = '';
|
| 509 |
+
if (global.conversationState && global.conversationState.context.messages.length > 1) {
|
| 510 |
+
console.log('[generate-ai-code-stream] Building conversation context');
|
| 511 |
+
console.log('[generate-ai-code-stream] Total messages:', global.conversationState.context.messages.length);
|
| 512 |
+
console.log('[generate-ai-code-stream] Total edits:', global.conversationState.context.edits.length);
|
| 513 |
+
|
| 514 |
+
conversationContext = `\n\n## Conversation History (Recent)\n`;
|
| 515 |
+
|
| 516 |
+
// Include only the last 3 edits to save context
|
| 517 |
+
const recentEdits = global.conversationState.context.edits.slice(-3);
|
| 518 |
+
if (recentEdits.length > 0) {
|
| 519 |
+
console.log('[generate-ai-code-stream] Including', recentEdits.length, 'recent edits in context');
|
| 520 |
+
conversationContext += `\n### Recent Edits:\n`;
|
| 521 |
+
recentEdits.forEach(edit => {
|
| 522 |
+
conversationContext += `- "${edit.userRequest}" → ${edit.editType} (${edit.targetFiles.map(f => f.split('/').pop()).join(', ')})\n`;
|
| 523 |
+
});
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
// Include recently created files - CRITICAL for preventing duplicates
|
| 527 |
+
const recentMsgs = global.conversationState.context.messages.slice(-5);
|
| 528 |
+
const recentlyCreatedFiles: string[] = [];
|
| 529 |
+
recentMsgs.forEach(msg => {
|
| 530 |
+
if (msg.metadata?.editedFiles) {
|
| 531 |
+
recentlyCreatedFiles.push(...msg.metadata.editedFiles);
|
| 532 |
+
}
|
| 533 |
+
});
|
| 534 |
+
|
| 535 |
+
if (recentlyCreatedFiles.length > 0) {
|
| 536 |
+
const uniqueFiles = [...new Set(recentlyCreatedFiles)];
|
| 537 |
+
conversationContext += `\n### 🚨 RECENTLY CREATED/EDITED FILES (DO NOT RECREATE THESE):\n`;
|
| 538 |
+
uniqueFiles.forEach(file => {
|
| 539 |
+
conversationContext += `- ${file}\n`;
|
| 540 |
+
});
|
| 541 |
+
conversationContext += `\nIf the user mentions any of these components, UPDATE the existing file!\n`;
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
// Include only last 5 messages for context (reduced from 10)
|
| 545 |
+
const recentMessages = recentMsgs;
|
| 546 |
+
if (recentMessages.length > 2) { // More than just current message
|
| 547 |
+
conversationContext += `\n### Recent Messages:\n`;
|
| 548 |
+
recentMessages.slice(0, -1).forEach(msg => { // Exclude current message
|
| 549 |
+
if (msg.role === 'user') {
|
| 550 |
+
const truncatedContent = msg.content.length > 100 ? msg.content.substring(0, 100) + '...' : msg.content;
|
| 551 |
+
conversationContext += `- "${truncatedContent}"\n`;
|
| 552 |
+
}
|
| 553 |
+
});
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
// Include only last 2 major changes
|
| 557 |
+
const majorChanges = global.conversationState.context.projectEvolution.majorChanges.slice(-2);
|
| 558 |
+
if (majorChanges.length > 0) {
|
| 559 |
+
conversationContext += `\n### Recent Changes:\n`;
|
| 560 |
+
majorChanges.forEach(change => {
|
| 561 |
+
conversationContext += `- ${change.description}\n`;
|
| 562 |
+
});
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
// Keep user preferences - they're concise
|
| 566 |
+
const userPrefs = analyzeUserPreferences(global.conversationState.context.messages);
|
| 567 |
+
if (userPrefs.commonPatterns.length > 0) {
|
| 568 |
+
conversationContext += `\n### User Preferences:\n`;
|
| 569 |
+
conversationContext += `- Edit style: ${userPrefs.preferredEditStyle}\n`;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
// Limit total conversation context length
|
| 573 |
+
if (conversationContext.length > 2000) {
|
| 574 |
+
conversationContext = conversationContext.substring(0, 2000) + '\n[Context truncated to prevent length errors]';
|
| 575 |
+
}
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
// Build system prompt with conversation awareness
|
| 579 |
+
let systemPrompt = `You are an expert React developer with perfect memory of the conversation. You maintain context across messages and remember scraped websites, generated components, and applied code. Generate clean, modern React code for Vite applications.
|
| 580 |
+
${conversationContext}
|
| 581 |
+
|
| 582 |
+
🚨 CRITICAL RULES - YOUR MOST IMPORTANT INSTRUCTIONS:
|
| 583 |
+
1. **DO EXACTLY WHAT IS ASKED - NOTHING MORE, NOTHING LESS**
|
| 584 |
+
- Don't add features not requested
|
| 585 |
+
- Don't fix unrelated issues
|
| 586 |
+
- Don't improve things not mentioned
|
| 587 |
+
2. **CHECK App.jsx FIRST** - ALWAYS see what components exist before creating new ones
|
| 588 |
+
3. **NAVIGATION LIVES IN Header.jsx** - Don't create Nav.jsx if Header exists with nav
|
| 589 |
+
4. **USE STANDARD TAILWIND CLASSES ONLY**:
|
| 590 |
+
- ✅ CORRECT: bg-white, text-black, bg-blue-500, bg-gray-100, text-gray-900
|
| 591 |
+
- ❌ WRONG: bg-background, text-foreground, bg-primary, bg-muted, text-secondary
|
| 592 |
+
- Use ONLY classes from the official Tailwind CSS documentation
|
| 593 |
+
5. **FILE COUNT LIMITS**:
|
| 594 |
+
- Simple style/text change = 1 file ONLY
|
| 595 |
+
- New component = 2 files MAX (component + parent)
|
| 596 |
+
- If >3 files, YOU'RE DOING TOO MUCH
|
| 597 |
+
6. **DO NOT CREATE SVGs FROM SCRATCH**:
|
| 598 |
+
- NEVER generate custom SVG code unless explicitly asked
|
| 599 |
+
- Use existing icon libraries (lucide-react, heroicons, etc.)
|
| 600 |
+
- Or use placeholder elements/text if icons are not critical
|
| 601 |
+
- Only create custom SVGs when user specifically requests "create an SVG" or "draw an SVG"
|
| 602 |
+
|
| 603 |
+
COMPONENT RELATIONSHIPS (CHECK THESE FIRST):
|
| 604 |
+
- Navigation usually lives INSIDE Header.jsx, not separate Nav.jsx
|
| 605 |
+
- Logo is typically in Header, not standalone
|
| 606 |
+
- Footer often contains nav links already
|
| 607 |
+
- Menu/Hamburger is part of Header, not separate
|
| 608 |
+
|
| 609 |
+
PACKAGE USAGE RULES:
|
| 610 |
+
- DO NOT use react-router-dom unless user explicitly asks for routing
|
| 611 |
+
- For simple nav links in a single-page app, use scroll-to-section or href="#"
|
| 612 |
+
- Only add routing if building a multi-page application
|
| 613 |
+
- Common packages are auto-installed from your imports
|
| 614 |
+
|
| 615 |
+
WEBSITE CLONING REQUIREMENTS:
|
| 616 |
+
When recreating/cloning a website, you MUST include:
|
| 617 |
+
1. **Header with Navigation** - Usually Header.jsx containing nav
|
| 618 |
+
2. **Hero Section** - The main landing area (Hero.jsx)
|
| 619 |
+
3. **Main Content Sections** - Features, Services, About, etc.
|
| 620 |
+
4. **Footer** - Contact info, links, copyright (Footer.jsx)
|
| 621 |
+
5. **App.jsx** - Main app component that imports and uses all components
|
| 622 |
+
|
| 623 |
+
${isEdit ? `CRITICAL: THIS IS AN EDIT TO AN EXISTING APPLICATION
|
| 624 |
+
|
| 625 |
+
YOU MUST FOLLOW THESE EDIT RULES:
|
| 626 |
+
0. NEVER create tailwind.config.js, vite.config.js, package.json, or any other config files - they already exist!
|
| 627 |
+
1. DO NOT regenerate the entire application
|
| 628 |
+
2. DO NOT create files that already exist (like App.jsx, index.css, tailwind.config.js)
|
| 629 |
+
3. ONLY edit the EXACT files needed for the requested change - NO MORE, NO LESS
|
| 630 |
+
4. If the user says "update the header", ONLY edit the Header component - DO NOT touch Footer, Hero, or any other components
|
| 631 |
+
5. If the user says "change the color", ONLY edit the relevant style or component file - DO NOT "improve" other parts
|
| 632 |
+
6. If you're unsure which file to edit, choose the SINGLE most specific one related to the request
|
| 633 |
+
7. IMPORTANT: When adding new components or libraries:
|
| 634 |
+
- Create the new component file
|
| 635 |
+
- UPDATE ONLY the parent component that will use it
|
| 636 |
+
- Example: Adding a Newsletter component means:
|
| 637 |
+
* Create Newsletter.jsx
|
| 638 |
+
* Update ONLY the file that will use it (e.g., Footer.jsx OR App.jsx) - NOT both
|
| 639 |
+
8. When adding npm packages:
|
| 640 |
+
- Import them ONLY in the files where they're actually used
|
| 641 |
+
- The system will auto-install missing packages
|
| 642 |
+
|
| 643 |
+
CRITICAL FILE MODIFICATION RULES - VIOLATION = FAILURE:
|
| 644 |
+
- **NEVER TRUNCATE FILES** - Always return COMPLETE files with ALL content
|
| 645 |
+
- **NO ELLIPSIS (...)** - Include every single line of code, no skipping
|
| 646 |
+
- Files MUST be complete and runnable - include ALL imports, functions, JSX, and closing tags
|
| 647 |
+
- Count the files you're about to generate
|
| 648 |
+
- If the user asked to change ONE thing, you should generate ONE file (or at most two if adding a new component)
|
| 649 |
+
- DO NOT "fix" or "improve" files that weren't mentioned in the request
|
| 650 |
+
- DO NOT update multiple components when only one was requested
|
| 651 |
+
- DO NOT add features the user didn't ask for
|
| 652 |
+
- RESIST the urge to be "helpful" by updating related files
|
| 653 |
+
|
| 654 |
+
CRITICAL: DO NOT REDESIGN OR REIMAGINE COMPONENTS
|
| 655 |
+
- "update" means make a small change, NOT redesign the entire component
|
| 656 |
+
- "change X to Y" means ONLY change X to Y, nothing else
|
| 657 |
+
- "fix" means repair what's broken, NOT rewrite everything
|
| 658 |
+
- "remove X" means delete X from the existing file, NOT create a new file
|
| 659 |
+
- "delete X" means remove X from where it currently exists
|
| 660 |
+
- Preserve ALL existing functionality and design unless explicitly asked to change it
|
| 661 |
+
|
| 662 |
+
NEVER CREATE NEW FILES WHEN THE USER ASKS TO REMOVE/DELETE SOMETHING
|
| 663 |
+
If the user says "remove X", you must:
|
| 664 |
+
1. Find which existing file contains X
|
| 665 |
+
2. Edit that file to remove X
|
| 666 |
+
3. DO NOT create any new files
|
| 667 |
+
|
| 668 |
+
${editContext ? `
|
| 669 |
+
TARGETED EDIT MODE ACTIVE
|
| 670 |
+
- Edit Type: ${editContext.editIntent.type}
|
| 671 |
+
- Confidence: ${editContext.editIntent.confidence}
|
| 672 |
+
- Files to Edit: ${editContext.primaryFiles.join(', ')}
|
| 673 |
+
|
| 674 |
+
🚨 CRITICAL RULE - VIOLATION WILL RESULT IN FAILURE 🚨
|
| 675 |
+
YOU MUST ***ONLY*** GENERATE THE FILES LISTED ABOVE!
|
| 676 |
+
|
| 677 |
+
ABSOLUTE REQUIREMENTS:
|
| 678 |
+
1. COUNT the files in "Files to Edit" - that's EXACTLY how many files you must generate
|
| 679 |
+
2. If "Files to Edit" shows ONE file, generate ONLY that ONE file
|
| 680 |
+
3. DO NOT generate App.jsx unless it's EXPLICITLY listed in "Files to Edit"
|
| 681 |
+
4. DO NOT generate ANY components that aren't listed in "Files to Edit"
|
| 682 |
+
5. DO NOT "helpfully" update related files
|
| 683 |
+
6. DO NOT fix unrelated issues you notice
|
| 684 |
+
7. DO NOT improve code quality in files not being edited
|
| 685 |
+
8. DO NOT add bonus features
|
| 686 |
+
|
| 687 |
+
EXAMPLE VIOLATIONS (THESE ARE FAILURES):
|
| 688 |
+
❌ User says "update the hero" → You update Hero, Header, Footer, and App.jsx
|
| 689 |
+
❌ User says "change header color" → You redesign the entire header
|
| 690 |
+
❌ User says "fix the button" → You update multiple components
|
| 691 |
+
❌ Files to Edit shows "Hero.jsx" → You also generate App.jsx "to integrate it"
|
| 692 |
+
❌ Files to Edit shows "Header.jsx" → You also update Footer.jsx "for consistency"
|
| 693 |
+
|
| 694 |
+
CORRECT BEHAVIOR (THIS IS SUCCESS):
|
| 695 |
+
✅ User says "update the hero" → You ONLY edit Hero.jsx with the requested change
|
| 696 |
+
✅ User says "change header color" → You ONLY change the color in Header.jsx
|
| 697 |
+
✅ User says "fix the button" → You ONLY fix the specific button issue
|
| 698 |
+
✅ Files to Edit shows "Hero.jsx" → You generate ONLY Hero.jsx
|
| 699 |
+
✅ Files to Edit shows "Header.jsx, Nav.jsx" → You generate EXACTLY 2 files: Header.jsx and Nav.jsx
|
| 700 |
+
|
| 701 |
+
THE AI INTENT ANALYZER HAS ALREADY DETERMINED THE FILES.
|
| 702 |
+
DO NOT SECOND-GUESS IT.
|
| 703 |
+
DO NOT ADD MORE FILES.
|
| 704 |
+
ONLY OUTPUT THE EXACT FILES LISTED IN "Files to Edit".
|
| 705 |
+
` : ''}
|
| 706 |
+
|
| 707 |
+
VIOLATION OF THESE RULES WILL RESULT IN FAILURE!
|
| 708 |
+
` : ''}
|
| 709 |
+
|
| 710 |
+
CRITICAL INCREMENTAL UPDATE RULES:
|
| 711 |
+
- When the user asks for additions or modifications (like "add a videos page", "create a new component", "update the header"):
|
| 712 |
+
- DO NOT regenerate the entire application
|
| 713 |
+
- DO NOT recreate files that already exist unless explicitly asked
|
| 714 |
+
- ONLY create/modify the specific files needed for the requested change
|
| 715 |
+
- Preserve all existing functionality and files
|
| 716 |
+
- If adding a new page/route, integrate it with the existing routing system
|
| 717 |
+
- Reference existing components and styles rather than duplicating them
|
| 718 |
+
- NEVER recreate config files (tailwind.config.js, vite.config.js, package.json, etc.)
|
| 719 |
+
|
| 720 |
+
IMPORTANT: When the user asks for edits or modifications:
|
| 721 |
+
- You have access to the current file contents in the context
|
| 722 |
+
- Make targeted changes to existing files rather than regenerating everything
|
| 723 |
+
- Preserve the existing structure and only modify what's requested
|
| 724 |
+
- If you need to see a specific file that's not in context, mention it
|
| 725 |
+
|
| 726 |
+
IMPORTANT: You have access to the full conversation context including:
|
| 727 |
+
- Previously scraped websites and their content
|
| 728 |
+
- Components already generated and applied
|
| 729 |
+
- The current project being worked on
|
| 730 |
+
- Recent conversation history
|
| 731 |
+
- Any Vite errors that need to be resolved
|
| 732 |
+
|
| 733 |
+
When the user references "the app", "the website", or "the site" without specifics, refer to:
|
| 734 |
+
1. The most recently scraped website in the context
|
| 735 |
+
2. The current project name in the context
|
| 736 |
+
3. The files currently in the sandbox
|
| 737 |
+
|
| 738 |
+
If you see scraped websites in the context, you're working on a clone/recreation of that site.
|
| 739 |
+
|
| 740 |
+
CRITICAL UI/UX RULES:
|
| 741 |
+
- NEVER use emojis in any code, text, console logs, or UI elements
|
| 742 |
+
- ALWAYS ensure responsive design using proper Tailwind classes (sm:, md:, lg:, xl:)
|
| 743 |
+
- ALWAYS use proper mobile-first responsive design patterns
|
| 744 |
+
- NEVER hardcode pixel widths - use relative units and responsive classes
|
| 745 |
+
- ALWAYS test that the layout works on mobile devices (320px and up)
|
| 746 |
+
- ALWAYS make sections full-width by default - avoid max-w-7xl or similar constraints
|
| 747 |
+
- For full-width layouts: use className="w-full" or no width constraint at all
|
| 748 |
+
- Only add max-width constraints when explicitly needed for readability (like blog posts)
|
| 749 |
+
- Prefer system fonts and clean typography
|
| 750 |
+
- Ensure all interactive elements have proper hover/focus states
|
| 751 |
+
- Use proper semantic HTML elements for accessibility
|
| 752 |
+
|
| 753 |
+
CRITICAL STYLING RULES - MUST FOLLOW:
|
| 754 |
+
- NEVER use inline styles with style={{ }} in JSX
|
| 755 |
+
- NEVER use <style jsx> tags or any CSS-in-JS solutions
|
| 756 |
+
- NEVER create App.css, Component.css, or any component-specific CSS files
|
| 757 |
+
- NEVER import './App.css' or any CSS files except index.css
|
| 758 |
+
- ALWAYS use Tailwind CSS classes for ALL styling
|
| 759 |
+
- ONLY create src/index.css with the @tailwind directives
|
| 760 |
+
- The ONLY CSS file should be src/index.css with:
|
| 761 |
+
@tailwind base;
|
| 762 |
+
@tailwind components;
|
| 763 |
+
@tailwind utilities;
|
| 764 |
+
- Use Tailwind's full utility set: spacing, colors, typography, flexbox, grid, animations, etc.
|
| 765 |
+
- ALWAYS add smooth transitions and animations where appropriate:
|
| 766 |
+
- Use transition-all, transition-colors, transition-opacity for hover states
|
| 767 |
+
- Use animate-fade-in, animate-pulse, animate-bounce for engaging UI elements
|
| 768 |
+
- Add hover:scale-105 or hover:scale-110 for interactive elements
|
| 769 |
+
- Use transform and transition utilities for smooth interactions
|
| 770 |
+
- For complex layouts, combine Tailwind utilities rather than writing custom CSS
|
| 771 |
+
- NEVER use non-standard Tailwind classes like "border-border", "bg-background", "text-foreground", etc.
|
| 772 |
+
- Use standard Tailwind classes only:
|
| 773 |
+
- For borders: use "border-gray-200", "border-gray-300", etc. NOT "border-border"
|
| 774 |
+
- For backgrounds: use "bg-white", "bg-gray-100", etc. NOT "bg-background"
|
| 775 |
+
- For text: use "text-gray-900", "text-black", etc. NOT "text-foreground"
|
| 776 |
+
- Examples of good Tailwind usage:
|
| 777 |
+
- Buttons: className="px-4 py-2 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 hover:shadow-lg transform hover:scale-105 transition-all duration-200"
|
| 778 |
+
- Cards: className="bg-white rounded-lg shadow-md p-6 border border-gray-200 hover:shadow-xl transition-shadow duration-300"
|
| 779 |
+
- Full-width sections: className="w-full px-4 sm:px-6 lg:px-8"
|
| 780 |
+
- Constrained content (only when needed): className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
|
| 781 |
+
- Dark backgrounds: className="min-h-screen bg-gray-900 text-white"
|
| 782 |
+
- Hero sections: className="animate-fade-in-up"
|
| 783 |
+
- Feature cards: className="transform hover:scale-105 transition-transform duration-300"
|
| 784 |
+
- CTAs: className="animate-pulse hover:animate-none"
|
| 785 |
+
|
| 786 |
+
CRITICAL STRING AND SYNTAX RULES:
|
| 787 |
+
- ALWAYS escape apostrophes in strings: use \' instead of ' or use double quotes
|
| 788 |
+
- ALWAYS escape quotes properly in JSX attributes
|
| 789 |
+
- NEVER use curly quotes or smart quotes ('' "" '' "") - only straight quotes (' ")
|
| 790 |
+
- ALWAYS convert smart/curly quotes to straight quotes:
|
| 791 |
+
- ' and ' → '
|
| 792 |
+
- " and " → "
|
| 793 |
+
- Any other Unicode quotes → straight quotes
|
| 794 |
+
- When strings contain apostrophes, either:
|
| 795 |
+
1. Use double quotes: "you're" instead of 'you're'
|
| 796 |
+
2. Escape the apostrophe: 'you\'re'
|
| 797 |
+
- When working with scraped content, ALWAYS sanitize quotes first
|
| 798 |
+
- Replace all smart quotes with straight quotes before using in code
|
| 799 |
+
- Be extra careful with user-generated content or scraped text
|
| 800 |
+
- Always validate that JSX syntax is correct before generating
|
| 801 |
+
|
| 802 |
+
CRITICAL CODE SNIPPET DISPLAY RULES:
|
| 803 |
+
- When displaying code examples in JSX, NEVER put raw curly braces { } in text
|
| 804 |
+
- ALWAYS wrap code snippets in template literals with backticks
|
| 805 |
+
- For code examples in components, use one of these patterns:
|
| 806 |
+
1. Template literals: <div>{\`const example = { key: 'value' }\`}</div>
|
| 807 |
+
2. Pre/code blocks: <pre><code>{\`your code here\`}</code></pre>
|
| 808 |
+
3. Escape braces: <div>{'{'}key: value{'}'}</div>
|
| 809 |
+
- NEVER do this: <div>const example = { key: 'value' }</div> (causes parse errors)
|
| 810 |
+
- For multi-line code snippets, always use:
|
| 811 |
+
<pre className="bg-gray-900 text-gray-100 p-4 rounded">
|
| 812 |
+
<code>{\`
|
| 813 |
+
// Your code here
|
| 814 |
+
const example = {
|
| 815 |
+
key: 'value'
|
| 816 |
+
}
|
| 817 |
+
\`}</code>
|
| 818 |
+
</pre>
|
| 819 |
+
|
| 820 |
+
CRITICAL: When asked to create a React app or components:
|
| 821 |
+
- ALWAYS CREATE ALL FILES IN FULL - never provide partial implementations
|
| 822 |
+
- ALWAYS CREATE EVERY COMPONENT that you import - no placeholders
|
| 823 |
+
- ALWAYS IMPLEMENT COMPLETE FUNCTIONALITY - don't leave TODOs unless explicitly asked
|
| 824 |
+
- If you're recreating a website, implement ALL sections and features completely
|
| 825 |
+
- NEVER create tailwind.config.js - it's already configured in the template
|
| 826 |
+
- ALWAYS include a Navigation/Header component (Nav.jsx or Header.jsx) - websites need navigation!
|
| 827 |
+
|
| 828 |
+
REQUIRED COMPONENTS for website clones:
|
| 829 |
+
1. Nav.jsx or Header.jsx - Navigation bar with links (NEVER SKIP THIS!)
|
| 830 |
+
2. Hero.jsx - Main landing section
|
| 831 |
+
3. Features/Services/Products sections - Based on the site content
|
| 832 |
+
4. Footer.jsx - Footer with links and info
|
| 833 |
+
5. App.jsx - Main component that imports and arranges all components
|
| 834 |
+
- NEVER create vite.config.js - it's already configured in the template
|
| 835 |
+
- NEVER create package.json - it's already configured in the template
|
| 836 |
+
|
| 837 |
+
WHEN WORKING WITH SCRAPED CONTENT:
|
| 838 |
+
- ALWAYS sanitize all text content before using in code
|
| 839 |
+
- Convert ALL smart quotes to straight quotes
|
| 840 |
+
- Example transformations:
|
| 841 |
+
- "Firecrawl's API" → "Firecrawl's API" or "Firecrawl\\'s API"
|
| 842 |
+
- 'It's amazing' → "It's amazing" or 'It\\'s amazing'
|
| 843 |
+
- "Best tool ever" → "Best tool ever"
|
| 844 |
+
- When in doubt, use double quotes for strings containing apostrophes
|
| 845 |
+
- For testimonials or quotes from scraped content, ALWAYS clean the text:
|
| 846 |
+
- Bad: content: 'Moved our internal agent's web scraping...'
|
| 847 |
+
- Good: content: "Moved our internal agent's web scraping..."
|
| 848 |
+
- Also good: content: 'Moved our internal agent\\'s web scraping...'
|
| 849 |
+
|
| 850 |
+
When generating code, FOLLOW THIS PROCESS:
|
| 851 |
+
1. ALWAYS generate src/index.css FIRST - this establishes the styling foundation
|
| 852 |
+
2. List ALL components you plan to import in App.jsx
|
| 853 |
+
3. Count them - if there are 10 imports, you MUST create 10 component files
|
| 854 |
+
4. Generate src/index.css first (with proper CSS reset and base styles)
|
| 855 |
+
5. Generate App.jsx second
|
| 856 |
+
6. Then generate EVERY SINGLE component file you imported
|
| 857 |
+
7. Do NOT stop until all imports are satisfied
|
| 858 |
+
|
| 859 |
+
Use this XML format for React components only (DO NOT create tailwind.config.js - it already exists):
|
| 860 |
+
|
| 861 |
+
<file path="src/index.css">
|
| 862 |
+
@tailwind base;
|
| 863 |
+
@tailwind components;
|
| 864 |
+
@tailwind utilities;
|
| 865 |
+
</file>
|
| 866 |
+
|
| 867 |
+
<file path="src/App.jsx">
|
| 868 |
+
// Main App component that imports and uses other components
|
| 869 |
+
// Use Tailwind classes: className="min-h-screen bg-gray-50"
|
| 870 |
+
</file>
|
| 871 |
+
|
| 872 |
+
<file path="src/components/Example.jsx">
|
| 873 |
+
// Your React component code here
|
| 874 |
+
// Use Tailwind classes for ALL styling
|
| 875 |
+
</file>
|
| 876 |
+
|
| 877 |
+
CRITICAL COMPLETION RULES:
|
| 878 |
+
1. NEVER say "I'll continue with the remaining components"
|
| 879 |
+
2. NEVER say "Would you like me to proceed?"
|
| 880 |
+
3. NEVER use <continue> tags
|
| 881 |
+
4. Generate ALL components in ONE response
|
| 882 |
+
5. If App.jsx imports 10 components, generate ALL 10
|
| 883 |
+
6. Complete EVERYTHING before ending your response
|
| 884 |
+
|
| 885 |
+
With 16,000 tokens available, you have plenty of space to generate a complete application. Use it!
|
| 886 |
+
|
| 887 |
+
UNDERSTANDING USER INTENT FOR INCREMENTAL VS FULL GENERATION:
|
| 888 |
+
- "add/create/make a [specific feature]" → Add ONLY that feature to existing app
|
| 889 |
+
- "add a videos page" → Create ONLY Videos.jsx and update routing
|
| 890 |
+
- "update the header" → Modify ONLY header component
|
| 891 |
+
- "fix the styling" → Update ONLY the affected components
|
| 892 |
+
- "change X to Y" → Find the file containing X and modify it
|
| 893 |
+
- "make the header black" → Find Header component and change its color
|
| 894 |
+
- "rebuild/recreate/start over" → Full regeneration
|
| 895 |
+
- Default to incremental updates when working on an existing app
|
| 896 |
+
|
| 897 |
+
SURGICAL EDIT RULES (CRITICAL FOR PERFORMANCE):
|
| 898 |
+
- **PREFER TARGETED CHANGES**: Don't regenerate entire components for small edits
|
| 899 |
+
- For color/style changes: Edit ONLY the specific className or style prop
|
| 900 |
+
- For text changes: Change ONLY the text content, keep everything else
|
| 901 |
+
- For adding elements: INSERT into existing JSX, don't rewrite the whole return
|
| 902 |
+
- **PRESERVE EXISTING CODE**: Keep all imports, functions, and unrelated code exactly as-is
|
| 903 |
+
- Maximum files to edit:
|
| 904 |
+
- Style change = 1 file ONLY
|
| 905 |
+
- Text change = 1 file ONLY
|
| 906 |
+
- New feature = 2 files MAX (feature + parent)
|
| 907 |
+
- If you're editing >3 files for a simple request, STOP - you're doing too much
|
| 908 |
+
|
| 909 |
+
EXAMPLES OF CORRECT SURGICAL EDITS:
|
| 910 |
+
✅ "change header to black" → Find className="..." in Header.jsx, change ONLY color classes
|
| 911 |
+
✅ "update hero text" → Find the <h1> or <p> in Hero.jsx, change ONLY the text inside
|
| 912 |
+
✅ "add a button to hero" → Find the return statement, ADD button, keep everything else
|
| 913 |
+
❌ WRONG: Regenerating entire Header.jsx to change one color
|
| 914 |
+
❌ WRONG: Rewriting Hero.jsx to add one button
|
| 915 |
+
|
| 916 |
+
NAVIGATION/HEADER INTELLIGENCE:
|
| 917 |
+
- ALWAYS check App.jsx imports first
|
| 918 |
+
- Navigation is usually INSIDE Header.jsx, not separate
|
| 919 |
+
- If user says "nav", check Header.jsx FIRST
|
| 920 |
+
- Only create Nav.jsx if no navigation exists anywhere
|
| 921 |
+
- Logo, menu, hamburger = all typically in Header
|
| 922 |
+
|
| 923 |
+
CRITICAL: When files are provided in the context:
|
| 924 |
+
1. The user is asking you to MODIFY the existing app, not create a new one
|
| 925 |
+
2. Find the relevant file(s) from the provided context
|
| 926 |
+
3. Generate ONLY the files that need changes
|
| 927 |
+
4. Do NOT ask to see files - they are already provided in the context above
|
| 928 |
+
5. Make the requested change immediately`;
|
| 929 |
+
|
| 930 |
+
// If Morph Fast Apply is enabled (edit mode + MORPH_API_KEY), force <edit> block output
|
| 931 |
+
const morphFastApplyEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
|
| 932 |
+
if (morphFastApplyEnabled) {
|
| 933 |
+
systemPrompt += `
|
| 934 |
+
|
| 935 |
+
MORPH FAST APPLY MODE (EDIT-ONLY):
|
| 936 |
+
- Output edits as <edit> blocks, not full <file> blocks, for files that already exist.
|
| 937 |
+
- Format for each edit:
|
| 938 |
+
<edit target_file="src/components/Header.jsx">
|
| 939 |
+
<instructions>Describe the minimal change, single sentence.</instructions>
|
| 940 |
+
<update>Provide the SMALLEST code snippet necessary to perform the change.</update>
|
| 941 |
+
</edit>
|
| 942 |
+
- Only use <file> blocks when you must CREATE a brand-new file.
|
| 943 |
+
- Prefer ONE edit block for a simple change; multiple edits only if absolutely needed for separate files.
|
| 944 |
+
- Keep updates minimal and precise; do not rewrite entire files.
|
| 945 |
+
`;
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
// Build full prompt with context
|
| 949 |
+
let fullPrompt = prompt;
|
| 950 |
+
if (context) {
|
| 951 |
+
const contextParts = [];
|
| 952 |
+
|
| 953 |
+
if (context.sandboxId) {
|
| 954 |
+
contextParts.push(`Current sandbox ID: ${context.sandboxId}`);
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
if (context.structure) {
|
| 958 |
+
contextParts.push(`Current file structure:\n${context.structure}`);
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
// Use backend file cache instead of frontend-provided files
|
| 962 |
+
let backendFiles = global.sandboxState?.fileCache?.files || {};
|
| 963 |
+
let hasBackendFiles = Object.keys(backendFiles).length > 0;
|
| 964 |
+
|
| 965 |
+
console.log('[generate-ai-code-stream] Backend file cache status:');
|
| 966 |
+
console.log('[generate-ai-code-stream] - Has sandboxState:', !!global.sandboxState);
|
| 967 |
+
console.log('[generate-ai-code-stream] - Has fileCache:', !!global.sandboxState?.fileCache);
|
| 968 |
+
console.log('[generate-ai-code-stream] - File count:', Object.keys(backendFiles).length);
|
| 969 |
+
console.log('[generate-ai-code-stream] - Has manifest:', !!global.sandboxState?.fileCache?.manifest);
|
| 970 |
+
|
| 971 |
+
// If no backend files and we're in edit mode, try to fetch from sandbox
|
| 972 |
+
if (!hasBackendFiles && isEdit && (global.activeSandbox || context?.sandboxId)) {
|
| 973 |
+
console.log('[generate-ai-code-stream] No backend files, attempting to fetch from sandbox...');
|
| 974 |
+
|
| 975 |
+
try {
|
| 976 |
+
const filesResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/get-sandbox-files`, {
|
| 977 |
+
method: 'GET',
|
| 978 |
+
headers: { 'Content-Type': 'application/json' }
|
| 979 |
+
});
|
| 980 |
+
|
| 981 |
+
if (filesResponse.ok) {
|
| 982 |
+
const filesData = await filesResponse.json();
|
| 983 |
+
if (filesData.success && filesData.files) {
|
| 984 |
+
console.log('[generate-ai-code-stream] Successfully fetched', Object.keys(filesData.files).length, 'files from sandbox');
|
| 985 |
+
|
| 986 |
+
// Initialize sandboxState if needed
|
| 987 |
+
if (!global.sandboxState) {
|
| 988 |
+
global.sandboxState = {
|
| 989 |
+
fileCache: {
|
| 990 |
+
files: {},
|
| 991 |
+
lastSync: Date.now(),
|
| 992 |
+
sandboxId: context?.sandboxId || 'unknown'
|
| 993 |
+
}
|
| 994 |
+
} as any;
|
| 995 |
+
} else if (!global.sandboxState.fileCache) {
|
| 996 |
+
global.sandboxState.fileCache = {
|
| 997 |
+
files: {},
|
| 998 |
+
lastSync: Date.now(),
|
| 999 |
+
sandboxId: context?.sandboxId || 'unknown'
|
| 1000 |
+
};
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
// Store files in cache
|
| 1004 |
+
for (const [path, content] of Object.entries(filesData.files)) {
|
| 1005 |
+
const normalizedPath = path.replace('/home/user/app/', '');
|
| 1006 |
+
if (global.sandboxState.fileCache) {
|
| 1007 |
+
global.sandboxState.fileCache.files[normalizedPath] = {
|
| 1008 |
+
content: content as string,
|
| 1009 |
+
lastModified: Date.now()
|
| 1010 |
+
};
|
| 1011 |
+
}
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
if (filesData.manifest && global.sandboxState.fileCache) {
|
| 1015 |
+
global.sandboxState.fileCache.manifest = filesData.manifest;
|
| 1016 |
+
|
| 1017 |
+
// Now try to analyze edit intent with the fetched manifest
|
| 1018 |
+
if (!editContext) {
|
| 1019 |
+
console.log('[generate-ai-code-stream] Analyzing edit intent with fetched manifest');
|
| 1020 |
+
try {
|
| 1021 |
+
const intentResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/analyze-edit-intent`, {
|
| 1022 |
+
method: 'POST',
|
| 1023 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1024 |
+
body: JSON.stringify({ prompt, manifest: filesData.manifest, model })
|
| 1025 |
+
});
|
| 1026 |
+
|
| 1027 |
+
if (intentResponse.ok) {
|
| 1028 |
+
const { searchPlan } = await intentResponse.json();
|
| 1029 |
+
console.log('[generate-ai-code-stream] Search plan received:', searchPlan);
|
| 1030 |
+
|
| 1031 |
+
// Create edit context from AI analysis
|
| 1032 |
+
// Note: We can't execute search here without file contents, so fall back to keyword method
|
| 1033 |
+
const fileContext = selectFilesForEdit(prompt, filesData.manifest);
|
| 1034 |
+
editContext = fileContext;
|
| 1035 |
+
enhancedSystemPrompt = fileContext.systemPrompt;
|
| 1036 |
+
|
| 1037 |
+
console.log('[generate-ai-code-stream] Edit context created with', editContext.primaryFiles.length, 'primary files');
|
| 1038 |
+
}
|
| 1039 |
+
} catch (error) {
|
| 1040 |
+
console.error('[generate-ai-code-stream] Failed to analyze edit intent:', error);
|
| 1041 |
+
}
|
| 1042 |
+
}
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
// Update variables
|
| 1046 |
+
backendFiles = global.sandboxState.fileCache?.files || {};
|
| 1047 |
+
hasBackendFiles = Object.keys(backendFiles).length > 0;
|
| 1048 |
+
console.log('[generate-ai-code-stream] Updated backend cache with fetched files');
|
| 1049 |
+
}
|
| 1050 |
+
}
|
| 1051 |
+
} catch (error) {
|
| 1052 |
+
console.error('[generate-ai-code-stream] Failed to fetch sandbox files:', error);
|
| 1053 |
+
}
|
| 1054 |
+
}
|
| 1055 |
+
|
| 1056 |
+
// Include current file contents from backend cache
|
| 1057 |
+
if (hasBackendFiles) {
|
| 1058 |
+
// If we have edit context, use intelligent file selection
|
| 1059 |
+
if (editContext && editContext.primaryFiles.length > 0) {
|
| 1060 |
+
contextParts.push('\nEXISTING APPLICATION - TARGETED EDIT MODE');
|
| 1061 |
+
contextParts.push(`\n${editContext.systemPrompt || enhancedSystemPrompt}\n`);
|
| 1062 |
+
|
| 1063 |
+
// Get contents of primary and context files
|
| 1064 |
+
const primaryFileContents = await getFileContents(editContext.primaryFiles, global.sandboxState!.fileCache!.manifest!);
|
| 1065 |
+
const contextFileContents = await getFileContents(editContext.contextFiles, global.sandboxState!.fileCache!.manifest!);
|
| 1066 |
+
|
| 1067 |
+
// Format files for AI
|
| 1068 |
+
const formattedFiles = formatFilesForAI(primaryFileContents, contextFileContents);
|
| 1069 |
+
contextParts.push(formattedFiles);
|
| 1070 |
+
|
| 1071 |
+
contextParts.push('\nIMPORTANT: Only modify the files listed under "Files to Edit". The context files are provided for reference only.');
|
| 1072 |
+
} else {
|
| 1073 |
+
// Fallback to showing all files if no edit context
|
| 1074 |
+
console.log('[generate-ai-code-stream] WARNING: Using fallback mode - no edit context available');
|
| 1075 |
+
contextParts.push('\nEXISTING APPLICATION - TARGETED EDIT REQUIRED');
|
| 1076 |
+
contextParts.push('\nYou MUST analyze the user request and determine which specific file(s) to edit.');
|
| 1077 |
+
contextParts.push('\nCurrent project files (DO NOT regenerate all of these):');
|
| 1078 |
+
|
| 1079 |
+
const fileEntries = Object.entries(backendFiles);
|
| 1080 |
+
console.log(`[generate-ai-code-stream] Using backend cache: ${fileEntries.length} files`);
|
| 1081 |
+
|
| 1082 |
+
// Show file list first for reference
|
| 1083 |
+
contextParts.push('\n### File List:');
|
| 1084 |
+
for (const [path] of fileEntries) {
|
| 1085 |
+
contextParts.push(`- ${path}`);
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
// Include ALL files as context in fallback mode
|
| 1089 |
+
contextParts.push('\n### File Contents (ALL FILES FOR CONTEXT):');
|
| 1090 |
+
for (const [path, fileData] of fileEntries) {
|
| 1091 |
+
const content = fileData.content;
|
| 1092 |
+
if (typeof content === 'string') {
|
| 1093 |
+
contextParts.push(`\n<file path="${path}">\n${content}\n</file>`);
|
| 1094 |
+
}
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
contextParts.push('\n🚨 CRITICAL INSTRUCTIONS - VIOLATION = FAILURE 🚨');
|
| 1098 |
+
contextParts.push('1. Analyze the user request: "' + prompt + '"');
|
| 1099 |
+
contextParts.push('2. Identify the MINIMUM number of files that need editing (usually just ONE)');
|
| 1100 |
+
contextParts.push('3. PRESERVE ALL EXISTING CONTENT in those files');
|
| 1101 |
+
contextParts.push('4. ONLY ADD/MODIFY the specific part requested');
|
| 1102 |
+
contextParts.push('5. DO NOT regenerate entire components from scratch');
|
| 1103 |
+
contextParts.push('6. DO NOT change unrelated parts of any file');
|
| 1104 |
+
contextParts.push('7. Generate ONLY the files that MUST be changed - NO EXTRAS');
|
| 1105 |
+
contextParts.push('\n⚠️ FILE COUNT RULE:');
|
| 1106 |
+
contextParts.push('- Simple change (color, text, spacing) = 1 file ONLY');
|
| 1107 |
+
contextParts.push('- Adding new component = 2 files MAX (new component + parent that imports it)');
|
| 1108 |
+
contextParts.push('- DO NOT exceed these limits unless absolutely necessary');
|
| 1109 |
+
contextParts.push('\nEXAMPLES OF CORRECT BEHAVIOR:');
|
| 1110 |
+
contextParts.push('✅ "add a chart to the hero" → Edit ONLY Hero.jsx, ADD the chart, KEEP everything else');
|
| 1111 |
+
contextParts.push('✅ "change header to black" → Edit ONLY Header.jsx, change ONLY the color');
|
| 1112 |
+
contextParts.push('✅ "fix spacing in footer" → Edit ONLY Footer.jsx, adjust ONLY spacing');
|
| 1113 |
+
contextParts.push('\nEXAMPLES OF FAILURES:');
|
| 1114 |
+
contextParts.push('❌ "change header color" → You edit Header, Footer, and App "for consistency"');
|
| 1115 |
+
contextParts.push('❌ "add chart to hero" → You regenerate the entire Hero component');
|
| 1116 |
+
contextParts.push('❌ "fix button" → You update 5 different component files');
|
| 1117 |
+
contextParts.push('\n⚠️ FINAL WARNING:');
|
| 1118 |
+
contextParts.push('If you generate MORE files than necessary, you have FAILED');
|
| 1119 |
+
contextParts.push('If you DELETE or REWRITE existing functionality, you have FAILED');
|
| 1120 |
+
contextParts.push('ONLY change what was EXPLICITLY requested - NOTHING MORE');
|
| 1121 |
+
}
|
| 1122 |
+
} else if (context.currentFiles && Object.keys(context.currentFiles).length > 0) {
|
| 1123 |
+
// Fallback to frontend-provided files if backend cache is empty
|
| 1124 |
+
console.log('[generate-ai-code-stream] Warning: Backend cache empty, using frontend files');
|
| 1125 |
+
contextParts.push('\nEXISTING APPLICATION - DO NOT REGENERATE FROM SCRATCH');
|
| 1126 |
+
contextParts.push('Current project files (modify these, do not recreate):');
|
| 1127 |
+
|
| 1128 |
+
const fileEntries = Object.entries(context.currentFiles);
|
| 1129 |
+
for (const [path, content] of fileEntries) {
|
| 1130 |
+
if (typeof content === 'string') {
|
| 1131 |
+
contextParts.push(`\n<file path="${path}">\n${content}\n</file>`);
|
| 1132 |
+
}
|
| 1133 |
+
}
|
| 1134 |
+
contextParts.push('\nThe above files already exist. When the user asks to modify something (like "change the header color to black"), find the relevant file above and generate ONLY that file with the requested changes.');
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
// Add explicit edit mode indicator
|
| 1138 |
+
if (isEdit) {
|
| 1139 |
+
contextParts.push('\nEDIT MODE ACTIVE');
|
| 1140 |
+
contextParts.push('This is an incremental update to an existing application.');
|
| 1141 |
+
contextParts.push('DO NOT regenerate App.jsx, index.css, or other core files unless explicitly requested.');
|
| 1142 |
+
contextParts.push('ONLY create or modify the specific files needed for the user\'s request.');
|
| 1143 |
+
contextParts.push('\n⚠️ CRITICAL FILE OUTPUT FORMAT - VIOLATION = FAILURE:');
|
| 1144 |
+
contextParts.push('YOU MUST OUTPUT EVERY FILE IN THIS EXACT XML FORMAT:');
|
| 1145 |
+
contextParts.push('<file path="src/components/ComponentName.jsx">');
|
| 1146 |
+
contextParts.push('// Complete file content here');
|
| 1147 |
+
contextParts.push('</file>');
|
| 1148 |
+
contextParts.push('<file path="src/index.css">');
|
| 1149 |
+
contextParts.push('/* CSS content here */');
|
| 1150 |
+
contextParts.push('</file>');
|
| 1151 |
+
contextParts.push('\n❌ NEVER OUTPUT: "Generated Files: index.css, App.jsx"');
|
| 1152 |
+
contextParts.push('❌ NEVER LIST FILE NAMES WITHOUT CONTENT');
|
| 1153 |
+
contextParts.push('✅ ALWAYS: One <file> tag per file with COMPLETE content');
|
| 1154 |
+
contextParts.push('✅ ALWAYS: Include EVERY file you modified');
|
| 1155 |
+
} else if (!hasBackendFiles) {
|
| 1156 |
+
// First generation mode - make it beautiful!
|
| 1157 |
+
contextParts.push('\n🎨 FIRST GENERATION MODE - CREATE SOMETHING BEAUTIFUL!');
|
| 1158 |
+
contextParts.push('\nThis is the user\'s FIRST experience. Make it impressive:');
|
| 1159 |
+
contextParts.push('1. **USE TAILWIND PROPERLY** - Use standard Tailwind color classes');
|
| 1160 |
+
contextParts.push('2. **NO PLACEHOLDERS** - Use real content, not lorem ipsum');
|
| 1161 |
+
contextParts.push('3. **COMPLETE COMPONENTS** - Header, Hero, Features, Footer minimum');
|
| 1162 |
+
contextParts.push('4. **VISUAL POLISH** - Shadows, hover states, transitions');
|
| 1163 |
+
contextParts.push('5. **STANDARD CLASSES** - bg-white, text-gray-900, bg-blue-500, NOT bg-background');
|
| 1164 |
+
contextParts.push('\nCreate a polished, professional application that works perfectly on first load.');
|
| 1165 |
+
contextParts.push('\n⚠️ OUTPUT FORMAT:');
|
| 1166 |
+
contextParts.push('Use <file path="...">content</file> tags for EVERY file');
|
| 1167 |
+
contextParts.push('NEVER output "Generated Files:" as plain text');
|
| 1168 |
+
}
|
| 1169 |
+
|
| 1170 |
+
// Add conversation context (scraped websites, etc)
|
| 1171 |
+
if (context.conversationContext) {
|
| 1172 |
+
if (context.conversationContext.scrapedWebsites?.length > 0) {
|
| 1173 |
+
contextParts.push('\nScraped Websites in Context:');
|
| 1174 |
+
context.conversationContext.scrapedWebsites.forEach((site: any) => {
|
| 1175 |
+
contextParts.push(`\nURL: ${site.url}`);
|
| 1176 |
+
contextParts.push(`Scraped: ${new Date(site.timestamp).toLocaleString()}`);
|
| 1177 |
+
if (site.content) {
|
| 1178 |
+
// Include a summary of the scraped content
|
| 1179 |
+
const contentPreview = typeof site.content === 'string'
|
| 1180 |
+
? site.content.substring(0, 1000)
|
| 1181 |
+
: JSON.stringify(site.content).substring(0, 1000);
|
| 1182 |
+
contextParts.push(`Content Preview: ${contentPreview}...`);
|
| 1183 |
+
}
|
| 1184 |
+
});
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
if (context.conversationContext.currentProject) {
|
| 1188 |
+
contextParts.push(`\nCurrent Project: ${context.conversationContext.currentProject}`);
|
| 1189 |
+
}
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
if (contextParts.length > 0) {
|
| 1193 |
+
if (morphFastApplyEnabled) {
|
| 1194 |
+
contextParts.push('\nOUTPUT FORMAT (REQUIRED IN MORPH MODE):');
|
| 1195 |
+
contextParts.push('<edit target_file="src/components/Component.jsx">');
|
| 1196 |
+
contextParts.push('<instructions>Minimal, precise instruction.</instructions>');
|
| 1197 |
+
contextParts.push('<update>// Smallest necessary snippet</update>');
|
| 1198 |
+
contextParts.push('</edit>');
|
| 1199 |
+
contextParts.push('\nIf you need to create a NEW file, then and only then output a full file:');
|
| 1200 |
+
contextParts.push('<file path="src/components/NewComponent.jsx">');
|
| 1201 |
+
contextParts.push('// Full file content when creating new files');
|
| 1202 |
+
contextParts.push('</file>');
|
| 1203 |
+
}
|
| 1204 |
+
fullPrompt = `CONTEXT:\n${contextParts.join('\n')}\n\nUSER REQUEST:\n${prompt}`;
|
| 1205 |
+
}
|
| 1206 |
+
}
|
| 1207 |
+
|
| 1208 |
+
await sendProgress({ type: 'status', message: 'Planning application structure...' });
|
| 1209 |
+
|
| 1210 |
+
console.log('\n[generate-ai-code-stream] Starting streaming response...\n');
|
| 1211 |
+
|
| 1212 |
+
// Track packages that need to be installed
|
| 1213 |
+
const packagesToInstall: string[] = [];
|
| 1214 |
+
|
| 1215 |
+
// Determine which provider to use based on model
|
| 1216 |
+
const isAnthropic = model.startsWith('anthropic/');
|
| 1217 |
+
const isGoogle = model.startsWith('google/');
|
| 1218 |
+
const isOpenAI = model.startsWith('openai/');
|
| 1219 |
+
const isKimiGroq = model === 'moonshotai/kimi-k2-instruct-0905';
|
| 1220 |
+
const modelProvider = isAnthropic ? anthropic :
|
| 1221 |
+
(isOpenAI ? openai :
|
| 1222 |
+
(isGoogle ? googleGenerativeAI :
|
| 1223 |
+
(isKimiGroq ? groq : groq)));
|
| 1224 |
+
|
| 1225 |
+
// Fix model name transformation for different providers
|
| 1226 |
+
let actualModel: string;
|
| 1227 |
+
if (isAnthropic) {
|
| 1228 |
+
actualModel = model.replace('anthropic/', '');
|
| 1229 |
+
} else if (isOpenAI) {
|
| 1230 |
+
actualModel = model.replace('openai/', '');
|
| 1231 |
+
} else if (isKimiGroq) {
|
| 1232 |
+
// Kimi on Groq - use full model string
|
| 1233 |
+
actualModel = 'moonshotai/kimi-k2-instruct-0905';
|
| 1234 |
+
} else if (isGoogle) {
|
| 1235 |
+
// Google uses specific model names - convert our naming to theirs
|
| 1236 |
+
actualModel = model.replace('google/', '');
|
| 1237 |
+
} else {
|
| 1238 |
+
actualModel = model;
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
console.log(`[generate-ai-code-stream] Using provider: ${isAnthropic ? 'Anthropic' : isGoogle ? 'Google' : isOpenAI ? 'OpenAI' : 'Groq'}, model: ${actualModel}`);
|
| 1242 |
+
console.log(`[generate-ai-code-stream] AI Gateway enabled: ${isUsingAIGateway}`);
|
| 1243 |
+
console.log(`[generate-ai-code-stream] Model string: ${model}`);
|
| 1244 |
+
|
| 1245 |
+
// Make streaming API call with appropriate provider
|
| 1246 |
+
const streamOptions: any = {
|
| 1247 |
+
model: modelProvider(actualModel),
|
| 1248 |
+
messages: [
|
| 1249 |
+
{
|
| 1250 |
+
role: 'system',
|
| 1251 |
+
content: systemPrompt + `
|
| 1252 |
+
|
| 1253 |
+
🚨 CRITICAL CODE GENERATION RULES - VIOLATION = FAILURE 🚨:
|
| 1254 |
+
1. NEVER truncate ANY code - ALWAYS write COMPLETE files
|
| 1255 |
+
2. NEVER use "..." anywhere in your code - this causes syntax errors
|
| 1256 |
+
3. NEVER cut off strings mid-sentence - COMPLETE every string
|
| 1257 |
+
4. NEVER leave incomplete class names or attributes
|
| 1258 |
+
5. ALWAYS close ALL tags, quotes, brackets, and parentheses
|
| 1259 |
+
6. If you run out of space, prioritize completing the current file
|
| 1260 |
+
|
| 1261 |
+
CRITICAL STRING RULES TO PREVENT SYNTAX ERRORS:
|
| 1262 |
+
- NEVER write: className="px-8 py-4 bg-black text-white font-bold neobrut-border neobr...
|
| 1263 |
+
- ALWAYS write: className="px-8 py-4 bg-black text-white font-bold neobrut-border neobrut-shadow"
|
| 1264 |
+
- COMPLETE every className attribute
|
| 1265 |
+
- COMPLETE every string literal
|
| 1266 |
+
- NO ellipsis (...) ANYWHERE in code
|
| 1267 |
+
|
| 1268 |
+
PACKAGE RULES:
|
| 1269 |
+
- For INITIAL generation: Use ONLY React, no external packages
|
| 1270 |
+
- For EDITS: You may use packages, specify them with <package> tags
|
| 1271 |
+
- NEVER install packages like @mendable/firecrawl-js unless explicitly requested
|
| 1272 |
+
|
| 1273 |
+
Examples of SYNTAX ERRORS (NEVER DO THIS):
|
| 1274 |
+
❌ className="px-4 py-2 bg-blue-600 hover:bg-blue-7...
|
| 1275 |
+
❌ <button className="btn btn-primary btn-...
|
| 1276 |
+
❌ const title = "Welcome to our...
|
| 1277 |
+
❌ import { useState, useEffect, ... } from 'react'
|
| 1278 |
+
|
| 1279 |
+
Examples of CORRECT CODE (ALWAYS DO THIS):
|
| 1280 |
+
✅ className="px-4 py-2 bg-blue-600 hover:bg-blue-700"
|
| 1281 |
+
✅ <button className="btn btn-primary btn-large">
|
| 1282 |
+
✅ const title = "Welcome to our application"
|
| 1283 |
+
✅ import { useState, useEffect, useCallback } from 'react'
|
| 1284 |
+
|
| 1285 |
+
REMEMBER: It's better to generate fewer COMPLETE files than many INCOMPLETE files.`
|
| 1286 |
+
},
|
| 1287 |
+
{
|
| 1288 |
+
role: 'user',
|
| 1289 |
+
content: fullPrompt + `
|
| 1290 |
+
|
| 1291 |
+
CRITICAL: You MUST complete EVERY file you start. If you write:
|
| 1292 |
+
<file path="src/components/Hero.jsx">
|
| 1293 |
+
|
| 1294 |
+
You MUST include the closing </file> tag and ALL the code in between.
|
| 1295 |
+
|
| 1296 |
+
NEVER write partial code like:
|
| 1297 |
+
<h1>Build and deploy on the AI Cloud.</h1>
|
| 1298 |
+
<p>Some text...</p> ❌ WRONG
|
| 1299 |
+
|
| 1300 |
+
ALWAYS write complete code:
|
| 1301 |
+
<h1>Build and deploy on the AI Cloud.</h1>
|
| 1302 |
+
<p>Some text here with full content</p> ✅ CORRECT
|
| 1303 |
+
|
| 1304 |
+
If you're running out of space, generate FEWER files but make them COMPLETE.
|
| 1305 |
+
It's better to have 3 complete files than 10 incomplete files.`
|
| 1306 |
+
}
|
| 1307 |
+
],
|
| 1308 |
+
maxTokens: 8192, // Reduce to ensure completion
|
| 1309 |
+
stopSequences: [] // Don't stop early
|
| 1310 |
+
// Note: Neither Groq nor Anthropic models support tool/function calling in this context
|
| 1311 |
+
// We use XML tags for package detection instead
|
| 1312 |
+
};
|
| 1313 |
+
|
| 1314 |
+
// Add temperature for non-reasoning models
|
| 1315 |
+
if (!model.startsWith('openai/gpt-5')) {
|
| 1316 |
+
streamOptions.temperature = 0.7;
|
| 1317 |
+
}
|
| 1318 |
+
|
| 1319 |
+
// Add reasoning effort for GPT-5 models
|
| 1320 |
+
if (isOpenAI) {
|
| 1321 |
+
streamOptions.experimental_providerMetadata = {
|
| 1322 |
+
openai: {
|
| 1323 |
+
reasoningEffort: 'high'
|
| 1324 |
+
}
|
| 1325 |
+
};
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
let result;
|
| 1329 |
+
let retryCount = 0;
|
| 1330 |
+
const maxRetries = 2;
|
| 1331 |
+
|
| 1332 |
+
while (retryCount <= maxRetries) {
|
| 1333 |
+
try {
|
| 1334 |
+
result = await streamText(streamOptions);
|
| 1335 |
+
break; // Success, exit retry loop
|
| 1336 |
+
} catch (streamError: any) {
|
| 1337 |
+
console.error(`[generate-ai-code-stream] Error calling streamText (attempt ${retryCount + 1}/${maxRetries + 1}):`, streamError);
|
| 1338 |
+
|
| 1339 |
+
// Check if this is a Groq service unavailable error
|
| 1340 |
+
const isGroqServiceError = isKimiGroq && streamError.message?.includes('Service unavailable');
|
| 1341 |
+
const isRetryableError = streamError.message?.includes('Service unavailable') ||
|
| 1342 |
+
streamError.message?.includes('rate limit') ||
|
| 1343 |
+
streamError.message?.includes('timeout');
|
| 1344 |
+
|
| 1345 |
+
if (retryCount < maxRetries && isRetryableError) {
|
| 1346 |
+
retryCount++;
|
| 1347 |
+
console.log(`[generate-ai-code-stream] Retrying in ${retryCount * 2} seconds...`);
|
| 1348 |
+
|
| 1349 |
+
// Send progress update about retry
|
| 1350 |
+
await sendProgress({
|
| 1351 |
+
type: 'info',
|
| 1352 |
+
message: `Service temporarily unavailable, retrying (attempt ${retryCount + 1}/${maxRetries + 1})...`
|
| 1353 |
+
});
|
| 1354 |
+
|
| 1355 |
+
// Wait before retry with exponential backoff
|
| 1356 |
+
await new Promise(resolve => setTimeout(resolve, retryCount * 2000));
|
| 1357 |
+
|
| 1358 |
+
// If Groq fails, try switching to a fallback model
|
| 1359 |
+
if (isGroqServiceError && retryCount === maxRetries) {
|
| 1360 |
+
console.log('[generate-ai-code-stream] Groq service unavailable, falling back to GPT-4');
|
| 1361 |
+
streamOptions.model = openai('gpt-4-turbo');
|
| 1362 |
+
actualModel = 'gpt-4-turbo';
|
| 1363 |
+
}
|
| 1364 |
+
} else {
|
| 1365 |
+
// Final error, send to user
|
| 1366 |
+
await sendProgress({
|
| 1367 |
+
type: 'error',
|
| 1368 |
+
message: `Failed to initialize ${isGoogle ? 'Gemini' : isAnthropic ? 'Claude' : isOpenAI ? 'GPT-5' : isKimiGroq ? 'Kimi (Groq)' : 'Groq'} streaming: ${streamError.message}`
|
| 1369 |
+
});
|
| 1370 |
+
|
| 1371 |
+
// If this is a Google model error, provide helpful info
|
| 1372 |
+
if (isGoogle) {
|
| 1373 |
+
await sendProgress({
|
| 1374 |
+
type: 'info',
|
| 1375 |
+
message: 'Tip: Make sure your GEMINI_API_KEY is set correctly and has proper permissions.'
|
| 1376 |
+
});
|
| 1377 |
+
}
|
| 1378 |
+
|
| 1379 |
+
throw streamError;
|
| 1380 |
+
}
|
| 1381 |
+
}
|
| 1382 |
+
}
|
| 1383 |
+
|
| 1384 |
+
// Stream the response and parse in real-time
|
| 1385 |
+
let generatedCode = '';
|
| 1386 |
+
let currentFile = '';
|
| 1387 |
+
let currentFilePath = '';
|
| 1388 |
+
let componentCount = 0;
|
| 1389 |
+
let isInFile = false;
|
| 1390 |
+
let isInTag = false;
|
| 1391 |
+
let conversationalBuffer = '';
|
| 1392 |
+
|
| 1393 |
+
// Buffer for incomplete tags
|
| 1394 |
+
let tagBuffer = '';
|
| 1395 |
+
|
| 1396 |
+
// Stream the response and parse for packages in real-time
|
| 1397 |
+
for await (const textPart of result?.textStream || []) {
|
| 1398 |
+
const text = textPart || '';
|
| 1399 |
+
generatedCode += text;
|
| 1400 |
+
currentFile += text;
|
| 1401 |
+
|
| 1402 |
+
// Combine with buffer for tag detection
|
| 1403 |
+
const searchText = tagBuffer + text;
|
| 1404 |
+
|
| 1405 |
+
// Log streaming chunks to console
|
| 1406 |
+
process.stdout.write(text);
|
| 1407 |
+
|
| 1408 |
+
// Check if we're entering or leaving a tag
|
| 1409 |
+
const hasOpenTag = /<(file|package|packages|explanation|command|structure|template)\b/.test(text);
|
| 1410 |
+
const hasCloseTag = /<\/(file|package|packages|explanation|command|structure|template)>/.test(text);
|
| 1411 |
+
|
| 1412 |
+
if (hasOpenTag) {
|
| 1413 |
+
// Send any buffered conversational text before the tag
|
| 1414 |
+
if (conversationalBuffer.trim() && !isInTag) {
|
| 1415 |
+
await sendProgress({
|
| 1416 |
+
type: 'conversation',
|
| 1417 |
+
text: conversationalBuffer.trim()
|
| 1418 |
+
});
|
| 1419 |
+
conversationalBuffer = '';
|
| 1420 |
+
}
|
| 1421 |
+
isInTag = true;
|
| 1422 |
+
}
|
| 1423 |
+
|
| 1424 |
+
if (hasCloseTag) {
|
| 1425 |
+
isInTag = false;
|
| 1426 |
+
}
|
| 1427 |
+
|
| 1428 |
+
// If we're not in a tag, buffer as conversational text
|
| 1429 |
+
if (!isInTag && !hasOpenTag) {
|
| 1430 |
+
conversationalBuffer += text;
|
| 1431 |
+
}
|
| 1432 |
+
|
| 1433 |
+
// Stream the raw text for live preview
|
| 1434 |
+
await sendProgress({
|
| 1435 |
+
type: 'stream',
|
| 1436 |
+
text: text,
|
| 1437 |
+
raw: true
|
| 1438 |
+
});
|
| 1439 |
+
|
| 1440 |
+
// Debug: Log every 100 characters streamed
|
| 1441 |
+
if (generatedCode.length % 100 < text.length) {
|
| 1442 |
+
console.log(`[generate-ai-code-stream] Streamed ${generatedCode.length} chars`);
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
// Check for package tags in buffered text (ONLY for edits, not initial generation)
|
| 1446 |
+
let lastIndex = 0;
|
| 1447 |
+
if (isEdit) {
|
| 1448 |
+
const packageRegex = /<package>([^<]+)<\/package>/g;
|
| 1449 |
+
let packageMatch;
|
| 1450 |
+
|
| 1451 |
+
while ((packageMatch = packageRegex.exec(searchText)) !== null) {
|
| 1452 |
+
const packageName = packageMatch[1].trim();
|
| 1453 |
+
if (packageName && !packagesToInstall.includes(packageName)) {
|
| 1454 |
+
packagesToInstall.push(packageName);
|
| 1455 |
+
console.log(`[generate-ai-code-stream] Package detected: ${packageName}`);
|
| 1456 |
+
await sendProgress({
|
| 1457 |
+
type: 'package',
|
| 1458 |
+
name: packageName,
|
| 1459 |
+
message: `Package detected: ${packageName}`
|
| 1460 |
+
});
|
| 1461 |
+
}
|
| 1462 |
+
lastIndex = packageMatch.index + packageMatch[0].length;
|
| 1463 |
+
}
|
| 1464 |
+
}
|
| 1465 |
+
|
| 1466 |
+
// Keep unmatched portion in buffer for next iteration
|
| 1467 |
+
tagBuffer = searchText.substring(Math.max(0, lastIndex - 50)); // Keep last 50 chars
|
| 1468 |
+
|
| 1469 |
+
// Check for file boundaries
|
| 1470 |
+
if (text.includes('<file path="')) {
|
| 1471 |
+
const pathMatch = text.match(/<file path="([^"]+)"/);
|
| 1472 |
+
if (pathMatch) {
|
| 1473 |
+
currentFilePath = pathMatch[1];
|
| 1474 |
+
isInFile = true;
|
| 1475 |
+
currentFile = text;
|
| 1476 |
+
}
|
| 1477 |
+
}
|
| 1478 |
+
|
| 1479 |
+
// Check for file end
|
| 1480 |
+
if (isInFile && currentFile.includes('</file>')) {
|
| 1481 |
+
isInFile = false;
|
| 1482 |
+
|
| 1483 |
+
// Send component progress update
|
| 1484 |
+
if (currentFilePath.includes('components/')) {
|
| 1485 |
+
componentCount++;
|
| 1486 |
+
const componentName = currentFilePath.split('/').pop()?.replace('.jsx', '') || 'Component';
|
| 1487 |
+
await sendProgress({
|
| 1488 |
+
type: 'component',
|
| 1489 |
+
name: componentName,
|
| 1490 |
+
path: currentFilePath,
|
| 1491 |
+
index: componentCount
|
| 1492 |
+
});
|
| 1493 |
+
} else if (currentFilePath.includes('App.jsx')) {
|
| 1494 |
+
await sendProgress({
|
| 1495 |
+
type: 'app',
|
| 1496 |
+
message: 'Generated main App.jsx',
|
| 1497 |
+
path: currentFilePath
|
| 1498 |
+
});
|
| 1499 |
+
}
|
| 1500 |
+
|
| 1501 |
+
currentFile = '';
|
| 1502 |
+
currentFilePath = '';
|
| 1503 |
+
}
|
| 1504 |
+
}
|
| 1505 |
+
|
| 1506 |
+
console.log('\n\n[generate-ai-code-stream] Streaming complete.');
|
| 1507 |
+
|
| 1508 |
+
// Send any remaining conversational text
|
| 1509 |
+
if (conversationalBuffer.trim()) {
|
| 1510 |
+
await sendProgress({
|
| 1511 |
+
type: 'conversation',
|
| 1512 |
+
text: conversationalBuffer.trim()
|
| 1513 |
+
});
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
// Also parse <packages> tag for multiple packages - ONLY for edits
|
| 1517 |
+
if (isEdit) {
|
| 1518 |
+
const packagesRegex = /<packages>([\s\S]*?)<\/packages>/g;
|
| 1519 |
+
let packagesMatch;
|
| 1520 |
+
while ((packagesMatch = packagesRegex.exec(generatedCode)) !== null) {
|
| 1521 |
+
const packagesContent = packagesMatch[1].trim();
|
| 1522 |
+
const packagesList = packagesContent.split(/[\n,]+/)
|
| 1523 |
+
.map(pkg => pkg.trim())
|
| 1524 |
+
.filter(pkg => pkg.length > 0);
|
| 1525 |
+
|
| 1526 |
+
for (const packageName of packagesList) {
|
| 1527 |
+
if (!packagesToInstall.includes(packageName)) {
|
| 1528 |
+
packagesToInstall.push(packageName);
|
| 1529 |
+
console.log(`[generate-ai-code-stream] Package from <packages> tag: ${packageName}`);
|
| 1530 |
+
await sendProgress({
|
| 1531 |
+
type: 'package',
|
| 1532 |
+
name: packageName,
|
| 1533 |
+
message: `Package detected: ${packageName}`
|
| 1534 |
+
});
|
| 1535 |
+
}
|
| 1536 |
+
}
|
| 1537 |
+
}
|
| 1538 |
+
}
|
| 1539 |
+
|
| 1540 |
+
// Function to extract packages from import statements
|
| 1541 |
+
function extractPackagesFromCode(content: string): string[] {
|
| 1542 |
+
const packages: string[] = [];
|
| 1543 |
+
// Match ES6 imports
|
| 1544 |
+
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
|
| 1545 |
+
let importMatch;
|
| 1546 |
+
|
| 1547 |
+
while ((importMatch = importRegex.exec(content)) !== null) {
|
| 1548 |
+
const importPath = importMatch[1];
|
| 1549 |
+
// Skip relative imports and built-in React
|
| 1550 |
+
if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
|
| 1551 |
+
importPath !== 'react' && importPath !== 'react-dom' &&
|
| 1552 |
+
!importPath.startsWith('@/')) {
|
| 1553 |
+
// Extract package name (handle scoped packages like @heroicons/react)
|
| 1554 |
+
const packageName = importPath.startsWith('@')
|
| 1555 |
+
? importPath.split('/').slice(0, 2).join('/')
|
| 1556 |
+
: importPath.split('/')[0];
|
| 1557 |
+
|
| 1558 |
+
if (!packages.includes(packageName)) {
|
| 1559 |
+
packages.push(packageName);
|
| 1560 |
+
}
|
| 1561 |
+
}
|
| 1562 |
+
}
|
| 1563 |
+
|
| 1564 |
+
return packages;
|
| 1565 |
+
}
|
| 1566 |
+
|
| 1567 |
+
// Parse files and send progress for each
|
| 1568 |
+
const fileRegex = /<file path="([^"]+)">([\s\S]*?)<\/file>/g;
|
| 1569 |
+
const files = [];
|
| 1570 |
+
let match;
|
| 1571 |
+
|
| 1572 |
+
while ((match = fileRegex.exec(generatedCode)) !== null) {
|
| 1573 |
+
const filePath = match[1];
|
| 1574 |
+
const content = match[2].trim();
|
| 1575 |
+
files.push({ path: filePath, content });
|
| 1576 |
+
|
| 1577 |
+
// Extract packages from file content - ONLY for edits
|
| 1578 |
+
if (isEdit) {
|
| 1579 |
+
const filePackages = extractPackagesFromCode(content);
|
| 1580 |
+
for (const pkg of filePackages) {
|
| 1581 |
+
if (!packagesToInstall.includes(pkg)) {
|
| 1582 |
+
packagesToInstall.push(pkg);
|
| 1583 |
+
console.log(`[generate-ai-code-stream] Package detected from imports: ${pkg}`);
|
| 1584 |
+
await sendProgress({
|
| 1585 |
+
type: 'package',
|
| 1586 |
+
name: pkg,
|
| 1587 |
+
message: `Package detected from imports: ${pkg}`
|
| 1588 |
+
});
|
| 1589 |
+
}
|
| 1590 |
+
}
|
| 1591 |
+
}
|
| 1592 |
+
|
| 1593 |
+
// Send progress for each file (reusing componentCount from streaming)
|
| 1594 |
+
if (filePath.includes('components/')) {
|
| 1595 |
+
const componentName = filePath.split('/').pop()?.replace('.jsx', '') || 'Component';
|
| 1596 |
+
await sendProgress({
|
| 1597 |
+
type: 'component',
|
| 1598 |
+
name: componentName,
|
| 1599 |
+
path: filePath,
|
| 1600 |
+
index: componentCount
|
| 1601 |
+
});
|
| 1602 |
+
} else if (filePath.includes('App.jsx')) {
|
| 1603 |
+
await sendProgress({
|
| 1604 |
+
type: 'app',
|
| 1605 |
+
message: 'Generated main App.jsx',
|
| 1606 |
+
path: filePath
|
| 1607 |
+
});
|
| 1608 |
+
}
|
| 1609 |
+
}
|
| 1610 |
+
|
| 1611 |
+
// Extract explanation
|
| 1612 |
+
const explanationMatch = generatedCode.match(/<explanation>([\s\S]*?)<\/explanation>/);
|
| 1613 |
+
const explanation = explanationMatch ? explanationMatch[1].trim() : 'Code generated successfully!';
|
| 1614 |
+
|
| 1615 |
+
// Validate generated code for truncation issues
|
| 1616 |
+
const truncationWarnings: string[] = [];
|
| 1617 |
+
|
| 1618 |
+
// Skip ellipsis checking entirely - too many false positives with spread operators, loading text, etc.
|
| 1619 |
+
|
| 1620 |
+
// Check for unclosed file tags
|
| 1621 |
+
const fileOpenCount = (generatedCode.match(/<file path="/g) || []).length;
|
| 1622 |
+
const fileCloseCount = (generatedCode.match(/<\/file>/g) || []).length;
|
| 1623 |
+
if (fileOpenCount !== fileCloseCount) {
|
| 1624 |
+
truncationWarnings.push(`Unclosed file tags detected: ${fileOpenCount} open, ${fileCloseCount} closed`);
|
| 1625 |
+
}
|
| 1626 |
+
|
| 1627 |
+
// Check for files that seem truncated (very short or ending abruptly)
|
| 1628 |
+
const truncationCheckRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
|
| 1629 |
+
let truncationMatch;
|
| 1630 |
+
while ((truncationMatch = truncationCheckRegex.exec(generatedCode)) !== null) {
|
| 1631 |
+
const filePath = truncationMatch[1];
|
| 1632 |
+
const content = truncationMatch[2];
|
| 1633 |
+
|
| 1634 |
+
// Only check for really obvious HTML truncation - file ends with opening tag
|
| 1635 |
+
if (content.trim().endsWith('<') || content.trim().endsWith('</')) {
|
| 1636 |
+
truncationWarnings.push(`File ${filePath} appears to have incomplete HTML tags`);
|
| 1637 |
+
}
|
| 1638 |
+
|
| 1639 |
+
// Skip "..." check - too many false positives with loading text, etc.
|
| 1640 |
+
|
| 1641 |
+
// Only check for SEVERE truncation issues
|
| 1642 |
+
if (filePath.match(/\.(jsx?|tsx?)$/)) {
|
| 1643 |
+
// Only check for severely unmatched brackets (more than 3 difference)
|
| 1644 |
+
const openBraces = (content.match(/{/g) || []).length;
|
| 1645 |
+
const closeBraces = (content.match(/}/g) || []).length;
|
| 1646 |
+
const braceDiff = Math.abs(openBraces - closeBraces);
|
| 1647 |
+
if (braceDiff > 3) { // Only flag severe mismatches
|
| 1648 |
+
truncationWarnings.push(`File ${filePath} has severely unmatched braces (${openBraces} open, ${closeBraces} closed)`);
|
| 1649 |
+
}
|
| 1650 |
+
|
| 1651 |
+
// Check if file is extremely short and looks incomplete
|
| 1652 |
+
if (content.length < 20 && content.includes('function') && !content.includes('}')) {
|
| 1653 |
+
truncationWarnings.push(`File ${filePath} appears severely truncated`);
|
| 1654 |
+
}
|
| 1655 |
+
}
|
| 1656 |
+
}
|
| 1657 |
+
|
| 1658 |
+
// Handle truncation with automatic retry (if enabled in config)
|
| 1659 |
+
if (truncationWarnings.length > 0 && appConfig.codeApplication.enableTruncationRecovery) {
|
| 1660 |
+
console.warn('[generate-ai-code-stream] Truncation detected, attempting to fix:', truncationWarnings);
|
| 1661 |
+
|
| 1662 |
+
await sendProgress({
|
| 1663 |
+
type: 'warning',
|
| 1664 |
+
message: 'Detected incomplete code generation. Attempting to complete...',
|
| 1665 |
+
warnings: truncationWarnings
|
| 1666 |
+
});
|
| 1667 |
+
|
| 1668 |
+
// Try to fix truncated files automatically
|
| 1669 |
+
const truncatedFiles: string[] = [];
|
| 1670 |
+
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
|
| 1671 |
+
let match;
|
| 1672 |
+
|
| 1673 |
+
while ((match = fileRegex.exec(generatedCode)) !== null) {
|
| 1674 |
+
const filePath = match[1];
|
| 1675 |
+
const content = match[2];
|
| 1676 |
+
|
| 1677 |
+
// Check if this file appears truncated - be more selective
|
| 1678 |
+
const hasEllipsis = content.includes('...') &&
|
| 1679 |
+
!content.includes('...rest') &&
|
| 1680 |
+
!content.includes('...props') &&
|
| 1681 |
+
!content.includes('spread');
|
| 1682 |
+
|
| 1683 |
+
const endsAbruptly = content.trim().endsWith('...') ||
|
| 1684 |
+
content.trim().endsWith(',') ||
|
| 1685 |
+
content.trim().endsWith('(');
|
| 1686 |
+
|
| 1687 |
+
const hasUnclosedTags = content.includes('</') &&
|
| 1688 |
+
!content.match(/<\/[a-zA-Z0-9]+>/) &&
|
| 1689 |
+
content.includes('<');
|
| 1690 |
+
|
| 1691 |
+
const tooShort = content.length < 50 && filePath.match(/\.(jsx?|tsx?)$/);
|
| 1692 |
+
|
| 1693 |
+
// Check for unmatched braces specifically
|
| 1694 |
+
const openBraceCount = (content.match(/{/g) || []).length;
|
| 1695 |
+
const closeBraceCount = (content.match(/}/g) || []).length;
|
| 1696 |
+
const hasUnmatchedBraces = Math.abs(openBraceCount - closeBraceCount) > 1;
|
| 1697 |
+
|
| 1698 |
+
const isTruncated = (hasEllipsis && endsAbruptly) ||
|
| 1699 |
+
hasUnclosedTags ||
|
| 1700 |
+
(tooShort && !content.includes('export')) ||
|
| 1701 |
+
hasUnmatchedBraces;
|
| 1702 |
+
|
| 1703 |
+
if (isTruncated) {
|
| 1704 |
+
truncatedFiles.push(filePath);
|
| 1705 |
+
}
|
| 1706 |
+
}
|
| 1707 |
+
|
| 1708 |
+
// If we have truncated files, try to regenerate them
|
| 1709 |
+
if (truncatedFiles.length > 0) {
|
| 1710 |
+
console.log('[generate-ai-code-stream] Attempting to regenerate truncated files:', truncatedFiles);
|
| 1711 |
+
|
| 1712 |
+
for (const filePath of truncatedFiles) {
|
| 1713 |
+
await sendProgress({
|
| 1714 |
+
type: 'info',
|
| 1715 |
+
message: `Completing ${filePath}...`
|
| 1716 |
+
});
|
| 1717 |
+
|
| 1718 |
+
try {
|
| 1719 |
+
// Create a focused prompt to complete just this file
|
| 1720 |
+
const completionPrompt = `Complete the following file that was truncated. Provide the FULL file content.
|
| 1721 |
+
|
| 1722 |
+
File: ${filePath}
|
| 1723 |
+
Original request: ${prompt}
|
| 1724 |
+
|
| 1725 |
+
Provide the complete file content without any truncation. Include all necessary imports, complete all functions, and close all tags properly.`;
|
| 1726 |
+
|
| 1727 |
+
// Make a focused API call to complete this specific file
|
| 1728 |
+
// Create a new client for the completion based on the provider
|
| 1729 |
+
let completionClient;
|
| 1730 |
+
if (model.includes('gpt') || model.includes('openai')) {
|
| 1731 |
+
completionClient = openai;
|
| 1732 |
+
} else if (model.includes('claude')) {
|
| 1733 |
+
completionClient = anthropic;
|
| 1734 |
+
} else if (model === 'moonshotai/kimi-k2-instruct-0905') {
|
| 1735 |
+
completionClient = groq;
|
| 1736 |
+
} else {
|
| 1737 |
+
completionClient = groq;
|
| 1738 |
+
}
|
| 1739 |
+
|
| 1740 |
+
// Determine the correct model name for the completion
|
| 1741 |
+
let completionModelName: string;
|
| 1742 |
+
if (model === 'moonshotai/kimi-k2-instruct-0905') {
|
| 1743 |
+
completionModelName = 'moonshotai/kimi-k2-instruct-0905';
|
| 1744 |
+
} else if (model.includes('openai')) {
|
| 1745 |
+
completionModelName = model.replace('openai/', '');
|
| 1746 |
+
} else if (model.includes('anthropic')) {
|
| 1747 |
+
completionModelName = model.replace('anthropic/', '');
|
| 1748 |
+
} else if (model.includes('google')) {
|
| 1749 |
+
completionModelName = model.replace('google/', '');
|
| 1750 |
+
} else {
|
| 1751 |
+
completionModelName = model;
|
| 1752 |
+
}
|
| 1753 |
+
|
| 1754 |
+
const completionResult = await streamText({
|
| 1755 |
+
model: completionClient(completionModelName),
|
| 1756 |
+
messages: [
|
| 1757 |
+
{
|
| 1758 |
+
role: 'system',
|
| 1759 |
+
content: 'You are completing a truncated file. Provide the complete, working file content.'
|
| 1760 |
+
},
|
| 1761 |
+
{ role: 'user', content: completionPrompt }
|
| 1762 |
+
],
|
| 1763 |
+
temperature: model.startsWith('openai/gpt-5') ? undefined : appConfig.ai.defaultTemperature
|
| 1764 |
+
});
|
| 1765 |
+
|
| 1766 |
+
// Get the full text from the stream
|
| 1767 |
+
let completedContent = '';
|
| 1768 |
+
for await (const chunk of completionResult.textStream) {
|
| 1769 |
+
completedContent += chunk;
|
| 1770 |
+
}
|
| 1771 |
+
|
| 1772 |
+
// Replace the truncated file in the generatedCode
|
| 1773 |
+
const filePattern = new RegExp(
|
| 1774 |
+
`<file path="${filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}">[\\s\\S]*?(?:</file>|$)`,
|
| 1775 |
+
'g'
|
| 1776 |
+
);
|
| 1777 |
+
|
| 1778 |
+
// Extract just the code content (remove any markdown or explanation)
|
| 1779 |
+
let cleanContent = completedContent;
|
| 1780 |
+
if (cleanContent.includes('```')) {
|
| 1781 |
+
const codeMatch = cleanContent.match(/```[\w]*\n([\s\S]*?)```/);
|
| 1782 |
+
if (codeMatch) {
|
| 1783 |
+
cleanContent = codeMatch[1];
|
| 1784 |
+
}
|
| 1785 |
+
}
|
| 1786 |
+
|
| 1787 |
+
generatedCode = generatedCode.replace(
|
| 1788 |
+
filePattern,
|
| 1789 |
+
`<file path="${filePath}">\n${cleanContent}\n</file>`
|
| 1790 |
+
);
|
| 1791 |
+
|
| 1792 |
+
console.log(`[generate-ai-code-stream] Successfully completed ${filePath}`);
|
| 1793 |
+
|
| 1794 |
+
} catch (completionError) {
|
| 1795 |
+
console.error(`[generate-ai-code-stream] Failed to complete ${filePath}:`, completionError);
|
| 1796 |
+
await sendProgress({
|
| 1797 |
+
type: 'warning',
|
| 1798 |
+
message: `Could not auto-complete ${filePath}. Manual review may be needed.`
|
| 1799 |
+
});
|
| 1800 |
+
}
|
| 1801 |
+
}
|
| 1802 |
+
|
| 1803 |
+
// Clear the warnings after attempting fixes
|
| 1804 |
+
truncationWarnings.length = 0;
|
| 1805 |
+
await sendProgress({
|
| 1806 |
+
type: 'info',
|
| 1807 |
+
message: 'Truncation recovery complete'
|
| 1808 |
+
});
|
| 1809 |
+
}
|
| 1810 |
+
}
|
| 1811 |
+
|
| 1812 |
+
// Send completion with packages info
|
| 1813 |
+
await sendProgress({
|
| 1814 |
+
type: 'complete',
|
| 1815 |
+
generatedCode,
|
| 1816 |
+
explanation,
|
| 1817 |
+
files: files.length,
|
| 1818 |
+
components: componentCount,
|
| 1819 |
+
model,
|
| 1820 |
+
packagesToInstall: packagesToInstall.length > 0 ? packagesToInstall : undefined,
|
| 1821 |
+
warnings: truncationWarnings.length > 0 ? truncationWarnings : undefined
|
| 1822 |
+
});
|
| 1823 |
+
|
| 1824 |
+
// Track edit in conversation history
|
| 1825 |
+
if (isEdit && editContext && global.conversationState) {
|
| 1826 |
+
const editRecord: ConversationEdit = {
|
| 1827 |
+
timestamp: Date.now(),
|
| 1828 |
+
userRequest: prompt,
|
| 1829 |
+
editType: editContext.editIntent.type,
|
| 1830 |
+
targetFiles: editContext.primaryFiles,
|
| 1831 |
+
confidence: editContext.editIntent.confidence,
|
| 1832 |
+
outcome: 'success' // Assuming success if we got here
|
| 1833 |
+
};
|
| 1834 |
+
|
| 1835 |
+
global.conversationState.context.edits.push(editRecord);
|
| 1836 |
+
|
| 1837 |
+
// Track major changes
|
| 1838 |
+
if (editContext.editIntent.type === 'ADD_FEATURE' || files.length > 3) {
|
| 1839 |
+
global.conversationState.context.projectEvolution.majorChanges.push({
|
| 1840 |
+
timestamp: Date.now(),
|
| 1841 |
+
description: editContext.editIntent.description,
|
| 1842 |
+
filesAffected: editContext.primaryFiles
|
| 1843 |
+
});
|
| 1844 |
+
}
|
| 1845 |
+
|
| 1846 |
+
// Update last updated timestamp
|
| 1847 |
+
global.conversationState.lastUpdated = Date.now();
|
| 1848 |
+
|
| 1849 |
+
console.log('[generate-ai-code-stream] Updated conversation history with edit:', editRecord);
|
| 1850 |
+
}
|
| 1851 |
+
|
| 1852 |
+
} catch (error) {
|
| 1853 |
+
console.error('[generate-ai-code-stream] Stream processing error:', error);
|
| 1854 |
+
|
| 1855 |
+
// Check if it's a tool validation error
|
| 1856 |
+
if ((error as any).message?.includes('tool call validation failed')) {
|
| 1857 |
+
console.error('[generate-ai-code-stream] Tool call validation error - this may be due to the AI model sending incorrect parameters');
|
| 1858 |
+
await sendProgress({
|
| 1859 |
+
type: 'warning',
|
| 1860 |
+
message: 'Package installation tool encountered an issue. Packages will be detected from imports instead.'
|
| 1861 |
+
});
|
| 1862 |
+
// Continue processing - packages can still be detected from the code
|
| 1863 |
+
} else {
|
| 1864 |
+
await sendProgress({
|
| 1865 |
+
type: 'error',
|
| 1866 |
+
error: (error as Error).message
|
| 1867 |
+
});
|
| 1868 |
+
}
|
| 1869 |
+
} finally {
|
| 1870 |
+
await writer.close();
|
| 1871 |
+
}
|
| 1872 |
+
})();
|
| 1873 |
+
|
| 1874 |
+
// Return the stream with proper headers for streaming support
|
| 1875 |
+
return new Response(stream.readable, {
|
| 1876 |
+
headers: {
|
| 1877 |
+
'Content-Type': 'text/event-stream',
|
| 1878 |
+
'Cache-Control': 'no-cache',
|
| 1879 |
+
'Connection': 'keep-alive',
|
| 1880 |
+
'Transfer-Encoding': 'chunked',
|
| 1881 |
+
'Content-Encoding': 'none', // Prevent compression that can break streaming
|
| 1882 |
+
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
| 1883 |
+
'Access-Control-Allow-Origin': '*',
|
| 1884 |
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
| 1885 |
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
| 1886 |
+
},
|
| 1887 |
+
});
|
| 1888 |
+
|
| 1889 |
+
} catch (error) {
|
| 1890 |
+
console.error('[generate-ai-code-stream] Error:', error);
|
| 1891 |
+
return NextResponse.json({
|
| 1892 |
+
success: false,
|
| 1893 |
+
error: (error as Error).message
|
| 1894 |
+
}, { status: 500 });
|
| 1895 |
+
}
|
| 1896 |
+
}
|
app/api/get-sandbox-files/route.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
import { parseJavaScriptFile, buildComponentTree } from '@/lib/file-parser';
|
| 3 |
+
import { FileManifest, FileInfo, RouteInfo } from '@/types/file-manifest';
|
| 4 |
+
// SandboxState type used implicitly through global.activeSandbox
|
| 5 |
+
|
| 6 |
+
declare global {
|
| 7 |
+
var activeSandbox: any;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export async function GET() {
|
| 11 |
+
try {
|
| 12 |
+
if (!global.activeSandbox) {
|
| 13 |
+
return NextResponse.json({
|
| 14 |
+
success: false,
|
| 15 |
+
error: 'No active sandbox'
|
| 16 |
+
}, { status: 404 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
console.log('[get-sandbox-files] Fetching and analyzing file structure...');
|
| 20 |
+
|
| 21 |
+
// Get list of all relevant files
|
| 22 |
+
const findResult = await global.activeSandbox.runCommand({
|
| 23 |
+
cmd: 'find',
|
| 24 |
+
args: [
|
| 25 |
+
'.',
|
| 26 |
+
'-name', 'node_modules', '-prune', '-o',
|
| 27 |
+
'-name', '.git', '-prune', '-o',
|
| 28 |
+
'-name', 'dist', '-prune', '-o',
|
| 29 |
+
'-name', 'build', '-prune', '-o',
|
| 30 |
+
'-type', 'f',
|
| 31 |
+
'(',
|
| 32 |
+
'-name', '*.jsx',
|
| 33 |
+
'-o', '-name', '*.js',
|
| 34 |
+
'-o', '-name', '*.tsx',
|
| 35 |
+
'-o', '-name', '*.ts',
|
| 36 |
+
'-o', '-name', '*.css',
|
| 37 |
+
'-o', '-name', '*.json',
|
| 38 |
+
')',
|
| 39 |
+
'-print'
|
| 40 |
+
]
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
if (findResult.exitCode !== 0) {
|
| 44 |
+
throw new Error('Failed to list files');
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const fileList = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
| 48 |
+
console.log('[get-sandbox-files] Found', fileList.length, 'files');
|
| 49 |
+
|
| 50 |
+
// Read content of each file (limit to reasonable sizes)
|
| 51 |
+
const filesContent: Record<string, string> = {};
|
| 52 |
+
|
| 53 |
+
for (const filePath of fileList) {
|
| 54 |
+
try {
|
| 55 |
+
// Check file size first
|
| 56 |
+
const statResult = await global.activeSandbox.runCommand({
|
| 57 |
+
cmd: 'stat',
|
| 58 |
+
args: ['-f', '%z', filePath]
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
if (statResult.exitCode === 0) {
|
| 62 |
+
const fileSize = parseInt(await statResult.stdout());
|
| 63 |
+
|
| 64 |
+
// Only read files smaller than 10KB
|
| 65 |
+
if (fileSize < 10000) {
|
| 66 |
+
const catResult = await global.activeSandbox.runCommand({
|
| 67 |
+
cmd: 'cat',
|
| 68 |
+
args: [filePath]
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
if (catResult.exitCode === 0) {
|
| 72 |
+
const content = await catResult.stdout();
|
| 73 |
+
// Remove leading './' from path
|
| 74 |
+
const relativePath = filePath.replace(/^\.\//, '');
|
| 75 |
+
filesContent[relativePath] = content;
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
} catch (parseError) {
|
| 80 |
+
console.debug('Error parsing component info:', parseError);
|
| 81 |
+
// Skip files that can't be read
|
| 82 |
+
continue;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Get directory structure
|
| 87 |
+
const treeResult = await global.activeSandbox.runCommand({
|
| 88 |
+
cmd: 'find',
|
| 89 |
+
args: ['.', '-type', 'd', '-not', '-path', '*/node_modules*', '-not', '-path', '*/.git*']
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
let structure = '';
|
| 93 |
+
if (treeResult.exitCode === 0) {
|
| 94 |
+
const dirs = (await treeResult.stdout()).split('\n').filter((d: string) => d.trim());
|
| 95 |
+
structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// Build enhanced file manifest
|
| 99 |
+
const fileManifest: FileManifest = {
|
| 100 |
+
files: {},
|
| 101 |
+
routes: [],
|
| 102 |
+
componentTree: {},
|
| 103 |
+
entryPoint: '',
|
| 104 |
+
styleFiles: [],
|
| 105 |
+
timestamp: Date.now(),
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
// Process each file
|
| 109 |
+
for (const [relativePath, content] of Object.entries(filesContent)) {
|
| 110 |
+
const fullPath = `/${relativePath}`;
|
| 111 |
+
|
| 112 |
+
// Create base file info
|
| 113 |
+
const fileInfo: FileInfo = {
|
| 114 |
+
content: content,
|
| 115 |
+
type: 'utility',
|
| 116 |
+
path: fullPath,
|
| 117 |
+
relativePath,
|
| 118 |
+
lastModified: Date.now(),
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
// Parse JavaScript/JSX files
|
| 122 |
+
if (relativePath.match(/\.(jsx?|tsx?)$/)) {
|
| 123 |
+
const parseResult = parseJavaScriptFile(content, fullPath);
|
| 124 |
+
Object.assign(fileInfo, parseResult);
|
| 125 |
+
|
| 126 |
+
// Identify entry point
|
| 127 |
+
if (relativePath === 'src/main.jsx' || relativePath === 'src/index.jsx') {
|
| 128 |
+
fileManifest.entryPoint = fullPath;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Identify App.jsx
|
| 132 |
+
if (relativePath === 'src/App.jsx' || relativePath === 'App.jsx') {
|
| 133 |
+
fileManifest.entryPoint = fileManifest.entryPoint || fullPath;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Track style files
|
| 138 |
+
if (relativePath.endsWith('.css')) {
|
| 139 |
+
fileManifest.styleFiles.push(fullPath);
|
| 140 |
+
fileInfo.type = 'style';
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
fileManifest.files[fullPath] = fileInfo;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Build component tree
|
| 147 |
+
fileManifest.componentTree = buildComponentTree(fileManifest.files);
|
| 148 |
+
|
| 149 |
+
// Extract routes (simplified - looks for Route components or page pattern)
|
| 150 |
+
fileManifest.routes = extractRoutes(fileManifest.files);
|
| 151 |
+
|
| 152 |
+
// Update global file cache with manifest
|
| 153 |
+
if (global.sandboxState?.fileCache) {
|
| 154 |
+
global.sandboxState.fileCache.manifest = fileManifest;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
return NextResponse.json({
|
| 158 |
+
success: true,
|
| 159 |
+
files: filesContent,
|
| 160 |
+
structure,
|
| 161 |
+
fileCount: Object.keys(filesContent).length,
|
| 162 |
+
manifest: fileManifest,
|
| 163 |
+
});
|
| 164 |
+
|
| 165 |
+
} catch (error) {
|
| 166 |
+
console.error('[get-sandbox-files] Error:', error);
|
| 167 |
+
return NextResponse.json({
|
| 168 |
+
success: false,
|
| 169 |
+
error: (error as Error).message
|
| 170 |
+
}, { status: 500 });
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
function extractRoutes(files: Record<string, FileInfo>): RouteInfo[] {
|
| 175 |
+
const routes: RouteInfo[] = [];
|
| 176 |
+
|
| 177 |
+
// Look for React Router usage
|
| 178 |
+
for (const [path, fileInfo] of Object.entries(files)) {
|
| 179 |
+
if (fileInfo.content.includes('<Route') || fileInfo.content.includes('createBrowserRouter')) {
|
| 180 |
+
// Extract route definitions (simplified)
|
| 181 |
+
const routeMatches = fileInfo.content.matchAll(/path=["']([^"']+)["'].*(?:element|component)={([^}]+)}/g);
|
| 182 |
+
|
| 183 |
+
for (const match of routeMatches) {
|
| 184 |
+
const [, routePath] = match;
|
| 185 |
+
// componentRef available in match but not used currently
|
| 186 |
+
routes.push({
|
| 187 |
+
path: routePath,
|
| 188 |
+
component: path,
|
| 189 |
+
});
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
// Check for Next.js style pages
|
| 194 |
+
if (fileInfo.relativePath.startsWith('pages/') || fileInfo.relativePath.startsWith('src/pages/')) {
|
| 195 |
+
const routePath = '/' + fileInfo.relativePath
|
| 196 |
+
.replace(/^(src\/)?pages\//, '')
|
| 197 |
+
.replace(/\.(jsx?|tsx?)$/, '')
|
| 198 |
+
.replace(/index$/, '');
|
| 199 |
+
|
| 200 |
+
routes.push({
|
| 201 |
+
path: routePath,
|
| 202 |
+
component: path,
|
| 203 |
+
});
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
return routes;
|
| 208 |
+
}
|
app/api/install-packages-v2/route.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
import { SandboxProvider } from '@/lib/sandbox/types';
|
| 3 |
+
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
| 4 |
+
|
| 5 |
+
declare global {
|
| 6 |
+
var activeSandboxProvider: any;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export async function POST(request: NextRequest) {
|
| 10 |
+
try {
|
| 11 |
+
const { packages } = await request.json();
|
| 12 |
+
|
| 13 |
+
if (!packages || !Array.isArray(packages) || packages.length === 0) {
|
| 14 |
+
return NextResponse.json({
|
| 15 |
+
success: false,
|
| 16 |
+
error: 'Packages array is required'
|
| 17 |
+
}, { status: 400 });
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Get provider from sandbox manager or global state
|
| 21 |
+
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
|
| 22 |
+
|
| 23 |
+
if (!provider) {
|
| 24 |
+
return NextResponse.json({
|
| 25 |
+
success: false,
|
| 26 |
+
error: 'No active sandbox'
|
| 27 |
+
}, { status: 400 });
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
console.log(`[install-packages-v2] Installing: ${packages.join(', ')}`);
|
| 31 |
+
|
| 32 |
+
const result = await provider.installPackages(packages);
|
| 33 |
+
|
| 34 |
+
return NextResponse.json({
|
| 35 |
+
success: result.success,
|
| 36 |
+
output: result.stdout,
|
| 37 |
+
error: result.stderr,
|
| 38 |
+
message: result.success ? 'Packages installed successfully' : 'Package installation failed'
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
} catch (error) {
|
| 42 |
+
console.error('[install-packages-v2] Error:', error);
|
| 43 |
+
return NextResponse.json({
|
| 44 |
+
success: false,
|
| 45 |
+
error: (error as Error).message
|
| 46 |
+
}, { status: 500 });
|
| 47 |
+
}
|
| 48 |
+
}
|
app/api/install-packages/route.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
var activeSandbox: any;
|
| 5 |
+
var activeSandboxProvider: any;
|
| 6 |
+
var sandboxData: any;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export async function POST(request: NextRequest) {
|
| 10 |
+
try {
|
| 11 |
+
const { packages } = await request.json();
|
| 12 |
+
// sandboxId not used - using global sandbox
|
| 13 |
+
|
| 14 |
+
if (!packages || !Array.isArray(packages) || packages.length === 0) {
|
| 15 |
+
return NextResponse.json({
|
| 16 |
+
success: false,
|
| 17 |
+
error: 'Packages array is required'
|
| 18 |
+
}, { status: 400 });
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Validate and deduplicate package names
|
| 22 |
+
const validPackages = [...new Set(packages)]
|
| 23 |
+
.filter(pkg => pkg && typeof pkg === 'string' && pkg.trim() !== '')
|
| 24 |
+
.map(pkg => pkg.trim());
|
| 25 |
+
|
| 26 |
+
if (validPackages.length === 0) {
|
| 27 |
+
return NextResponse.json({
|
| 28 |
+
success: false,
|
| 29 |
+
error: 'No valid package names provided'
|
| 30 |
+
}, { status: 400 });
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Log if duplicates were found
|
| 34 |
+
if (packages.length !== validPackages.length) {
|
| 35 |
+
console.log(`[install-packages] Cleaned packages: removed ${packages.length - validPackages.length} invalid/duplicate entries`);
|
| 36 |
+
console.log(`[install-packages] Original:`, packages);
|
| 37 |
+
console.log(`[install-packages] Cleaned:`, validPackages);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Get active sandbox provider
|
| 41 |
+
const provider = global.activeSandboxProvider;
|
| 42 |
+
|
| 43 |
+
if (!provider) {
|
| 44 |
+
return NextResponse.json({
|
| 45 |
+
success: false,
|
| 46 |
+
error: 'No active sandbox provider available'
|
| 47 |
+
}, { status: 400 });
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
console.log('[install-packages] Installing packages:', validPackages);
|
| 51 |
+
|
| 52 |
+
// Create a response stream for real-time updates
|
| 53 |
+
const encoder = new TextEncoder();
|
| 54 |
+
const stream = new TransformStream();
|
| 55 |
+
const writer = stream.writable.getWriter();
|
| 56 |
+
|
| 57 |
+
// Function to send progress updates
|
| 58 |
+
const sendProgress = async (data: any) => {
|
| 59 |
+
const message = `data: ${JSON.stringify(data)}\n\n`;
|
| 60 |
+
await writer.write(encoder.encode(message));
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
// Start installation in background
|
| 64 |
+
(async (providerInstance) => {
|
| 65 |
+
try {
|
| 66 |
+
await sendProgress({
|
| 67 |
+
type: 'start',
|
| 68 |
+
message: `Installing ${validPackages.length} package${validPackages.length > 1 ? 's' : ''}...`,
|
| 69 |
+
packages: validPackages
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
// Stop any existing development server first
|
| 73 |
+
await sendProgress({ type: 'status', message: 'Stopping development server...' });
|
| 74 |
+
|
| 75 |
+
try {
|
| 76 |
+
// Try to kill any running dev server processes
|
| 77 |
+
await providerInstance.runCommand('pkill -f vite');
|
| 78 |
+
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit
|
| 79 |
+
} catch (killError) {
|
| 80 |
+
// It's OK if no process is found
|
| 81 |
+
console.debug('[install-packages] No existing dev server found:', killError);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Check which packages are already installed
|
| 85 |
+
await sendProgress({
|
| 86 |
+
type: 'status',
|
| 87 |
+
message: 'Checking installed packages...'
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
let packagesToInstall = validPackages;
|
| 91 |
+
|
| 92 |
+
try {
|
| 93 |
+
// Read package.json to check existing dependencies
|
| 94 |
+
let packageJsonContent = '';
|
| 95 |
+
try {
|
| 96 |
+
packageJsonContent = await providerInstance.readFile('package.json');
|
| 97 |
+
} catch (error) {
|
| 98 |
+
console.log('[install-packages] Error reading package.json:', error);
|
| 99 |
+
}
|
| 100 |
+
if (packageJsonContent) {
|
| 101 |
+
const packageJson = JSON.parse(packageJsonContent);
|
| 102 |
+
|
| 103 |
+
const dependencies = packageJson.dependencies || {};
|
| 104 |
+
const devDependencies = packageJson.devDependencies || {};
|
| 105 |
+
const allDeps = { ...dependencies, ...devDependencies };
|
| 106 |
+
|
| 107 |
+
const alreadyInstalled = [];
|
| 108 |
+
const needInstall = [];
|
| 109 |
+
|
| 110 |
+
for (const pkg of validPackages) {
|
| 111 |
+
// Handle scoped packages
|
| 112 |
+
const pkgName = pkg.startsWith('@') ? pkg : pkg.split('@')[0];
|
| 113 |
+
|
| 114 |
+
if (allDeps[pkgName]) {
|
| 115 |
+
alreadyInstalled.push(pkgName);
|
| 116 |
+
} else {
|
| 117 |
+
needInstall.push(pkg);
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
packagesToInstall = needInstall;
|
| 122 |
+
|
| 123 |
+
if (alreadyInstalled.length > 0) {
|
| 124 |
+
await sendProgress({
|
| 125 |
+
type: 'info',
|
| 126 |
+
message: `Already installed: ${alreadyInstalled.join(', ')}`
|
| 127 |
+
});
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
} catch (error) {
|
| 131 |
+
console.error('[install-packages] Error checking existing packages:', error);
|
| 132 |
+
// If we can't check, just try to install all packages
|
| 133 |
+
packagesToInstall = validPackages;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
if (packagesToInstall.length === 0) {
|
| 137 |
+
await sendProgress({
|
| 138 |
+
type: 'success',
|
| 139 |
+
message: 'All packages are already installed',
|
| 140 |
+
installedPackages: [],
|
| 141 |
+
alreadyInstalled: validPackages
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
// Restart dev server
|
| 145 |
+
await sendProgress({ type: 'status', message: 'Restarting development server...' });
|
| 146 |
+
|
| 147 |
+
await providerInstance.restartViteServer();
|
| 148 |
+
|
| 149 |
+
await sendProgress({
|
| 150 |
+
type: 'complete',
|
| 151 |
+
message: 'Dev server restarted!',
|
| 152 |
+
installedPackages: []
|
| 153 |
+
});
|
| 154 |
+
|
| 155 |
+
return;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Install only packages that aren't already installed
|
| 159 |
+
await sendProgress({
|
| 160 |
+
type: 'info',
|
| 161 |
+
message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}`
|
| 162 |
+
});
|
| 163 |
+
|
| 164 |
+
// Install packages using provider method
|
| 165 |
+
const installResult = await providerInstance.installPackages(packagesToInstall);
|
| 166 |
+
|
| 167 |
+
// Get install output - ensure stdout/stderr are strings
|
| 168 |
+
const stdout = String(installResult.stdout || '');
|
| 169 |
+
const stderr = String(installResult.stderr || '');
|
| 170 |
+
|
| 171 |
+
if (stdout) {
|
| 172 |
+
const lines = stdout.split('\n').filter(line => line.trim());
|
| 173 |
+
for (const line of lines) {
|
| 174 |
+
if (line.includes('npm WARN')) {
|
| 175 |
+
await sendProgress({ type: 'warning', message: line });
|
| 176 |
+
} else if (line.trim()) {
|
| 177 |
+
await sendProgress({ type: 'output', message: line });
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
if (stderr) {
|
| 183 |
+
const errorLines = stderr.split('\n').filter(line => line.trim());
|
| 184 |
+
for (const line of errorLines) {
|
| 185 |
+
if (line.includes('ERESOLVE')) {
|
| 186 |
+
await sendProgress({
|
| 187 |
+
type: 'warning',
|
| 188 |
+
message: `Dependency conflict resolved with --legacy-peer-deps: ${line}`
|
| 189 |
+
});
|
| 190 |
+
} else if (line.trim()) {
|
| 191 |
+
await sendProgress({ type: 'error', message: line });
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
if (installResult.exitCode === 0) {
|
| 197 |
+
await sendProgress({
|
| 198 |
+
type: 'success',
|
| 199 |
+
message: `Successfully installed: ${packagesToInstall.join(', ')}`,
|
| 200 |
+
installedPackages: packagesToInstall
|
| 201 |
+
});
|
| 202 |
+
} else {
|
| 203 |
+
await sendProgress({
|
| 204 |
+
type: 'error',
|
| 205 |
+
message: 'Package installation failed'
|
| 206 |
+
});
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
// Restart development server
|
| 210 |
+
await sendProgress({ type: 'status', message: 'Restarting development server...' });
|
| 211 |
+
|
| 212 |
+
try {
|
| 213 |
+
await providerInstance.restartViteServer();
|
| 214 |
+
|
| 215 |
+
// Wait a bit for the server to start
|
| 216 |
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
| 217 |
+
|
| 218 |
+
await sendProgress({
|
| 219 |
+
type: 'complete',
|
| 220 |
+
message: 'Package installation complete and dev server restarted!',
|
| 221 |
+
installedPackages: packagesToInstall
|
| 222 |
+
});
|
| 223 |
+
} catch (error) {
|
| 224 |
+
await sendProgress({
|
| 225 |
+
type: 'error',
|
| 226 |
+
message: `Failed to restart dev server: ${(error as Error).message}`
|
| 227 |
+
});
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
} catch (error) {
|
| 231 |
+
const errorMessage = (error as Error).message;
|
| 232 |
+
if (errorMessage && errorMessage !== 'undefined') {
|
| 233 |
+
await sendProgress({
|
| 234 |
+
type: 'error',
|
| 235 |
+
message: errorMessage
|
| 236 |
+
});
|
| 237 |
+
}
|
| 238 |
+
} finally {
|
| 239 |
+
await writer.close();
|
| 240 |
+
}
|
| 241 |
+
})(provider);
|
| 242 |
+
|
| 243 |
+
// Return the stream
|
| 244 |
+
return new Response(stream.readable, {
|
| 245 |
+
headers: {
|
| 246 |
+
'Content-Type': 'text/event-stream',
|
| 247 |
+
'Cache-Control': 'no-cache',
|
| 248 |
+
'Connection': 'keep-alive',
|
| 249 |
+
},
|
| 250 |
+
});
|
| 251 |
+
|
| 252 |
+
} catch (error) {
|
| 253 |
+
console.error('[install-packages] Error:', error);
|
| 254 |
+
return NextResponse.json({
|
| 255 |
+
success: false,
|
| 256 |
+
error: (error as Error).message
|
| 257 |
+
}, { status: 500 });
|
| 258 |
+
}
|
| 259 |
+
}
|
app/api/kill-sandbox/route.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
var activeSandboxProvider: any;
|
| 5 |
+
var sandboxData: any;
|
| 6 |
+
var existingFiles: Set<string>;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export async function POST() {
|
| 10 |
+
try {
|
| 11 |
+
console.log('[kill-sandbox] Stopping active sandbox...');
|
| 12 |
+
|
| 13 |
+
let sandboxKilled = false;
|
| 14 |
+
|
| 15 |
+
// Stop existing sandbox if any
|
| 16 |
+
if (global.activeSandboxProvider) {
|
| 17 |
+
try {
|
| 18 |
+
await global.activeSandboxProvider.terminate();
|
| 19 |
+
sandboxKilled = true;
|
| 20 |
+
console.log('[kill-sandbox] Sandbox stopped successfully');
|
| 21 |
+
} catch (e) {
|
| 22 |
+
console.error('[kill-sandbox] Failed to stop sandbox:', e);
|
| 23 |
+
}
|
| 24 |
+
global.activeSandboxProvider = null;
|
| 25 |
+
global.sandboxData = null;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// Clear existing files tracking
|
| 29 |
+
if (global.existingFiles) {
|
| 30 |
+
global.existingFiles.clear();
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
return NextResponse.json({
|
| 34 |
+
success: true,
|
| 35 |
+
sandboxKilled,
|
| 36 |
+
message: 'Sandbox cleaned up successfully'
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
} catch (error) {
|
| 40 |
+
console.error('[kill-sandbox] Error:', error);
|
| 41 |
+
return NextResponse.json(
|
| 42 |
+
{
|
| 43 |
+
success: false,
|
| 44 |
+
error: (error as Error).message
|
| 45 |
+
},
|
| 46 |
+
{ status: 500 }
|
| 47 |
+
);
|
| 48 |
+
}
|
| 49 |
+
}
|
app/api/monitor-vite-logs/route.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
var activeSandbox: any;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export async function GET() {
|
| 8 |
+
try {
|
| 9 |
+
if (!global.activeSandbox) {
|
| 10 |
+
return NextResponse.json({
|
| 11 |
+
success: false,
|
| 12 |
+
error: 'No active sandbox'
|
| 13 |
+
}, { status: 400 });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
console.log('[monitor-vite-logs] Checking Vite process logs...');
|
| 17 |
+
|
| 18 |
+
const errors: any[] = [];
|
| 19 |
+
|
| 20 |
+
// Check if there's an error file from previous runs
|
| 21 |
+
try {
|
| 22 |
+
const catResult = await global.activeSandbox.runCommand({
|
| 23 |
+
cmd: 'cat',
|
| 24 |
+
args: ['/tmp/vite-errors.json']
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
if (catResult.exitCode === 0) {
|
| 28 |
+
const errorFileContent = await catResult.stdout();
|
| 29 |
+
const data = JSON.parse(errorFileContent);
|
| 30 |
+
errors.push(...(data.errors || []));
|
| 31 |
+
}
|
| 32 |
+
} catch {
|
| 33 |
+
// No error file exists, that's OK
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Look for any Vite-related log files that might contain errors
|
| 37 |
+
try {
|
| 38 |
+
const findResult = await global.activeSandbox.runCommand({
|
| 39 |
+
cmd: 'find',
|
| 40 |
+
args: ['/tmp', '-name', '*vite*', '-type', 'f']
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
if (findResult.exitCode === 0) {
|
| 44 |
+
const logFiles = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
| 45 |
+
|
| 46 |
+
for (const logFile of logFiles.slice(0, 3)) {
|
| 47 |
+
try {
|
| 48 |
+
const grepResult = await global.activeSandbox.runCommand({
|
| 49 |
+
cmd: 'grep',
|
| 50 |
+
args: ['-i', 'failed to resolve import', logFile]
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
if (grepResult.exitCode === 0) {
|
| 54 |
+
const errorLines = (await grepResult.stdout()).split('\n').filter((line: string) => line.trim());
|
| 55 |
+
|
| 56 |
+
for (const line of errorLines) {
|
| 57 |
+
// Extract package name from error line
|
| 58 |
+
const importMatch = line.match(/"([^"]+)"/);
|
| 59 |
+
if (importMatch) {
|
| 60 |
+
const importPath = importMatch[1];
|
| 61 |
+
|
| 62 |
+
// Skip relative imports
|
| 63 |
+
if (!importPath.startsWith('.')) {
|
| 64 |
+
// Extract base package name
|
| 65 |
+
let packageName;
|
| 66 |
+
if (importPath.startsWith('@')) {
|
| 67 |
+
const parts = importPath.split('/');
|
| 68 |
+
packageName = parts.length >= 2 ? parts.slice(0, 2).join('/') : importPath;
|
| 69 |
+
} else {
|
| 70 |
+
packageName = importPath.split('/')[0];
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
const errorObj = {
|
| 74 |
+
type: "npm-missing",
|
| 75 |
+
package: packageName,
|
| 76 |
+
message: `Failed to resolve import "${importPath}"`,
|
| 77 |
+
file: "Unknown"
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
// Avoid duplicates
|
| 81 |
+
if (!errors.some(e => e.package === errorObj.package)) {
|
| 82 |
+
errors.push(errorObj);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
} catch {
|
| 89 |
+
// Skip if grep fails
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
} catch {
|
| 94 |
+
// No log files found, that's OK
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// Deduplicate errors by package name
|
| 98 |
+
const uniqueErrors: any[] = [];
|
| 99 |
+
const seenPackages = new Set<string>();
|
| 100 |
+
|
| 101 |
+
for (const error of errors) {
|
| 102 |
+
if (error.package && !seenPackages.has(error.package)) {
|
| 103 |
+
seenPackages.add(error.package);
|
| 104 |
+
uniqueErrors.push(error);
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
return NextResponse.json({
|
| 109 |
+
success: true,
|
| 110 |
+
hasErrors: uniqueErrors.length > 0,
|
| 111 |
+
errors: uniqueErrors
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
} catch (error) {
|
| 115 |
+
console.error('[monitor-vite-logs] Error:', error);
|
| 116 |
+
return NextResponse.json({
|
| 117 |
+
success: false,
|
| 118 |
+
error: (error as Error).message
|
| 119 |
+
}, { status: 500 });
|
| 120 |
+
}
|
| 121 |
+
}
|
app/api/report-vite-error/route.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
var viteErrors: any[];
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
// Initialize global viteErrors array if it doesn't exist
|
| 8 |
+
if (!global.viteErrors) {
|
| 9 |
+
global.viteErrors = [];
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export async function POST(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const { error, file, type = 'runtime-error' } = await request.json();
|
| 15 |
+
|
| 16 |
+
if (!error) {
|
| 17 |
+
return NextResponse.json({
|
| 18 |
+
success: false,
|
| 19 |
+
error: 'Error message is required'
|
| 20 |
+
}, { status: 400 });
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Parse the error to extract useful information
|
| 24 |
+
const errorObj: any = {
|
| 25 |
+
type,
|
| 26 |
+
message: error,
|
| 27 |
+
file: file || 'unknown',
|
| 28 |
+
timestamp: new Date().toISOString()
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
// Extract import information if it's an import error
|
| 32 |
+
const importMatch = error.match(/Failed to resolve import ['"]([^'"]+)['"] from ['"]([^'"]+)['"]/);
|
| 33 |
+
if (importMatch) {
|
| 34 |
+
errorObj.type = 'import-error';
|
| 35 |
+
errorObj.import = importMatch[1];
|
| 36 |
+
errorObj.file = importMatch[2];
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Add to global errors array
|
| 40 |
+
global.viteErrors.push(errorObj);
|
| 41 |
+
|
| 42 |
+
// Keep only last 50 errors
|
| 43 |
+
if (global.viteErrors.length > 50) {
|
| 44 |
+
global.viteErrors = global.viteErrors.slice(-50);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
console.log('[report-vite-error] Error reported:', errorObj);
|
| 48 |
+
|
| 49 |
+
return NextResponse.json({
|
| 50 |
+
success: true,
|
| 51 |
+
message: 'Error reported successfully',
|
| 52 |
+
error: errorObj
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
} catch (error) {
|
| 56 |
+
console.error('[report-vite-error] Error:', error);
|
| 57 |
+
return NextResponse.json({
|
| 58 |
+
success: false,
|
| 59 |
+
error: (error as Error).message
|
| 60 |
+
}, { status: 500 });
|
| 61 |
+
}
|
| 62 |
+
}
|
app/api/restart-vite/route.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
var activeSandbox: any;
|
| 5 |
+
var activeSandboxProvider: any;
|
| 6 |
+
var lastViteRestartTime: number;
|
| 7 |
+
var viteRestartInProgress: boolean;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const RESTART_COOLDOWN_MS = 5000; // 5 second cooldown between restarts
|
| 11 |
+
|
| 12 |
+
export async function POST() {
|
| 13 |
+
try {
|
| 14 |
+
// Check both v1 and v2 global references
|
| 15 |
+
const provider = global.activeSandbox || global.activeSandboxProvider;
|
| 16 |
+
|
| 17 |
+
if (!provider) {
|
| 18 |
+
return NextResponse.json({
|
| 19 |
+
success: false,
|
| 20 |
+
error: 'No active sandbox'
|
| 21 |
+
}, { status: 400 });
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Check if restart is already in progress
|
| 25 |
+
if (global.viteRestartInProgress) {
|
| 26 |
+
console.log('[restart-vite] Vite restart already in progress, skipping...');
|
| 27 |
+
return NextResponse.json({
|
| 28 |
+
success: true,
|
| 29 |
+
message: 'Vite restart already in progress'
|
| 30 |
+
});
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Check cooldown
|
| 34 |
+
const now = Date.now();
|
| 35 |
+
if (global.lastViteRestartTime && (now - global.lastViteRestartTime) < RESTART_COOLDOWN_MS) {
|
| 36 |
+
const remainingTime = Math.ceil((RESTART_COOLDOWN_MS - (now - global.lastViteRestartTime)) / 1000);
|
| 37 |
+
console.log(`[restart-vite] Cooldown active, ${remainingTime}s remaining`);
|
| 38 |
+
return NextResponse.json({
|
| 39 |
+
success: true,
|
| 40 |
+
message: `Vite was recently restarted, cooldown active (${remainingTime}s remaining)`
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Set the restart flag
|
| 45 |
+
global.viteRestartInProgress = true;
|
| 46 |
+
|
| 47 |
+
console.log('[restart-vite] Using provider method to restart Vite...');
|
| 48 |
+
|
| 49 |
+
// Use the provider's restartViteServer method if available
|
| 50 |
+
if (typeof provider.restartViteServer === 'function') {
|
| 51 |
+
await provider.restartViteServer();
|
| 52 |
+
console.log('[restart-vite] Vite restarted via provider method');
|
| 53 |
+
} else {
|
| 54 |
+
// Fallback to manual restart using provider's runCommand
|
| 55 |
+
console.log('[restart-vite] Fallback to manual Vite restart...');
|
| 56 |
+
|
| 57 |
+
// Kill existing Vite processes
|
| 58 |
+
try {
|
| 59 |
+
await provider.runCommand('pkill -f vite');
|
| 60 |
+
console.log('[restart-vite] Killed existing Vite processes');
|
| 61 |
+
|
| 62 |
+
// Wait a moment for processes to terminate
|
| 63 |
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
| 64 |
+
} catch {
|
| 65 |
+
console.log('[restart-vite] No existing Vite processes found');
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// Clear any error tracking files
|
| 69 |
+
try {
|
| 70 |
+
await provider.runCommand('bash -c "echo \'{\\"errors\\": [], \\"lastChecked\\": '+ Date.now() +'}\' > /tmp/vite-errors.json"');
|
| 71 |
+
} catch {
|
| 72 |
+
// Ignore if this fails
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Start Vite dev server in background
|
| 76 |
+
await provider.runCommand('sh -c "nohup npm run dev > /tmp/vite.log 2>&1 &"');
|
| 77 |
+
console.log('[restart-vite] Vite dev server restarted');
|
| 78 |
+
|
| 79 |
+
// Wait for Vite to start up
|
| 80 |
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Update global state
|
| 84 |
+
global.lastViteRestartTime = Date.now();
|
| 85 |
+
global.viteRestartInProgress = false;
|
| 86 |
+
|
| 87 |
+
return NextResponse.json({
|
| 88 |
+
success: true,
|
| 89 |
+
message: 'Vite restarted successfully'
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
} catch (error) {
|
| 93 |
+
console.error('[restart-vite] Error:', error);
|
| 94 |
+
|
| 95 |
+
// Clear the restart flag on error
|
| 96 |
+
global.viteRestartInProgress = false;
|
| 97 |
+
|
| 98 |
+
return NextResponse.json({
|
| 99 |
+
success: false,
|
| 100 |
+
error: (error as Error).message
|
| 101 |
+
}, { status: 500 });
|
| 102 |
+
}
|
| 103 |
+
}
|
app/api/run-command-v2/route.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
import { SandboxProvider } from '@/lib/sandbox/types';
|
| 3 |
+
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
| 4 |
+
|
| 5 |
+
// Get active sandbox provider from global state
|
| 6 |
+
declare global {
|
| 7 |
+
var activeSandboxProvider: any;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export async function POST(request: NextRequest) {
|
| 11 |
+
try {
|
| 12 |
+
const { command } = await request.json();
|
| 13 |
+
|
| 14 |
+
if (!command) {
|
| 15 |
+
return NextResponse.json({
|
| 16 |
+
success: false,
|
| 17 |
+
error: 'Command is required'
|
| 18 |
+
}, { status: 400 });
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Get provider from sandbox manager or global state
|
| 22 |
+
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
|
| 23 |
+
|
| 24 |
+
if (!provider) {
|
| 25 |
+
return NextResponse.json({
|
| 26 |
+
success: false,
|
| 27 |
+
error: 'No active sandbox'
|
| 28 |
+
}, { status: 400 });
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
console.log(`[run-command-v2] Executing: ${command}`);
|
| 32 |
+
|
| 33 |
+
const result = await provider.runCommand(command);
|
| 34 |
+
|
| 35 |
+
return NextResponse.json({
|
| 36 |
+
success: result.success,
|
| 37 |
+
output: result.stdout,
|
| 38 |
+
error: result.stderr,
|
| 39 |
+
exitCode: result.exitCode,
|
| 40 |
+
message: result.success ? 'Command executed successfully' : 'Command failed'
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
} catch (error) {
|
| 44 |
+
console.error('[run-command-v2] Error:', error);
|
| 45 |
+
return NextResponse.json({
|
| 46 |
+
success: false,
|
| 47 |
+
error: (error as Error).message
|
| 48 |
+
}, { status: 500 });
|
| 49 |
+
}
|
| 50 |
+
}
|
app/api/run-command/route.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
// Get active sandbox from global state (in production, use a proper state management solution)
|
| 4 |
+
declare global {
|
| 5 |
+
var activeSandbox: any;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export async function POST(request: NextRequest) {
|
| 9 |
+
try {
|
| 10 |
+
const { command } = await request.json();
|
| 11 |
+
|
| 12 |
+
if (!command) {
|
| 13 |
+
return NextResponse.json({
|
| 14 |
+
success: false,
|
| 15 |
+
error: 'Command is required'
|
| 16 |
+
}, { status: 400 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
if (!global.activeSandbox) {
|
| 20 |
+
return NextResponse.json({
|
| 21 |
+
success: false,
|
| 22 |
+
error: 'No active sandbox'
|
| 23 |
+
}, { status: 400 });
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
console.log(`[run-command] Executing: ${command}`);
|
| 27 |
+
|
| 28 |
+
// Parse command and arguments
|
| 29 |
+
const commandParts = command.trim().split(/\s+/);
|
| 30 |
+
const cmd = commandParts[0];
|
| 31 |
+
const args = commandParts.slice(1);
|
| 32 |
+
|
| 33 |
+
// Execute command using Vercel Sandbox
|
| 34 |
+
const result = await global.activeSandbox.runCommand({
|
| 35 |
+
cmd,
|
| 36 |
+
args
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// Get output streams
|
| 40 |
+
const stdout = await result.stdout();
|
| 41 |
+
const stderr = await result.stderr();
|
| 42 |
+
|
| 43 |
+
const output = [
|
| 44 |
+
stdout ? `STDOUT:\n${stdout}` : '',
|
| 45 |
+
stderr ? `\nSTDERR:\n${stderr}` : '',
|
| 46 |
+
`\nExit code: ${result.exitCode}`
|
| 47 |
+
].filter(Boolean).join('');
|
| 48 |
+
|
| 49 |
+
return NextResponse.json({
|
| 50 |
+
success: true,
|
| 51 |
+
output,
|
| 52 |
+
exitCode: result.exitCode,
|
| 53 |
+
message: result.exitCode === 0 ? 'Command executed successfully' : 'Command completed with non-zero exit code'
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
} catch (error) {
|
| 57 |
+
console.error('[run-command] Error:', error);
|
| 58 |
+
return NextResponse.json({
|
| 59 |
+
success: false,
|
| 60 |
+
error: (error as Error).message
|
| 61 |
+
}, { status: 500 });
|
| 62 |
+
}
|
| 63 |
+
}
|
app/api/sandbox-logs/route.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
var activeSandbox: any;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export async function GET() {
|
| 8 |
+
try {
|
| 9 |
+
if (!global.activeSandbox) {
|
| 10 |
+
return NextResponse.json({
|
| 11 |
+
success: false,
|
| 12 |
+
error: 'No active sandbox'
|
| 13 |
+
}, { status: 400 });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
console.log('[sandbox-logs] Fetching Vite dev server logs...');
|
| 17 |
+
|
| 18 |
+
// Check if Vite processes are running
|
| 19 |
+
const psResult = await global.activeSandbox.runCommand({
|
| 20 |
+
cmd: 'ps',
|
| 21 |
+
args: ['aux']
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
let viteRunning = false;
|
| 25 |
+
const logContent: string[] = [];
|
| 26 |
+
|
| 27 |
+
if (psResult.exitCode === 0) {
|
| 28 |
+
const psOutput = await psResult.stdout();
|
| 29 |
+
const viteProcesses = psOutput.split('\n').filter((line: string) =>
|
| 30 |
+
line.toLowerCase().includes('vite') ||
|
| 31 |
+
line.toLowerCase().includes('npm run dev')
|
| 32 |
+
);
|
| 33 |
+
|
| 34 |
+
viteRunning = viteProcesses.length > 0;
|
| 35 |
+
|
| 36 |
+
if (viteRunning) {
|
| 37 |
+
logContent.push("Vite is running");
|
| 38 |
+
logContent.push(...viteProcesses.slice(0, 3)); // Show first 3 processes
|
| 39 |
+
} else {
|
| 40 |
+
logContent.push("Vite process not found");
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Try to read any recent log files
|
| 45 |
+
try {
|
| 46 |
+
const findResult = await global.activeSandbox.runCommand({
|
| 47 |
+
cmd: 'find',
|
| 48 |
+
args: ['/tmp', '-name', '*vite*', '-name', '*.log', '-type', 'f']
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
if (findResult.exitCode === 0) {
|
| 52 |
+
const logFiles = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
| 53 |
+
|
| 54 |
+
for (const logFile of logFiles.slice(0, 2)) {
|
| 55 |
+
try {
|
| 56 |
+
const catResult = await global.activeSandbox.runCommand({
|
| 57 |
+
cmd: 'tail',
|
| 58 |
+
args: ['-n', '10', logFile]
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
if (catResult.exitCode === 0) {
|
| 62 |
+
const logFileContent = await catResult.stdout();
|
| 63 |
+
logContent.push(`--- ${logFile} ---`);
|
| 64 |
+
logContent.push(logFileContent);
|
| 65 |
+
}
|
| 66 |
+
} catch {
|
| 67 |
+
// Skip if can't read log file
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
} catch {
|
| 72 |
+
// No log files found, that's OK
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
return NextResponse.json({
|
| 76 |
+
success: true,
|
| 77 |
+
hasErrors: false,
|
| 78 |
+
logs: logContent,
|
| 79 |
+
status: viteRunning ? 'running' : 'stopped'
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
} catch (error) {
|
| 83 |
+
console.error('[sandbox-logs] Error:', error);
|
| 84 |
+
return NextResponse.json({
|
| 85 |
+
success: false,
|
| 86 |
+
error: (error as Error).message
|
| 87 |
+
}, { status: 500 });
|
| 88 |
+
}
|
| 89 |
+
}
|
app/api/sandbox-status/route.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
| 3 |
+
|
| 4 |
+
declare global {
|
| 5 |
+
var activeSandboxProvider: any;
|
| 6 |
+
var sandboxData: any;
|
| 7 |
+
var existingFiles: Set<string>;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export async function GET() {
|
| 11 |
+
try {
|
| 12 |
+
// Check sandbox manager first, then fall back to global state
|
| 13 |
+
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
|
| 14 |
+
const sandboxExists = !!provider;
|
| 15 |
+
|
| 16 |
+
let sandboxHealthy = false;
|
| 17 |
+
let sandboxInfo = null;
|
| 18 |
+
|
| 19 |
+
if (sandboxExists && provider) {
|
| 20 |
+
try {
|
| 21 |
+
// Check if sandbox is healthy by getting its info
|
| 22 |
+
const providerInfo = provider.getSandboxInfo();
|
| 23 |
+
sandboxHealthy = !!providerInfo;
|
| 24 |
+
|
| 25 |
+
sandboxInfo = {
|
| 26 |
+
sandboxId: providerInfo?.sandboxId || global.sandboxData?.sandboxId,
|
| 27 |
+
url: providerInfo?.url || global.sandboxData?.url,
|
| 28 |
+
filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [],
|
| 29 |
+
lastHealthCheck: new Date().toISOString()
|
| 30 |
+
};
|
| 31 |
+
} catch (error) {
|
| 32 |
+
console.error('[sandbox-status] Health check failed:', error);
|
| 33 |
+
sandboxHealthy = false;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
return NextResponse.json({
|
| 38 |
+
success: true,
|
| 39 |
+
active: sandboxExists,
|
| 40 |
+
healthy: sandboxHealthy,
|
| 41 |
+
sandboxData: sandboxInfo,
|
| 42 |
+
message: sandboxHealthy
|
| 43 |
+
? 'Sandbox is active and healthy'
|
| 44 |
+
: sandboxExists
|
| 45 |
+
? 'Sandbox exists but is not responding'
|
| 46 |
+
: 'No active sandbox'
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
} catch (error) {
|
| 50 |
+
console.error('[sandbox-status] Error:', error);
|
| 51 |
+
return NextResponse.json({
|
| 52 |
+
success: false,
|
| 53 |
+
active: false,
|
| 54 |
+
error: (error as Error).message
|
| 55 |
+
}, { status: 500 });
|
| 56 |
+
}
|
| 57 |
+
}
|
app/api/scrape-screenshot/route.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
import FirecrawlApp from '@mendable/firecrawl-js';
|
| 3 |
+
|
| 4 |
+
export async function POST(req: NextRequest) {
|
| 5 |
+
try {
|
| 6 |
+
const { url } = await req.json();
|
| 7 |
+
|
| 8 |
+
if (!url) {
|
| 9 |
+
return NextResponse.json({ error: 'URL is required' }, { status: 400 });
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
// Initialize Firecrawl with API key from environment
|
| 13 |
+
const apiKey = process.env.FIRECRAWL_API_KEY;
|
| 14 |
+
|
| 15 |
+
if (!apiKey) {
|
| 16 |
+
console.error("FIRECRAWL_API_KEY not configured");
|
| 17 |
+
return NextResponse.json({
|
| 18 |
+
error: 'Firecrawl API key not configured'
|
| 19 |
+
}, { status: 500 });
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const app = new FirecrawlApp({ apiKey });
|
| 23 |
+
|
| 24 |
+
console.log('[scrape-screenshot] Attempting to capture screenshot for:', url);
|
| 25 |
+
console.log('[scrape-screenshot] Using Firecrawl API key:', apiKey ? 'Present' : 'Missing');
|
| 26 |
+
|
| 27 |
+
// Use the new v4 scrape method (not scrapeUrl)
|
| 28 |
+
const scrapeResult = await app.scrape(url, {
|
| 29 |
+
formats: ['screenshot'], // Request screenshot format
|
| 30 |
+
waitFor: 3000, // Wait for page to fully load
|
| 31 |
+
timeout: 30000,
|
| 32 |
+
onlyMainContent: false, // Get full page for screenshot
|
| 33 |
+
actions: [
|
| 34 |
+
{
|
| 35 |
+
type: 'wait',
|
| 36 |
+
milliseconds: 2000 // Additional wait for dynamic content
|
| 37 |
+
}
|
| 38 |
+
]
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
console.log('[scrape-screenshot] Full scrape result:', JSON.stringify(scrapeResult, null, 2));
|
| 42 |
+
console.log('[scrape-screenshot] Scrape result type:', typeof scrapeResult);
|
| 43 |
+
console.log('[scrape-screenshot] Scrape result keys:', Object.keys(scrapeResult));
|
| 44 |
+
|
| 45 |
+
// The Firecrawl v4 API might return data directly without a success flag
|
| 46 |
+
// Check if we have data with screenshot
|
| 47 |
+
if (scrapeResult && scrapeResult.screenshot) {
|
| 48 |
+
// Direct screenshot response
|
| 49 |
+
return NextResponse.json({
|
| 50 |
+
success: true,
|
| 51 |
+
screenshot: scrapeResult.screenshot,
|
| 52 |
+
metadata: scrapeResult.metadata || {}
|
| 53 |
+
});
|
| 54 |
+
} else if ((scrapeResult as any)?.data?.screenshot) {
|
| 55 |
+
// Nested data structure
|
| 56 |
+
return NextResponse.json({
|
| 57 |
+
success: true,
|
| 58 |
+
screenshot: (scrapeResult as any).data.screenshot,
|
| 59 |
+
metadata: (scrapeResult as any).data.metadata || {}
|
| 60 |
+
});
|
| 61 |
+
} else if ((scrapeResult as any)?.success === false) {
|
| 62 |
+
// Explicit failure
|
| 63 |
+
console.error('[scrape-screenshot] Firecrawl API error:', (scrapeResult as any).error);
|
| 64 |
+
throw new Error((scrapeResult as any).error || 'Failed to capture screenshot');
|
| 65 |
+
} else {
|
| 66 |
+
// No screenshot in response
|
| 67 |
+
console.error('[scrape-screenshot] No screenshot in response. Full response:', JSON.stringify(scrapeResult, null, 2));
|
| 68 |
+
throw new Error('Screenshot not available in response - check console for full response structure');
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
} catch (error: any) {
|
| 72 |
+
console.error('[scrape-screenshot] Screenshot capture error:', error);
|
| 73 |
+
console.error('[scrape-screenshot] Error stack:', error.stack);
|
| 74 |
+
|
| 75 |
+
// Provide fallback response for development - removed NODE_ENV check as it doesn't work in Next.js production builds
|
| 76 |
+
|
| 77 |
+
return NextResponse.json({
|
| 78 |
+
error: error.message || 'Failed to capture screenshot'
|
| 79 |
+
}, { status: 500 });
|
| 80 |
+
}
|
| 81 |
+
}
|
app/api/scrape-url-enhanced/route.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
// Function to sanitize smart quotes and other problematic characters
|
| 4 |
+
function sanitizeQuotes(text: string): string {
|
| 5 |
+
return text
|
| 6 |
+
// Replace smart single quotes
|
| 7 |
+
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
|
| 8 |
+
// Replace smart double quotes
|
| 9 |
+
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
| 10 |
+
// Replace other quote-like characters
|
| 11 |
+
.replace(/[\u00AB\u00BB]/g, '"') // Guillemets
|
| 12 |
+
.replace(/[\u2039\u203A]/g, "'") // Single guillemets
|
| 13 |
+
// Replace other problematic characters
|
| 14 |
+
.replace(/[\u2013\u2014]/g, '-') // En dash and em dash
|
| 15 |
+
.replace(/[\u2026]/g, '...') // Ellipsis
|
| 16 |
+
.replace(/[\u00A0]/g, ' '); // Non-breaking space
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export async function POST(request: NextRequest) {
|
| 20 |
+
try {
|
| 21 |
+
const { url } = await request.json();
|
| 22 |
+
|
| 23 |
+
if (!url) {
|
| 24 |
+
return NextResponse.json({
|
| 25 |
+
success: false,
|
| 26 |
+
error: 'URL is required'
|
| 27 |
+
}, { status: 400 });
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
console.log('[scrape-url-enhanced] Scraping with Firecrawl:', url);
|
| 31 |
+
|
| 32 |
+
const FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY;
|
| 33 |
+
if (!FIRECRAWL_API_KEY) {
|
| 34 |
+
throw new Error('FIRECRAWL_API_KEY environment variable is not set');
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Make request to Firecrawl API with maxAge for 500% faster scraping
|
| 38 |
+
const firecrawlResponse = await fetch('https://api.firecrawl.dev/v1/scrape', {
|
| 39 |
+
method: 'POST',
|
| 40 |
+
headers: {
|
| 41 |
+
'Authorization': `Bearer ${FIRECRAWL_API_KEY}`,
|
| 42 |
+
'Content-Type': 'application/json'
|
| 43 |
+
},
|
| 44 |
+
body: JSON.stringify({
|
| 45 |
+
url,
|
| 46 |
+
formats: ['markdown', 'html', 'screenshot'],
|
| 47 |
+
waitFor: 3000,
|
| 48 |
+
timeout: 30000,
|
| 49 |
+
blockAds: true,
|
| 50 |
+
maxAge: 3600000, // Use cached data if less than 1 hour old (500% faster!)
|
| 51 |
+
actions: [
|
| 52 |
+
{
|
| 53 |
+
type: 'wait',
|
| 54 |
+
milliseconds: 2000
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
type: 'screenshot',
|
| 58 |
+
fullPage: false // Just visible viewport for performance
|
| 59 |
+
}
|
| 60 |
+
]
|
| 61 |
+
})
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
if (!firecrawlResponse.ok) {
|
| 65 |
+
const error = await firecrawlResponse.text();
|
| 66 |
+
throw new Error(`Firecrawl API error: ${error}`);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const data = await firecrawlResponse.json();
|
| 70 |
+
|
| 71 |
+
if (!data.success || !data.data) {
|
| 72 |
+
throw new Error('Failed to scrape content');
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
const { markdown, metadata, screenshot, actions } = data.data;
|
| 76 |
+
// html available but not used in current implementation
|
| 77 |
+
|
| 78 |
+
// Get screenshot from either direct field or actions result
|
| 79 |
+
const screenshotUrl = screenshot || actions?.screenshots?.[0] || null;
|
| 80 |
+
|
| 81 |
+
// Sanitize the markdown content
|
| 82 |
+
const sanitizedMarkdown = sanitizeQuotes(markdown || '');
|
| 83 |
+
|
| 84 |
+
// Extract structured data from the response
|
| 85 |
+
const title = metadata?.title || '';
|
| 86 |
+
const description = metadata?.description || '';
|
| 87 |
+
|
| 88 |
+
// Format content for AI
|
| 89 |
+
const formattedContent = `
|
| 90 |
+
Title: ${sanitizeQuotes(title)}
|
| 91 |
+
Description: ${sanitizeQuotes(description)}
|
| 92 |
+
URL: ${url}
|
| 93 |
+
|
| 94 |
+
Main Content:
|
| 95 |
+
${sanitizedMarkdown}
|
| 96 |
+
`.trim();
|
| 97 |
+
|
| 98 |
+
return NextResponse.json({
|
| 99 |
+
success: true,
|
| 100 |
+
url,
|
| 101 |
+
content: formattedContent,
|
| 102 |
+
screenshot: screenshotUrl,
|
| 103 |
+
structured: {
|
| 104 |
+
title: sanitizeQuotes(title),
|
| 105 |
+
description: sanitizeQuotes(description),
|
| 106 |
+
content: sanitizedMarkdown,
|
| 107 |
+
url,
|
| 108 |
+
screenshot: screenshotUrl
|
| 109 |
+
},
|
| 110 |
+
metadata: {
|
| 111 |
+
scraper: 'firecrawl-enhanced',
|
| 112 |
+
timestamp: new Date().toISOString(),
|
| 113 |
+
contentLength: formattedContent.length,
|
| 114 |
+
cached: data.data.cached || false, // Indicates if data came from cache
|
| 115 |
+
...metadata
|
| 116 |
+
},
|
| 117 |
+
message: 'URL scraped successfully with Firecrawl (with caching for 500% faster performance)'
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
} catch (error) {
|
| 121 |
+
console.error('[scrape-url-enhanced] Error:', error);
|
| 122 |
+
return NextResponse.json({
|
| 123 |
+
success: false,
|
| 124 |
+
error: (error as Error).message
|
| 125 |
+
}, { status: 500 });
|
| 126 |
+
}
|
| 127 |
+
}
|
app/api/scrape-website/route.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import FirecrawlApp from '@mendable/firecrawl-js';
|
| 3 |
+
|
| 4 |
+
export async function POST(request: NextRequest) {
|
| 5 |
+
try {
|
| 6 |
+
const { url, formats = ['markdown', 'html'], options = {} } = await request.json();
|
| 7 |
+
|
| 8 |
+
if (!url) {
|
| 9 |
+
return NextResponse.json(
|
| 10 |
+
{ error: "URL is required" },
|
| 11 |
+
{ status: 400 }
|
| 12 |
+
);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
// Initialize Firecrawl with API key from environment
|
| 16 |
+
const apiKey = process.env.FIRECRAWL_API_KEY;
|
| 17 |
+
|
| 18 |
+
if (!apiKey) {
|
| 19 |
+
console.error("FIRECRAWL_API_KEY not configured");
|
| 20 |
+
// For demo purposes, return mock data if API key is not set
|
| 21 |
+
return NextResponse.json({
|
| 22 |
+
success: true,
|
| 23 |
+
data: {
|
| 24 |
+
title: "Example Website",
|
| 25 |
+
content: `This is a mock response for ${url}. Configure FIRECRAWL_API_KEY to enable real scraping.`,
|
| 26 |
+
description: "A sample website",
|
| 27 |
+
markdown: `# Example Website\n\nThis is mock content for demonstration purposes.`,
|
| 28 |
+
html: `<h1>Example Website</h1><p>This is mock content for demonstration purposes.</p>`,
|
| 29 |
+
metadata: {
|
| 30 |
+
title: "Example Website",
|
| 31 |
+
description: "A sample website",
|
| 32 |
+
sourceURL: url,
|
| 33 |
+
statusCode: 200
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
});
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
const app = new FirecrawlApp({ apiKey });
|
| 40 |
+
|
| 41 |
+
// Scrape the website using the latest SDK patterns
|
| 42 |
+
// Include screenshot if requested in formats
|
| 43 |
+
const scrapeResult = await app.scrape(url, {
|
| 44 |
+
formats: formats,
|
| 45 |
+
onlyMainContent: options.onlyMainContent !== false, // Default to true for cleaner content
|
| 46 |
+
waitFor: options.waitFor || 2000, // Wait for dynamic content
|
| 47 |
+
timeout: options.timeout || 30000,
|
| 48 |
+
...options // Pass through any additional options
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
// Handle the response according to the latest SDK structure
|
| 52 |
+
const result = scrapeResult as any;
|
| 53 |
+
if (result.success === false) {
|
| 54 |
+
throw new Error(result.error || "Failed to scrape website");
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// The SDK may return data directly or nested
|
| 58 |
+
const data = result.data || result;
|
| 59 |
+
|
| 60 |
+
return NextResponse.json({
|
| 61 |
+
success: true,
|
| 62 |
+
data: {
|
| 63 |
+
title: data?.metadata?.title || "Untitled",
|
| 64 |
+
content: data?.markdown || data?.html || "",
|
| 65 |
+
description: data?.metadata?.description || "",
|
| 66 |
+
markdown: data?.markdown || "",
|
| 67 |
+
html: data?.html || "",
|
| 68 |
+
metadata: data?.metadata || {},
|
| 69 |
+
screenshot: data?.screenshot || null,
|
| 70 |
+
links: data?.links || [],
|
| 71 |
+
// Include raw data for flexibility
|
| 72 |
+
raw: data
|
| 73 |
+
}
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
} catch (error) {
|
| 77 |
+
console.error("Error scraping website:", error);
|
| 78 |
+
|
| 79 |
+
// Return a more detailed error response
|
| 80 |
+
return NextResponse.json({
|
| 81 |
+
success: false,
|
| 82 |
+
error: error instanceof Error ? error.message : "Failed to scrape website",
|
| 83 |
+
// Provide mock data as fallback for development
|
| 84 |
+
data: {
|
| 85 |
+
title: "Example Website",
|
| 86 |
+
content: "This is fallback content due to an error. Please check your configuration.",
|
| 87 |
+
description: "Error occurred while scraping",
|
| 88 |
+
markdown: `# Error\n\n${error instanceof Error ? error.message : 'Unknown error occurred'}`,
|
| 89 |
+
html: `<h1>Error</h1><p>${error instanceof Error ? error.message : 'Unknown error occurred'}</p>`,
|
| 90 |
+
metadata: {
|
| 91 |
+
title: "Error",
|
| 92 |
+
description: "Failed to scrape website",
|
| 93 |
+
statusCode: 500
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
}, { status: 500 });
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Optional: Add OPTIONS handler for CORS if needed
|
| 101 |
+
export async function OPTIONS() {
|
| 102 |
+
return new NextResponse(null, {
|
| 103 |
+
status: 200,
|
| 104 |
+
headers: {
|
| 105 |
+
'Access-Control-Allow-Origin': '*',
|
| 106 |
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
| 107 |
+
'Access-Control-Allow-Headers': 'Content-Type',
|
| 108 |
+
},
|
| 109 |
+
});
|
| 110 |
+
}
|
app/api/search/route.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
export async function POST(req: NextRequest) {
|
| 4 |
+
try {
|
| 5 |
+
const { query } = await req.json();
|
| 6 |
+
|
| 7 |
+
if (!query) {
|
| 8 |
+
return NextResponse.json({ error: 'Query is required' }, { status: 400 });
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
// Use Firecrawl search to get top 10 results with screenshots
|
| 12 |
+
const searchResponse = await fetch('https://api.firecrawl.dev/v1/search', {
|
| 13 |
+
method: 'POST',
|
| 14 |
+
headers: {
|
| 15 |
+
'Content-Type': 'application/json',
|
| 16 |
+
'Authorization': `Bearer ${process.env.FIRECRAWL_API_KEY}`,
|
| 17 |
+
},
|
| 18 |
+
body: JSON.stringify({
|
| 19 |
+
query,
|
| 20 |
+
limit: 10,
|
| 21 |
+
scrapeOptions: {
|
| 22 |
+
formats: ['markdown', 'screenshot'],
|
| 23 |
+
onlyMainContent: true,
|
| 24 |
+
},
|
| 25 |
+
}),
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
if (!searchResponse.ok) {
|
| 29 |
+
throw new Error('Search failed');
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const searchData = await searchResponse.json();
|
| 33 |
+
|
| 34 |
+
// Format results with screenshots and markdown
|
| 35 |
+
const results = searchData.data?.map((result: any) => ({
|
| 36 |
+
url: result.url,
|
| 37 |
+
title: result.title || result.url,
|
| 38 |
+
description: result.description || '',
|
| 39 |
+
screenshot: result.screenshot || null,
|
| 40 |
+
markdown: result.markdown || '',
|
| 41 |
+
})) || [];
|
| 42 |
+
|
| 43 |
+
return NextResponse.json({ results });
|
| 44 |
+
} catch (error) {
|
| 45 |
+
console.error('Search error:', error);
|
| 46 |
+
return NextResponse.json(
|
| 47 |
+
{ error: 'Failed to perform search' },
|
| 48 |
+
{ status: 500 }
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
}
|
app/api/text-generation/route.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import OpenAI from 'openai';
|
| 2 |
+
import { OpenAIStream, StreamingTextResponse } from 'ai';
|
| 3 |
+
|
| 4 |
+
// Create an OpenAI API client (that's edge friendly!)
|
| 5 |
+
const openai = new OpenAI({
|
| 6 |
+
apiKey: process.env.HELMHOLTZ_API_KEY || '',
|
| 7 |
+
baseURL: 'https://api.helmholtz-blablador.fz-juelich.de/v1',
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
// IMPORTANT! Set the runtime to edge
|
| 11 |
+
export const runtime = 'edge';
|
| 12 |
+
|
| 13 |
+
export async function POST(req: Request) {
|
| 14 |
+
const { prompt, model } = await req.json();
|
| 15 |
+
|
| 16 |
+
// Ask OpenAI for a streaming chat completion given the prompt
|
| 17 |
+
const response = await openai.chat.completions.create({
|
| 18 |
+
model: model || 'alias-code',
|
| 19 |
+
stream: true,
|
| 20 |
+
messages: [
|
| 21 |
+
{
|
| 22 |
+
role: 'user',
|
| 23 |
+
content: prompt,
|
| 24 |
+
},
|
| 25 |
+
],
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
// Convert the response into a friendly text-stream
|
| 29 |
+
const stream = OpenAIStream(response);
|
| 30 |
+
// Respond with the stream
|
| 31 |
+
return new StreamingTextResponse(stream);
|
| 32 |
+
}
|
app/builder/page.tsx
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from "react";
|
| 4 |
+
import { useRouter } from "next/navigation";
|
| 5 |
+
import { toast } from "sonner";
|
| 6 |
+
|
| 7 |
+
export default function BuilderPage() {
|
| 8 |
+
const [targetUrl, setTargetUrl] = useState<string>("");
|
| 9 |
+
const [selectedStyle, setSelectedStyle] = useState<string>("modern");
|
| 10 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 11 |
+
const [previewUrl, setPreviewUrl] = useState<string>("");
|
| 12 |
+
const [progress, setProgress] = useState<string>("Initializing...");
|
| 13 |
+
const [generatedCode, setGeneratedCode] = useState<string>("");
|
| 14 |
+
const router = useRouter();
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
// Get the URL and style from sessionStorage
|
| 18 |
+
const url = sessionStorage.getItem('targetUrl');
|
| 19 |
+
const style = sessionStorage.getItem('selectedStyle');
|
| 20 |
+
|
| 21 |
+
if (!url) {
|
| 22 |
+
router.push('/');
|
| 23 |
+
return;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
setTargetUrl(url);
|
| 27 |
+
setSelectedStyle(style || "modern");
|
| 28 |
+
|
| 29 |
+
// Start the website generation process
|
| 30 |
+
generateWebsite(url, style || "modern");
|
| 31 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 32 |
+
}, [router]);
|
| 33 |
+
|
| 34 |
+
const generateWebsite = async (url: string, style: string) => {
|
| 35 |
+
try {
|
| 36 |
+
setProgress("Analyzing website...");
|
| 37 |
+
|
| 38 |
+
// For demo purposes, we'll generate a simple HTML template
|
| 39 |
+
// In production, this would call the actual scraping and generation APIs
|
| 40 |
+
const mockGeneratedCode = `
|
| 41 |
+
<!DOCTYPE html>
|
| 42 |
+
<html lang="en">
|
| 43 |
+
<head>
|
| 44 |
+
<meta charset="UTF-8">
|
| 45 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 46 |
+
<title>${style} Website - Reimagined</title>
|
| 47 |
+
<style>
|
| 48 |
+
:root {
|
| 49 |
+
--primary: ${style === 'modern' ? '#FA5D19' : style === 'playful' ? '#9061ff' : style === 'professional' ? '#2a6dfb' : '#eb3424'};
|
| 50 |
+
--background: ${style === 'modern' ? '#ffffff' : style === 'playful' ? '#f9f9f9' : style === 'professional' ? '#f5f5f5' : '#fafafa'};
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
* {
|
| 54 |
+
margin: 0;
|
| 55 |
+
padding: 0;
|
| 56 |
+
box-sizing: border-box;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
body {
|
| 60 |
+
font-family: system-ui, -apple-system, sans-serif;
|
| 61 |
+
background: var(--background);
|
| 62 |
+
color: #262626;
|
| 63 |
+
line-height: 1.6;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
header {
|
| 67 |
+
background: white;
|
| 68 |
+
border-bottom: 1px solid #ededed;
|
| 69 |
+
padding: 2rem;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
nav {
|
| 73 |
+
max-width: 1200px;
|
| 74 |
+
margin: 0 auto;
|
| 75 |
+
display: flex;
|
| 76 |
+
justify-content: space-between;
|
| 77 |
+
align-items: center;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.logo {
|
| 81 |
+
font-size: 1.5rem;
|
| 82 |
+
font-weight: bold;
|
| 83 |
+
color: var(--primary);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
main {
|
| 87 |
+
max-width: 1200px;
|
| 88 |
+
margin: 4rem auto;
|
| 89 |
+
padding: 0 2rem;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.hero {
|
| 93 |
+
text-align: center;
|
| 94 |
+
margin-bottom: 4rem;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
h1 {
|
| 98 |
+
font-size: 3rem;
|
| 99 |
+
margin-bottom: 1rem;
|
| 100 |
+
background: linear-gradient(135deg, var(--primary), #262626);
|
| 101 |
+
-webkit-background-clip: text;
|
| 102 |
+
-webkit-text-fill-color: transparent;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.subtitle {
|
| 106 |
+
font-size: 1.25rem;
|
| 107 |
+
color: #666;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.cta-button {
|
| 111 |
+
display: inline-block;
|
| 112 |
+
margin-top: 2rem;
|
| 113 |
+
padding: 1rem 2rem;
|
| 114 |
+
background: var(--primary);
|
| 115 |
+
color: white;
|
| 116 |
+
text-decoration: none;
|
| 117 |
+
border-radius: 0.5rem;
|
| 118 |
+
transition: transform 0.2s;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.cta-button:hover {
|
| 122 |
+
transform: scale(1.05);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.features {
|
| 126 |
+
display: grid;
|
| 127 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 128 |
+
gap: 2rem;
|
| 129 |
+
margin-top: 4rem;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.feature {
|
| 133 |
+
padding: 2rem;
|
| 134 |
+
background: white;
|
| 135 |
+
border-radius: 1rem;
|
| 136 |
+
border: 1px solid #ededed;
|
| 137 |
+
transition: box-shadow 0.2s;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.feature:hover {
|
| 141 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.feature h3 {
|
| 145 |
+
margin-bottom: 1rem;
|
| 146 |
+
color: var(--primary);
|
| 147 |
+
}
|
| 148 |
+
</style>
|
| 149 |
+
</head>
|
| 150 |
+
<body>
|
| 151 |
+
<header>
|
| 152 |
+
<nav>
|
| 153 |
+
<div class="logo">Reimagined</div>
|
| 154 |
+
<div>
|
| 155 |
+
<a href="#features" style="margin-right: 2rem; color: #666; text-decoration: none;">Features</a>
|
| 156 |
+
<a href="#about" style="margin-right: 2rem; color: #666; text-decoration: none;">About</a>
|
| 157 |
+
<a href="#contact" style="color: #666; text-decoration: none;">Contact</a>
|
| 158 |
+
</div>
|
| 159 |
+
</nav>
|
| 160 |
+
</header>
|
| 161 |
+
|
| 162 |
+
<main>
|
| 163 |
+
<div class="hero">
|
| 164 |
+
<h1>Welcome to Your ${style === 'modern' ? 'Modern' : style === 'playful' ? 'Playful' : style === 'professional' ? 'Professional' : 'Artistic'} Website</h1>
|
| 165 |
+
<p class="subtitle">Reimagined from ${url}</p>
|
| 166 |
+
<a href="#" class="cta-button">Get Started</a>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div class="features" id="features">
|
| 170 |
+
<div class="feature">
|
| 171 |
+
<h3>Fast</h3>
|
| 172 |
+
<p>Lightning-fast performance optimized for modern web standards.</p>
|
| 173 |
+
</div>
|
| 174 |
+
<div class="feature">
|
| 175 |
+
<h3>Responsive</h3>
|
| 176 |
+
<p>Looks great on all devices, from mobile to desktop.</p>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="feature">
|
| 179 |
+
<h3>Beautiful</h3>
|
| 180 |
+
<p>Stunning design that captures attention and drives engagement.</p>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</main>
|
| 184 |
+
</body>
|
| 185 |
+
</html>`;
|
| 186 |
+
|
| 187 |
+
setGeneratedCode(mockGeneratedCode);
|
| 188 |
+
|
| 189 |
+
// Create a blob URL for the preview
|
| 190 |
+
const blob = new Blob([mockGeneratedCode], { type: 'text/html' });
|
| 191 |
+
const blobUrl = URL.createObjectURL(blob);
|
| 192 |
+
setPreviewUrl(blobUrl);
|
| 193 |
+
|
| 194 |
+
setProgress("Website ready!");
|
| 195 |
+
setIsLoading(false);
|
| 196 |
+
|
| 197 |
+
// Show success message
|
| 198 |
+
toast.success("Website generated successfully!");
|
| 199 |
+
|
| 200 |
+
} catch (error) {
|
| 201 |
+
console.error("Error generating website:", error);
|
| 202 |
+
toast.error("Failed to generate website. Please try again.");
|
| 203 |
+
setProgress("Error occurred");
|
| 204 |
+
setTimeout(() => router.push('/'), 2000);
|
| 205 |
+
}
|
| 206 |
+
};
|
| 207 |
+
|
| 208 |
+
const downloadCode = () => {
|
| 209 |
+
const blob = new Blob([generatedCode], { type: 'text/html' });
|
| 210 |
+
const url = URL.createObjectURL(blob);
|
| 211 |
+
const a = document.createElement('a');
|
| 212 |
+
a.href = url;
|
| 213 |
+
a.download = 'website.html';
|
| 214 |
+
document.body.appendChild(a);
|
| 215 |
+
a.click();
|
| 216 |
+
document.body.removeChild(a);
|
| 217 |
+
URL.revokeObjectURL(url);
|
| 218 |
+
toast.success("Code downloaded!");
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
return (
|
| 222 |
+
<div className="min-h-screen bg-background-base">
|
| 223 |
+
<div className="flex h-screen">
|
| 224 |
+
{/* Sidebar */}
|
| 225 |
+
<div className="w-80 bg-white border-r border-border-faint p-24 flex flex-col">
|
| 226 |
+
<h2 className="text-title-small font-semibold mb-16">Building Your Website</h2>
|
| 227 |
+
|
| 228 |
+
<div className="space-y-12 flex-1">
|
| 229 |
+
<div>
|
| 230 |
+
<div className="text-label-small text-black-alpha-56 mb-4">Target URL</div>
|
| 231 |
+
<div className="text-body-medium text-accent-black truncate">{targetUrl}</div>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<div>
|
| 235 |
+
<div className="text-label-small text-black-alpha-56 mb-4">Style</div>
|
| 236 |
+
<div className="text-body-medium text-accent-black capitalize">{selectedStyle}</div>
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
<div>
|
| 240 |
+
<div className="text-label-small text-black-alpha-56 mb-4">Status</div>
|
| 241 |
+
<div className="text-body-medium text-heat-100">{progress}</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<div className="space-y-8">
|
| 246 |
+
{!isLoading && (
|
| 247 |
+
<button
|
| 248 |
+
onClick={downloadCode}
|
| 249 |
+
className="w-full py-12 px-16 bg-heat-100 hover:bg-heat-200 text-white rounded-10 text-label-medium transition-all"
|
| 250 |
+
>
|
| 251 |
+
Download Code
|
| 252 |
+
</button>
|
| 253 |
+
)}
|
| 254 |
+
|
| 255 |
+
<button
|
| 256 |
+
onClick={() => router.push('/')}
|
| 257 |
+
className="w-full py-12 px-16 bg-black-alpha-4 hover:bg-black-alpha-6 rounded-10 text-label-medium transition-all"
|
| 258 |
+
>
|
| 259 |
+
Start Over
|
| 260 |
+
</button>
|
| 261 |
+
</div>
|
| 262 |
+
</div>
|
| 263 |
+
|
| 264 |
+
{/* Preview */}
|
| 265 |
+
<div className="flex-1 bg-gray-50">
|
| 266 |
+
{isLoading ? (
|
| 267 |
+
<div className="flex items-center justify-center h-full">
|
| 268 |
+
<div className="text-center">
|
| 269 |
+
<div className="w-48 h-48 border-4 border-heat-100 border-t-transparent rounded-full animate-spin mb-16 mx-auto"></div>
|
| 270 |
+
<p className="text-body-large text-black-alpha-56">{progress}</p>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
) : (
|
| 274 |
+
previewUrl && (
|
| 275 |
+
<iframe
|
| 276 |
+
src={previewUrl}
|
| 277 |
+
className="w-full h-full border-0"
|
| 278 |
+
title="Website Preview"
|
| 279 |
+
/>
|
| 280 |
+
)
|
| 281 |
+
)}
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
);
|
| 286 |
+
}
|
app/favicon.ico
ADDED
|
|
app/fonts/GeistMonoVF.woff
ADDED
|
Binary file (58.1 kB). View file
|
|
|
app/fonts/GeistVF.woff
ADDED
|
Binary file (58.5 kB). View file
|
|
|
app/generation/page.tsx
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app/globals.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "../styles/main.css";
|
app/landing.tsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
// useState not currently used but kept for future interactivity
|
| 4 |
+
import Link from "next/link";
|
| 5 |
+
|
| 6 |
+
// Import shared components
|
| 7 |
+
import { HeaderProvider } from "@/components/shared/header/HeaderContext";
|
| 8 |
+
// import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit"; // Not used in current implementation
|
| 9 |
+
import HeaderWrapper from "@/components/shared/header/Wrapper/Wrapper";
|
| 10 |
+
import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/Wrapper";
|
| 11 |
+
import ButtonUI from "@/components/ui/shadcn/button";
|
| 12 |
+
|
| 13 |
+
// Import hero section components
|
| 14 |
+
import HomeHeroBackground from "@/components/app/(home)/sections/hero/Background/Background";
|
| 15 |
+
import { BackgroundOuterPiece } from "@/components/app/(home)/sections/hero/Background/BackgroundOuterPiece";
|
| 16 |
+
import HomeHeroBadge from "@/components/app/(home)/sections/hero/Badge/Badge";
|
| 17 |
+
import HomeHeroPixi from "@/components/app/(home)/sections/hero/Pixi/Pixi";
|
| 18 |
+
import HomeHeroTitle from "@/components/app/(home)/sections/hero/Title/Title";
|
| 19 |
+
import HeroInput from "@/components/app/(home)/sections/hero-input/HeroInput";
|
| 20 |
+
import { Connector } from "@/components/shared/layout/curvy-rect";
|
| 21 |
+
import HeroFlame from "@/components/shared/effects/flame/hero-flame";
|
| 22 |
+
import FirecrawlIcon from "@/components/FirecrawlIcon";
|
| 23 |
+
import FirecrawlLogo from "@/components/FirecrawlLogo";
|
| 24 |
+
|
| 25 |
+
export default function LandingPage() {
|
| 26 |
+
return (
|
| 27 |
+
<HeaderProvider>
|
| 28 |
+
<div className="min-h-screen bg-background-base">
|
| 29 |
+
{/* Header/Navigation Section */}
|
| 30 |
+
<HeaderDropdownWrapper />
|
| 31 |
+
|
| 32 |
+
<div className="sticky top-0 left-0 w-full z-[101] bg-background-base header">
|
| 33 |
+
<div className="absolute top-0 cmw-container border-x border-border-faint h-full pointer-events-none" />
|
| 34 |
+
<div className="h-1 bg-border-faint w-full left-0 -bottom-1 absolute" />
|
| 35 |
+
|
| 36 |
+
<div className="cmw-container absolute h-full pointer-events-none top-0">
|
| 37 |
+
<Connector className="absolute -left-[10.5px] -bottom-11" />
|
| 38 |
+
<Connector className="absolute -right-[10.5px] -bottom-11" />
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<HeaderWrapper>
|
| 42 |
+
<div className="max-w-[900px] mx-auto w-full flex justify-between items-center">
|
| 43 |
+
<div className="flex gap-24 items-center">
|
| 44 |
+
<Link href="/" className="flex items-center gap-2">
|
| 45 |
+
<FirecrawlIcon className="w-7 h-7 text-accent-black" />
|
| 46 |
+
<FirecrawlLogo />
|
| 47 |
+
</Link>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<div className="flex gap-8">
|
| 51 |
+
<Link
|
| 52 |
+
href="https://github.com/mendableai/open-lovable"
|
| 53 |
+
target="_blank"
|
| 54 |
+
className="contents"
|
| 55 |
+
>
|
| 56 |
+
<ButtonUI variant="tertiary">
|
| 57 |
+
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
| 58 |
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
| 59 |
+
</svg>
|
| 60 |
+
Use this Template
|
| 61 |
+
</ButtonUI>
|
| 62 |
+
</Link>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
</HeaderWrapper>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
{/* Hero Section */}
|
| 69 |
+
<section className="overflow-x-clip" id="home-hero">
|
| 70 |
+
<div className="pt-28 lg:pt-254 lg:-mt-100 pb-115 relative" id="hero-content">
|
| 71 |
+
<HomeHeroPixi />
|
| 72 |
+
<HeroFlame />
|
| 73 |
+
<BackgroundOuterPiece />
|
| 74 |
+
<HomeHeroBackground />
|
| 75 |
+
|
| 76 |
+
<div className="relative container px-16">
|
| 77 |
+
<HomeHeroBadge />
|
| 78 |
+
<HomeHeroTitle />
|
| 79 |
+
|
| 80 |
+
{/* Hero Input */}
|
| 81 |
+
<div className="mt-24">
|
| 82 |
+
<HeroInput />
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
</section>
|
| 87 |
+
</div>
|
| 88 |
+
</HeaderProvider>
|
| 89 |
+
);
|
| 90 |
+
}
|
app/layout.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Inter, Roboto_Mono } from "next/font/google";
|
| 3 |
+
import localFont from "next/font/local";
|
| 4 |
+
import "./globals.css";
|
| 5 |
+
|
| 6 |
+
const inter = Inter({
|
| 7 |
+
subsets: ["latin"],
|
| 8 |
+
variable: "--font-inter"
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
const geistSans = localFont({
|
| 12 |
+
src: "./fonts/GeistVF.woff",
|
| 13 |
+
variable: "--font-geist-sans",
|
| 14 |
+
weight: "100 900",
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
const geistMono = localFont({
|
| 18 |
+
src: "./fonts/GeistMonoVF.woff",
|
| 19 |
+
variable: "--font-geist-mono",
|
| 20 |
+
weight: "100 900",
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
const robotoMono = Roboto_Mono({
|
| 24 |
+
subsets: ["latin"],
|
| 25 |
+
variable: "--font-roboto-mono",
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
export const metadata: Metadata = {
|
| 29 |
+
title: "Open Lovable v3",
|
| 30 |
+
description: "Re-imagine any website in seconds with AI-powered website builder.",
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
export default function RootLayout({
|
| 34 |
+
children,
|
| 35 |
+
}: Readonly<{
|
| 36 |
+
children: React.ReactNode;
|
| 37 |
+
}>) {
|
| 38 |
+
return (
|
| 39 |
+
<html lang="en">
|
| 40 |
+
<body className={`${inter.variable} ${geistSans.variable} ${geistMono.variable} ${robotoMono.variable} font-sans`}>
|
| 41 |
+
{children}
|
| 42 |
+
</body>
|
| 43 |
+
</html>
|
| 44 |
+
);
|
| 45 |
+
}
|
app/page.tsx
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import Link from "next/link";
|
| 4 |
+
import Image from "next/image";
|
| 5 |
+
import { useState } from "react";
|
| 6 |
+
import { useRouter } from "next/navigation";
|
| 7 |
+
import { appConfig } from '@/config/app.config';
|
| 8 |
+
import { toast } from "sonner";
|
| 9 |
+
|
| 10 |
+
// Import shared components
|
| 11 |
+
import { Connector } from "@/components/shared/layout/curvy-rect";
|
| 12 |
+
import HeroFlame from "@/components/shared/effects/flame/hero-flame";
|
| 13 |
+
import AsciiExplosion from "@/components/shared/effects/flame/ascii-explosion";
|
| 14 |
+
import { HeaderProvider } from "@/components/shared/header/HeaderContext";
|
| 15 |
+
|
| 16 |
+
// Import hero section components
|
| 17 |
+
import HomeHeroBackground from "@/components/app/(home)/sections/hero/Background/Background";
|
| 18 |
+
import { BackgroundOuterPiece } from "@/components/app/(home)/sections/hero/Background/BackgroundOuterPiece";
|
| 19 |
+
import HomeHeroBadge from "@/components/app/(home)/sections/hero/Badge/Badge";
|
| 20 |
+
import HomeHeroPixi from "@/components/app/(home)/sections/hero/Pixi/Pixi";
|
| 21 |
+
import HomeHeroTitle from "@/components/app/(home)/sections/hero/Title/Title";
|
| 22 |
+
import HeroInputSubmitButton from "@/components/app/(home)/sections/hero-input/Button/Button";
|
| 23 |
+
// import Globe from "@/components/app/(home)/sections/hero-input/_svg/Globe";
|
| 24 |
+
|
| 25 |
+
// Import header components
|
| 26 |
+
import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit";
|
| 27 |
+
import HeaderWrapper from "@/components/shared/header/Wrapper/Wrapper";
|
| 28 |
+
import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/Wrapper";
|
| 29 |
+
import GithubIcon from "@/components/shared/header/Github/_svg/GithubIcon";
|
| 30 |
+
import ButtonUI from "@/components/ui/shadcn/button"
|
| 31 |
+
|
| 32 |
+
interface SearchResult {
|
| 33 |
+
url: string;
|
| 34 |
+
title: string;
|
| 35 |
+
description: string;
|
| 36 |
+
screenshot: string | null;
|
| 37 |
+
markdown: string;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export default function HomePage() {
|
| 41 |
+
const [url, setUrl] = useState<string>("");
|
| 42 |
+
const [selectedStyle, setSelectedStyle] = useState<string>("1");
|
| 43 |
+
const [selectedModel, setSelectedModel] = useState<string>(appConfig.ai.defaultModel);
|
| 44 |
+
const [isValidUrl, setIsValidUrl] = useState<boolean>(false);
|
| 45 |
+
const [showSearchTiles, setShowSearchTiles] = useState<boolean>(false);
|
| 46 |
+
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
| 47 |
+
const [isSearching, setIsSearching] = useState<boolean>(false);
|
| 48 |
+
const [hasSearched, setHasSearched] = useState<boolean>(false);
|
| 49 |
+
const [isFadingOut, setIsFadingOut] = useState<boolean>(false);
|
| 50 |
+
const [showSelectMessage, setShowSelectMessage] = useState<boolean>(false);
|
| 51 |
+
const [showInstructionsForIndex, setShowInstructionsForIndex] = useState<number | null>(null);
|
| 52 |
+
const [additionalInstructions, setAdditionalInstructions] = useState<string>('');
|
| 53 |
+
const [extendBrandStyles, setExtendBrandStyles] = useState<boolean>(false);
|
| 54 |
+
const router = useRouter();
|
| 55 |
+
|
| 56 |
+
// Simple URL validation
|
| 57 |
+
const validateUrl = (urlString: string) => {
|
| 58 |
+
if (!urlString) return false;
|
| 59 |
+
// Basic URL pattern - accepts domains with or without protocol
|
| 60 |
+
const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
|
| 61 |
+
return urlPattern.test(urlString.toLowerCase());
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
// Check if input is a URL (contains a dot)
|
| 65 |
+
const isURL = (str: string): boolean => {
|
| 66 |
+
const urlPattern = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/;
|
| 67 |
+
return urlPattern.test(str.trim());
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
const styles = [
|
| 71 |
+
{ id: "1", name: "Glassmorphism", description: "Frosted glass effect" },
|
| 72 |
+
{ id: "2", name: "Neumorphism", description: "Soft 3D shadows" },
|
| 73 |
+
{ id: "3", name: "Brutalism", description: "Bold and raw" },
|
| 74 |
+
{ id: "4", name: "Minimalist", description: "Clean and simple" },
|
| 75 |
+
{ id: "5", name: "Dark Mode", description: "Dark theme design" },
|
| 76 |
+
{ id: "6", name: "Gradient Rich", description: "Vibrant gradients" },
|
| 77 |
+
{ id: "7", name: "3D Depth", description: "Dimensional layers" },
|
| 78 |
+
{ id: "8", name: "Retro Wave", description: "80s inspired" },
|
| 79 |
+
];
|
| 80 |
+
|
| 81 |
+
const models = appConfig.ai.availableModels.map(model => ({
|
| 82 |
+
id: model,
|
| 83 |
+
name: appConfig.ai.modelDisplayNames[model] || model,
|
| 84 |
+
}));
|
| 85 |
+
|
| 86 |
+
const handleSubmit = async (selectedResult?: SearchResult) => {
|
| 87 |
+
const inputValue = url.trim();
|
| 88 |
+
|
| 89 |
+
if (!inputValue) {
|
| 90 |
+
toast.error("Please enter a URL or search term");
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Validate brand extension mode requirements
|
| 95 |
+
if (extendBrandStyles && isURL(inputValue) && !additionalInstructions.trim()) {
|
| 96 |
+
toast.error("Please describe what you want to build with this brand's styles");
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// If it's a search result being selected, fade out and redirect
|
| 101 |
+
if (selectedResult) {
|
| 102 |
+
setIsFadingOut(true);
|
| 103 |
+
|
| 104 |
+
// Wait for fade animation
|
| 105 |
+
setTimeout(() => {
|
| 106 |
+
sessionStorage.setItem('targetUrl', selectedResult.url);
|
| 107 |
+
sessionStorage.setItem('selectedStyle', selectedStyle);
|
| 108 |
+
sessionStorage.setItem('selectedModel', selectedModel);
|
| 109 |
+
sessionStorage.setItem('autoStart', 'true');
|
| 110 |
+
if (selectedResult.markdown) {
|
| 111 |
+
sessionStorage.setItem('siteMarkdown', selectedResult.markdown);
|
| 112 |
+
}
|
| 113 |
+
router.push('/generation');
|
| 114 |
+
}, 500);
|
| 115 |
+
return;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// If it's a URL, check if we're extending brand styles or cloning
|
| 119 |
+
if (isURL(inputValue)) {
|
| 120 |
+
if (extendBrandStyles) {
|
| 121 |
+
// Brand extension mode - extract brand styles and use them with the prompt
|
| 122 |
+
sessionStorage.setItem('targetUrl', inputValue);
|
| 123 |
+
sessionStorage.setItem('selectedModel', selectedModel);
|
| 124 |
+
sessionStorage.setItem('autoStart', 'true');
|
| 125 |
+
sessionStorage.setItem('brandExtensionMode', 'true');
|
| 126 |
+
sessionStorage.setItem('brandExtensionPrompt', additionalInstructions || '');
|
| 127 |
+
router.push('/generation');
|
| 128 |
+
} else {
|
| 129 |
+
// Normal clone mode
|
| 130 |
+
sessionStorage.setItem('targetUrl', inputValue);
|
| 131 |
+
sessionStorage.setItem('selectedStyle', selectedStyle);
|
| 132 |
+
sessionStorage.setItem('selectedModel', selectedModel);
|
| 133 |
+
sessionStorage.setItem('autoStart', 'true');
|
| 134 |
+
router.push('/generation');
|
| 135 |
+
}
|
| 136 |
+
} else {
|
| 137 |
+
// It's a search term, fade out if results exist, then search
|
| 138 |
+
if (hasSearched && searchResults.length > 0) {
|
| 139 |
+
setIsFadingOut(true);
|
| 140 |
+
|
| 141 |
+
setTimeout(async () => {
|
| 142 |
+
setSearchResults([]);
|
| 143 |
+
setIsFadingOut(false);
|
| 144 |
+
setShowSelectMessage(true);
|
| 145 |
+
|
| 146 |
+
// Perform new search
|
| 147 |
+
await performSearch(inputValue);
|
| 148 |
+
setHasSearched(true);
|
| 149 |
+
setShowSearchTiles(true);
|
| 150 |
+
setShowSelectMessage(false);
|
| 151 |
+
|
| 152 |
+
// Smooth scroll to carousel
|
| 153 |
+
setTimeout(() => {
|
| 154 |
+
const carouselSection = document.querySelector('.carousel-section');
|
| 155 |
+
if (carouselSection) {
|
| 156 |
+
carouselSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 157 |
+
}
|
| 158 |
+
}, 300);
|
| 159 |
+
}, 500);
|
| 160 |
+
} else {
|
| 161 |
+
// First search, no fade needed
|
| 162 |
+
setShowSelectMessage(true);
|
| 163 |
+
setIsSearching(true);
|
| 164 |
+
setHasSearched(true);
|
| 165 |
+
setShowSearchTiles(true);
|
| 166 |
+
|
| 167 |
+
// Scroll to carousel area immediately
|
| 168 |
+
setTimeout(() => {
|
| 169 |
+
const carouselSection = document.querySelector('.carousel-section');
|
| 170 |
+
if (carouselSection) {
|
| 171 |
+
carouselSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 172 |
+
}
|
| 173 |
+
}, 100);
|
| 174 |
+
|
| 175 |
+
await performSearch(inputValue);
|
| 176 |
+
setShowSelectMessage(false);
|
| 177 |
+
setIsSearching(false);
|
| 178 |
+
|
| 179 |
+
// Smooth scroll to carousel
|
| 180 |
+
setTimeout(() => {
|
| 181 |
+
const carouselSection = document.querySelector('.carousel-section');
|
| 182 |
+
if (carouselSection) {
|
| 183 |
+
carouselSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 184 |
+
}
|
| 185 |
+
}, 300);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
};
|
| 189 |
+
|
| 190 |
+
// Perform search when user types
|
| 191 |
+
const performSearch = async (searchQuery: string) => {
|
| 192 |
+
if (!searchQuery.trim() || isURL(searchQuery)) {
|
| 193 |
+
setSearchResults([]);
|
| 194 |
+
setShowSearchTiles(false);
|
| 195 |
+
return;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
setIsSearching(true);
|
| 199 |
+
setShowSearchTiles(true);
|
| 200 |
+
try {
|
| 201 |
+
const response = await fetch('/api/search', {
|
| 202 |
+
method: 'POST',
|
| 203 |
+
headers: { 'Content-Type': 'application/json' },
|
| 204 |
+
body: JSON.stringify({ query: searchQuery }),
|
| 205 |
+
});
|
| 206 |
+
|
| 207 |
+
if (response.ok) {
|
| 208 |
+
const data = await response.json();
|
| 209 |
+
setSearchResults(data.results || []);
|
| 210 |
+
setShowSearchTiles(true);
|
| 211 |
+
}
|
| 212 |
+
} catch (error) {
|
| 213 |
+
console.error('Search error:', error);
|
| 214 |
+
} finally {
|
| 215 |
+
setIsSearching(false);
|
| 216 |
+
}
|
| 217 |
+
};
|
| 218 |
+
|
| 219 |
+
return (
|
| 220 |
+
<HeaderProvider>
|
| 221 |
+
<div className="min-h-screen bg-background-base">
|
| 222 |
+
{/* Header/Navigation Section */}
|
| 223 |
+
<HeaderDropdownWrapper />
|
| 224 |
+
|
| 225 |
+
<div className="sticky top-0 left-0 w-full z-[101] bg-background-base header">
|
| 226 |
+
<div className="absolute top-0 cmw-container border-x border-border-faint h-full pointer-events-none" />
|
| 227 |
+
<div className="h-1 bg-border-faint w-full left-0 -bottom-1 absolute" />
|
| 228 |
+
<div className="cmw-container absolute h-full pointer-events-none top-0">
|
| 229 |
+
<Connector className="absolute -left-[10.5px] -bottom-11" />
|
| 230 |
+
<Connector className="absolute -right-[10.5px] -bottom-11" />
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
<HeaderWrapper>
|
| 234 |
+
<div className="max-w-[900px] mx-auto w-full flex justify-between items-center">
|
| 235 |
+
<div className="flex gap-24 items-center">
|
| 236 |
+
<HeaderBrandKit />
|
| 237 |
+
</div>
|
| 238 |
+
<div className="flex gap-8">
|
| 239 |
+
<a
|
| 240 |
+
className="contents"
|
| 241 |
+
href="https://github.com/mendableai/open-lovable"
|
| 242 |
+
target="_blank"
|
| 243 |
+
>
|
| 244 |
+
<ButtonUI variant="tertiary">
|
| 245 |
+
<GithubIcon />
|
| 246 |
+
Use this Template
|
| 247 |
+
</ButtonUI>
|
| 248 |
+
</a>
|
| 249 |
+
<Link href="/text-generation">
|
| 250 |
+
<ButtonUI variant="tertiary">
|
| 251 |
+
Text Generation
|
| 252 |
+
</ButtonUI>
|
| 253 |
+
</Link>
|
| 254 |
+
</div>
|
| 255 |
+
</div>
|
| 256 |
+
</HeaderWrapper>
|
| 257 |
+
</div>
|
| 258 |
+
|
| 259 |
+
{/* Hero Section */}
|
| 260 |
+
<section className="overflow-x-clip" id="home-hero">
|
| 261 |
+
<div className="pt-28 lg:pt-254 lg:-mt-100 pb-115 relative" id="hero-content">
|
| 262 |
+
<HomeHeroPixi />
|
| 263 |
+
<HeroFlame />
|
| 264 |
+
<BackgroundOuterPiece />
|
| 265 |
+
<HomeHeroBackground />
|
| 266 |
+
|
| 267 |
+
<div className="relative container px-16">
|
| 268 |
+
<HomeHeroBadge />
|
| 269 |
+
<HomeHeroTitle />
|
| 270 |
+
<p className="text-center text-body-large">
|
| 271 |
+
Clone brand format or re-imagine any website, in seconds.
|
| 272 |
+
</p>
|
| 273 |
+
<Link
|
| 274 |
+
className="bg-black-alpha-4 hover:bg-black-alpha-6 rounded-6 px-8 lg:px-6 text-label-large h-30 lg:h-24 block mt-8 mx-auto w-max gap-4 transition-all"
|
| 275 |
+
href="#"
|
| 276 |
+
onClick={(e) => e.preventDefault()}
|
| 277 |
+
>
|
| 278 |
+
Powered by Firecrawl.
|
| 279 |
+
</Link>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
|
| 283 |
+
{/* Mini Playground Input */}
|
| 284 |
+
<div className="container lg:contents !p-16 relative -mt-90">
|
| 285 |
+
<div className="absolute top-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
|
| 286 |
+
<div className="absolute bottom-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
|
| 287 |
+
<Connector className="-top-10 -left-[10.5px] lg:hidden" />
|
| 288 |
+
<Connector className="-top-10 -right-[10.5px] lg:hidden" />
|
| 289 |
+
<Connector className="-bottom-10 -left-[10.5px] lg:hidden" />
|
| 290 |
+
<Connector className="-bottom-10 -right-[10.5px] lg:hidden" />
|
| 291 |
+
|
| 292 |
+
{/* Hero Input Component */}
|
| 293 |
+
<div className="max-w-552 mx-auto z-[11] lg:z-[2]">
|
| 294 |
+
<div className="rounded-20 -mt-30 lg:-mt-30">
|
| 295 |
+
<div
|
| 296 |
+
className="bg-white rounded-20 relative z-10"
|
| 297 |
+
style={{
|
| 298 |
+
boxShadow:
|
| 299 |
+
"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), 0px 0px 0px 10px #F9F9F9",
|
| 300 |
+
}}
|
| 301 |
+
>
|
| 302 |
+
|
| 303 |
+
<div className="p-[28px] flex gap-12 items-center w-full relative bg-white rounded-20">
|
| 304 |
+
{/* Show different UI when search results are displayed */}
|
| 305 |
+
{hasSearched && searchResults.length > 0 && !isFadingOut ? (
|
| 306 |
+
<>
|
| 307 |
+
{/* Selection mode icon */}
|
| 308 |
+
<svg
|
| 309 |
+
width="20"
|
| 310 |
+
height="20"
|
| 311 |
+
viewBox="0 0 20 20"
|
| 312 |
+
fill="none"
|
| 313 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 314 |
+
className="opacity-40 flex-shrink-0"
|
| 315 |
+
>
|
| 316 |
+
<rect x="2" y="4" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
|
| 317 |
+
<rect x="11" y="4" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
|
| 318 |
+
<rect x="2" y="11" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
|
| 319 |
+
<rect x="11" y="11" width="7" height="5" rx="1" stroke="currentColor" strokeWidth="1.5"/>
|
| 320 |
+
</svg>
|
| 321 |
+
|
| 322 |
+
{/* Selection message */}
|
| 323 |
+
<div className="flex-1 text-body-input text-accent-black">
|
| 324 |
+
Select which site to clone from the results below
|
| 325 |
+
</div>
|
| 326 |
+
|
| 327 |
+
{/* Search again button */}
|
| 328 |
+
<button
|
| 329 |
+
onClick={(e) => {
|
| 330 |
+
e.preventDefault();
|
| 331 |
+
setIsFadingOut(true);
|
| 332 |
+
setTimeout(() => {
|
| 333 |
+
setSearchResults([]);
|
| 334 |
+
setHasSearched(false);
|
| 335 |
+
setShowSearchTiles(false);
|
| 336 |
+
setIsFadingOut(false);
|
| 337 |
+
setUrl('');
|
| 338 |
+
}, 500);
|
| 339 |
+
}}
|
| 340 |
+
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"
|
| 341 |
+
>
|
| 342 |
+
<svg
|
| 343 |
+
width="16"
|
| 344 |
+
height="16"
|
| 345 |
+
viewBox="0 0 16 16"
|
| 346 |
+
fill="none"
|
| 347 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 348 |
+
className="opacity-60"
|
| 349 |
+
>
|
| 350 |
+
<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"/>
|
| 351 |
+
</svg>
|
| 352 |
+
<span>Search Again</span>
|
| 353 |
+
</button>
|
| 354 |
+
</>
|
| 355 |
+
) : (
|
| 356 |
+
<>
|
| 357 |
+
{isURL(url) ? (
|
| 358 |
+
// Scrape icon for URLs
|
| 359 |
+
<svg
|
| 360 |
+
width="20"
|
| 361 |
+
height="20"
|
| 362 |
+
viewBox="0 0 20 20"
|
| 363 |
+
fill="none"
|
| 364 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 365 |
+
className="opacity-40 flex-shrink-0"
|
| 366 |
+
>
|
| 367 |
+
<rect x="3" y="3" width="14" height="14" rx="2" stroke="currentColor" strokeWidth="1.5"/>
|
| 368 |
+
<path d="M7 10L9 12L13 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
| 369 |
+
</svg>
|
| 370 |
+
) : (
|
| 371 |
+
// Search icon for search terms
|
| 372 |
+
<svg
|
| 373 |
+
width="20"
|
| 374 |
+
height="20"
|
| 375 |
+
viewBox="0 0 20 20"
|
| 376 |
+
fill="none"
|
| 377 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 378 |
+
className="opacity-40 flex-shrink-0"
|
| 379 |
+
>
|
| 380 |
+
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" strokeWidth="1.5"/>
|
| 381 |
+
<path d="M12.5 12.5L16.5 16.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
| 382 |
+
</svg>
|
| 383 |
+
)}
|
| 384 |
+
<input
|
| 385 |
+
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"
|
| 386 |
+
placeholder="Enter URL or search term..."
|
| 387 |
+
type="text"
|
| 388 |
+
value={url}
|
| 389 |
+
disabled={isSearching}
|
| 390 |
+
onChange={(e) => {
|
| 391 |
+
const value = e.target.value;
|
| 392 |
+
setUrl(value);
|
| 393 |
+
setIsValidUrl(validateUrl(value));
|
| 394 |
+
// Reset search state when input changes
|
| 395 |
+
if (value.trim() === "") {
|
| 396 |
+
setShowSearchTiles(false);
|
| 397 |
+
setHasSearched(false);
|
| 398 |
+
setSearchResults([]);
|
| 399 |
+
}
|
| 400 |
+
}}
|
| 401 |
+
onKeyDown={(e) => {
|
| 402 |
+
if (e.key === "Enter" && !isSearching) {
|
| 403 |
+
e.preventDefault();
|
| 404 |
+
handleSubmit();
|
| 405 |
+
}
|
| 406 |
+
}}
|
| 407 |
+
onFocus={() => {
|
| 408 |
+
if (url.trim() && !isURL(url) && searchResults.length > 0) {
|
| 409 |
+
setShowSearchTiles(true);
|
| 410 |
+
}
|
| 411 |
+
}}
|
| 412 |
+
/>
|
| 413 |
+
<div
|
| 414 |
+
onClick={(e) => {
|
| 415 |
+
e.preventDefault();
|
| 416 |
+
if (!isSearching) {
|
| 417 |
+
handleSubmit();
|
| 418 |
+
}
|
| 419 |
+
}}
|
| 420 |
+
className={isSearching ? 'pointer-events-none' : ''}
|
| 421 |
+
>
|
| 422 |
+
<HeroInputSubmitButton
|
| 423 |
+
dirty={url.length > 0}
|
| 424 |
+
buttonText={isURL(url) ? 'Scrape Site' : 'Search'}
|
| 425 |
+
disabled={isSearching}
|
| 426 |
+
/>
|
| 427 |
+
</div>
|
| 428 |
+
</>
|
| 429 |
+
)}
|
| 430 |
+
</div>
|
| 431 |
+
|
| 432 |
+
{/* Options Section - Only show when valid URL */}
|
| 433 |
+
<div className={`overflow-hidden transition-all duration-500 ease-in-out ${
|
| 434 |
+
isValidUrl ? (extendBrandStyles ? 'max-h-[400px]' : 'max-h-[300px]') + ' opacity-100' : 'max-h-0 opacity-0'
|
| 435 |
+
}`}>
|
| 436 |
+
<div className="px-[28px] pt-0 pb-[28px]">
|
| 437 |
+
<div className="border-t border-gray-100 bg-white">
|
| 438 |
+
{/* Extend Brand Styles Toggle */}
|
| 439 |
+
<div className={`transition-all duration-300 transform ${
|
| 440 |
+
isValidUrl ? 'translate-y-0 opacity-100' : '-translate-y-2 opacity-0'
|
| 441 |
+
}`} style={{ transitionDelay: '50ms' }}>
|
| 442 |
+
<div className="py-8 grid grid-cols-2 items-center gap-12 group cursor-pointer" onClick={() => setExtendBrandStyles(!extendBrandStyles)}>
|
| 443 |
+
<div className="flex select-none">
|
| 444 |
+
<div className="flex lg-max:flex-col whitespace-nowrap flex-wrap min-w-0 gap-8 lg:justify-between flex-1">
|
| 445 |
+
<div className="text-xs font-medium text-black-alpha-72 transition-all group-hover:text-accent-black relative">
|
| 446 |
+
Extend brand styles
|
| 447 |
+
</div>
|
| 448 |
+
</div>
|
| 449 |
+
</div>
|
| 450 |
+
<div className="flex justify-end">
|
| 451 |
+
<button
|
| 452 |
+
className="transition-all relative rounded-full group bg-black-alpha-10"
|
| 453 |
+
type="button"
|
| 454 |
+
onClick={(e) => {
|
| 455 |
+
e.stopPropagation();
|
| 456 |
+
setExtendBrandStyles(!extendBrandStyles);
|
| 457 |
+
}}
|
| 458 |
+
style={{
|
| 459 |
+
width: '50px',
|
| 460 |
+
height: '20px',
|
| 461 |
+
boxShadow: 'rgba(0, 0, 0, 0.02) 0px 6px 12px 0px inset, rgba(0, 0, 0, 0.02) 0px 0.75px 0.75px 0px inset, rgba(0, 0, 0, 0.04) 0px 0.25px 0.25px 0px inset'
|
| 462 |
+
}}
|
| 463 |
+
>
|
| 464 |
+
<div
|
| 465 |
+
className={`overlay transition-opacity ${extendBrandStyles ? 'opacity-100' : 'opacity-0'}`}
|
| 466 |
+
style={{ background: 'color(display-p3 0.9059 0.3294 0.0784)', backgroundColor: '#FA4500' }}
|
| 467 |
+
/>
|
| 468 |
+
<div
|
| 469 |
+
className="top-[2px] left-[2px] transition-all absolute rounded-full bg-accent-white"
|
| 470 |
+
style={{
|
| 471 |
+
width: '28px',
|
| 472 |
+
height: '16px',
|
| 473 |
+
boxShadow: 'rgba(0, 0, 0, 0.06) 0px 6px 12px -3px, rgba(0, 0, 0, 0.06) 0px 3px 6px -1px, rgba(0, 0, 0, 0.04) 0px 1px 2px 0px, rgba(0, 0, 0, 0.08) 0px 0.5px 0.5px 0px',
|
| 474 |
+
transform: extendBrandStyles ? 'translateX(16px)' : 'none'
|
| 475 |
+
}}
|
| 476 |
+
/>
|
| 477 |
+
</button>
|
| 478 |
+
</div>
|
| 479 |
+
</div>
|
| 480 |
+
</div>
|
| 481 |
+
|
| 482 |
+
{/* Brand Extension Prompt - Show when toggle is enabled */}
|
| 483 |
+
{extendBrandStyles && (
|
| 484 |
+
<div className="pb-10 transition-all duration-300 opacity-100">
|
| 485 |
+
<textarea
|
| 486 |
+
value={additionalInstructions}
|
| 487 |
+
onChange={(e) => setAdditionalInstructions(e.target.value)}
|
| 488 |
+
placeholder="Describe the new functionality you want to build using this brand's styles..."
|
| 489 |
+
className="w-full px-4 py-10 text-xs font-medium text-gray-700 bg-gray-50 rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 placeholder:text-gray-400 min-h-[80px] resize-none"
|
| 490 |
+
/>
|
| 491 |
+
</div>
|
| 492 |
+
)}
|
| 493 |
+
|
| 494 |
+
{/* Style Selector - Hide when brand extension mode is enabled */}
|
| 495 |
+
{!extendBrandStyles && (
|
| 496 |
+
<div className={`mb-2 transition-all duration-300 transform ${
|
| 497 |
+
isValidUrl ? 'translate-y-0 opacity-100' : '-translate-y-2 opacity-0'
|
| 498 |
+
}`} style={{ transitionDelay: '100ms' }}>
|
| 499 |
+
<div className="grid grid-cols-4 gap-2">
|
| 500 |
+
{styles.map((style, index) => (
|
| 501 |
+
<button
|
| 502 |
+
key={style.id}
|
| 503 |
+
onClick={() => setSelectedStyle(style.id)}
|
| 504 |
+
className={`
|
| 505 |
+
${selectedStyle === style.id
|
| 506 |
+
? 'bg-heat-100 hover:bg-heat-200 flex items-center justify-center button relative text-label-medium button-primary group/button rounded-10 p-8 text-accent-white active:scale-[0.995] border-0'
|
| 507 |
+
: 'border-gray-200 hover:border-gray-300 bg-white text-gray-700 py-3.5 px-4 rounded text-xs font-medium border text-center'
|
| 508 |
+
}
|
| 509 |
+
transition-all
|
| 510 |
+
${isValidUrl ? 'opacity-100' : 'opacity-0'}
|
| 511 |
+
`}
|
| 512 |
+
style={{
|
| 513 |
+
transitionDelay: `${150 + index * 30}ms`,
|
| 514 |
+
transition: 'all 0.3s ease-in-out'
|
| 515 |
+
}}
|
| 516 |
+
>
|
| 517 |
+
{selectedStyle === style.id && (
|
| 518 |
+
<div className="button-background absolute inset-0 rounded-10 pointer-events-none" />
|
| 519 |
+
)}
|
| 520 |
+
<span className={selectedStyle === style.id ? 'relative' : ''}>
|
| 521 |
+
{style.name}
|
| 522 |
+
</span>
|
| 523 |
+
</button>
|
| 524 |
+
))}
|
| 525 |
+
</div>
|
| 526 |
+
</div>
|
| 527 |
+
)}
|
| 528 |
+
|
| 529 |
+
{/* Model Selector Dropdown and Additional Instructions */}
|
| 530 |
+
<div className={`flex items-center gap-3 mt-2 pb-4 transition-all duration-300 transform ${
|
| 531 |
+
isValidUrl ? 'translate-y-0 opacity-100' : '-translate-y-2 opacity-0'
|
| 532 |
+
}`} style={{ transitionDelay: '400ms' }}>
|
| 533 |
+
{/* Model Dropdown */}
|
| 534 |
+
<select
|
| 535 |
+
value={selectedModel}
|
| 536 |
+
onChange={(e) => setSelectedModel(e.target.value)}
|
| 537 |
+
className={`px-3 py-2.5 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 ${extendBrandStyles ? 'flex-1' : ''}`}
|
| 538 |
+
>
|
| 539 |
+
{models.map((model) => (
|
| 540 |
+
<option key={model.id} value={model.id}>
|
| 541 |
+
{model.name}
|
| 542 |
+
</option>
|
| 543 |
+
))}
|
| 544 |
+
</select>
|
| 545 |
+
|
| 546 |
+
{/* Additional Instructions - Hidden when extend brand styles is enabled */}
|
| 547 |
+
{!extendBrandStyles && (
|
| 548 |
+
<input
|
| 549 |
+
type="text"
|
| 550 |
+
className="flex-1 px-3 py-2.5 text-xs font-medium text-gray-700 bg-gray-50 rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 placeholder:text-gray-400"
|
| 551 |
+
placeholder="Additional instructions (optional)"
|
| 552 |
+
onChange={(e) => sessionStorage.setItem('additionalInstructions', e.target.value)}
|
| 553 |
+
/>
|
| 554 |
+
)}
|
| 555 |
+
</div>
|
| 556 |
+
</div>
|
| 557 |
+
</div>
|
| 558 |
+
</div>
|
| 559 |
+
|
| 560 |
+
</div>
|
| 561 |
+
|
| 562 |
+
<div className="h-248 top-84 cw-768 pointer-events-none absolute overflow-clip -z-10">
|
| 563 |
+
<AsciiExplosion className="-top-200" />
|
| 564 |
+
</div>
|
| 565 |
+
</div>
|
| 566 |
+
</div>
|
| 567 |
+
</div>
|
| 568 |
+
</section>
|
| 569 |
+
|
| 570 |
+
{/* Full-width oval carousel section */}
|
| 571 |
+
{showSearchTiles && hasSearched && (
|
| 572 |
+
<section className={`carousel-section relative w-full overflow-hidden mt-32 mb-32 transition-opacity duration-500 ${
|
| 573 |
+
isFadingOut ? 'opacity-0' : 'opacity-100'
|
| 574 |
+
}`}>
|
| 575 |
+
<div className="absolute inset-0 bg-gradient-to-b from-gray-50/50 to-white rounded-[50%] transform scale-x-150 -translate-y-24" />
|
| 576 |
+
|
| 577 |
+
{isSearching ? (
|
| 578 |
+
// Loading state with animated scrolling skeletons
|
| 579 |
+
<div className="relative h-[250px] overflow-hidden">
|
| 580 |
+
{/* Edge fade overlays */}
|
| 581 |
+
<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%)'}} />
|
| 582 |
+
<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%)'}} />
|
| 583 |
+
|
| 584 |
+
<div className="carousel-container absolute left-0 flex gap-12 py-4">
|
| 585 |
+
{/* Duplicate skeleton tiles for continuous scroll */}
|
| 586 |
+
{[...Array(10), ...Array(10)].map((_, index) => (
|
| 587 |
+
<div
|
| 588 |
+
key={`loading-${index}`}
|
| 589 |
+
className="flex-shrink-0 w-[400px] h-[240px] rounded-lg overflow-hidden border-2 border-gray-200/30 bg-white relative"
|
| 590 |
+
>
|
| 591 |
+
<div className="absolute inset-0 skeleton-shimmer">
|
| 592 |
+
<div className="absolute inset-0 bg-gradient-to-r from-gray-100 via-gray-50 to-gray-100 skeleton-gradient" />
|
| 593 |
+
</div>
|
| 594 |
+
|
| 595 |
+
{/* Fake browser UI - 5x bigger */}
|
| 596 |
+
<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">
|
| 597 |
+
<div className="flex gap-3">
|
| 598 |
+
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" />
|
| 599 |
+
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" style={{ animationDelay: '0.1s' }} />
|
| 600 |
+
<div className="w-5 h-5 rounded-full bg-gray-300 animate-pulse" style={{ animationDelay: '0.2s' }} />
|
| 601 |
+
</div>
|
| 602 |
+
<div className="flex-1 h-8 bg-gray-200 rounded-md mx-6 animate-pulse" />
|
| 603 |
+
</div>
|
| 604 |
+
|
| 605 |
+
{/* Content skeleton - positioned just below nav bar */}
|
| 606 |
+
<div className="absolute top-44 left-4 right-4">
|
| 607 |
+
<div className="h-3 bg-gray-200 rounded w-3/4 mb-2 animate-pulse" />
|
| 608 |
+
<div className="h-3 bg-gray-150 rounded w-1/2 mb-2 animate-pulse" style={{ animationDelay: '0.2s' }} />
|
| 609 |
+
<div className="h-3 bg-gray-150 rounded w-2/3 animate-pulse" style={{ animationDelay: '0.3s' }} />
|
| 610 |
+
</div>
|
| 611 |
+
</div>
|
| 612 |
+
))}
|
| 613 |
+
</div>
|
| 614 |
+
</div>
|
| 615 |
+
) : searchResults.length > 0 ? (
|
| 616 |
+
// Actual results
|
| 617 |
+
<div className="relative h-[250px] overflow-hidden">
|
| 618 |
+
{/* Edge fade overlays */}
|
| 619 |
+
<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%)'}} />
|
| 620 |
+
<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%)'}} />
|
| 621 |
+
|
| 622 |
+
<div className="carousel-container absolute left-0 flex gap-12 py-4">
|
| 623 |
+
{/* Duplicate results for infinite scroll */}
|
| 624 |
+
{[...searchResults, ...searchResults].map((result, index) => (
|
| 625 |
+
<div
|
| 626 |
+
key={`${result.url}-${index}`}
|
| 627 |
+
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"
|
| 628 |
+
onMouseLeave={() => {
|
| 629 |
+
if (showInstructionsForIndex === index) {
|
| 630 |
+
setShowInstructionsForIndex(null);
|
| 631 |
+
setAdditionalInstructions('');
|
| 632 |
+
}
|
| 633 |
+
}}
|
| 634 |
+
>
|
| 635 |
+
{/* Hover overlay with clone buttons or instructions input */}
|
| 636 |
+
<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">
|
| 637 |
+
{showInstructionsForIndex === index ? (
|
| 638 |
+
/* Instructions input view - matching main input style exactly */
|
| 639 |
+
<div className="w-full max-w-[380px]">
|
| 640 |
+
<div className="bg-white rounded-20" style={{
|
| 641 |
+
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)"
|
| 642 |
+
}}>
|
| 643 |
+
{/* Input area matching main search */}
|
| 644 |
+
<div className="p-16 flex gap-12 items-start w-full relative">
|
| 645 |
+
{/* Instructions icon */}
|
| 646 |
+
<div className="mt-2 flex-shrink-0">
|
| 647 |
+
<svg
|
| 648 |
+
width="20"
|
| 649 |
+
height="20"
|
| 650 |
+
viewBox="0 0 20 20"
|
| 651 |
+
fill="none"
|
| 652 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 653 |
+
className="opacity-40"
|
| 654 |
+
>
|
| 655 |
+
<path d="M5 5H15M5 10H15M5 15H10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
| 656 |
+
</svg>
|
| 657 |
+
</div>
|
| 658 |
+
|
| 659 |
+
<textarea
|
| 660 |
+
value={additionalInstructions}
|
| 661 |
+
onChange={(e) => setAdditionalInstructions(e.target.value)}
|
| 662 |
+
placeholder="Describe your customizations..."
|
| 663 |
+
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]"
|
| 664 |
+
autoFocus
|
| 665 |
+
onClick={(e) => e.stopPropagation()}
|
| 666 |
+
onKeyDown={(e) => {
|
| 667 |
+
if (e.key === 'Escape') {
|
| 668 |
+
e.stopPropagation();
|
| 669 |
+
setShowInstructionsForIndex(null);
|
| 670 |
+
setAdditionalInstructions('');
|
| 671 |
+
}
|
| 672 |
+
}}
|
| 673 |
+
/>
|
| 674 |
+
</div>
|
| 675 |
+
|
| 676 |
+
{/* Divider */}
|
| 677 |
+
<div className="border-t border-black-alpha-5" />
|
| 678 |
+
|
| 679 |
+
{/* Buttons area matching main style */}
|
| 680 |
+
<div className="p-10 flex justify-between items-center">
|
| 681 |
+
<button
|
| 682 |
+
onClick={(e) => {
|
| 683 |
+
e.stopPropagation();
|
| 684 |
+
setShowInstructionsForIndex(null);
|
| 685 |
+
setAdditionalInstructions('');
|
| 686 |
+
}}
|
| 687 |
+
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"
|
| 688 |
+
>
|
| 689 |
+
<svg
|
| 690 |
+
width="20"
|
| 691 |
+
height="20"
|
| 692 |
+
viewBox="0 0 20 20"
|
| 693 |
+
fill="none"
|
| 694 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 695 |
+
>
|
| 696 |
+
<path d="M12 5L7 10L12 15" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
| 697 |
+
</svg>
|
| 698 |
+
</button>
|
| 699 |
+
|
| 700 |
+
<button
|
| 701 |
+
onClick={(e) => {
|
| 702 |
+
e.stopPropagation();
|
| 703 |
+
if (additionalInstructions.trim()) {
|
| 704 |
+
sessionStorage.setItem('additionalInstructions', additionalInstructions);
|
| 705 |
+
handleSubmit(result);
|
| 706 |
+
}
|
| 707 |
+
}}
|
| 708 |
+
disabled={!additionalInstructions.trim()}
|
| 709 |
+
className={`
|
| 710 |
+
button relative rounded-10 px-8 py-8 text-label-medium font-medium
|
| 711 |
+
flex items-center justify-center gap-6
|
| 712 |
+
${additionalInstructions.trim()
|
| 713 |
+
? 'button-primary text-accent-white active:scale-[0.995]'
|
| 714 |
+
: 'bg-black-alpha-4 text-black-alpha-24 cursor-not-allowed'
|
| 715 |
+
}
|
| 716 |
+
`}
|
| 717 |
+
>
|
| 718 |
+
{additionalInstructions.trim() && <div className="button-background absolute inset-0 rounded-10 pointer-events-none" />}
|
| 719 |
+
<span className="px-6 relative">Apply & Clone</span>
|
| 720 |
+
<svg
|
| 721 |
+
width="20"
|
| 722 |
+
height="20"
|
| 723 |
+
viewBox="0 0 20 20"
|
| 724 |
+
fill="none"
|
| 725 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 726 |
+
className="relative"
|
| 727 |
+
>
|
| 728 |
+
<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"/>
|
| 729 |
+
</svg>
|
| 730 |
+
</button>
|
| 731 |
+
</div>
|
| 732 |
+
</div>
|
| 733 |
+
</div>
|
| 734 |
+
) : (
|
| 735 |
+
/* Default buttons view */
|
| 736 |
+
<>
|
| 737 |
+
<div className="text-white text-center mb-3">
|
| 738 |
+
<p className="text-base font-semibold mb-0.5">{result.title}</p>
|
| 739 |
+
<p className="text-[11px] opacity-80">Choose how to clone this site</p>
|
| 740 |
+
</div>
|
| 741 |
+
|
| 742 |
+
<div className="flex gap-3 justify-center">
|
| 743 |
+
{/* Instant Clone Button - Orange/Heat style */}
|
| 744 |
+
<button
|
| 745 |
+
onClick={(e) => {
|
| 746 |
+
e.stopPropagation();
|
| 747 |
+
handleSubmit(result);
|
| 748 |
+
}}
|
| 749 |
+
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]"
|
| 750 |
+
>
|
| 751 |
+
<div className="button-background absolute inset-0 rounded-10 pointer-events-none" />
|
| 752 |
+
<svg
|
| 753 |
+
width="20"
|
| 754 |
+
height="20"
|
| 755 |
+
viewBox="0 0 20 20"
|
| 756 |
+
fill="none"
|
| 757 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 758 |
+
className="relative"
|
| 759 |
+
>
|
| 760 |
+
<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"/>
|
| 761 |
+
</svg>
|
| 762 |
+
<span className="px-6 relative">Instant Clone</span>
|
| 763 |
+
</button>
|
| 764 |
+
|
| 765 |
+
{/* Instructions Button - Gray style */}
|
| 766 |
+
<button
|
| 767 |
+
onClick={(e) => {
|
| 768 |
+
e.stopPropagation();
|
| 769 |
+
setShowInstructionsForIndex(index);
|
| 770 |
+
setAdditionalInstructions('');
|
| 771 |
+
}}
|
| 772 |
+
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]"
|
| 773 |
+
>
|
| 774 |
+
<svg
|
| 775 |
+
width="20"
|
| 776 |
+
height="20"
|
| 777 |
+
viewBox="0 0 20 20"
|
| 778 |
+
fill="none"
|
| 779 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 780 |
+
className="opacity-60"
|
| 781 |
+
>
|
| 782 |
+
<path d="M5 5H15M5 10H15M5 15H10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
| 783 |
+
<path d="M14 14L16 16L14 18" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
| 784 |
+
</svg>
|
| 785 |
+
<span className="px-6">Add Instructions</span>
|
| 786 |
+
</button>
|
| 787 |
+
</div>
|
| 788 |
+
</>
|
| 789 |
+
)}
|
| 790 |
+
</div>
|
| 791 |
+
|
| 792 |
+
{result.screenshot ? (
|
| 793 |
+
<div className="relative w-full h-full">
|
| 794 |
+
<Image
|
| 795 |
+
src={result.screenshot}
|
| 796 |
+
alt={result.title}
|
| 797 |
+
fill
|
| 798 |
+
className="object-cover object-top"
|
| 799 |
+
loading="lazy"
|
| 800 |
+
/>
|
| 801 |
+
</div>
|
| 802 |
+
) : (
|
| 803 |
+
<div className="w-full h-full bg-gradient-to-br from-gray-100 to-gray-50 flex items-center justify-center">
|
| 804 |
+
<div className="text-center">
|
| 805 |
+
<div className="w-16 h-16 rounded-full bg-gray-200 mx-auto mb-3 flex items-center justify-center">
|
| 806 |
+
<svg
|
| 807 |
+
width="32"
|
| 808 |
+
height="32"
|
| 809 |
+
viewBox="0 0 24 24"
|
| 810 |
+
fill="none"
|
| 811 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 812 |
+
className="text-gray-400"
|
| 813 |
+
>
|
| 814 |
+
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="1.5"/>
|
| 815 |
+
<path d="M3 9H21" stroke="currentColor" strokeWidth="1.5"/>
|
| 816 |
+
<circle cx="6" cy="6" r="1" fill="currentColor"/>
|
| 817 |
+
<circle cx="9" cy="6" r="1" fill="currentColor"/>
|
| 818 |
+
<circle cx="12" cy="6" r="1" fill="currentColor"/>
|
| 819 |
+
</svg>
|
| 820 |
+
</div>
|
| 821 |
+
<p className="text-gray-500 text-sm font-medium">{result.title}</p>
|
| 822 |
+
</div>
|
| 823 |
+
</div>
|
| 824 |
+
)}
|
| 825 |
+
</div>
|
| 826 |
+
))}
|
| 827 |
+
</div>
|
| 828 |
+
</div>
|
| 829 |
+
) : (
|
| 830 |
+
// No results state
|
| 831 |
+
<div className="relative h-[250px] flex items-center justify-center">
|
| 832 |
+
<div className="text-center">
|
| 833 |
+
<div className="mb-4">
|
| 834 |
+
<svg className="w-16 h-16 mx-auto text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 835 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
| 836 |
+
</svg>
|
| 837 |
+
</div>
|
| 838 |
+
<p className="text-gray-500 text-lg">No results found</p>
|
| 839 |
+
<p className="text-gray-400 text-sm mt-1">Try a different search term</p>
|
| 840 |
+
</div>
|
| 841 |
+
</div>
|
| 842 |
+
)}
|
| 843 |
+
</section>
|
| 844 |
+
)}
|
| 845 |
+
|
| 846 |
+
</div>
|
| 847 |
+
|
| 848 |
+
<style jsx>{`
|
| 849 |
+
@keyframes infiniteScroll {
|
| 850 |
+
from {
|
| 851 |
+
transform: translateX(0);
|
| 852 |
+
}
|
| 853 |
+
to {
|
| 854 |
+
transform: translateX(-50%);
|
| 855 |
+
}
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
@keyframes shimmer {
|
| 859 |
+
0% {
|
| 860 |
+
transform: translateX(-100%);
|
| 861 |
+
}
|
| 862 |
+
100% {
|
| 863 |
+
transform: translateX(100%);
|
| 864 |
+
}
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
@keyframes fadeIn {
|
| 868 |
+
from {
|
| 869 |
+
opacity: 0;
|
| 870 |
+
transform: translateY(10px);
|
| 871 |
+
}
|
| 872 |
+
to {
|
| 873 |
+
opacity: 1;
|
| 874 |
+
transform: translateY(0);
|
| 875 |
+
}
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.carousel-container {
|
| 879 |
+
animation: infiniteScroll 30s linear infinite;
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
.carousel-container:hover {
|
| 883 |
+
animation-play-state: paused;
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
.skeleton-shimmer {
|
| 887 |
+
position: relative;
|
| 888 |
+
overflow: hidden;
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
.skeleton-gradient {
|
| 892 |
+
animation: shimmer 2s infinite;
|
| 893 |
+
}
|
| 894 |
+
`}</style>
|
| 895 |
+
</HeaderProvider>
|
| 896 |
+
);
|
| 897 |
+
}
|
app/text-generation/page.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import TextGeneration from '@/components/app/TextGeneration';
|
| 2 |
+
|
| 3 |
+
export default function TextGenerationPage() {
|
| 4 |
+
return (
|
| 5 |
+
<main>
|
| 6 |
+
<div className="md:px-8 flex flex-col items-center gap-4">
|
| 7 |
+
<TextGeneration />
|
| 8 |
+
</div>
|
| 9 |
+
</main>
|
| 10 |
+
);
|
| 11 |
+
}
|
atoms/sheets.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { atom } from 'jotai';
|
| 2 |
+
|
| 3 |
+
export const isMobileSheetOpenAtom = atom(false);
|
bun.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
colors.json
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"heat-4": {
|
| 3 |
+
"hex": "fa5d190a",
|
| 4 |
+
"p3": "0.980392 0.364706 0.098039 / 0.039216"
|
| 5 |
+
},
|
| 6 |
+
"heat-8": {
|
| 7 |
+
"hex": "fa5d1914",
|
| 8 |
+
"p3": "0.980392 0.364706 0.098039 / 0.078431"
|
| 9 |
+
},
|
| 10 |
+
"heat-12": {
|
| 11 |
+
"hex": "fa5d191f",
|
| 12 |
+
"p3": "0.980392 0.364706 0.098039 / 0.121569"
|
| 13 |
+
},
|
| 14 |
+
"heat-16": {
|
| 15 |
+
"hex": "fa5d1929",
|
| 16 |
+
"p3": "0.980392 0.364706 0.098039 / 0.160784"
|
| 17 |
+
},
|
| 18 |
+
"heat-20": {
|
| 19 |
+
"hex": "fa5d1933",
|
| 20 |
+
"p3": "0.980392 0.364706 0.098039 / 0.200000"
|
| 21 |
+
},
|
| 22 |
+
"heat-40": {
|
| 23 |
+
"hex": "fa5d1966",
|
| 24 |
+
"p3": "0.980392 0.364706 0.098039 / 0.400000"
|
| 25 |
+
},
|
| 26 |
+
"heat-90": {
|
| 27 |
+
"hex": "fa5d19e6",
|
| 28 |
+
"p3": "0.980392 0.364706 0.098039 / 0.900000"
|
| 29 |
+
},
|
| 30 |
+
"heat-100": {
|
| 31 |
+
"hex": "fa5d19ff",
|
| 32 |
+
"p3": "0.980392 0.364706 0.098039 / 1.000000"
|
| 33 |
+
},
|
| 34 |
+
"accent-black": {
|
| 35 |
+
"hex": "262626ff",
|
| 36 |
+
"p3": "0.149020 0.149020 0.149020 / 1.000000"
|
| 37 |
+
},
|
| 38 |
+
"accent-white": {
|
| 39 |
+
"hex": "ffffffff",
|
| 40 |
+
"p3": "1.000000 1.000000 1.000000 / 1.000000"
|
| 41 |
+
},
|
| 42 |
+
"accent-amethyst": {
|
| 43 |
+
"hex": "9061ffff",
|
| 44 |
+
"p3": "0.564706 0.380392 1.000000 / 1.000000"
|
| 45 |
+
},
|
| 46 |
+
"accent-bluetron": {
|
| 47 |
+
"hex": "2a6dfbff",
|
| 48 |
+
"p3": "0.164706 0.427451 0.984314 / 1.000000"
|
| 49 |
+
},
|
| 50 |
+
"accent-crimson": {
|
| 51 |
+
"hex": "eb3424ff",
|
| 52 |
+
"p3": "0.921569 0.203922 0.141176 / 1.000000"
|
| 53 |
+
},
|
| 54 |
+
"accent-forest": {
|
| 55 |
+
"hex": "42c366ff",
|
| 56 |
+
"p3": "0.258824 0.764706 0.400000 / 1.000000"
|
| 57 |
+
},
|
| 58 |
+
"accent-honey": {
|
| 59 |
+
"hex": "ecb730ff",
|
| 60 |
+
"p3": "0.925490 0.717647 0.188235 / 1.000000"
|
| 61 |
+
},
|
| 62 |
+
"black-alpha-1": {
|
| 63 |
+
"hex": "00000003",
|
| 64 |
+
"p3": "0.000000 0.000000 0.000000 / 0.011765"
|
| 65 |
+
},
|
| 66 |
+
"black-alpha-2": {
|
| 67 |
+
"hex": "00000005",
|
| 68 |
+
"p3": "0.000000 0.000000 0.000000 / 0.019608"
|
| 69 |
+
},
|
| 70 |
+
"black-alpha-3": {
|
| 71 |
+
"hex": "00000008",
|
| 72 |
+
"p3": "0.000000 0.000000 0.000000 / 0.031373"
|
| 73 |
+
},
|
| 74 |
+
"black-alpha-4": {
|
| 75 |
+
"hex": "0000000a",
|
| 76 |
+
"p3": "0.000000 0.000000 0.000000 / 0.039216"
|
| 77 |
+
},
|
| 78 |
+
"black-alpha-5": {
|
| 79 |
+
"hex": "0000000d",
|
| 80 |
+
"p3": "0.000000 0.000000 0.000000 / 0.050980"
|
| 81 |
+
},
|
| 82 |
+
"black-alpha-6": {
|
| 83 |
+
"hex": "0000000f",
|
| 84 |
+
"p3": "0.000000 0.000000 0.000000 / 0.058824"
|
| 85 |
+
},
|
| 86 |
+
"black-alpha-7": {
|
| 87 |
+
"hex": "00000012",
|
| 88 |
+
"p3": "0.000000 0.000000 0.000000 / 0.070588"
|
| 89 |
+
},
|
| 90 |
+
"black-alpha-8": {
|
| 91 |
+
"hex": "00000014",
|
| 92 |
+
"p3": "0.000000 0.000000 0.000000 / 0.078431"
|
| 93 |
+
},
|
| 94 |
+
"black-alpha-10": {
|
| 95 |
+
"hex": "0000001a",
|
| 96 |
+
"p3": "0.000000 0.000000 0.000000 / 0.101961"
|
| 97 |
+
},
|
| 98 |
+
"black-alpha-12": {
|
| 99 |
+
"hex": "0000001f",
|
| 100 |
+
"p3": "0.000000 0.000000 0.000000 / 0.121569"
|
| 101 |
+
},
|
| 102 |
+
"black-alpha-16": {
|
| 103 |
+
"hex": "00000029",
|
| 104 |
+
"p3": "0.000000 0.000000 0.000000 / 0.160784"
|
| 105 |
+
},
|
| 106 |
+
"black-alpha-20": {
|
| 107 |
+
"hex": "00000033",
|
| 108 |
+
"p3": "0.000000 0.000000 0.000000 / 0.200000"
|
| 109 |
+
},
|
| 110 |
+
"black-alpha-24": {
|
| 111 |
+
"hex": "0000003d",
|
| 112 |
+
"p3": "0.000000 0.000000 0.000000 / 0.239216"
|
| 113 |
+
},
|
| 114 |
+
"black-alpha-32": {
|
| 115 |
+
"hex": "26262652",
|
| 116 |
+
"p3": "0.149020 0.149020 0.149020 / 0.321569"
|
| 117 |
+
},
|
| 118 |
+
"black-alpha-40": {
|
| 119 |
+
"hex": "26262666",
|
| 120 |
+
"p3": "0.149020 0.149020 0.149020 / 0.400000"
|
| 121 |
+
},
|
| 122 |
+
"black-alpha-48": {
|
| 123 |
+
"hex": "2626267a",
|
| 124 |
+
"p3": "0.149020 0.149020 0.149020 / 0.478431"
|
| 125 |
+
},
|
| 126 |
+
"black-alpha-56": {
|
| 127 |
+
"hex": "2626268f",
|
| 128 |
+
"p3": "0.149020 0.149020 0.149020 / 0.560784"
|
| 129 |
+
},
|
| 130 |
+
"black-alpha-64": {
|
| 131 |
+
"hex": "262626a3",
|
| 132 |
+
"p3": "0.149020 0.149020 0.149020 / 0.639216"
|
| 133 |
+
},
|
| 134 |
+
"black-alpha-72": {
|
| 135 |
+
"hex": "262626b8",
|
| 136 |
+
"p3": "0.149020 0.149020 0.149020 / 0.721569"
|
| 137 |
+
},
|
| 138 |
+
"black-alpha-88": {
|
| 139 |
+
"hex": "262626e0",
|
| 140 |
+
"p3": "0.149020 0.149020 0.149020 / 0.878431"
|
| 141 |
+
},
|
| 142 |
+
"white-alpha-56": {
|
| 143 |
+
"hex": "ffffff8f",
|
| 144 |
+
"p3": "1.000000 1.000000 1.000000 / 0.560784"
|
| 145 |
+
},
|
| 146 |
+
"white-alpha-72": {
|
| 147 |
+
"hex": "ffffffb8",
|
| 148 |
+
"p3": "1.000000 1.000000 1.000000 / 0.721569"
|
| 149 |
+
},
|
| 150 |
+
"border-faint": {
|
| 151 |
+
"hex": "edededff",
|
| 152 |
+
"p3": "0.929412 0.929412 0.929412 / 1.000000"
|
| 153 |
+
},
|
| 154 |
+
"border-muted": {
|
| 155 |
+
"hex": "e8e8e8ff",
|
| 156 |
+
"p3": "0.909804 0.909804 0.909804 / 1.000000"
|
| 157 |
+
},
|
| 158 |
+
"border-loud": {
|
| 159 |
+
"hex": "e6e6e6ff",
|
| 160 |
+
"p3": "0.901961 0.901961 0.901961 / 1.000000"
|
| 161 |
+
},
|
| 162 |
+
"illustrations-faint": {
|
| 163 |
+
"hex": "edededff",
|
| 164 |
+
"p3": "0.929412 0.929412 0.929412 / 1.000000"
|
| 165 |
+
},
|
| 166 |
+
"illustrations-muted": {
|
| 167 |
+
"hex": "e6e6e6ff",
|
| 168 |
+
"p3": "0.901961 0.901961 0.901961 / 1.000000"
|
| 169 |
+
},
|
| 170 |
+
"illustrations-default": {
|
| 171 |
+
"hex": "dbdbdbff",
|
| 172 |
+
"p3": "0.858824 0.858824 0.858824 / 1.000000"
|
| 173 |
+
},
|
| 174 |
+
"background-lighter": {
|
| 175 |
+
"hex": "fbfbfbff",
|
| 176 |
+
"p3": "0.984314 0.984314 0.984314 / 1.000000"
|
| 177 |
+
},
|
| 178 |
+
"background-base": {
|
| 179 |
+
"hex": "f9f9f9ff",
|
| 180 |
+
"p3": "0.976471 0.976471 0.976471 / 1.000000"
|
| 181 |
+
}
|
| 182 |
+
}
|
components/CodeApplicationProgress.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
+
|
| 4 |
+
export interface CodeApplicationState {
|
| 5 |
+
stage: 'analyzing' | 'installing' | 'applying' | 'complete' | null;
|
| 6 |
+
packages?: string[];
|
| 7 |
+
installedPackages?: string[];
|
| 8 |
+
filesGenerated?: string[];
|
| 9 |
+
message?: string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
interface CodeApplicationProgressProps {
|
| 13 |
+
state: CodeApplicationState;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export default function CodeApplicationProgress({ state }: CodeApplicationProgressProps) {
|
| 17 |
+
if (!state.stage || state.stage === 'complete') return null;
|
| 18 |
+
|
| 19 |
+
return (
|
| 20 |
+
<AnimatePresence mode="wait">
|
| 21 |
+
<motion.div
|
| 22 |
+
key="loading"
|
| 23 |
+
initial={{ opacity: 0, y: 10 }}
|
| 24 |
+
animate={{ opacity: 1, y: 0 }}
|
| 25 |
+
exit={{ opacity: 0, y: -10 }}
|
| 26 |
+
transition={{ duration: 0.3 }}
|
| 27 |
+
className="inline-block bg-gray-100 rounded-[10px] p-3 mt-2"
|
| 28 |
+
>
|
| 29 |
+
<div className="flex items-center gap-3">
|
| 30 |
+
{/* Rotating loading indicator */}
|
| 31 |
+
<motion.div
|
| 32 |
+
animate={{ rotate: 360 }}
|
| 33 |
+
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
| 34 |
+
className="w-4 h-4"
|
| 35 |
+
>
|
| 36 |
+
<svg className="w-full h-full" viewBox="0 0 24 24" fill="none">
|
| 37 |
+
<circle
|
| 38 |
+
cx="12"
|
| 39 |
+
cy="12"
|
| 40 |
+
r="10"
|
| 41 |
+
stroke="currentColor"
|
| 42 |
+
strokeWidth="2"
|
| 43 |
+
strokeLinecap="round"
|
| 44 |
+
strokeDasharray="31.416"
|
| 45 |
+
strokeDashoffset="10"
|
| 46 |
+
className="text-gray-700"
|
| 47 |
+
/>
|
| 48 |
+
</svg>
|
| 49 |
+
</motion.div>
|
| 50 |
+
|
| 51 |
+
{/* Simple loading text */}
|
| 52 |
+
<div className="text-sm font-medium text-gray-700">
|
| 53 |
+
Applying to sandbox...
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</motion.div>
|
| 57 |
+
</AnimatePresence>
|
| 58 |
+
);
|
| 59 |
+
}
|
components/FirecrawlIcon.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function FirecrawlIcon({ className = "w-5 h-5" }: { className?: string }) {
|
| 2 |
+
return (
|
| 3 |
+
<svg
|
| 4 |
+
className={className}
|
| 5 |
+
fill="none"
|
| 6 |
+
viewBox="0 0 20 20"
|
| 7 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 8 |
+
>
|
| 9 |
+
<path
|
| 10 |
+
d="M13.7605 6.61389C13.138 6.79867 12.6687 7.21667 12.3251 7.67073C12.2513 7.76819 12.0975 7.69495 12.1268 7.57552C12.7848 4.86978 11.9155 2.6209 9.20582 1.51393C9.06836 1.4576 8.92527 1.58097 8.96132 1.72519C10.1939 6.67417 5.00941 6.25673 5.66459 11.8671C5.67585 11.9634 5.56769 12.0293 5.48882 11.973C5.2432 11.7967 4.96885 11.4288 4.78069 11.1702C4.72548 11.0942 4.60605 11.1156 4.5807 11.2063C4.43085 11.7482 4.35986 12.2586 4.35986 12.7656C4.35986 14.7373 5.37333 16.473 6.90734 17.4791C6.99522 17.5366 7.10789 17.4543 7.07804 17.3535C6.99917 17.0887 6.95466 16.8093 6.95128 16.5203C6.95128 16.3429 6.96255 16.1615 6.99015 15.9925C7.05438 15.5677 7.20197 15.1632 7.44985 14.7948C8.29995 13.5188 10.0041 12.2862 9.73199 10.6125C9.71453 10.5066 9.83959 10.4368 9.91846 10.5094C11.119 11.6063 11.3567 13.0817 11.1595 14.405C11.1426 14.5199 11.2868 14.5813 11.3595 14.4912C11.5432 14.2613 11.7674 14.0596 12.0113 13.9081C12.0722 13.8703 12.1533 13.8991 12.1764 13.9667C12.3121 14.3616 12.5138 14.7323 12.7042 15.1029C12.9318 15.5485 13.0529 16.0573 13.0338 16.5958C13.0242 16.8578 12.9808 17.1113 12.9082 17.3524C12.8772 17.4543 12.9887 17.5394 13.0783 17.4808C14.6134 16.4747 15.6275 14.739 15.6275 12.7662C15.6275 12.0806 15.5075 11.4085 15.2804 10.7787C14.8044 9.45766 13.5966 8.46561 13.9019 6.74403C13.9166 6.66178 13.8405 6.59023 13.7605 6.61389Z"
|
| 11 |
+
fill="currentColor"
|
| 12 |
+
/>
|
| 13 |
+
</svg>
|
| 14 |
+
);
|
| 15 |
+
}
|