Leon4gr45 commited on
Commit
75fefa7
·
verified ·
1 Parent(s): f0e5adf

Deploy to clean space

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .cursor/mcp.json +8 -0
  2. .env.example +46 -0
  3. .gitignore +60 -0
  4. Dockerfile +26 -0
  5. LICENSE +21 -0
  6. PROJECT_README.md +67 -0
  7. README.md +6 -6
  8. app/api/analyze-edit-intent/route.ts +190 -0
  9. app/api/apply-ai-code-stream/route.ts +799 -0
  10. app/api/apply-ai-code/route.ts +800 -0
  11. app/api/check-vite-errors/route.ts +12 -0
  12. app/api/clear-vite-errors-cache/route.ts +26 -0
  13. app/api/conversation-state/route.ts +160 -0
  14. app/api/create-ai-sandbox-v2/route.ts +103 -0
  15. app/api/create-ai-sandbox/route.ts +384 -0
  16. app/api/create-zip/route.ts +70 -0
  17. app/api/detect-and-install-packages/route.ts +189 -0
  18. app/api/extract-brand-styles/route.ts +72 -0
  19. app/api/generate-ai-code-stream/route.ts +1896 -0
  20. app/api/get-sandbox-files/route.ts +208 -0
  21. app/api/install-packages-v2/route.ts +48 -0
  22. app/api/install-packages/route.ts +259 -0
  23. app/api/kill-sandbox/route.ts +49 -0
  24. app/api/monitor-vite-logs/route.ts +121 -0
  25. app/api/report-vite-error/route.ts +62 -0
  26. app/api/restart-vite/route.ts +103 -0
  27. app/api/run-command-v2/route.ts +50 -0
  28. app/api/run-command/route.ts +63 -0
  29. app/api/sandbox-logs/route.ts +89 -0
  30. app/api/sandbox-status/route.ts +57 -0
  31. app/api/scrape-screenshot/route.ts +81 -0
  32. app/api/scrape-url-enhanced/route.ts +127 -0
  33. app/api/scrape-website/route.ts +110 -0
  34. app/api/search/route.ts +51 -0
  35. app/api/text-generation/route.ts +32 -0
  36. app/builder/page.tsx +286 -0
  37. app/favicon.ico +0 -0
  38. app/fonts/GeistMonoVF.woff +0 -0
  39. app/fonts/GeistVF.woff +0 -0
  40. app/generation/page.tsx +0 -0
  41. app/globals.css +1 -0
  42. app/landing.tsx +90 -0
  43. app/layout.tsx +45 -0
  44. app/page.tsx +897 -0
  45. app/text-generation/page.tsx +11 -0
  46. atoms/sheets.ts +3 -0
  47. bun.lock +0 -0
  48. colors.json +182 -0
  49. components/CodeApplicationProgress.tsx +59 -0
  50. 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
- title: Openoperator
3
- emoji: 🐠
4
- colorFrom: blue
5
- colorTo: gray
 
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
+ }