Merge branch 'main' into morph-fast-apply
This commit is contained in:
@@ -5,20 +5,30 @@ import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||
import { generateObject } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import type { FileManifest } from '@/types/file-manifest';
|
||||
// import type { FileManifest } from '@/types/file-manifest'; // Type is used implicitly through manifest parameter
|
||||
|
||||
// Check if we're using Vercel AI Gateway
|
||||
const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY;
|
||||
const aiGatewayBaseURL = 'https://ai-gateway.vercel.sh/v1';
|
||||
|
||||
const groq = createGroq({
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GROQ_API_KEY,
|
||||
baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined,
|
||||
});
|
||||
|
||||
const anthropic = createAnthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1',
|
||||
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.ANTHROPIC_API_KEY,
|
||||
baseURL: isUsingAIGateway ? aiGatewayBaseURL : (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1'),
|
||||
});
|
||||
|
||||
const openai = createOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
baseURL: process.env.OPENAI_BASE_URL,
|
||||
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.OPENAI_API_KEY,
|
||||
baseURL: isUsingAIGateway ? aiGatewayBaseURL : process.env.OPENAI_BASE_URL,
|
||||
});
|
||||
|
||||
const googleGenerativeAI = createGoogleGenerativeAI({
|
||||
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GEMINI_API_KEY,
|
||||
baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined,
|
||||
});
|
||||
|
||||
// Schema for the AI's search plan - not file selection!
|
||||
@@ -66,7 +76,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Create a summary of available files for the AI
|
||||
const validFiles = Object.entries(manifest.files as Record<string, any>)
|
||||
.filter(([path, info]) => {
|
||||
.filter(([path]) => {
|
||||
// Filter out invalid paths
|
||||
return path.includes('.') && !path.match(/\/\d+$/);
|
||||
});
|
||||
@@ -74,7 +84,7 @@ export async function POST(request: NextRequest) {
|
||||
const fileSummary = validFiles
|
||||
.map(([path, info]: [string, any]) => {
|
||||
const componentName = info.componentInfo?.name || path.split('/').pop();
|
||||
const hasImports = info.imports?.length > 0;
|
||||
// const hasImports = info.imports?.length > 0; // Kept for future use
|
||||
const childComponents = info.componentInfo?.childComponents?.join(', ') || 'none';
|
||||
return `- ${path} (${componentName}, renders: ${childComponents})`;
|
||||
})
|
||||
@@ -104,7 +114,7 @@ export async function POST(request: NextRequest) {
|
||||
aiModel = openai(model.replace('openai/', ''));
|
||||
}
|
||||
} else if (model.startsWith('google/')) {
|
||||
aiModel = createGoogleGenerativeAI(model.replace('google/', ''));
|
||||
aiModel = googleGenerativeAI(model.replace('google/', ''));
|
||||
} else {
|
||||
// Default to groq if model format is unclear
|
||||
aiModel = groq(model);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { parseMorphEdits, applyMorphEditToFile } from '@/lib/morph-fast-apply';
|
||||
import { Sandbox } from '@e2b/code-interpreter';
|
||||
// Sandbox import not needed - using global sandbox from sandbox-manager
|
||||
import type { SandboxState } from '@/types/sandbox';
|
||||
import type { ConversationState } from '@/types/conversation';
|
||||
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
||||
|
||||
declare global {
|
||||
var conversationState: ConversationState | null;
|
||||
var activeSandbox: any;
|
||||
var activeSandboxProvider: any;
|
||||
var existingFiles: Set<string>;
|
||||
var sandboxState: SandboxState;
|
||||
}
|
||||
@@ -301,73 +302,88 @@ export async function POST(request: NextRequest) {
|
||||
global.existingFiles = new Set<string>();
|
||||
}
|
||||
|
||||
// First, always check the global state for active sandbox
|
||||
let sandbox = global.activeSandbox;
|
||||
// Try to get provider from sandbox manager first
|
||||
let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
|
||||
|
||||
// If we don't have a sandbox in this instance but we have a sandboxId,
|
||||
// reconnect to the existing sandbox
|
||||
if (!sandbox && sandboxId) {
|
||||
console.log(`[apply-ai-code-stream] Sandbox ${sandboxId} not in this instance, attempting reconnect...`);
|
||||
// Fall back to global state if not found in manager
|
||||
if (!provider) {
|
||||
provider = global.activeSandboxProvider;
|
||||
}
|
||||
|
||||
// If we have a sandboxId but no provider, try to get or create one
|
||||
if (!provider && sandboxId) {
|
||||
console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`);
|
||||
|
||||
try {
|
||||
// Reconnect to the existing sandbox using E2B's connect method
|
||||
sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY });
|
||||
console.log(`[apply-ai-code-stream] Successfully reconnected to sandbox ${sandboxId}`);
|
||||
provider = await sandboxManager.getOrCreateProvider(sandboxId);
|
||||
|
||||
// Store the reconnected sandbox globally for this instance
|
||||
global.activeSandbox = sandbox;
|
||||
|
||||
// Update sandbox data if needed
|
||||
if (!global.sandboxData) {
|
||||
const host = (sandbox as any).getHost(5173);
|
||||
global.sandboxData = {
|
||||
sandboxId,
|
||||
url: `https://${host}`
|
||||
};
|
||||
// If we got a new provider (not reconnected), we need to create a new sandbox
|
||||
if (!provider.getSandboxInfo()) {
|
||||
console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`);
|
||||
await provider.createSandbox();
|
||||
await provider.setupViteApp();
|
||||
sandboxManager.registerSandbox(sandboxId, provider);
|
||||
}
|
||||
|
||||
// Initialize existingFiles if not already
|
||||
if (!global.existingFiles) {
|
||||
global.existingFiles = new Set<string>();
|
||||
}
|
||||
} catch (reconnectError) {
|
||||
console.error(`[apply-ai-code-stream] Failed to reconnect to sandbox ${sandboxId}:`, reconnectError);
|
||||
|
||||
// If reconnection fails, we'll still try to return a meaningful response
|
||||
// Update legacy global state
|
||||
global.activeSandboxProvider = provider;
|
||||
console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`);
|
||||
} catch (providerError) {
|
||||
console.error(`[apply-ai-code-stream] Failed to get or create provider for sandbox ${sandboxId}:`, providerError);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `Failed to reconnect to sandbox ${sandboxId}. The sandbox may have expired or been terminated.`,
|
||||
error: `Failed to create sandbox provider for ${sandboxId}. The sandbox may have expired.`,
|
||||
results: {
|
||||
filesCreated: [],
|
||||
packagesInstalled: [],
|
||||
commandsExecuted: [],
|
||||
errors: [`Sandbox reconnection failed: ${(reconnectError as Error).message}`]
|
||||
errors: [`Sandbox provider creation failed: ${(providerError as Error).message}`]
|
||||
},
|
||||
explanation: parsed.explanation,
|
||||
structure: parsed.structure,
|
||||
parsedFiles: parsed.files,
|
||||
message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox reconnection failed.`
|
||||
});
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// If no sandbox at all and no sandboxId provided, return an error
|
||||
if (!sandbox && !sandboxId) {
|
||||
console.log('[apply-ai-code-stream] No sandbox available and no sandboxId provided');
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'No active sandbox found. Please create a sandbox first.',
|
||||
results: {
|
||||
filesCreated: [],
|
||||
packagesInstalled: [],
|
||||
commandsExecuted: [],
|
||||
errors: ['No sandbox available']
|
||||
},
|
||||
explanation: parsed.explanation,
|
||||
structure: parsed.structure,
|
||||
parsedFiles: parsed.files,
|
||||
message: `Parsed ${parsed.files.length} files but no sandbox available to apply them.`
|
||||
});
|
||||
// If we still don't have a provider, create a new one
|
||||
if (!provider) {
|
||||
console.log(`[apply-ai-code-stream] No active provider found, creating new sandbox...`);
|
||||
try {
|
||||
const { SandboxFactory } = await import('@/lib/sandbox/factory');
|
||||
provider = SandboxFactory.create();
|
||||
const sandboxInfo = await provider.createSandbox();
|
||||
await provider.setupViteApp();
|
||||
|
||||
// Register with sandbox manager
|
||||
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
|
||||
|
||||
// Store in legacy global state
|
||||
global.activeSandboxProvider = provider;
|
||||
global.sandboxData = {
|
||||
sandboxId: sandboxInfo.sandboxId,
|
||||
url: sandboxInfo.url
|
||||
};
|
||||
|
||||
console.log(`[apply-ai-code-stream] Created new sandbox successfully`);
|
||||
} catch (createError) {
|
||||
console.error(`[apply-ai-code-stream] Failed to create new sandbox:`, createError);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `Failed to create new sandbox: ${createError instanceof Error ? createError.message : 'Unknown error'}`,
|
||||
results: {
|
||||
filesCreated: [],
|
||||
packagesInstalled: [],
|
||||
commandsExecuted: [],
|
||||
errors: [`Sandbox creation failed: ${createError instanceof Error ? createError.message : 'Unknown error'}`]
|
||||
},
|
||||
explanation: parsed.explanation,
|
||||
structure: parsed.structure,
|
||||
parsedFiles: parsed.files,
|
||||
message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox creation failed.`
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Create a response stream for real-time updates
|
||||
@@ -381,8 +397,8 @@ export async function POST(request: NextRequest) {
|
||||
await writer.write(encoder.encode(message));
|
||||
};
|
||||
|
||||
// Start processing in background (pass sandbox and request to the async function)
|
||||
(async (sandboxInstance, req) => {
|
||||
// Start processing in background (pass provider and request to the async function)
|
||||
(async (providerInstance, req) => {
|
||||
const results = {
|
||||
filesCreated: [] as string[],
|
||||
filesUpdated: [] as string[],
|
||||
@@ -447,7 +463,7 @@ export async function POST(request: NextRequest) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
packages: uniquePackages,
|
||||
sandboxId: sandboxId || (sandboxInstance as any).sandboxId
|
||||
sandboxId: sandboxId || providerInstance.getSandboxInfo()?.sandboxId
|
||||
})
|
||||
});
|
||||
|
||||
@@ -478,8 +494,8 @@ export async function POST(request: NextRequest) {
|
||||
if (data.type === 'success' && data.installedPackages) {
|
||||
results.packagesInstalled = data.installedPackages;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
} catch (parseError) {
|
||||
console.debug('Error parsing terminal output:', parseError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,7 +605,6 @@ export async function POST(request: NextRequest) {
|
||||
normalizedPath = 'src/' + normalizedPath;
|
||||
}
|
||||
|
||||
const fullPath = `/home/user/app/${normalizedPath}`;
|
||||
const isUpdate = global.existingFiles.has(normalizedPath);
|
||||
|
||||
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
|
||||
@@ -598,19 +613,23 @@ export async function POST(request: NextRequest) {
|
||||
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
||||
}
|
||||
|
||||
// Write the file using Python (code-interpreter SDK)
|
||||
const escapedContent = fileContent
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"""/g, '\\"\\"\\"')
|
||||
.replace(/\$/g, '\\$');
|
||||
// Fix common Tailwind CSS errors in CSS files
|
||||
if (file.path.endsWith('.css')) {
|
||||
// Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
|
||||
fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl');
|
||||
// Replace any other non-existent shadow utilities
|
||||
fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl');
|
||||
fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
|
||||
}
|
||||
|
||||
await sandboxInstance.runCode(`
|
||||
import os
|
||||
os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True)
|
||||
with open("${fullPath}", 'w') as f:
|
||||
f.write("""${escapedContent}""")
|
||||
print(f"File written: ${fullPath}")
|
||||
`);
|
||||
// Create directory if needed
|
||||
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
|
||||
if (dirPath) {
|
||||
await providerInstance.runCommand(`mkdir -p ${dirPath}`);
|
||||
}
|
||||
|
||||
// Write the file using provider
|
||||
await providerInstance.writeFile(normalizedPath, fileContent);
|
||||
|
||||
// Update file cache
|
||||
if (global.sandboxState?.fileCache) {
|
||||
@@ -663,27 +682,30 @@ print(f"File written: ${fullPath}")
|
||||
action: 'executing'
|
||||
});
|
||||
|
||||
// Use E2B commands.run() for cleaner execution
|
||||
const result = await sandboxInstance.commands.run(cmd, {
|
||||
cwd: '/home/user/app',
|
||||
timeout: 60,
|
||||
on_stdout: async (data: string) => {
|
||||
await sendProgress({
|
||||
type: 'command-output',
|
||||
command: cmd,
|
||||
output: data,
|
||||
stream: 'stdout'
|
||||
});
|
||||
},
|
||||
on_stderr: async (data: string) => {
|
||||
await sendProgress({
|
||||
type: 'command-output',
|
||||
command: cmd,
|
||||
output: data,
|
||||
stream: 'stderr'
|
||||
});
|
||||
}
|
||||
});
|
||||
// Use provider runCommand
|
||||
const result = await providerInstance.runCommand(cmd);
|
||||
|
||||
// Get command output from provider result
|
||||
const stdout = result.stdout;
|
||||
const stderr = result.stderr;
|
||||
|
||||
if (stdout) {
|
||||
await sendProgress({
|
||||
type: 'command-output',
|
||||
command: cmd,
|
||||
output: stdout,
|
||||
stream: 'stdout'
|
||||
});
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
await sendProgress({
|
||||
type: 'command-output',
|
||||
command: cmd,
|
||||
output: stderr,
|
||||
stream: 'stderr'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.commandsExecuted) {
|
||||
results.commandsExecuted.push(cmd);
|
||||
@@ -750,7 +772,7 @@ print(f"File written: ${fullPath}")
|
||||
} finally {
|
||||
await writer.close();
|
||||
}
|
||||
})(sandbox, request);
|
||||
})(provider, request);
|
||||
|
||||
// Return the stream
|
||||
return new Response(stream.readable, {
|
||||
@@ -760,7 +782,7 @@ print(f"File written: ${fullPath}")
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Apply AI code stream error:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
+115
-31
@@ -129,6 +129,7 @@ function parseAIResponse(response: string): ParsedResponse {
|
||||
|
||||
declare global {
|
||||
var activeSandbox: any;
|
||||
var activeSandboxProvider: any;
|
||||
var existingFiles: Set<string>;
|
||||
var sandboxState: SandboxState;
|
||||
}
|
||||
@@ -157,8 +158,11 @@ export async function POST(request: NextRequest) {
|
||||
global.existingFiles = new Set<string>();
|
||||
}
|
||||
|
||||
// Get the active sandbox or provider
|
||||
const sandbox = global.activeSandbox || global.activeSandboxProvider;
|
||||
|
||||
// If no active sandbox, just return parsed results
|
||||
if (!global.activeSandbox) {
|
||||
if (!sandbox) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
results: {
|
||||
@@ -174,6 +178,30 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// Verify sandbox is ready before applying code
|
||||
console.log('[apply-ai-code] Verifying sandbox is ready...');
|
||||
|
||||
// For Vercel sandboxes, check if Vite is running
|
||||
if (sandbox.constructor?.name === 'VercelProvider' || sandbox.getSandboxInfo?.()?.provider === 'vercel') {
|
||||
console.log('[apply-ai-code] Detected Vercel sandbox, checking Vite status...');
|
||||
try {
|
||||
// Check if Vite process is running
|
||||
const checkResult = await sandbox.runCommand('pgrep -f vite');
|
||||
if (!checkResult || !checkResult.stdout) {
|
||||
console.log('[apply-ai-code] Vite not running, starting it...');
|
||||
// Start Vite if not running
|
||||
await sandbox.runCommand('sh -c "cd /vercel/sandbox && nohup npm run dev > /tmp/vite.log 2>&1 &"');
|
||||
// Wait for Vite to start
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
console.log('[apply-ai-code] Vite started, proceeding with code application');
|
||||
} else {
|
||||
console.log('[apply-ai-code] Vite is already running');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[apply-ai-code] Could not check Vite status, proceeding anyway:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to active sandbox
|
||||
console.log('[apply-ai-code] Applying code to sandbox...');
|
||||
console.log('[apply-ai-code] Is edit mode:', isEdit);
|
||||
@@ -403,11 +431,28 @@ export async function POST(request: NextRequest) {
|
||||
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
||||
}
|
||||
|
||||
// Fix common Tailwind CSS errors in CSS files
|
||||
if (file.path.endsWith('.css')) {
|
||||
// Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
|
||||
fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl');
|
||||
// Replace any other non-existent shadow utilities
|
||||
fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl');
|
||||
fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
|
||||
}
|
||||
|
||||
console.log(`[apply-ai-code] Writing file using E2B files API: ${fullPath}`);
|
||||
|
||||
try {
|
||||
// Use the correct E2B API - sandbox.files.write()
|
||||
await global.activeSandbox.files.write(fullPath, fileContent);
|
||||
// Check if we're using provider pattern (v2) or direct sandbox (v1)
|
||||
if (sandbox.writeFile) {
|
||||
// V2: Provider pattern (Vercel/E2B provider)
|
||||
await sandbox.writeFile(file.path, fileContent);
|
||||
} else if (sandbox.files?.write) {
|
||||
// V1: Direct E2B sandbox
|
||||
await sandbox.files.write(fullPath, fileContent);
|
||||
} else {
|
||||
throw new Error('Unsupported sandbox type');
|
||||
}
|
||||
console.log(`[apply-ai-code] Successfully wrote file: ${fullPath}`);
|
||||
|
||||
// Update file cache
|
||||
@@ -499,15 +544,17 @@ function App() {
|
||||
export default App;`;
|
||||
|
||||
try {
|
||||
await global.activeSandbox.runCode(`
|
||||
file_path = "/home/user/app/src/App.jsx"
|
||||
file_content = """${appContent.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"""
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(file_content)
|
||||
|
||||
print(f"Auto-generated: {file_path}")
|
||||
`);
|
||||
// Use provider pattern if available
|
||||
if (sandbox.writeFile) {
|
||||
await sandbox.writeFile('src/App.jsx', appContent);
|
||||
} else if (sandbox.writeFiles) {
|
||||
await sandbox.writeFiles([{
|
||||
path: 'src/App.jsx',
|
||||
content: Buffer.from(appContent)
|
||||
}]);
|
||||
}
|
||||
|
||||
console.log('Auto-generated: src/App.jsx');
|
||||
results.filesCreated.push('src/App.jsx (auto-generated)');
|
||||
} catch (error) {
|
||||
results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`);
|
||||
@@ -526,9 +573,7 @@ print(f"Auto-generated: {file_path}")
|
||||
|
||||
if (!isEdit && !indexCssInParsed && !indexCssExists) {
|
||||
try {
|
||||
await global.activeSandbox.runCode(`
|
||||
file_path = "/home/user/app/src/index.css"
|
||||
file_content = """@tailwind base;
|
||||
const indexCssContent = `@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -550,15 +595,22 @@ body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}"""
|
||||
}`;
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(file_content)
|
||||
|
||||
print(f"Auto-generated: {file_path}")
|
||||
`);
|
||||
// Use provider pattern if available
|
||||
if (sandbox.writeFile) {
|
||||
await sandbox.writeFile('src/index.css', indexCssContent);
|
||||
} else if (sandbox.writeFiles) {
|
||||
await sandbox.writeFiles([{
|
||||
path: 'src/index.css',
|
||||
content: Buffer.from(indexCssContent)
|
||||
}]);
|
||||
}
|
||||
|
||||
console.log('Auto-generated: src/index.css');
|
||||
results.filesCreated.push('src/index.css (with Tailwind)');
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('Failed to create index.css:', error);
|
||||
results.errors.push('Failed to create index.css with Tailwind');
|
||||
}
|
||||
}
|
||||
@@ -567,15 +619,47 @@ print(f"Auto-generated: {file_path}")
|
||||
// Execute commands
|
||||
for (const cmd of parsed.commands) {
|
||||
try {
|
||||
await global.activeSandbox.runCode(`
|
||||
import subprocess
|
||||
os.chdir('/home/user/app')
|
||||
result = subprocess.run(${JSON.stringify(cmd.split(' '))}, capture_output=True, text=True)
|
||||
print(f"Executed: ${cmd}")
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print(f"Errors: {result.stderr}")
|
||||
`);
|
||||
// Parse command and arguments
|
||||
const commandParts = cmd.trim().split(/\s+/);
|
||||
const cmdName = commandParts[0];
|
||||
const args = commandParts.slice(1);
|
||||
|
||||
// Execute command using sandbox
|
||||
let result;
|
||||
if (sandbox.runCommand && typeof sandbox.runCommand === 'function') {
|
||||
// Check if this is a provider pattern sandbox
|
||||
const testResult = await sandbox.runCommand(cmd);
|
||||
if (testResult && typeof testResult === 'object' && 'stdout' in testResult) {
|
||||
// Provider returns CommandResult directly
|
||||
result = testResult;
|
||||
} else {
|
||||
// Direct sandbox - expects object with cmd and args
|
||||
result = await sandbox.runCommand({
|
||||
cmd: cmdName,
|
||||
args
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Executed: ${cmd}`);
|
||||
|
||||
// Handle result based on type
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
if (result) {
|
||||
if (typeof result.stdout === 'string') {
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr || '';
|
||||
} else if (typeof result.stdout === 'function') {
|
||||
stdout = await result.stdout();
|
||||
stderr = await result.stderr();
|
||||
}
|
||||
}
|
||||
|
||||
if (stdout) console.log(stdout);
|
||||
if (stderr) console.log(`Errors: ${stderr}`);
|
||||
|
||||
results.commandsExecuted.push(cmd);
|
||||
} catch (error) {
|
||||
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
|
||||
|
||||
@@ -59,10 +59,26 @@ export async function POST(request: NextRequest) {
|
||||
case 'clear-old':
|
||||
// Clear old conversation data but keep recent context
|
||||
if (!global.conversationState) {
|
||||
// Initialize conversation state if it doesn't exist
|
||||
global.conversationState = {
|
||||
conversationId: `conv-${Date.now()}`,
|
||||
startedAt: Date.now(),
|
||||
lastUpdated: Date.now(),
|
||||
context: {
|
||||
messages: [],
|
||||
edits: [],
|
||||
projectEvolution: { majorChanges: [] },
|
||||
userPreferences: {}
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[conversation-state] Initialized new conversation state for clear-old');
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'No active conversation to clear'
|
||||
}, { status: 400 });
|
||||
success: true,
|
||||
message: 'New conversation state initialized',
|
||||
state: global.conversationState
|
||||
});
|
||||
}
|
||||
|
||||
// Keep only recent data
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { SandboxFactory } from '@/lib/sandbox/factory';
|
||||
// SandboxProvider type is used through SandboxFactory
|
||||
import type { SandboxState } from '@/types/sandbox';
|
||||
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
||||
|
||||
// Store active sandbox globally
|
||||
declare global {
|
||||
var activeSandboxProvider: any;
|
||||
var sandboxData: any;
|
||||
var existingFiles: Set<string>;
|
||||
var sandboxState: SandboxState;
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
console.log('[create-ai-sandbox-v2] Creating sandbox...');
|
||||
|
||||
// Clean up all existing sandboxes
|
||||
console.log('[create-ai-sandbox-v2] Cleaning up existing sandboxes...');
|
||||
await sandboxManager.terminateAll();
|
||||
|
||||
// Also clean up legacy global state
|
||||
if (global.activeSandboxProvider) {
|
||||
try {
|
||||
await global.activeSandboxProvider.terminate();
|
||||
} catch (e) {
|
||||
console.error('Failed to terminate legacy global sandbox:', e);
|
||||
}
|
||||
global.activeSandboxProvider = null;
|
||||
}
|
||||
|
||||
// Clear existing files tracking
|
||||
if (global.existingFiles) {
|
||||
global.existingFiles.clear();
|
||||
} else {
|
||||
global.existingFiles = new Set<string>();
|
||||
}
|
||||
|
||||
// Create new sandbox using factory
|
||||
const provider = SandboxFactory.create();
|
||||
const sandboxInfo = await provider.createSandbox();
|
||||
|
||||
console.log('[create-ai-sandbox-v2] Setting up Vite React app...');
|
||||
await provider.setupViteApp();
|
||||
|
||||
// Register with sandbox manager
|
||||
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
|
||||
|
||||
// Also store in legacy global state for backward compatibility
|
||||
global.activeSandboxProvider = provider;
|
||||
global.sandboxData = {
|
||||
sandboxId: sandboxInfo.sandboxId,
|
||||
url: sandboxInfo.url
|
||||
};
|
||||
|
||||
// Initialize sandbox state
|
||||
global.sandboxState = {
|
||||
fileCache: {
|
||||
files: {},
|
||||
lastSync: Date.now(),
|
||||
sandboxId: sandboxInfo.sandboxId
|
||||
},
|
||||
sandbox: provider, // Store the provider instead of raw sandbox
|
||||
sandboxData: {
|
||||
sandboxId: sandboxInfo.sandboxId,
|
||||
url: sandboxInfo.url
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[create-ai-sandbox-v2] Sandbox ready at:', sandboxInfo.url);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
sandboxId: sandboxInfo.sandboxId,
|
||||
url: sandboxInfo.url,
|
||||
provider: sandboxInfo.provider,
|
||||
message: 'Sandbox created and Vite React app initialized'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[create-ai-sandbox-v2] Error:', error);
|
||||
|
||||
// Clean up on error
|
||||
await sandboxManager.terminateAll();
|
||||
if (global.activeSandboxProvider) {
|
||||
try {
|
||||
await global.activeSandboxProvider.terminate();
|
||||
} catch (e) {
|
||||
console.error('Failed to terminate sandbox on error:', e);
|
||||
}
|
||||
global.activeSandboxProvider = null;
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Failed to create sandbox',
|
||||
details: error instanceof Error ? error.stack : undefined
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+217
-198
@@ -1,5 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Sandbox } from '@e2b/code-interpreter';
|
||||
import { Sandbox } from '@vercel/sandbox';
|
||||
import type { SandboxState } from '@/types/sandbox';
|
||||
import { appConfig } from '@/config/app.config';
|
||||
|
||||
@@ -9,23 +9,74 @@ declare global {
|
||||
var sandboxData: any;
|
||||
var existingFiles: Set<string>;
|
||||
var sandboxState: SandboxState;
|
||||
var sandboxCreationInProgress: boolean;
|
||||
var sandboxCreationPromise: Promise<any> | null;
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
// Check if sandbox creation is already in progress
|
||||
if (global.sandboxCreationInProgress && global.sandboxCreationPromise) {
|
||||
console.log('[create-ai-sandbox] Sandbox creation already in progress, waiting for existing creation...');
|
||||
try {
|
||||
const existingResult = await global.sandboxCreationPromise;
|
||||
console.log('[create-ai-sandbox] Returning existing sandbox creation result');
|
||||
return NextResponse.json(existingResult);
|
||||
} catch (error) {
|
||||
console.error('[create-ai-sandbox] Existing sandbox creation failed:', error);
|
||||
// Continue with new creation if the existing one failed
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we already have an active sandbox
|
||||
if (global.activeSandbox && global.sandboxData) {
|
||||
console.log('[create-ai-sandbox] Returning existing active sandbox');
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
sandboxId: global.sandboxData.sandboxId,
|
||||
url: global.sandboxData.url
|
||||
});
|
||||
}
|
||||
|
||||
// Set the creation flag
|
||||
global.sandboxCreationInProgress = true;
|
||||
|
||||
// Create the promise that other requests can await
|
||||
global.sandboxCreationPromise = createSandboxInternal();
|
||||
|
||||
try {
|
||||
const result = await global.sandboxCreationPromise;
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('[create-ai-sandbox] Sandbox creation failed:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Failed to create sandbox',
|
||||
details: error instanceof Error ? error.stack : undefined
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
global.sandboxCreationInProgress = false;
|
||||
global.sandboxCreationPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function createSandboxInternal() {
|
||||
let sandbox: any = null;
|
||||
|
||||
try {
|
||||
console.log('[create-ai-sandbox] Creating base sandbox...');
|
||||
console.log('[create-ai-sandbox] Creating Vercel sandbox...');
|
||||
|
||||
// Kill existing sandbox if any
|
||||
if (global.activeSandbox) {
|
||||
console.log('[create-ai-sandbox] Killing existing sandbox...');
|
||||
console.log('[create-ai-sandbox] Stopping existing sandbox...');
|
||||
try {
|
||||
await global.activeSandbox.kill();
|
||||
await global.activeSandbox.stop();
|
||||
} catch (e) {
|
||||
console.error('Failed to close existing sandbox:', e);
|
||||
console.error('Failed to stop existing sandbox:', e);
|
||||
}
|
||||
global.activeSandbox = null;
|
||||
global.sandboxData = null;
|
||||
}
|
||||
|
||||
// Clear existing files tracking
|
||||
@@ -35,81 +86,102 @@ export async function POST() {
|
||||
global.existingFiles = new Set<string>();
|
||||
}
|
||||
|
||||
// Create base sandbox - we'll set up Vite ourselves for full control
|
||||
console.log(`[create-ai-sandbox] Creating base E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`);
|
||||
sandbox = await Sandbox.create({
|
||||
apiKey: process.env.E2B_API_KEY,
|
||||
timeoutMs: appConfig.e2b.timeoutMs
|
||||
});
|
||||
// Create Vercel sandbox with flexible authentication
|
||||
console.log(`[create-ai-sandbox] Creating Vercel sandbox with ${appConfig.vercelSandbox.timeoutMinutes} minute timeout...`);
|
||||
|
||||
const sandboxId = (sandbox as any).sandboxId || Date.now().toString();
|
||||
const host = (sandbox as any).getHost(appConfig.e2b.vitePort);
|
||||
// Prepare sandbox configuration
|
||||
const sandboxConfig: any = {
|
||||
timeout: appConfig.vercelSandbox.timeoutMs,
|
||||
runtime: appConfig.vercelSandbox.runtime,
|
||||
ports: [appConfig.vercelSandbox.devPort]
|
||||
};
|
||||
|
||||
// Add authentication parameters if using personal access token
|
||||
if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
|
||||
console.log('[create-ai-sandbox] Using personal access token authentication');
|
||||
sandboxConfig.teamId = process.env.VERCEL_TEAM_ID;
|
||||
sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID;
|
||||
sandboxConfig.token = process.env.VERCEL_TOKEN;
|
||||
} else if (process.env.VERCEL_OIDC_TOKEN) {
|
||||
console.log('[create-ai-sandbox] Using OIDC token authentication');
|
||||
} else {
|
||||
console.log('[create-ai-sandbox] No authentication found - relying on default Vercel authentication');
|
||||
}
|
||||
|
||||
sandbox = await Sandbox.create(sandboxConfig);
|
||||
|
||||
const sandboxId = sandbox.sandboxId;
|
||||
console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`);
|
||||
console.log(`[create-ai-sandbox] Sandbox host: ${host}`);
|
||||
|
||||
// Set up a basic Vite React app using Python to write files
|
||||
// Set up a basic Vite React app
|
||||
console.log('[create-ai-sandbox] Setting up Vite React app...');
|
||||
|
||||
// Write all files in a single Python script to avoid multiple executions
|
||||
const setupScript = `
|
||||
import os
|
||||
import json
|
||||
// First, change to the working directory
|
||||
await sandbox.runCommand('pwd');
|
||||
// workDir is defined in appConfig - not needed here
|
||||
|
||||
// Get the sandbox URL using the correct Vercel Sandbox API
|
||||
const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort);
|
||||
|
||||
// Extract the hostname from the sandbox URL for Vite config
|
||||
const sandboxHostname = new URL(sandboxUrl).hostname;
|
||||
console.log(`[create-ai-sandbox] Sandbox hostname: ${sandboxHostname}`);
|
||||
|
||||
print('Setting up React app with Vite and Tailwind...')
|
||||
|
||||
# Create directory structure
|
||||
os.makedirs('/home/user/app/src', exist_ok=True)
|
||||
|
||||
# Package.json
|
||||
package_json = {
|
||||
"name": "sandbox-app",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"vite": "^4.3.9",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"postcss": "^8.4.31",
|
||||
"autoprefixer": "^10.4.16"
|
||||
}
|
||||
}
|
||||
|
||||
with open('/home/user/app/package.json', 'w') as f:
|
||||
json.dump(package_json, f, indent=2)
|
||||
print('✓ package.json')
|
||||
|
||||
# Vite config for E2B - with allowedHosts
|
||||
vite_config = """import { defineConfig } from 'vite'
|
||||
// Create the Vite config content with the proper hostname (using string concatenation)
|
||||
const viteConfigContent = `import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// E2B-compatible Vite configuration
|
||||
// Vercel Sandbox compatible Vite configuration
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
port: ${appConfig.vercelSandbox.devPort},
|
||||
strictPort: true,
|
||||
hmr: false,
|
||||
allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1']
|
||||
hmr: true,
|
||||
allowedHosts: [
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'` + sandboxHostname + `', // Allow the Vercel Sandbox domain
|
||||
'.vercel.run', // Allow all Vercel sandbox domains
|
||||
'.vercel-sandbox.dev' // Fallback pattern
|
||||
]
|
||||
}
|
||||
})"""
|
||||
})`;
|
||||
|
||||
with open('/home/user/app/vite.config.js', 'w') as f:
|
||||
f.write(vite_config)
|
||||
print('✓ vite.config.js')
|
||||
|
||||
# Tailwind config - standard without custom design tokens
|
||||
tailwind_config = """/** @type {import('tailwindcss').Config} */
|
||||
// Create the project files (now we have the sandbox hostname)
|
||||
const projectFiles = [
|
||||
{
|
||||
path: 'package.json',
|
||||
content: Buffer.from(JSON.stringify({
|
||||
"name": "sandbox-app",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host --port 3000",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"vite": "^4.3.9",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"postcss": "^8.4.31",
|
||||
"autoprefixer": "^10.4.16"
|
||||
}
|
||||
}, null, 2))
|
||||
},
|
||||
{
|
||||
path: 'vite.config.js',
|
||||
content: Buffer.from(viteConfigContent)
|
||||
},
|
||||
{
|
||||
path: 'tailwind.config.js',
|
||||
content: Buffer.from(`/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
@@ -119,26 +191,20 @@ export default {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}"""
|
||||
|
||||
with open('/home/user/app/tailwind.config.js', 'w') as f:
|
||||
f.write(tailwind_config)
|
||||
print('✓ tailwind.config.js')
|
||||
|
||||
# PostCSS config
|
||||
postcss_config = """export default {
|
||||
}`)
|
||||
},
|
||||
{
|
||||
path: 'postcss.config.js',
|
||||
content: Buffer.from(`export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}"""
|
||||
|
||||
with open('/home/user/app/postcss.config.js', 'w') as f:
|
||||
f.write(postcss_config)
|
||||
print('✓ postcss.config.js')
|
||||
|
||||
# Index.html
|
||||
index_html = """<!DOCTYPE html>
|
||||
}`)
|
||||
},
|
||||
{
|
||||
path: 'index.html',
|
||||
content: Buffer.from(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -149,14 +215,11 @@ index_html = """<!DOCTYPE html>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
with open('/home/user/app/index.html', 'w') as f:
|
||||
f.write(index_html)
|
||||
print('✓ index.html')
|
||||
|
||||
# Main.jsx
|
||||
main_jsx = """import React from 'react'
|
||||
</html>`)
|
||||
},
|
||||
{
|
||||
path: 'src/main.jsx',
|
||||
content: Buffer.from(`import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
@@ -165,19 +228,18 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)"""
|
||||
|
||||
with open('/home/user/app/src/main.jsx', 'w') as f:
|
||||
f.write(main_jsx)
|
||||
print('✓ src/main.jsx')
|
||||
|
||||
# App.jsx with explicit Tailwind test
|
||||
app_jsx = """function App() {
|
||||
)`)
|
||||
},
|
||||
{
|
||||
path: 'src/App.jsx',
|
||||
content: Buffer.from(`function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
|
||||
<div className="text-center max-w-2xl">
|
||||
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-blue-500 to-purple-600 bg-clip-text text-transparent">
|
||||
Sandbox Ready
|
||||
</h1>
|
||||
<p className="text-lg text-gray-400">
|
||||
Sandbox Ready<br/>
|
||||
Start building your React app with Vite and Tailwind CSS!
|
||||
</p>
|
||||
</div>
|
||||
@@ -185,14 +247,11 @@ app_jsx = """function App() {
|
||||
)
|
||||
}
|
||||
|
||||
export default App"""
|
||||
|
||||
with open('/home/user/app/src/App.jsx', 'w') as f:
|
||||
f.write(app_jsx)
|
||||
print('✓ src/App.jsx')
|
||||
|
||||
# Index.css with explicit Tailwind directives
|
||||
index_css = """@tailwind base;
|
||||
export default App`)
|
||||
},
|
||||
{
|
||||
path: 'src/index.css',
|
||||
content: Buffer.from(`@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -216,99 +275,53 @@ index_css = """@tailwind base;
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background-color: rgb(17 24 39);
|
||||
}"""
|
||||
}`)
|
||||
}
|
||||
];
|
||||
|
||||
with open('/home/user/app/src/index.css', 'w') as f:
|
||||
f.write(index_css)
|
||||
print('✓ src/index.css')
|
||||
|
||||
print('\\nAll files created successfully!')
|
||||
`;
|
||||
|
||||
// Execute the setup script
|
||||
await sandbox.runCode(setupScript);
|
||||
// Create directory structure first
|
||||
await sandbox.runCommand({
|
||||
cmd: 'mkdir',
|
||||
args: ['-p', 'src']
|
||||
});
|
||||
|
||||
// Write all files
|
||||
await sandbox.writeFiles(projectFiles);
|
||||
console.log('[create-ai-sandbox] ✓ Project files created');
|
||||
|
||||
// Install dependencies
|
||||
console.log('[create-ai-sandbox] Installing dependencies...');
|
||||
await sandbox.runCode(`
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
print('Installing npm packages...')
|
||||
result = subprocess.run(
|
||||
['npm', 'install'],
|
||||
cwd='/home/user/app',
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print('✓ Dependencies installed successfully')
|
||||
else:
|
||||
print(f'⚠ Warning: npm install had issues: {result.stderr}')
|
||||
# Continue anyway as it might still work
|
||||
`);
|
||||
const installResult = await sandbox.runCommand({
|
||||
cmd: 'npm',
|
||||
args: ['install', '--loglevel', 'info']
|
||||
});
|
||||
if (installResult.exitCode === 0) {
|
||||
console.log('[create-ai-sandbox] ✓ Dependencies installed successfully');
|
||||
} else {
|
||||
console.log('[create-ai-sandbox] ⚠ Warning: npm install had issues but continuing...');
|
||||
}
|
||||
|
||||
// Start Vite dev server
|
||||
// Start Vite dev server in detached mode
|
||||
console.log('[create-ai-sandbox] Starting Vite dev server...');
|
||||
await sandbox.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
import time
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
|
||||
# Kill any existing Vite processes
|
||||
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
|
||||
time.sleep(1)
|
||||
|
||||
# Start Vite dev server
|
||||
env = os.environ.copy()
|
||||
env['FORCE_COLOR'] = '0'
|
||||
|
||||
process = subprocess.Popen(
|
||||
['npm', 'run', 'dev'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=env
|
||||
)
|
||||
|
||||
print(f'✓ Vite dev server started with PID: {process.pid}')
|
||||
print('Waiting for server to be ready...')
|
||||
`);
|
||||
const viteProcess = await sandbox.runCommand({
|
||||
cmd: 'npm',
|
||||
args: ['run', 'dev'],
|
||||
detached: true
|
||||
});
|
||||
|
||||
console.log('[create-ai-sandbox] ✓ Vite dev server started');
|
||||
|
||||
// Wait for Vite to be fully ready
|
||||
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
|
||||
|
||||
// Force Tailwind CSS to rebuild by touching the CSS file
|
||||
await sandbox.runCode(`
|
||||
import os
|
||||
import time
|
||||
|
||||
# Touch the CSS file to trigger rebuild
|
||||
css_file = '/home/user/app/src/index.css'
|
||||
if os.path.exists(css_file):
|
||||
os.utime(css_file, None)
|
||||
print('✓ Triggered CSS rebuild')
|
||||
|
||||
# Also ensure PostCSS processes it
|
||||
time.sleep(2)
|
||||
print('✓ Tailwind CSS should be loaded')
|
||||
`);
|
||||
await new Promise(resolve => setTimeout(resolve, appConfig.vercelSandbox.devServerStartupDelay));
|
||||
|
||||
// Store sandbox globally
|
||||
global.activeSandbox = sandbox;
|
||||
global.sandboxData = {
|
||||
sandboxId,
|
||||
url: `https://${host}`
|
||||
url: sandboxUrl,
|
||||
viteProcess
|
||||
};
|
||||
|
||||
// Set extended timeout on the sandbox instance if method available
|
||||
if (typeof sandbox.setTimeout === 'function') {
|
||||
sandbox.setTimeout(appConfig.e2b.timeoutMs);
|
||||
console.log(`[create-ai-sandbox] Set sandbox timeout to ${appConfig.e2b.timeoutMinutes} minutes`);
|
||||
}
|
||||
|
||||
// Initialize sandbox state
|
||||
global.sandboxState = {
|
||||
fileCache: {
|
||||
@@ -319,7 +332,7 @@ print('✓ Tailwind CSS should be loaded')
|
||||
sandbox,
|
||||
sandboxData: {
|
||||
sandboxId,
|
||||
url: `https://${host}`
|
||||
url: sandboxUrl
|
||||
}
|
||||
};
|
||||
|
||||
@@ -333,14 +346,22 @@ print('✓ Tailwind CSS should be loaded')
|
||||
global.existingFiles.add('tailwind.config.js');
|
||||
global.existingFiles.add('postcss.config.js');
|
||||
|
||||
console.log('[create-ai-sandbox] Sandbox ready at:', `https://${host}`);
|
||||
console.log('[create-ai-sandbox] Sandbox ready at:', sandboxUrl);
|
||||
|
||||
return NextResponse.json({
|
||||
const result = {
|
||||
success: true,
|
||||
sandboxId,
|
||||
url: `https://${host}`,
|
||||
message: 'Sandbox created and Vite React app initialized'
|
||||
});
|
||||
url: sandboxUrl,
|
||||
message: 'Vercel sandbox created and Vite React app initialized'
|
||||
};
|
||||
|
||||
// Store the result for reuse
|
||||
global.sandboxData = {
|
||||
...global.sandboxData,
|
||||
...result
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[create-ai-sandbox] Error:', error);
|
||||
@@ -348,18 +369,16 @@ print('✓ Tailwind CSS should be loaded')
|
||||
// Clean up on error
|
||||
if (sandbox) {
|
||||
try {
|
||||
await sandbox.kill();
|
||||
await sandbox.stop();
|
||||
} catch (e) {
|
||||
console.error('Failed to close sandbox on error:', e);
|
||||
console.error('Failed to stop sandbox on error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Failed to create sandbox',
|
||||
details: error instanceof Error ? error.stack : undefined
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
// Clear global state on error
|
||||
global.activeSandbox = null;
|
||||
global.sandboxData = null;
|
||||
|
||||
throw error; // Throw to be caught by the outer handler
|
||||
}
|
||||
}
|
||||
+38
-39
@@ -1,10 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
declare global {
|
||||
var activeSandbox: any;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
export async function POST() {
|
||||
try {
|
||||
if (!global.activeSandbox) {
|
||||
return NextResponse.json({
|
||||
@@ -15,41 +15,37 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
console.log('[create-zip] Creating project zip...');
|
||||
|
||||
// Create zip file in sandbox
|
||||
const result = await global.activeSandbox.runCode(`
|
||||
import zipfile
|
||||
import os
|
||||
import json
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
|
||||
# Create zip file
|
||||
with zipfile.ZipFile('/tmp/project.zip', 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk('.'):
|
||||
# Skip node_modules and .git
|
||||
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist']]
|
||||
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
arcname = os.path.relpath(file_path, '.')
|
||||
zipf.write(file_path, arcname)
|
||||
|
||||
# Get file size
|
||||
file_size = os.path.getsize('/tmp/project.zip')
|
||||
print(f" Created project.zip ({file_size} bytes)")
|
||||
`);
|
||||
// Create zip file in sandbox using standard commands
|
||||
const zipResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'bash',
|
||||
args: ['-c', `zip -r /tmp/project.zip . -x "node_modules/*" ".git/*" ".next/*" "dist/*" "build/*" "*.log"`]
|
||||
});
|
||||
|
||||
if (zipResult.exitCode !== 0) {
|
||||
const error = await zipResult.stderr();
|
||||
throw new Error(`Failed to create zip: ${error}`);
|
||||
}
|
||||
|
||||
const sizeResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'bash',
|
||||
args: ['-c', `ls -la /tmp/project.zip | awk '{print $5}'`]
|
||||
});
|
||||
|
||||
const fileSize = await sizeResult.stdout();
|
||||
console.log(`[create-zip] Created project.zip (${fileSize.trim()} bytes)`);
|
||||
|
||||
// Read the zip file and convert to base64
|
||||
const readResult = await global.activeSandbox.runCode(`
|
||||
import base64
|
||||
|
||||
with open('/tmp/project.zip', 'rb') as f:
|
||||
content = f.read()
|
||||
encoded = base64.b64encode(content).decode('utf-8')
|
||||
print(encoded)
|
||||
`);
|
||||
const readResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'base64',
|
||||
args: ['/tmp/project.zip']
|
||||
});
|
||||
|
||||
const base64Content = readResult.logs.stdout.join('').trim();
|
||||
if (readResult.exitCode !== 0) {
|
||||
const error = await readResult.stderr();
|
||||
throw new Error(`Failed to read zip file: ${error}`);
|
||||
}
|
||||
|
||||
const base64Content = (await readResult.stdout()).trim();
|
||||
|
||||
// Create a data URL for download
|
||||
const dataUrl = `data:application/zip;base64,${base64Content}`;
|
||||
@@ -57,15 +53,18 @@ with open('/tmp/project.zip', 'rb') as f:
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
dataUrl,
|
||||
fileName: 'e2b-project.zip',
|
||||
fileName: 'vercel-sandbox-project.zip',
|
||||
message: 'Zip file created successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[create-zip] Error:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
}, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -64,15 +64,7 @@ export async function POST(request: NextRequest) {
|
||||
const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process'];
|
||||
if (builtins.includes(imp)) return false;
|
||||
|
||||
// Extract package name (handle scoped packages and subpaths)
|
||||
const parts = imp.split('/');
|
||||
if (imp.startsWith('@')) {
|
||||
// Scoped package like @vitejs/plugin-react
|
||||
return true;
|
||||
} else {
|
||||
// Regular package, return just the first part
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Extract just the package names (without subpaths)
|
||||
@@ -101,153 +93,90 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Check which packages are already installed
|
||||
const checkResult = await global.activeSandbox.runCode(`
|
||||
import os
|
||||
import json
|
||||
|
||||
installed = []
|
||||
missing = []
|
||||
|
||||
packages = ${JSON.stringify(uniquePackages)}
|
||||
|
||||
for package in packages:
|
||||
# Handle scoped packages
|
||||
if package.startswith('@'):
|
||||
package_path = f"/home/user/app/node_modules/{package}"
|
||||
else:
|
||||
package_path = f"/home/user/app/node_modules/{package}"
|
||||
const installed: string[] = [];
|
||||
const missing: string[] = [];
|
||||
|
||||
if os.path.exists(package_path):
|
||||
installed.append(package)
|
||||
else:
|
||||
missing.append(package)
|
||||
for (const packageName of uniquePackages) {
|
||||
try {
|
||||
const checkResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'test',
|
||||
args: ['-d', `node_modules/${packageName}`]
|
||||
});
|
||||
|
||||
if (checkResult.exitCode === 0) {
|
||||
installed.push(packageName);
|
||||
} else {
|
||||
missing.push(packageName);
|
||||
}
|
||||
} catch (checkError) {
|
||||
// If test command fails, assume package is missing
|
||||
console.debug(`Package check failed for ${packageName}:`, checkError);
|
||||
missing.push(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
result = {
|
||||
'installed': installed,
|
||||
'missing': missing
|
||||
}
|
||||
console.log('[detect-and-install-packages] Package status:', { installed, missing });
|
||||
|
||||
print(json.dumps(result))
|
||||
`);
|
||||
|
||||
const status = JSON.parse(checkResult.logs.stdout.join(''));
|
||||
console.log('[detect-and-install-packages] Package status:', status);
|
||||
|
||||
if (status.missing.length === 0) {
|
||||
if (missing.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
packagesInstalled: [],
|
||||
packagesAlreadyInstalled: status.installed,
|
||||
packagesAlreadyInstalled: installed,
|
||||
message: 'All packages already installed'
|
||||
});
|
||||
}
|
||||
|
||||
// Install missing packages
|
||||
console.log('[detect-and-install-packages] Installing packages:', status.missing);
|
||||
console.log('[detect-and-install-packages] Installing packages:', missing);
|
||||
|
||||
const installResult = await global.activeSandbox.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
const installResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'npm',
|
||||
args: ['install', '--save', ...missing]
|
||||
});
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
packages_to_install = ${JSON.stringify(status.missing)}
|
||||
|
||||
# Join packages into a single install command
|
||||
packages_str = ' '.join(packages_to_install)
|
||||
cmd = f'npm install {packages_str} --save'
|
||||
|
||||
print(f"Running: {cmd}")
|
||||
|
||||
# Run npm install with explicit save flag
|
||||
result = subprocess.run(['npm', 'install', '--save'] + packages_to_install,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd='/home/user/app',
|
||||
timeout=60)
|
||||
|
||||
print("stdout:", result.stdout)
|
||||
if result.stderr:
|
||||
print("stderr:", result.stderr)
|
||||
|
||||
# Verify installation
|
||||
installed = []
|
||||
failed = []
|
||||
|
||||
for package in packages_to_install:
|
||||
# Handle scoped packages correctly
|
||||
if package.startswith('@'):
|
||||
# For scoped packages like @heroicons/react
|
||||
package_path = f"/home/user/app/node_modules/{package}"
|
||||
else:
|
||||
package_path = f"/home/user/app/node_modules/{package}"
|
||||
const stdout = await installResult.stdout();
|
||||
const stderr = await installResult.stderr();
|
||||
|
||||
if os.path.exists(package_path):
|
||||
installed.append(package)
|
||||
print(f"✓ Verified installation of {package}")
|
||||
else:
|
||||
# Check if it's a submodule of an installed package
|
||||
base_package = package.split('/')[0]
|
||||
if package.startswith('@'):
|
||||
# For @scope/package, the base is @scope/package
|
||||
base_package = '/'.join(package.split('/')[:2])
|
||||
|
||||
base_path = f"/home/user/app/node_modules/{base_package}"
|
||||
if os.path.exists(base_path):
|
||||
installed.append(package)
|
||||
print(f"✓ Verified installation of {package} (via {base_package})")
|
||||
else:
|
||||
failed.append(package)
|
||||
print(f"✗ Failed to verify installation of {package}")
|
||||
|
||||
result_data = {
|
||||
'installed': installed,
|
||||
'failed': failed,
|
||||
'returncode': result.returncode
|
||||
}
|
||||
|
||||
print("\\nResult:", json.dumps(result_data))
|
||||
`, { timeout: 60000 });
|
||||
|
||||
// Parse the result more safely
|
||||
let installStatus;
|
||||
try {
|
||||
const stdout = installResult.logs.stdout.join('');
|
||||
const resultMatch = stdout.match(/Result:\s*({.*})/);
|
||||
if (resultMatch) {
|
||||
installStatus = JSON.parse(resultMatch[1]);
|
||||
} else {
|
||||
// Fallback parsing
|
||||
const lines = stdout.split('\n');
|
||||
const resultLine = lines.find((line: string) => line.includes('Result:'));
|
||||
if (resultLine) {
|
||||
installStatus = JSON.parse(resultLine.split('Result:')[1].trim());
|
||||
} else {
|
||||
throw new Error('Could not find Result in output');
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('[detect-and-install-packages] Failed to parse install result:', parseError);
|
||||
console.error('[detect-and-install-packages] stdout:', installResult.logs.stdout.join(''));
|
||||
// Fallback to assuming all packages were installed
|
||||
installStatus = {
|
||||
installed: status.missing,
|
||||
failed: [],
|
||||
returncode: 0
|
||||
};
|
||||
console.log('[detect-and-install-packages] Install stdout:', stdout);
|
||||
if (stderr) {
|
||||
console.log('[detect-and-install-packages] Install stderr:', stderr);
|
||||
}
|
||||
|
||||
if (installStatus.failed.length > 0) {
|
||||
console.error('[detect-and-install-packages] Failed to install:', installStatus.failed);
|
||||
// Verify installation
|
||||
const finalInstalled: string[] = [];
|
||||
const failed: string[] = [];
|
||||
|
||||
for (const packageName of missing) {
|
||||
try {
|
||||
const verifyResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'test',
|
||||
args: ['-d', `node_modules/${packageName}`]
|
||||
});
|
||||
|
||||
if (verifyResult.exitCode === 0) {
|
||||
finalInstalled.push(packageName);
|
||||
console.log(`✓ Verified installation of ${packageName}`);
|
||||
} else {
|
||||
failed.push(packageName);
|
||||
console.log(`✗ Failed to verify installation of ${packageName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
failed.push(packageName);
|
||||
console.log(`✗ Error verifying ${packageName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
console.error('[detect-and-install-packages] Failed to install:', failed);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
packagesInstalled: installStatus.installed,
|
||||
packagesFailed: installStatus.failed,
|
||||
packagesAlreadyInstalled: status.installed,
|
||||
message: `Installed ${installStatus.installed.length} packages`,
|
||||
logs: installResult.logs.stdout.join('\n')
|
||||
packagesInstalled: finalInstalled,
|
||||
packagesFailed: failed,
|
||||
packagesAlreadyInstalled: installed,
|
||||
message: `Installed ${finalInstalled.length} packages`,
|
||||
logs: stdout
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,21 +11,37 @@ import { FileManifest } from '@/types/file-manifest';
|
||||
import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation';
|
||||
import { appConfig } from '@/config/app.config';
|
||||
|
||||
// Force dynamic route to enable streaming
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Check if we're using Vercel AI Gateway
|
||||
const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY;
|
||||
const aiGatewayBaseURL = 'https://ai-gateway.vercel.sh/v1';
|
||||
|
||||
console.log('[generate-ai-code-stream] AI Gateway config:', {
|
||||
isUsingAIGateway,
|
||||
hasGroqKey: !!process.env.GROQ_API_KEY,
|
||||
hasAIGatewayKey: !!process.env.AI_GATEWAY_API_KEY
|
||||
});
|
||||
|
||||
const groq = createGroq({
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GROQ_API_KEY,
|
||||
baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined,
|
||||
});
|
||||
|
||||
const anthropic = createAnthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1',
|
||||
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.ANTHROPIC_API_KEY,
|
||||
baseURL: isUsingAIGateway ? aiGatewayBaseURL : (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1'),
|
||||
});
|
||||
|
||||
const googleGenerativeAI = createGoogleGenerativeAI({
|
||||
apiKey: process.env.GEMINI_API_KEY,
|
||||
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GEMINI_API_KEY,
|
||||
baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined,
|
||||
});
|
||||
|
||||
const openai = createOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.OPENAI_API_KEY,
|
||||
baseURL: isUsingAIGateway ? aiGatewayBaseURL : process.env.OPENAI_BASE_URL,
|
||||
});
|
||||
|
||||
// Helper function to analyze user preferences from conversation history
|
||||
@@ -142,10 +158,18 @@ export async function POST(request: NextRequest) {
|
||||
const stream = new TransformStream();
|
||||
const writer = stream.writable.getWriter();
|
||||
|
||||
// Function to send progress updates
|
||||
// Function to send progress updates with flushing
|
||||
const sendProgress = async (data: any) => {
|
||||
const message = `data: ${JSON.stringify(data)}\n\n`;
|
||||
await writer.write(encoder.encode(message));
|
||||
try {
|
||||
await writer.write(encoder.encode(message));
|
||||
// Force flush by writing a keep-alive comment
|
||||
if (data.type === 'stream' || data.type === 'conversation') {
|
||||
await writer.write(encoder.encode(': keepalive\n\n'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[generate-ai-code-stream] Error writing to stream:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Start processing in background
|
||||
@@ -170,7 +194,7 @@ export async function POST(request: NextRequest) {
|
||||
if (manifest) {
|
||||
await sendProgress({ type: 'status', message: '🔍 Creating search plan...' });
|
||||
|
||||
const fileContents = global.sandboxState.fileCache.files;
|
||||
const fileContents = global.sandboxState.fileCache?.files || {};
|
||||
console.log('[generate-ai-code-stream] Files available for search:', Object.keys(fileContents).length);
|
||||
|
||||
// STEP 1: Get search plan from AI
|
||||
@@ -220,8 +244,9 @@ export async function POST(request: NextRequest) {
|
||||
console.log('[generate-ai-code-stream] Target selected:', target);
|
||||
|
||||
// Create surgical edit context with exact location
|
||||
const normalizedPath = target.filePath.replace('/home/user/app/', '');
|
||||
const fileContent = fileContents[normalizedPath]?.content || '';
|
||||
// normalizedPath would be: target.filePath.replace('/home/user/app/', '');
|
||||
// fileContent available but not used in current implementation
|
||||
// const fileContent = fileContents[normalizedPath]?.content || '';
|
||||
|
||||
// Build enhanced context with search results
|
||||
enhancedSystemPrompt = `
|
||||
@@ -331,7 +356,7 @@ User request: "${prompt}"`;
|
||||
|
||||
// For now, fall back to keyword search since we don't have file contents for search execution
|
||||
// This path happens when no manifest was initially available
|
||||
let targetFiles = [];
|
||||
let targetFiles: any[] = [];
|
||||
if (!searchPlan || searchPlan.searchTerms.length === 0) {
|
||||
console.warn('[generate-ai-code-stream] No target files after fetch, searching for relevant files');
|
||||
|
||||
@@ -569,6 +594,11 @@ ${conversationContext}
|
||||
- Simple style/text change = 1 file ONLY
|
||||
- New component = 2 files MAX (component + parent)
|
||||
- If >3 files, YOU'RE DOING TOO MUCH
|
||||
6. **DO NOT CREATE SVGs FROM SCRATCH**:
|
||||
- NEVER generate custom SVG code unless explicitly asked
|
||||
- Use existing icon libraries (lucide-react, heroicons, etc.)
|
||||
- Or use placeholder elements/text if icons are not critical
|
||||
- Only create custom SVGs when user specifically requests "create an SVG" or "draw an SVG"
|
||||
|
||||
COMPONENT RELATIONSHIPS (CHECK THESE FIRST):
|
||||
- Navigation usually lives INSIDE Header.jsx, not separate Nav.jsx
|
||||
@@ -973,13 +1003,15 @@ MORPH FAST APPLY MODE (EDIT-ONLY):
|
||||
// Store files in cache
|
||||
for (const [path, content] of Object.entries(filesData.files)) {
|
||||
const normalizedPath = path.replace('/home/user/app/', '');
|
||||
global.sandboxState.fileCache.files[normalizedPath] = {
|
||||
content: content as string,
|
||||
lastModified: Date.now()
|
||||
};
|
||||
if (global.sandboxState.fileCache) {
|
||||
global.sandboxState.fileCache.files[normalizedPath] = {
|
||||
content: content as string,
|
||||
lastModified: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (filesData.manifest) {
|
||||
if (filesData.manifest && global.sandboxState.fileCache) {
|
||||
global.sandboxState.fileCache.manifest = filesData.manifest;
|
||||
|
||||
// Now try to analyze edit intent with the fetched manifest
|
||||
@@ -1011,7 +1043,7 @@ MORPH FAST APPLY MODE (EDIT-ONLY):
|
||||
}
|
||||
|
||||
// Update variables
|
||||
backendFiles = global.sandboxState.fileCache.files;
|
||||
backendFiles = global.sandboxState.fileCache?.files || {};
|
||||
hasBackendFiles = Object.keys(backendFiles).length > 0;
|
||||
console.log('[generate-ai-code-stream] Updated backend cache with fetched files');
|
||||
}
|
||||
@@ -1183,11 +1215,32 @@ MORPH FAST APPLY MODE (EDIT-ONLY):
|
||||
// Determine which provider to use based on model
|
||||
const isAnthropic = model.startsWith('anthropic/');
|
||||
const isGoogle = model.startsWith('google/');
|
||||
const isOpenAI = model.startsWith('openai/gpt-5');
|
||||
const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq));
|
||||
const actualModel = isAnthropic ? model.replace('anthropic/', '') :
|
||||
(model === 'openai/gpt-5') ? 'gpt-5' :
|
||||
(isGoogle ? model.replace('google/', '') : model);
|
||||
const isOpenAI = model.startsWith('openai/');
|
||||
const isKimiGroq = model === 'moonshotai/kimi-k2-instruct-0905';
|
||||
const modelProvider = isAnthropic ? anthropic :
|
||||
(isOpenAI ? openai :
|
||||
(isGoogle ? googleGenerativeAI :
|
||||
(isKimiGroq ? groq : groq)));
|
||||
|
||||
// Fix model name transformation for different providers
|
||||
let actualModel: string;
|
||||
if (isAnthropic) {
|
||||
actualModel = model.replace('anthropic/', '');
|
||||
} else if (isOpenAI) {
|
||||
actualModel = model.replace('openai/', '');
|
||||
} else if (isKimiGroq) {
|
||||
// Kimi on Groq - use full model string
|
||||
actualModel = 'moonshotai/kimi-k2-instruct-0905';
|
||||
} else if (isGoogle) {
|
||||
// Google uses specific model names - convert our naming to theirs
|
||||
actualModel = model.replace('google/', '');
|
||||
} else {
|
||||
actualModel = model;
|
||||
}
|
||||
|
||||
console.log(`[generate-ai-code-stream] Using provider: ${isAnthropic ? 'Anthropic' : isGoogle ? 'Google' : isOpenAI ? 'OpenAI' : 'Groq'}, model: ${actualModel}`);
|
||||
console.log(`[generate-ai-code-stream] AI Gateway enabled: ${isUsingAIGateway}`);
|
||||
console.log(`[generate-ai-code-stream] Model string: ${model}`);
|
||||
|
||||
// Make streaming API call with appropriate provider
|
||||
const streamOptions: any = {
|
||||
@@ -1272,7 +1325,61 @@ It's better to have 3 complete files than 10 incomplete files.`
|
||||
};
|
||||
}
|
||||
|
||||
const result = await streamText(streamOptions);
|
||||
let result;
|
||||
let retryCount = 0;
|
||||
const maxRetries = 2;
|
||||
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
result = await streamText(streamOptions);
|
||||
break; // Success, exit retry loop
|
||||
} catch (streamError: any) {
|
||||
console.error(`[generate-ai-code-stream] Error calling streamText (attempt ${retryCount + 1}/${maxRetries + 1}):`, streamError);
|
||||
|
||||
// Check if this is a Groq service unavailable error
|
||||
const isGroqServiceError = isKimiGroq && streamError.message?.includes('Service unavailable');
|
||||
const isRetryableError = streamError.message?.includes('Service unavailable') ||
|
||||
streamError.message?.includes('rate limit') ||
|
||||
streamError.message?.includes('timeout');
|
||||
|
||||
if (retryCount < maxRetries && isRetryableError) {
|
||||
retryCount++;
|
||||
console.log(`[generate-ai-code-stream] Retrying in ${retryCount * 2} seconds...`);
|
||||
|
||||
// Send progress update about retry
|
||||
await sendProgress({
|
||||
type: 'info',
|
||||
message: `Service temporarily unavailable, retrying (attempt ${retryCount + 1}/${maxRetries + 1})...`
|
||||
});
|
||||
|
||||
// Wait before retry with exponential backoff
|
||||
await new Promise(resolve => setTimeout(resolve, retryCount * 2000));
|
||||
|
||||
// If Groq fails, try switching to a fallback model
|
||||
if (isGroqServiceError && retryCount === maxRetries) {
|
||||
console.log('[generate-ai-code-stream] Groq service unavailable, falling back to GPT-4');
|
||||
streamOptions.model = openai('gpt-4-turbo');
|
||||
actualModel = 'gpt-4-turbo';
|
||||
}
|
||||
} else {
|
||||
// Final error, send to user
|
||||
await sendProgress({
|
||||
type: 'error',
|
||||
message: `Failed to initialize ${isGoogle ? 'Gemini' : isAnthropic ? 'Claude' : isOpenAI ? 'GPT-5' : isKimiGroq ? 'Kimi (Groq)' : 'Groq'} streaming: ${streamError.message}`
|
||||
});
|
||||
|
||||
// If this is a Google model error, provide helpful info
|
||||
if (isGoogle) {
|
||||
await sendProgress({
|
||||
type: 'info',
|
||||
message: 'Tip: Make sure your GEMINI_API_KEY is set correctly and has proper permissions.'
|
||||
});
|
||||
}
|
||||
|
||||
throw streamError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream the response and parse in real-time
|
||||
let generatedCode = '';
|
||||
@@ -1287,7 +1394,7 @@ It's better to have 3 complete files than 10 incomplete files.`
|
||||
let tagBuffer = '';
|
||||
|
||||
// Stream the response and parse for packages in real-time
|
||||
for await (const textPart of result.textStream) {
|
||||
for await (const textPart of result?.textStream || []) {
|
||||
const text = textPart || '';
|
||||
generatedCode += text;
|
||||
currentFile += text;
|
||||
@@ -1330,6 +1437,11 @@ It's better to have 3 complete files than 10 incomplete files.`
|
||||
raw: true
|
||||
});
|
||||
|
||||
// Debug: Log every 100 characters streamed
|
||||
if (generatedCode.length % 100 < text.length) {
|
||||
console.log(`[generate-ai-code-stream] Streamed ${generatedCode.length} chars`);
|
||||
}
|
||||
|
||||
// Check for package tags in buffered text (ONLY for edits, not initial generation)
|
||||
let lastIndex = 0;
|
||||
if (isEdit) {
|
||||
@@ -1619,12 +1731,28 @@ Provide the complete file content without any truncation. Include all necessary
|
||||
completionClient = openai;
|
||||
} else if (model.includes('claude')) {
|
||||
completionClient = anthropic;
|
||||
} else if (model === 'moonshotai/kimi-k2-instruct-0905') {
|
||||
completionClient = groq;
|
||||
} else {
|
||||
completionClient = groq;
|
||||
}
|
||||
|
||||
// Determine the correct model name for the completion
|
||||
let completionModelName: string;
|
||||
if (model === 'moonshotai/kimi-k2-instruct-0905') {
|
||||
completionModelName = 'moonshotai/kimi-k2-instruct-0905';
|
||||
} else if (model.includes('openai')) {
|
||||
completionModelName = model.replace('openai/', '');
|
||||
} else if (model.includes('anthropic')) {
|
||||
completionModelName = model.replace('anthropic/', '');
|
||||
} else if (model.includes('google')) {
|
||||
completionModelName = model.replace('google/', '');
|
||||
} else {
|
||||
completionModelName = model;
|
||||
}
|
||||
|
||||
const completionResult = await streamText({
|
||||
model: completionClient(modelMapping[model] || model),
|
||||
model: completionClient(completionModelName),
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1632,8 +1760,7 @@ Provide the complete file content without any truncation. Include all necessary
|
||||
},
|
||||
{ role: 'user', content: completionPrompt }
|
||||
],
|
||||
temperature: isGPT5 ? undefined : appConfig.ai.defaultTemperature,
|
||||
maxTokens: appConfig.ai.truncationRecoveryMaxTokens
|
||||
temperature: model.startsWith('openai/gpt-5') ? undefined : appConfig.ai.defaultTemperature
|
||||
});
|
||||
|
||||
// Get the full text from the stream
|
||||
@@ -1744,12 +1871,18 @@ Provide the complete file content without any truncation. Include all necessary
|
||||
}
|
||||
})();
|
||||
|
||||
// Return the stream
|
||||
// Return the stream with proper headers for streaming support
|
||||
return new Response(stream.readable, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Transfer-Encoding': 'chunked',
|
||||
'Content-Encoding': 'none', // Prevent compression that can break streaming
|
||||
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { parseJavaScriptFile, buildComponentTree } from '@/lib/file-parser';
|
||||
import { FileManifest, FileInfo, RouteInfo } from '@/types/file-manifest';
|
||||
import type { SandboxState } from '@/types/sandbox';
|
||||
// SandboxState type used implicitly through global.activeSandbox
|
||||
|
||||
declare global {
|
||||
var activeSandbox: any;
|
||||
@@ -18,58 +18,82 @@ export async function GET() {
|
||||
|
||||
console.log('[get-sandbox-files] Fetching and analyzing file structure...');
|
||||
|
||||
// Get all React/JS/CSS files
|
||||
const result = await global.activeSandbox.runCode(`
|
||||
import os
|
||||
import json
|
||||
|
||||
def get_files_content(directory='/home/user/app', extensions=['.jsx', '.js', '.tsx', '.ts', '.css', '.json']):
|
||||
files_content = {}
|
||||
// Get list of all relevant files
|
||||
const findResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'find',
|
||||
args: [
|
||||
'.',
|
||||
'-name', 'node_modules', '-prune', '-o',
|
||||
'-name', '.git', '-prune', '-o',
|
||||
'-name', 'dist', '-prune', '-o',
|
||||
'-name', 'build', '-prune', '-o',
|
||||
'-type', 'f',
|
||||
'(',
|
||||
'-name', '*.jsx',
|
||||
'-o', '-name', '*.js',
|
||||
'-o', '-name', '*.tsx',
|
||||
'-o', '-name', '*.ts',
|
||||
'-o', '-name', '*.css',
|
||||
'-o', '-name', '*.json',
|
||||
')',
|
||||
'-print'
|
||||
]
|
||||
});
|
||||
|
||||
for root, dirs, files in os.walk(directory):
|
||||
# Skip node_modules and other unwanted directories
|
||||
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'dist', 'build']]
|
||||
if (findResult.exitCode !== 0) {
|
||||
throw new Error('Failed to list files');
|
||||
}
|
||||
|
||||
const fileList = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
||||
console.log('[get-sandbox-files] Found', fileList.length, 'files');
|
||||
|
||||
// Read content of each file (limit to reasonable sizes)
|
||||
const filesContent: Record<string, string> = {};
|
||||
|
||||
for (const filePath of fileList) {
|
||||
try {
|
||||
// Check file size first
|
||||
const statResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'stat',
|
||||
args: ['-f', '%z', filePath]
|
||||
});
|
||||
|
||||
for file in files:
|
||||
if any(file.endswith(ext) for ext in extensions):
|
||||
file_path = os.path.join(root, file)
|
||||
relative_path = os.path.relpath(file_path, '/home/user/app')
|
||||
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
# Only include files under 10KB to avoid huge responses
|
||||
if len(content) < 10000:
|
||||
files_content[relative_path] = content
|
||||
except:
|
||||
pass
|
||||
if (statResult.exitCode === 0) {
|
||||
const fileSize = parseInt(await statResult.stdout());
|
||||
|
||||
// Only read files smaller than 10KB
|
||||
if (fileSize < 10000) {
|
||||
const catResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'cat',
|
||||
args: [filePath]
|
||||
});
|
||||
|
||||
if (catResult.exitCode === 0) {
|
||||
const content = await catResult.stdout();
|
||||
// Remove leading './' from path
|
||||
const relativePath = filePath.replace(/^\.\//, '');
|
||||
filesContent[relativePath] = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.debug('Error parsing component info:', parseError);
|
||||
// Skip files that can't be read
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return files_content
|
||||
|
||||
# Get the files
|
||||
files = get_files_content()
|
||||
|
||||
# Also get the directory structure
|
||||
structure = []
|
||||
for root, dirs, files in os.walk('/home/user/app'):
|
||||
level = root.replace('/home/user/app', '').count(os.sep)
|
||||
indent = ' ' * 2 * level
|
||||
structure.append(f"{indent}{os.path.basename(root)}/")
|
||||
sub_indent = ' ' * 2 * (level + 1)
|
||||
for file in files:
|
||||
if not any(skip in root for skip in ['node_modules', '.git', 'dist', 'build']):
|
||||
structure.append(f"{sub_indent}{file}")
|
||||
|
||||
result = {
|
||||
'files': files,
|
||||
'structure': '\\n'.join(structure[:50]) # Limit structure to 50 lines
|
||||
}
|
||||
|
||||
print(json.dumps(result))
|
||||
`);
|
||||
|
||||
const output = result.logs.stdout.join('');
|
||||
const parsedResult = JSON.parse(output);
|
||||
// Get directory structure
|
||||
const treeResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'find',
|
||||
args: ['.', '-type', 'd', '-not', '-path', '*/node_modules*', '-not', '-path', '*/.git*']
|
||||
});
|
||||
|
||||
let structure = '';
|
||||
if (treeResult.exitCode === 0) {
|
||||
const dirs = (await treeResult.stdout()).split('\n').filter((d: string) => d.trim());
|
||||
structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines
|
||||
}
|
||||
|
||||
// Build enhanced file manifest
|
||||
const fileManifest: FileManifest = {
|
||||
@@ -82,12 +106,12 @@ print(json.dumps(result))
|
||||
};
|
||||
|
||||
// Process each file
|
||||
for (const [relativePath, content] of Object.entries(parsedResult.files)) {
|
||||
const fullPath = `/home/user/app/${relativePath}`;
|
||||
for (const [relativePath, content] of Object.entries(filesContent)) {
|
||||
const fullPath = `/${relativePath}`;
|
||||
|
||||
// Create base file info
|
||||
const fileInfo: FileInfo = {
|
||||
content: content as string,
|
||||
content: content,
|
||||
type: 'utility',
|
||||
path: fullPath,
|
||||
relativePath,
|
||||
@@ -96,7 +120,7 @@ print(json.dumps(result))
|
||||
|
||||
// Parse JavaScript/JSX files
|
||||
if (relativePath.match(/\.(jsx?|tsx?)$/)) {
|
||||
const parseResult = parseJavaScriptFile(content as string, fullPath);
|
||||
const parseResult = parseJavaScriptFile(content, fullPath);
|
||||
Object.assign(fileInfo, parseResult);
|
||||
|
||||
// Identify entry point
|
||||
@@ -132,9 +156,9 @@ print(json.dumps(result))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
files: parsedResult.files,
|
||||
structure: parsedResult.structure,
|
||||
fileCount: Object.keys(parsedResult.files).length,
|
||||
files: filesContent,
|
||||
structure,
|
||||
fileCount: Object.keys(filesContent).length,
|
||||
manifest: fileManifest,
|
||||
});
|
||||
|
||||
@@ -157,7 +181,8 @@ function extractRoutes(files: Record<string, FileInfo>): RouteInfo[] {
|
||||
const routeMatches = fileInfo.content.matchAll(/path=["']([^"']+)["'].*(?:element|component)={([^}]+)}/g);
|
||||
|
||||
for (const match of routeMatches) {
|
||||
const [, routePath, componentRef] = match;
|
||||
const [, routePath] = match;
|
||||
// componentRef available in match but not used currently
|
||||
routes.push({
|
||||
path: routePath,
|
||||
component: path,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { SandboxProvider } from '@/lib/sandbox/types';
|
||||
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
||||
|
||||
declare global {
|
||||
var activeSandboxProvider: any;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { packages } = await request.json();
|
||||
|
||||
if (!packages || !Array.isArray(packages) || packages.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Packages array is required'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Get provider from sandbox manager or global state
|
||||
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
|
||||
|
||||
if (!provider) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'No active sandbox'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
console.log(`[install-packages-v2] Installing: ${packages.join(', ')}`);
|
||||
|
||||
const result = await provider.installPackages(packages);
|
||||
|
||||
return NextResponse.json({
|
||||
success: result.success,
|
||||
output: result.stdout,
|
||||
error: result.stderr,
|
||||
message: result.success ? 'Packages installed successfully' : 'Package installation failed'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[install-packages-v2] Error:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
+114
-224
@@ -1,14 +1,15 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { Sandbox } from '@e2b/code-interpreter';
|
||||
|
||||
declare global {
|
||||
var activeSandbox: any;
|
||||
var activeSandboxProvider: any;
|
||||
var sandboxData: any;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { packages, sandboxId } = await request.json();
|
||||
const { packages } = await request.json();
|
||||
// sandboxId not used - using global sandbox
|
||||
|
||||
if (!packages || !Array.isArray(packages) || packages.length === 0) {
|
||||
return NextResponse.json({
|
||||
@@ -36,32 +37,17 @@ export async function POST(request: NextRequest) {
|
||||
console.log(`[install-packages] Cleaned:`, validPackages);
|
||||
}
|
||||
|
||||
// Try to get sandbox - either from global or reconnect
|
||||
let sandbox = global.activeSandbox;
|
||||
// Get active sandbox provider
|
||||
const provider = global.activeSandboxProvider;
|
||||
|
||||
if (!sandbox && sandboxId) {
|
||||
console.log(`[install-packages] Reconnecting to sandbox ${sandboxId}...`);
|
||||
try {
|
||||
sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY });
|
||||
global.activeSandbox = sandbox;
|
||||
console.log(`[install-packages] Successfully reconnected to sandbox ${sandboxId}`);
|
||||
} catch (error) {
|
||||
console.error(`[install-packages] Failed to reconnect to sandbox:`, error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `Failed to reconnect to sandbox: ${(error as Error).message}`
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
if (!sandbox) {
|
||||
if (!provider) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'No active sandbox available'
|
||||
error: 'No active sandbox provider available'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('[install-packages] Installing packages:', packages);
|
||||
console.log('[install-packages] Installing packages:', validPackages);
|
||||
|
||||
// Create a response stream for real-time updates
|
||||
const encoder = new TextEncoder();
|
||||
@@ -75,7 +61,7 @@ export async function POST(request: NextRequest) {
|
||||
};
|
||||
|
||||
// Start installation in background
|
||||
(async (sandboxInstance) => {
|
||||
(async (providerInstance) => {
|
||||
try {
|
||||
await sendProgress({
|
||||
type: 'start',
|
||||
@@ -83,23 +69,17 @@ export async function POST(request: NextRequest) {
|
||||
packages: validPackages
|
||||
});
|
||||
|
||||
// Kill any existing Vite process first
|
||||
// Stop any existing development server first
|
||||
await sendProgress({ type: 'status', message: 'Stopping development server...' });
|
||||
|
||||
await sandboxInstance.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
import signal
|
||||
|
||||
# Try to kill any existing Vite process
|
||||
try:
|
||||
with open('/tmp/vite-process.pid', 'r') as f:
|
||||
pid = int(f.read().strip())
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
print("Stopped existing Vite process")
|
||||
except:
|
||||
print("No existing Vite process found")
|
||||
`);
|
||||
try {
|
||||
// Try to kill any running dev server processes
|
||||
await providerInstance.runCommand('pkill -f vite');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit
|
||||
} catch (killError) {
|
||||
// It's OK if no process is found
|
||||
console.debug('[install-packages] No existing dev server found:', killError);
|
||||
}
|
||||
|
||||
// Check which packages are already installed
|
||||
await sendProgress({
|
||||
@@ -107,70 +87,52 @@ except:
|
||||
message: 'Checking installed packages...'
|
||||
});
|
||||
|
||||
const checkResult = await sandboxInstance.runCode(`
|
||||
import os
|
||||
import json
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
|
||||
# Read package.json to check installed packages
|
||||
try:
|
||||
with open('package.json', 'r') as f:
|
||||
package_json = json.load(f)
|
||||
|
||||
dependencies = package_json.get('dependencies', {})
|
||||
dev_dependencies = package_json.get('devDependencies', {})
|
||||
all_deps = {**dependencies, **dev_dependencies}
|
||||
|
||||
# Check which packages need to be installed
|
||||
packages_to_check = ${JSON.stringify(validPackages)}
|
||||
already_installed = []
|
||||
need_install = []
|
||||
|
||||
for pkg in packages_to_check:
|
||||
# Handle scoped packages
|
||||
if pkg.startswith('@'):
|
||||
pkg_name = pkg
|
||||
else:
|
||||
# Extract package name without version
|
||||
pkg_name = pkg.split('@')[0]
|
||||
|
||||
if pkg_name in all_deps:
|
||||
already_installed.append(pkg_name)
|
||||
else:
|
||||
need_install.append(pkg)
|
||||
|
||||
print(f"Already installed: {already_installed}")
|
||||
print(f"Need to install: {need_install}")
|
||||
print(f"NEED_INSTALL:{json.dumps(need_install)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking packages: {e}")
|
||||
print(f"NEED_INSTALL:{json.dumps(packages_to_check)}")
|
||||
`);
|
||||
|
||||
// Parse packages that need installation
|
||||
let packagesToInstall = validPackages;
|
||||
|
||||
// Check if checkResult has the expected structure
|
||||
if (checkResult && checkResult.results && checkResult.results[0] && checkResult.results[0].text) {
|
||||
const outputLines = checkResult.results[0].text.split('\n');
|
||||
for (const line of outputLines) {
|
||||
if (line.startsWith('NEED_INSTALL:')) {
|
||||
try {
|
||||
packagesToInstall = JSON.parse(line.substring('NEED_INSTALL:'.length));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse packages to install:', e);
|
||||
try {
|
||||
// Read package.json to check existing dependencies
|
||||
let packageJsonContent = '';
|
||||
try {
|
||||
packageJsonContent = await providerInstance.readFile('package.json');
|
||||
} catch (error) {
|
||||
console.log('[install-packages] Error reading package.json:', error);
|
||||
}
|
||||
if (packageJsonContent) {
|
||||
const packageJson = JSON.parse(packageJsonContent);
|
||||
|
||||
const dependencies = packageJson.dependencies || {};
|
||||
const devDependencies = packageJson.devDependencies || {};
|
||||
const allDeps = { ...dependencies, ...devDependencies };
|
||||
|
||||
const alreadyInstalled = [];
|
||||
const needInstall = [];
|
||||
|
||||
for (const pkg of validPackages) {
|
||||
// Handle scoped packages
|
||||
const pkgName = pkg.startsWith('@') ? pkg : pkg.split('@')[0];
|
||||
|
||||
if (allDeps[pkgName]) {
|
||||
alreadyInstalled.push(pkgName);
|
||||
} else {
|
||||
needInstall.push(pkg);
|
||||
}
|
||||
}
|
||||
|
||||
packagesToInstall = needInstall;
|
||||
|
||||
if (alreadyInstalled.length > 0) {
|
||||
await sendProgress({
|
||||
type: 'info',
|
||||
message: `Already installed: ${alreadyInstalled.join(', ')}`
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('[install-packages] Invalid checkResult structure:', checkResult);
|
||||
} catch (error) {
|
||||
console.error('[install-packages] Error checking existing packages:', error);
|
||||
// If we can't check, just try to install all packages
|
||||
packagesToInstall = validPackages;
|
||||
}
|
||||
|
||||
|
||||
if (packagesToInstall.length === 0) {
|
||||
await sendProgress({
|
||||
type: 'success',
|
||||
@@ -178,164 +140,92 @@ except Exception as e:
|
||||
installedPackages: [],
|
||||
alreadyInstalled: validPackages
|
||||
});
|
||||
|
||||
// Restart dev server
|
||||
await sendProgress({ type: 'status', message: 'Restarting development server...' });
|
||||
|
||||
await providerInstance.restartViteServer();
|
||||
|
||||
await sendProgress({
|
||||
type: 'complete',
|
||||
message: 'Dev server restarted!',
|
||||
installedPackages: []
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Install only packages that aren't already installed
|
||||
const packageList = packagesToInstall.join(' ');
|
||||
// Only send the npm install command message if we're actually installing new packages
|
||||
await sendProgress({
|
||||
type: 'info',
|
||||
message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}`
|
||||
});
|
||||
|
||||
const installResult = await sandboxInstance.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
|
||||
# Run npm install with output capture
|
||||
packages_to_install = ${JSON.stringify(packagesToInstall)}
|
||||
cmd_args = ['npm', 'install', '--legacy-peer-deps'] + packages_to_install
|
||||
|
||||
print(f"Running command: {' '.join(cmd_args)}")
|
||||
|
||||
process = subprocess.Popen(
|
||||
cmd_args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Stream output
|
||||
while True:
|
||||
output = process.stdout.readline()
|
||||
if output == '' and process.poll() is not None:
|
||||
break
|
||||
if output:
|
||||
print(output.strip())
|
||||
|
||||
# Get the return code
|
||||
rc = process.poll()
|
||||
|
||||
# Capture any stderr
|
||||
stderr = process.stderr.read()
|
||||
if stderr:
|
||||
print("STDERR:", stderr)
|
||||
if 'ERESOLVE' in stderr:
|
||||
print("ERESOLVE_ERROR: Dependency conflict detected - using --legacy-peer-deps flag")
|
||||
|
||||
print(f"\\nInstallation completed with code: {rc}")
|
||||
|
||||
# Verify packages were installed
|
||||
import json
|
||||
with open('/home/user/app/package.json', 'r') as f:
|
||||
package_json = json.load(f)
|
||||
|
||||
installed = []
|
||||
for pkg in ${JSON.stringify(packagesToInstall)}:
|
||||
if pkg in package_json.get('dependencies', {}):
|
||||
installed.append(pkg)
|
||||
print(f"✓ Verified {pkg}")
|
||||
else:
|
||||
print(f"✗ Package {pkg} not found in dependencies")
|
||||
// Install packages using provider method
|
||||
const installResult = await providerInstance.installPackages(packagesToInstall);
|
||||
|
||||
print(f"\\nVerified installed packages: {installed}")
|
||||
`, { timeout: 60000 }); // 60 second timeout for npm install
|
||||
// Get install output - ensure stdout/stderr are strings
|
||||
const stdout = String(installResult.stdout || '');
|
||||
const stderr = String(installResult.stderr || '');
|
||||
|
||||
// Send npm output
|
||||
const output = installResult?.output || installResult?.logs?.stdout?.join('\n') || '';
|
||||
const npmOutputLines = output.split('\n').filter((line: string) => line.trim());
|
||||
for (const line of npmOutputLines) {
|
||||
if (line.includes('STDERR:')) {
|
||||
const errorMsg = line.replace('STDERR:', '').trim();
|
||||
if (errorMsg && errorMsg !== 'undefined') {
|
||||
await sendProgress({ type: 'error', message: errorMsg });
|
||||
if (stdout) {
|
||||
const lines = stdout.split('\n').filter(line => line.trim());
|
||||
for (const line of lines) {
|
||||
if (line.includes('npm WARN')) {
|
||||
await sendProgress({ type: 'warning', message: line });
|
||||
} else if (line.trim()) {
|
||||
await sendProgress({ type: 'output', message: line });
|
||||
}
|
||||
} else if (line.includes('ERESOLVE_ERROR:')) {
|
||||
const msg = line.replace('ERESOLVE_ERROR:', '').trim();
|
||||
await sendProgress({
|
||||
type: 'warning',
|
||||
message: `Dependency conflict resolved with --legacy-peer-deps: ${msg}`
|
||||
});
|
||||
} else if (line.includes('npm WARN')) {
|
||||
await sendProgress({ type: 'warning', message: line });
|
||||
} else if (line.trim() && !line.includes('undefined')) {
|
||||
await sendProgress({ type: 'output', message: line });
|
||||
}
|
||||
}
|
||||
|
||||
// Check if installation was successful
|
||||
const installedMatch = output.match(/Verified installed packages: \[(.*?)\]/);
|
||||
let installedPackages: string[] = [];
|
||||
|
||||
if (installedMatch && installedMatch[1]) {
|
||||
installedPackages = installedMatch[1]
|
||||
.split(',')
|
||||
.map((p: string) => p.trim().replace(/'/g, ''))
|
||||
.filter((p: string) => p.length > 0);
|
||||
if (stderr) {
|
||||
const errorLines = stderr.split('\n').filter(line => line.trim());
|
||||
for (const line of errorLines) {
|
||||
if (line.includes('ERESOLVE')) {
|
||||
await sendProgress({
|
||||
type: 'warning',
|
||||
message: `Dependency conflict resolved with --legacy-peer-deps: ${line}`
|
||||
});
|
||||
} else if (line.trim()) {
|
||||
await sendProgress({ type: 'error', message: line });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (installedPackages.length > 0) {
|
||||
if (installResult.exitCode === 0) {
|
||||
await sendProgress({
|
||||
type: 'success',
|
||||
message: `Successfully installed: ${installedPackages.join(', ')}`,
|
||||
installedPackages
|
||||
message: `Successfully installed: ${packagesToInstall.join(', ')}`,
|
||||
installedPackages: packagesToInstall
|
||||
});
|
||||
} else {
|
||||
await sendProgress({
|
||||
type: 'error',
|
||||
message: 'Failed to verify package installation'
|
||||
message: 'Package installation failed'
|
||||
});
|
||||
}
|
||||
|
||||
// Restart Vite dev server
|
||||
// Restart development server
|
||||
await sendProgress({ type: 'status', message: 'Restarting development server...' });
|
||||
|
||||
await sandboxInstance.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
import time
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
|
||||
# Kill any existing Vite processes
|
||||
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
|
||||
time.sleep(1)
|
||||
|
||||
# Start Vite dev server
|
||||
env = os.environ.copy()
|
||||
env['FORCE_COLOR'] = '0'
|
||||
|
||||
process = subprocess.Popen(
|
||||
['npm', 'run', 'dev'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=env
|
||||
)
|
||||
|
||||
print(f'✓ Vite dev server restarted with PID: {process.pid}')
|
||||
|
||||
# Store process info for later
|
||||
with open('/tmp/vite-process.pid', 'w') as f:
|
||||
f.write(str(process.pid))
|
||||
|
||||
# Wait a bit for Vite to start up
|
||||
time.sleep(3)
|
||||
|
||||
# Touch files to trigger Vite reload
|
||||
subprocess.run(['touch', '/home/user/app/package.json'])
|
||||
subprocess.run(['touch', '/home/user/app/vite.config.js'])
|
||||
|
||||
print("Vite restarted and should now recognize all packages")
|
||||
`);
|
||||
|
||||
await sendProgress({
|
||||
type: 'complete',
|
||||
message: 'Package installation complete and dev server restarted!',
|
||||
installedPackages
|
||||
});
|
||||
try {
|
||||
await providerInstance.restartViteServer();
|
||||
|
||||
// Wait a bit for the server to start
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
await sendProgress({
|
||||
type: 'complete',
|
||||
message: 'Package installation complete and dev server restarted!',
|
||||
installedPackages: packagesToInstall
|
||||
});
|
||||
} catch (error) {
|
||||
await sendProgress({
|
||||
type: 'error',
|
||||
message: `Failed to restart dev server: ${(error as Error).message}`
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message;
|
||||
@@ -348,7 +238,7 @@ print("Vite restarted and should now recognize all packages")
|
||||
} finally {
|
||||
await writer.close();
|
||||
}
|
||||
})(sandbox);
|
||||
})(provider);
|
||||
|
||||
// Return the stream
|
||||
return new Response(stream.readable, {
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
declare global {
|
||||
var activeSandbox: any;
|
||||
var activeSandboxProvider: any;
|
||||
var sandboxData: any;
|
||||
var existingFiles: Set<string>;
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
console.log('[kill-sandbox] Killing active sandbox...');
|
||||
|
||||
console.log('[kill-sandbox] Stopping active sandbox...');
|
||||
|
||||
let sandboxKilled = false;
|
||||
|
||||
// Kill existing sandbox if any
|
||||
if (global.activeSandbox) {
|
||||
|
||||
// Stop existing sandbox if any
|
||||
if (global.activeSandboxProvider) {
|
||||
try {
|
||||
await global.activeSandbox.close();
|
||||
await global.activeSandboxProvider.terminate();
|
||||
sandboxKilled = true;
|
||||
console.log('[kill-sandbox] Sandbox closed successfully');
|
||||
console.log('[kill-sandbox] Sandbox stopped successfully');
|
||||
} catch (e) {
|
||||
console.error('[kill-sandbox] Failed to close sandbox:', e);
|
||||
console.error('[kill-sandbox] Failed to stop sandbox:', e);
|
||||
}
|
||||
global.activeSandbox = null;
|
||||
global.activeSandboxProvider = null;
|
||||
global.sandboxData = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,97 +15,100 @@ export async function GET() {
|
||||
|
||||
console.log('[monitor-vite-logs] Checking Vite process logs...');
|
||||
|
||||
// Check both the error file and recent logs
|
||||
const result = await global.activeSandbox.runCode(`
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
errors = []
|
||||
|
||||
# First check the error file
|
||||
try:
|
||||
with open('/tmp/vite-errors.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
errors.extend(data.get('errors', []))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Also check if we can get recent Vite logs
|
||||
try:
|
||||
# Try to get the Vite process PID
|
||||
with open('/tmp/vite-process.pid', 'r') as f:
|
||||
pid = int(f.read().strip())
|
||||
const errors: any[] = [];
|
||||
|
||||
# Check if process is still running and get its logs
|
||||
# This is a bit hacky but works for our use case
|
||||
result = subprocess.run(['ps', '-p', str(pid)], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
# Process is running, try to check for errors in output
|
||||
# Note: We can't easily get stdout/stderr from a running process
|
||||
# but we can check if there are new errors
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# Also scan the current console output for any HMR errors
|
||||
# This won't catch everything but helps with recent errors
|
||||
try:
|
||||
# Check if there's a log file we can read
|
||||
import os
|
||||
log_files = []
|
||||
for root, dirs, files in os.walk('/tmp'):
|
||||
for file in files:
|
||||
if 'vite' in file.lower() and file.endswith('.log'):
|
||||
log_files.append(os.path.join(root, file))
|
||||
// Check if there's an error file from previous runs
|
||||
try {
|
||||
const catResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'cat',
|
||||
args: ['/tmp/vite-errors.json']
|
||||
});
|
||||
|
||||
if (catResult.exitCode === 0) {
|
||||
const errorFileContent = await catResult.stdout();
|
||||
const data = JSON.parse(errorFileContent);
|
||||
errors.push(...(data.errors || []));
|
||||
}
|
||||
} catch {
|
||||
// No error file exists, that's OK
|
||||
}
|
||||
|
||||
for log_file in log_files[:5]: # Check up to 5 log files
|
||||
try:
|
||||
with open(log_file, 'r') as f:
|
||||
content = f.read()
|
||||
# Look for import errors
|
||||
import_errors = re.findall(r'Failed to resolve import "([^"]+)"', content)
|
||||
for pkg in import_errors:
|
||||
if not pkg.startswith('.'):
|
||||
# Extract base package name
|
||||
if pkg.startswith('@'):
|
||||
parts = pkg.split('/')
|
||||
final_pkg = '/'.join(parts[:2]) if len(parts) >= 2 else pkg
|
||||
else:
|
||||
final_pkg = pkg.split('/')[0]
|
||||
|
||||
error_obj = {
|
||||
"type": "npm-missing",
|
||||
"package": final_pkg,
|
||||
"message": f"Failed to resolve import \\"{pkg}\\"",
|
||||
"file": "Unknown"
|
||||
}
|
||||
|
||||
# Avoid duplicates
|
||||
if not any(e['package'] == error_obj['package'] for e in errors):
|
||||
errors.append(error_obj)
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Error scanning logs: {e}")
|
||||
|
||||
# Deduplicate errors
|
||||
unique_errors = []
|
||||
seen_packages = set()
|
||||
for error in errors:
|
||||
if error.get('package') and error['package'] not in seen_packages:
|
||||
seen_packages.add(error['package'])
|
||||
unique_errors.append(error)
|
||||
|
||||
print(json.dumps({"errors": unique_errors}))
|
||||
`, { timeout: 5000 });
|
||||
// Look for any Vite-related log files that might contain errors
|
||||
try {
|
||||
const findResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'find',
|
||||
args: ['/tmp', '-name', '*vite*', '-type', 'f']
|
||||
});
|
||||
|
||||
if (findResult.exitCode === 0) {
|
||||
const logFiles = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
||||
|
||||
for (const logFile of logFiles.slice(0, 3)) {
|
||||
try {
|
||||
const grepResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'grep',
|
||||
args: ['-i', 'failed to resolve import', logFile]
|
||||
});
|
||||
|
||||
if (grepResult.exitCode === 0) {
|
||||
const errorLines = (await grepResult.stdout()).split('\n').filter((line: string) => line.trim());
|
||||
|
||||
for (const line of errorLines) {
|
||||
// Extract package name from error line
|
||||
const importMatch = line.match(/"([^"]+)"/);
|
||||
if (importMatch) {
|
||||
const importPath = importMatch[1];
|
||||
|
||||
// Skip relative imports
|
||||
if (!importPath.startsWith('.')) {
|
||||
// Extract base package name
|
||||
let packageName;
|
||||
if (importPath.startsWith('@')) {
|
||||
const parts = importPath.split('/');
|
||||
packageName = parts.length >= 2 ? parts.slice(0, 2).join('/') : importPath;
|
||||
} else {
|
||||
packageName = importPath.split('/')[0];
|
||||
}
|
||||
|
||||
const errorObj = {
|
||||
type: "npm-missing",
|
||||
package: packageName,
|
||||
message: `Failed to resolve import "${importPath}"`,
|
||||
file: "Unknown"
|
||||
};
|
||||
|
||||
// Avoid duplicates
|
||||
if (!errors.some(e => e.package === errorObj.package)) {
|
||||
errors.push(errorObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip if grep fails
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No log files found, that's OK
|
||||
}
|
||||
|
||||
const data = JSON.parse(result.output || '{"errors": []}');
|
||||
// Deduplicate errors by package name
|
||||
const uniqueErrors: any[] = [];
|
||||
const seenPackages = new Set<string>();
|
||||
|
||||
for (const error of errors) {
|
||||
if (error.package && !seenPackages.has(error.package)) {
|
||||
seenPackages.add(error.package);
|
||||
uniqueErrors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
hasErrors: data.errors.length > 0,
|
||||
errors: data.errors
|
||||
hasErrors: uniqueErrors.length > 0,
|
||||
errors: uniqueErrors
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
+74
-107
@@ -2,132 +2,99 @@ import { NextResponse } from 'next/server';
|
||||
|
||||
declare global {
|
||||
var activeSandbox: any;
|
||||
var activeSandboxProvider: any;
|
||||
var lastViteRestartTime: number;
|
||||
var viteRestartInProgress: boolean;
|
||||
}
|
||||
|
||||
const RESTART_COOLDOWN_MS = 5000; // 5 second cooldown between restarts
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
if (!global.activeSandbox) {
|
||||
// Check both v1 and v2 global references
|
||||
const provider = global.activeSandbox || global.activeSandboxProvider;
|
||||
|
||||
if (!provider) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'No active sandbox'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('[restart-vite] Forcing Vite restart...');
|
||||
// Check if restart is already in progress
|
||||
if (global.viteRestartInProgress) {
|
||||
console.log('[restart-vite] Vite restart already in progress, skipping...');
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Vite restart already in progress'
|
||||
});
|
||||
}
|
||||
|
||||
// Kill existing Vite process and restart
|
||||
const result = await global.activeSandbox.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
import threading
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Kill existing Vite process
|
||||
try:
|
||||
with open('/tmp/vite-process.pid', 'r') as f:
|
||||
pid = int(f.read().strip())
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
print("Killed existing Vite process")
|
||||
time.sleep(1)
|
||||
except:
|
||||
print("No existing Vite process found")
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
|
||||
# Clear error file
|
||||
error_file = '/tmp/vite-errors.json'
|
||||
with open(error_file, 'w') as f:
|
||||
json.dump({"errors": [], "lastChecked": time.time()}, f)
|
||||
|
||||
# Function to monitor Vite output for errors
|
||||
def monitor_output(proc, error_file):
|
||||
while True:
|
||||
line = proc.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
// Check cooldown
|
||||
const now = Date.now();
|
||||
if (global.lastViteRestartTime && (now - global.lastViteRestartTime) < RESTART_COOLDOWN_MS) {
|
||||
const remainingTime = Math.ceil((RESTART_COOLDOWN_MS - (now - global.lastViteRestartTime)) / 1000);
|
||||
console.log(`[restart-vite] Cooldown active, ${remainingTime}s remaining`);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Vite was recently restarted, cooldown active (${remainingTime}s remaining)`
|
||||
});
|
||||
}
|
||||
|
||||
// Set the restart flag
|
||||
global.viteRestartInProgress = true;
|
||||
|
||||
console.log('[restart-vite] Using provider method to restart Vite...');
|
||||
|
||||
// Use the provider's restartViteServer method if available
|
||||
if (typeof provider.restartViteServer === 'function') {
|
||||
await provider.restartViteServer();
|
||||
console.log('[restart-vite] Vite restarted via provider method');
|
||||
} else {
|
||||
// Fallback to manual restart using provider's runCommand
|
||||
console.log('[restart-vite] Fallback to manual Vite restart...');
|
||||
|
||||
// Kill existing Vite processes
|
||||
try {
|
||||
await provider.runCommand('pkill -f vite');
|
||||
console.log('[restart-vite] Killed existing Vite processes');
|
||||
|
||||
sys.stdout.write(line) # Also print to console
|
||||
|
||||
# Check for import resolution errors
|
||||
if "Failed to resolve import" in line:
|
||||
try:
|
||||
# Extract package name from error
|
||||
import_match = line.find('"')
|
||||
if import_match != -1:
|
||||
end_match = line.find('"', import_match + 1)
|
||||
if end_match != -1:
|
||||
package_name = line[import_match + 1:end_match]
|
||||
# Skip relative imports
|
||||
if not package_name.startswith('.'):
|
||||
with open(error_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Handle scoped packages correctly
|
||||
if package_name.startswith('@'):
|
||||
# For @scope/package, keep the scope
|
||||
pkg_parts = package_name.split('/')
|
||||
if len(pkg_parts) >= 2:
|
||||
final_package = '/'.join(pkg_parts[:2])
|
||||
else:
|
||||
final_package = package_name
|
||||
else:
|
||||
# For regular packages, just take the first part
|
||||
final_package = package_name.split('/')[0]
|
||||
|
||||
error_obj = {
|
||||
"type": "npm-missing",
|
||||
"package": final_package,
|
||||
"message": line.strip(),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
# Avoid duplicates
|
||||
if not any(e['package'] == error_obj['package'] for e in data['errors']):
|
||||
data['errors'].append(error_obj)
|
||||
|
||||
with open(error_file, 'w') as f:
|
||||
json.dump(data, f)
|
||||
|
||||
print(f"WARNING: Detected missing package: {error_obj['package']}")
|
||||
except Exception as e:
|
||||
print(f"Error parsing Vite error: {e}")
|
||||
|
||||
# Start Vite with error monitoring
|
||||
process = subprocess.Popen(
|
||||
['npm', 'run', 'dev'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Start monitoring thread
|
||||
monitor_thread = threading.Thread(target=monitor_output, args=(process, error_file))
|
||||
monitor_thread.daemon = True
|
||||
monitor_thread.start()
|
||||
|
||||
print("Vite restarted successfully!")
|
||||
|
||||
# Store process info for later
|
||||
with open('/tmp/vite-process.pid', 'w') as f:
|
||||
f.write(str(process.pid))
|
||||
|
||||
# Wait for Vite to fully start
|
||||
time.sleep(5)
|
||||
print("Vite is ready")
|
||||
`);
|
||||
// Wait a moment for processes to terminate
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
} catch {
|
||||
console.log('[restart-vite] No existing Vite processes found');
|
||||
}
|
||||
|
||||
// Clear any error tracking files
|
||||
try {
|
||||
await provider.runCommand('bash -c "echo \'{\\"errors\\": [], \\"lastChecked\\": '+ Date.now() +'}\' > /tmp/vite-errors.json"');
|
||||
} catch {
|
||||
// Ignore if this fails
|
||||
}
|
||||
|
||||
// Start Vite dev server in background
|
||||
await provider.runCommand('sh -c "nohup npm run dev > /tmp/vite.log 2>&1 &"');
|
||||
console.log('[restart-vite] Vite dev server restarted');
|
||||
|
||||
// Wait for Vite to start up
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
}
|
||||
|
||||
// Update global state
|
||||
global.lastViteRestartTime = Date.now();
|
||||
global.viteRestartInProgress = false;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Vite restarted successfully',
|
||||
output: result.output
|
||||
message: 'Vite restarted successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[restart-vite] Error:', error);
|
||||
|
||||
// Clear the restart flag on error
|
||||
global.viteRestartInProgress = false;
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { SandboxProvider } from '@/lib/sandbox/types';
|
||||
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
||||
|
||||
// Get active sandbox provider from global state
|
||||
declare global {
|
||||
var activeSandboxProvider: any;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { command } = await request.json();
|
||||
|
||||
if (!command) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Command is required'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Get provider from sandbox manager or global state
|
||||
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
|
||||
|
||||
if (!provider) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'No active sandbox'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
console.log(`[run-command-v2] Executing: ${command}`);
|
||||
|
||||
const result = await provider.runCommand(command);
|
||||
|
||||
return NextResponse.json({
|
||||
success: result.success,
|
||||
output: result.stdout,
|
||||
error: result.stderr,
|
||||
exitCode: result.exitCode,
|
||||
message: result.success ? 'Command executed successfully' : 'Command failed'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[run-command-v2] Error:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { Sandbox } from '@e2b/code-interpreter';
|
||||
|
||||
// Get active sandbox from global state (in production, use a proper state management solution)
|
||||
declare global {
|
||||
@@ -26,30 +25,32 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
console.log(`[run-command] Executing: ${command}`);
|
||||
|
||||
const result = await global.activeSandbox.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
os.chdir('/home/user/app')
|
||||
result = subprocess.run(${JSON.stringify(command.split(' '))},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
shell=False)
|
||||
|
||||
print("STDOUT:")
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print("\\nSTDERR:")
|
||||
print(result.stderr)
|
||||
print(f"\\nReturn code: {result.returncode}")
|
||||
`);
|
||||
// Parse command and arguments
|
||||
const commandParts = command.trim().split(/\s+/);
|
||||
const cmd = commandParts[0];
|
||||
const args = commandParts.slice(1);
|
||||
|
||||
const output = result.logs.stdout.join('\n');
|
||||
// Execute command using Vercel Sandbox
|
||||
const result = await global.activeSandbox.runCommand({
|
||||
cmd,
|
||||
args
|
||||
});
|
||||
|
||||
// Get output streams
|
||||
const stdout = await result.stdout();
|
||||
const stderr = await result.stderr();
|
||||
|
||||
const output = [
|
||||
stdout ? `STDOUT:\n${stdout}` : '',
|
||||
stderr ? `\nSTDERR:\n${stderr}` : '',
|
||||
`\nExit code: ${result.exitCode}`
|
||||
].filter(Boolean).join('');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output,
|
||||
message: 'Command executed successfully'
|
||||
exitCode: result.exitCode,
|
||||
message: result.exitCode === 0 ? 'Command executed successfully' : 'Command completed with non-zero exit code'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
declare global {
|
||||
var activeSandbox: any;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
export async function GET() {
|
||||
try {
|
||||
if (!global.activeSandbox) {
|
||||
return NextResponse.json({
|
||||
@@ -15,55 +15,70 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
console.log('[sandbox-logs] Fetching Vite dev server logs...');
|
||||
|
||||
// Get the last N lines of the Vite dev server output
|
||||
const result = await global.activeSandbox.runCode(`
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# Try to get the Vite process output
|
||||
try:
|
||||
# Read the last 100 lines of any log files
|
||||
log_content = []
|
||||
// Check if Vite processes are running
|
||||
const psResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'ps',
|
||||
args: ['aux']
|
||||
});
|
||||
|
||||
# Check if there are any node processes running
|
||||
ps_result = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
|
||||
vite_processes = [line for line in ps_result.stdout.split('\\n') if 'vite' in line.lower()]
|
||||
let viteRunning = false;
|
||||
const logContent: string[] = [];
|
||||
|
||||
if vite_processes:
|
||||
log_content.append("Vite is running")
|
||||
else:
|
||||
log_content.append("Vite process not found")
|
||||
|
||||
# Try to capture recent console output (this is a simplified approach)
|
||||
# In a real implementation, you'd want to capture the Vite process output directly
|
||||
print(json.dumps({
|
||||
"hasErrors": False,
|
||||
"logs": log_content,
|
||||
"status": "running" if vite_processes else "stopped"
|
||||
}))
|
||||
except Exception as e:
|
||||
print(json.dumps({
|
||||
"hasErrors": True,
|
||||
"logs": [str(e)],
|
||||
"status": "error"
|
||||
}))
|
||||
`);
|
||||
|
||||
try {
|
||||
const logData = JSON.parse(result.output || '{}');
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
...logData
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
hasErrors: false,
|
||||
logs: [result.output],
|
||||
status: 'unknown'
|
||||
});
|
||||
if (psResult.exitCode === 0) {
|
||||
const psOutput = await psResult.stdout();
|
||||
const viteProcesses = psOutput.split('\n').filter((line: string) =>
|
||||
line.toLowerCase().includes('vite') ||
|
||||
line.toLowerCase().includes('npm run dev')
|
||||
);
|
||||
|
||||
viteRunning = viteProcesses.length > 0;
|
||||
|
||||
if (viteRunning) {
|
||||
logContent.push("Vite is running");
|
||||
logContent.push(...viteProcesses.slice(0, 3)); // Show first 3 processes
|
||||
} else {
|
||||
logContent.push("Vite process not found");
|
||||
}
|
||||
}
|
||||
|
||||
// Try to read any recent log files
|
||||
try {
|
||||
const findResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'find',
|
||||
args: ['/tmp', '-name', '*vite*', '-name', '*.log', '-type', 'f']
|
||||
});
|
||||
|
||||
if (findResult.exitCode === 0) {
|
||||
const logFiles = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
||||
|
||||
for (const logFile of logFiles.slice(0, 2)) {
|
||||
try {
|
||||
const catResult = await global.activeSandbox.runCommand({
|
||||
cmd: 'tail',
|
||||
args: ['-n', '10', logFile]
|
||||
});
|
||||
|
||||
if (catResult.exitCode === 0) {
|
||||
const logFileContent = await catResult.stdout();
|
||||
logContent.push(`--- ${logFile} ---`);
|
||||
logContent.push(logFileContent);
|
||||
}
|
||||
} catch {
|
||||
// Skip if can't read log file
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No log files found, that's OK
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
hasErrors: false,
|
||||
logs: logContent,
|
||||
status: viteRunning ? 'running' : 'stopped'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[sandbox-logs] Error:', error);
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
||||
|
||||
declare global {
|
||||
var activeSandbox: any;
|
||||
var activeSandboxProvider: any;
|
||||
var sandboxData: any;
|
||||
var existingFiles: Set<string>;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Check if sandbox exists
|
||||
const sandboxExists = !!global.activeSandbox;
|
||||
|
||||
// Check sandbox manager first, then fall back to global state
|
||||
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
|
||||
const sandboxExists = !!provider;
|
||||
|
||||
let sandboxHealthy = false;
|
||||
let sandboxInfo = null;
|
||||
|
||||
if (sandboxExists && global.activeSandbox) {
|
||||
|
||||
if (sandboxExists && provider) {
|
||||
try {
|
||||
// Since Python isn't available in the Vite template, just check if sandbox exists
|
||||
// The sandbox object existing is enough to confirm it's healthy
|
||||
sandboxHealthy = true;
|
||||
// Check if sandbox is healthy by getting its info
|
||||
const providerInfo = provider.getSandboxInfo();
|
||||
sandboxHealthy = !!providerInfo;
|
||||
|
||||
sandboxInfo = {
|
||||
sandboxId: global.sandboxData?.sandboxId,
|
||||
url: global.sandboxData?.url,
|
||||
sandboxId: providerInfo?.sandboxId || global.sandboxData?.sandboxId,
|
||||
url: providerInfo?.url || global.sandboxData?.url,
|
||||
filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [],
|
||||
lastHealthCheck: new Date().toISOString()
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import FirecrawlApp from '@mendable/firecrawl-js';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -8,49 +9,73 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'URL is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Use Firecrawl API to capture screenshot
|
||||
const firecrawlResponse = await fetch('https://api.firecrawl.dev/v1/scrape', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.FIRECRAWL_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
formats: ['screenshot'], // Regular viewport screenshot, not full page
|
||||
waitFor: 3000, // Wait for page to fully load
|
||||
timeout: 30000,
|
||||
blockAds: true,
|
||||
actions: [
|
||||
{
|
||||
type: 'wait',
|
||||
milliseconds: 2000 // Additional wait for dynamic content
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
if (!firecrawlResponse.ok) {
|
||||
const error = await firecrawlResponse.text();
|
||||
throw new Error(`Firecrawl API error: ${error}`);
|
||||
}
|
||||
|
||||
const data = await firecrawlResponse.json();
|
||||
// Initialize Firecrawl with API key from environment
|
||||
const apiKey = process.env.FIRECRAWL_API_KEY;
|
||||
|
||||
if (!data.success || !data.data?.screenshot) {
|
||||
throw new Error('Failed to capture screenshot');
|
||||
if (!apiKey) {
|
||||
console.error("FIRECRAWL_API_KEY not configured");
|
||||
return NextResponse.json({
|
||||
error: 'Firecrawl API key not configured'
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
const app = new FirecrawlApp({ apiKey });
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
screenshot: data.data.screenshot,
|
||||
metadata: data.data.metadata
|
||||
console.log('[scrape-screenshot] Attempting to capture screenshot for:', url);
|
||||
console.log('[scrape-screenshot] Using Firecrawl API key:', apiKey ? 'Present' : 'Missing');
|
||||
|
||||
// Use the new v4 scrape method (not scrapeUrl)
|
||||
const scrapeResult = await app.scrape(url, {
|
||||
formats: ['screenshot'], // Request screenshot format
|
||||
waitFor: 3000, // Wait for page to fully load
|
||||
timeout: 30000,
|
||||
onlyMainContent: false, // Get full page for screenshot
|
||||
actions: [
|
||||
{
|
||||
type: 'wait',
|
||||
milliseconds: 2000 // Additional wait for dynamic content
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
console.log('[scrape-screenshot] Full scrape result:', JSON.stringify(scrapeResult, null, 2));
|
||||
console.log('[scrape-screenshot] Scrape result type:', typeof scrapeResult);
|
||||
console.log('[scrape-screenshot] Scrape result keys:', Object.keys(scrapeResult));
|
||||
|
||||
// The Firecrawl v4 API might return data directly without a success flag
|
||||
// Check if we have data with screenshot
|
||||
if (scrapeResult && scrapeResult.screenshot) {
|
||||
// Direct screenshot response
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
screenshot: scrapeResult.screenshot,
|
||||
metadata: scrapeResult.metadata || {}
|
||||
});
|
||||
} else if ((scrapeResult as any)?.data?.screenshot) {
|
||||
// Nested data structure
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
screenshot: (scrapeResult as any).data.screenshot,
|
||||
metadata: (scrapeResult as any).data.metadata || {}
|
||||
});
|
||||
} else if ((scrapeResult as any)?.success === false) {
|
||||
// Explicit failure
|
||||
console.error('[scrape-screenshot] Firecrawl API error:', (scrapeResult as any).error);
|
||||
throw new Error((scrapeResult as any).error || 'Failed to capture screenshot');
|
||||
} else {
|
||||
// No screenshot in response
|
||||
console.error('[scrape-screenshot] No screenshot in response. Full response:', JSON.stringify(scrapeResult, null, 2));
|
||||
throw new Error('Screenshot not available in response - check console for full response structure');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Screenshot capture error:', error);
|
||||
console.error('[scrape-screenshot] Screenshot capture error:', error);
|
||||
console.error('[scrape-screenshot] Error stack:', error.stack);
|
||||
|
||||
// Provide fallback response for development - removed NODE_ENV check as it doesn't work in Next.js production builds
|
||||
|
||||
return NextResponse.json({
|
||||
error: error.message || 'Failed to capture screenshot'
|
||||
error: error.message || 'Failed to capture screenshot'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
formats: ['markdown', 'html'],
|
||||
formats: ['markdown', 'html', 'screenshot'],
|
||||
waitFor: 3000,
|
||||
timeout: 30000,
|
||||
blockAds: true,
|
||||
@@ -52,6 +52,10 @@ export async function POST(request: NextRequest) {
|
||||
{
|
||||
type: 'wait',
|
||||
milliseconds: 2000
|
||||
},
|
||||
{
|
||||
type: 'screenshot',
|
||||
fullPage: false // Just visible viewport for performance
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -68,7 +72,11 @@ export async function POST(request: NextRequest) {
|
||||
throw new Error('Failed to scrape content');
|
||||
}
|
||||
|
||||
const { markdown, html, metadata } = data.data;
|
||||
const { markdown, metadata, screenshot, actions } = data.data;
|
||||
// html available but not used in current implementation
|
||||
|
||||
// Get screenshot from either direct field or actions result
|
||||
const screenshotUrl = screenshot || actions?.screenshots?.[0] || null;
|
||||
|
||||
// Sanitize the markdown content
|
||||
const sanitizedMarkdown = sanitizeQuotes(markdown || '');
|
||||
@@ -91,11 +99,13 @@ ${sanitizedMarkdown}
|
||||
success: true,
|
||||
url,
|
||||
content: formattedContent,
|
||||
screenshot: screenshotUrl,
|
||||
structured: {
|
||||
title: sanitizeQuotes(title),
|
||||
description: sanitizeQuotes(description),
|
||||
content: sanitizedMarkdown,
|
||||
url
|
||||
url,
|
||||
screenshot: screenshotUrl
|
||||
},
|
||||
metadata: {
|
||||
scraper: 'firecrawl-enhanced',
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import FirecrawlApp from '@mendable/firecrawl-js';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { url, formats = ['markdown', 'html'], options = {} } = await request.json();
|
||||
|
||||
if (!url) {
|
||||
return NextResponse.json(
|
||||
{ error: "URL is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize Firecrawl with API key from environment
|
||||
const apiKey = process.env.FIRECRAWL_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
console.error("FIRECRAWL_API_KEY not configured");
|
||||
// For demo purposes, return mock data if API key is not set
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
title: "Example Website",
|
||||
content: `This is a mock response for ${url}. Configure FIRECRAWL_API_KEY to enable real scraping.`,
|
||||
description: "A sample website",
|
||||
markdown: `# Example Website\n\nThis is mock content for demonstration purposes.`,
|
||||
html: `<h1>Example Website</h1><p>This is mock content for demonstration purposes.</p>`,
|
||||
metadata: {
|
||||
title: "Example Website",
|
||||
description: "A sample website",
|
||||
sourceURL: url,
|
||||
statusCode: 200
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const app = new FirecrawlApp({ apiKey });
|
||||
|
||||
// Scrape the website using the latest SDK patterns
|
||||
// Include screenshot if requested in formats
|
||||
const scrapeResult = await app.scrape(url, {
|
||||
formats: formats,
|
||||
onlyMainContent: options.onlyMainContent !== false, // Default to true for cleaner content
|
||||
waitFor: options.waitFor || 2000, // Wait for dynamic content
|
||||
timeout: options.timeout || 30000,
|
||||
...options // Pass through any additional options
|
||||
});
|
||||
|
||||
// Handle the response according to the latest SDK structure
|
||||
const result = scrapeResult as any;
|
||||
if (result.success === false) {
|
||||
throw new Error(result.error || "Failed to scrape website");
|
||||
}
|
||||
|
||||
// The SDK may return data directly or nested
|
||||
const data = result.data || result;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
title: data?.metadata?.title || "Untitled",
|
||||
content: data?.markdown || data?.html || "",
|
||||
description: data?.metadata?.description || "",
|
||||
markdown: data?.markdown || "",
|
||||
html: data?.html || "",
|
||||
metadata: data?.metadata || {},
|
||||
screenshot: data?.screenshot || null,
|
||||
links: data?.links || [],
|
||||
// Include raw data for flexibility
|
||||
raw: data
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error scraping website:", error);
|
||||
|
||||
// Return a more detailed error response
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Failed to scrape website",
|
||||
// Provide mock data as fallback for development
|
||||
data: {
|
||||
title: "Example Website",
|
||||
content: "This is fallback content due to an error. Please check your configuration.",
|
||||
description: "Error occurred while scraping",
|
||||
markdown: `# Error\n\n${error instanceof Error ? error.message : 'Unknown error occurred'}`,
|
||||
html: `<h1>Error</h1><p>${error instanceof Error ? error.message : 'Unknown error occurred'}</p>`,
|
||||
metadata: {
|
||||
title: "Error",
|
||||
description: "Failed to scrape website",
|
||||
statusCode: 500
|
||||
}
|
||||
}
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: Add OPTIONS handler for CORS if needed
|
||||
export async function OPTIONS() {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { query } = await req.json();
|
||||
|
||||
if (!query) {
|
||||
return NextResponse.json({ error: 'Query is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Use Firecrawl search to get top 10 results with screenshots
|
||||
const searchResponse = await fetch('https://api.firecrawl.dev/v1/search', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.FIRECRAWL_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
limit: 10,
|
||||
scrapeOptions: {
|
||||
formats: ['markdown', 'screenshot'],
|
||||
onlyMainContent: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!searchResponse.ok) {
|
||||
throw new Error('Search failed');
|
||||
}
|
||||
|
||||
const searchData = await searchResponse.json();
|
||||
|
||||
// Format results with screenshots and markdown
|
||||
const results = searchData.data?.map((result: any) => ({
|
||||
url: result.url,
|
||||
title: result.title || result.url,
|
||||
description: result.description || '',
|
||||
screenshot: result.screenshot || null,
|
||||
markdown: result.markdown || '',
|
||||
})) || [];
|
||||
|
||||
return NextResponse.json({ results });
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to perform search' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function BuilderPage() {
|
||||
const [targetUrl, setTargetUrl] = useState<string>("");
|
||||
const [selectedStyle, setSelectedStyle] = useState<string>("modern");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [previewUrl, setPreviewUrl] = useState<string>("");
|
||||
const [progress, setProgress] = useState<string>("Initializing...");
|
||||
const [generatedCode, setGeneratedCode] = useState<string>("");
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Get the URL and style from sessionStorage
|
||||
const url = sessionStorage.getItem('targetUrl');
|
||||
const style = sessionStorage.getItem('selectedStyle');
|
||||
|
||||
if (!url) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
|
||||
setTargetUrl(url);
|
||||
setSelectedStyle(style || "modern");
|
||||
|
||||
// Start the website generation process
|
||||
generateWebsite(url, style || "modern");
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router]);
|
||||
|
||||
const generateWebsite = async (url: string, style: string) => {
|
||||
try {
|
||||
setProgress("Analyzing website...");
|
||||
|
||||
// For demo purposes, we'll generate a simple HTML template
|
||||
// In production, this would call the actual scraping and generation APIs
|
||||
const mockGeneratedCode = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${style} Website - Reimagined</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: ${style === 'modern' ? '#FA5D19' : style === 'playful' ? '#9061ff' : style === 'professional' ? '#2a6dfb' : '#eb3424'};
|
||||
--background: ${style === 'modern' ? '#ffffff' : style === 'playful' ? '#f9f9f9' : style === 'professional' ? '#f5f5f5' : '#fafafa'};
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--background);
|
||||
color: #262626;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
background: white;
|
||||
border-bottom: 1px solid #ededed;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 4rem auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(135deg, var(--primary), #262626);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem 2rem;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 0.5rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.feature {
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #ededed;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.feature:hover {
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.feature h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<div class="logo">Reimagined</div>
|
||||
<div>
|
||||
<a href="#features" style="margin-right: 2rem; color: #666; text-decoration: none;">Features</a>
|
||||
<a href="#about" style="margin-right: 2rem; color: #666; text-decoration: none;">About</a>
|
||||
<a href="#contact" style="color: #666; text-decoration: none;">Contact</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="hero">
|
||||
<h1>Welcome to Your ${style === 'modern' ? 'Modern' : style === 'playful' ? 'Playful' : style === 'professional' ? 'Professional' : 'Artistic'} Website</h1>
|
||||
<p class="subtitle">Reimagined from ${url}</p>
|
||||
<a href="#" class="cta-button">Get Started</a>
|
||||
</div>
|
||||
|
||||
<div class="features" id="features">
|
||||
<div class="feature">
|
||||
<h3>Fast</h3>
|
||||
<p>Lightning-fast performance optimized for modern web standards.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Responsive</h3>
|
||||
<p>Looks great on all devices, from mobile to desktop.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Beautiful</h3>
|
||||
<p>Stunning design that captures attention and drives engagement.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
setGeneratedCode(mockGeneratedCode);
|
||||
|
||||
// Create a blob URL for the preview
|
||||
const blob = new Blob([mockGeneratedCode], { type: 'text/html' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
setPreviewUrl(blobUrl);
|
||||
|
||||
setProgress("Website ready!");
|
||||
setIsLoading(false);
|
||||
|
||||
// Show success message
|
||||
toast.success("Website generated successfully!");
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error generating website:", error);
|
||||
toast.error("Failed to generate website. Please try again.");
|
||||
setProgress("Error occurred");
|
||||
setTimeout(() => router.push('/'), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadCode = () => {
|
||||
const blob = new Blob([generatedCode], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'website.html';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success("Code downloaded!");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background-base">
|
||||
<div className="flex h-screen">
|
||||
{/* Sidebar */}
|
||||
<div className="w-80 bg-white border-r border-border-faint p-24 flex flex-col">
|
||||
<h2 className="text-title-small font-semibold mb-16">Building Your Website</h2>
|
||||
|
||||
<div className="space-y-12 flex-1">
|
||||
<div>
|
||||
<div className="text-label-small text-black-alpha-56 mb-4">Target URL</div>
|
||||
<div className="text-body-medium text-accent-black truncate">{targetUrl}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-label-small text-black-alpha-56 mb-4">Style</div>
|
||||
<div className="text-body-medium text-accent-black capitalize">{selectedStyle}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-label-small text-black-alpha-56 mb-4">Status</div>
|
||||
<div className="text-body-medium text-heat-100">{progress}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{!isLoading && (
|
||||
<button
|
||||
onClick={downloadCode}
|
||||
className="w-full py-12 px-16 bg-heat-100 hover:bg-heat-200 text-white rounded-10 text-label-medium transition-all"
|
||||
>
|
||||
Download Code
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="w-full py-12 px-16 bg-black-alpha-4 hover:bg-black-alpha-6 rounded-10 text-label-medium transition-all"
|
||||
>
|
||||
Start Over
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="flex-1 bg-gray-50">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<div className="w-48 h-48 border-4 border-heat-100 border-t-transparent rounded-full animate-spin mb-16 mx-auto"></div>
|
||||
<p className="text-body-large text-black-alpha-56">{progress}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
previewUrl && (
|
||||
<iframe
|
||||
src={previewUrl}
|
||||
className="w-full h-full border-0"
|
||||
title="Website Preview"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export function ThemeLogo() {
|
||||
const { theme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
// Return light theme logo by default to avoid hydration mismatch
|
||||
return (
|
||||
<img
|
||||
src="/firecrawl-logo-with-fire.webp"
|
||||
alt="Firecrawl"
|
||||
className="h-8 w-auto"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const logoSrc = theme === "dark"
|
||||
? "/firecrawl-logo-with-fire-dark.webp"
|
||||
: "/firecrawl-logo-with-fire.webp"
|
||||
|
||||
return (
|
||||
<img
|
||||
src={logoSrc}
|
||||
alt="Firecrawl"
|
||||
className="h-8 w-auto"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme, resolvedTheme } = useTheme()
|
||||
const [mounted, setMounted] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(resolvedTheme === "light" ? "dark" : "light")
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
className="rounded-full fixed bottom-4 right-4 z-[9999] bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 shadow-lg transition-all duration-200 hover:scale-110"
|
||||
>
|
||||
{resolvedTheme === "light" ? (
|
||||
<MoonIcon className="h-[1.2rem] w-[1.2rem] transition-all duration-200" />
|
||||
) : (
|
||||
<SunIcon className="h-[1.2rem] w-[1.2rem] transition-all duration-200" />
|
||||
)}
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SunIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MoonIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform",
|
||||
"data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@@ -1,52 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ToggleProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof toggleVariants> {
|
||||
pressed?: boolean
|
||||
onPressedChange?: (pressed: boolean) => void
|
||||
}
|
||||
|
||||
const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
||||
({ className, variant, size, pressed, onPressedChange, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
aria-pressed={pressed}
|
||||
data-state={pressed ? "on" : "off"}
|
||||
onClick={() => onPressedChange?.(!pressed)}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Toggle.displayName = "Toggle"
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+1
-562
@@ -1,562 +1 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@keyframes slide {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(70px, 70px); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sunPulse {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes orbShrink {
|
||||
0% {
|
||||
transform: translateX(-50%) translateY(45%) scale(1.5);
|
||||
opacity: 0.2;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%) translateY(45%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes screenshot-pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.2;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes camera-float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-10px) rotate(-5deg);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(5px) rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lens-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pushUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInSmooth {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme configuration for Tailwind CSS v4 */
|
||||
@theme {
|
||||
--color-background: hsl(0 0% 100%);
|
||||
--color-foreground: hsl(240 10% 3.9%);
|
||||
--color-card: hsl(0 0% 100%);
|
||||
--color-card-foreground: hsl(240 10% 3.9%);
|
||||
--color-popover: hsl(0 0% 100%);
|
||||
--color-popover-foreground: hsl(240 10% 3.9%);
|
||||
--color-primary: hsl(25 95% 53%);
|
||||
--color-primary-foreground: hsl(0 0% 98%);
|
||||
--color-secondary: hsl(240 4.8% 95.9%);
|
||||
--color-secondary-foreground: hsl(240 5.9% 10%);
|
||||
--color-muted: hsl(240 4.8% 95.9%);
|
||||
--color-muted-foreground: hsl(240 3.8% 46.1%);
|
||||
--color-accent: hsl(240 4.8% 95.9%);
|
||||
--color-accent-foreground: hsl(240 5.9% 10%);
|
||||
--color-destructive: hsl(0 84.2% 60.2%);
|
||||
--color-destructive-foreground: hsl(0 0% 98%);
|
||||
--color-border: hsl(240 5.9% 90%);
|
||||
--color-input: hsl(240 5.9% 90%);
|
||||
--color-ring: hsl(25 95% 53%);
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Dark theme overrides */
|
||||
.dark {
|
||||
--color-background: hsl(240 10% 3.9%);
|
||||
--color-foreground: hsl(0 0% 98%);
|
||||
--color-card: hsl(240 10% 3.9%);
|
||||
--color-card-foreground: hsl(0 0% 98%);
|
||||
--color-popover: hsl(240 10% 3.9%);
|
||||
--color-popover-foreground: hsl(0 0% 98%);
|
||||
--color-primary: hsl(0 0% 98%);
|
||||
--color-primary-foreground: hsl(240 5.9% 10%);
|
||||
--color-secondary: hsl(240 3.7% 15.9%);
|
||||
--color-secondary-foreground: hsl(0 0% 98%);
|
||||
--color-muted: hsl(240 3.7% 15.9%);
|
||||
--color-muted-foreground: hsl(240 5% 64.9%);
|
||||
--color-accent: hsl(240 3.7% 15.9%);
|
||||
--color-accent-foreground: hsl(0 0% 98%);
|
||||
--color-destructive: hsl(0 62.8% 30.6%);
|
||||
--color-destructive-foreground: hsl(0 0% 98%);
|
||||
--color-border: hsl(240 3.7% 15.9%);
|
||||
--color-input: hsl(240 3.7% 15.9%);
|
||||
--color-ring: hsl(240 4.9% 83.9%);
|
||||
}
|
||||
|
||||
/* Dark mode styles for common elements */
|
||||
.dark .bg-white {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
/* !important */
|
||||
.dark .text-gray-900,
|
||||
.dark .text-zinc-900 {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
/* Dark mode for specific color values */
|
||||
.dark [class*="text-[#36322F]"] {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark .text-gray-600,
|
||||
.dark .text-zinc-500,
|
||||
.dark .text-zinc-600 {
|
||||
color: hsl(240 5% 64.9%);
|
||||
}
|
||||
|
||||
.dark .bg-gray-50,
|
||||
.dark .bg-gray-100 {
|
||||
background-color: hsl(240 3.7% 15.9%);
|
||||
}
|
||||
|
||||
.dark .border-gray-200,
|
||||
.dark .border-zinc-300 {
|
||||
border-color: hsl(240 3.7% 15.9%);
|
||||
}
|
||||
|
||||
.dark .bg-gray-900 {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
/* Dark mode for specific Open Lovable elements */
|
||||
.dark .bg-orange-400\/50,
|
||||
.dark .bg-orange-300\/30,
|
||||
.dark .bg-orange-200\/20 {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .bg-yellow-300\/40 {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
/* Dark mode for design style buttons */
|
||||
.dark .p-3.rounded-lg.border.border-gray-200.bg-white {
|
||||
background-color: hsl(240 3.7% 15.9%);
|
||||
border-color: hsl(240 3.7% 15.9%);
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark .p-3.rounded-lg.border.border-gray-200.bg-white:hover {
|
||||
background-color: hsl(240 3.7% 20%);
|
||||
border-color: hsl(25 95% 53%);
|
||||
}
|
||||
|
||||
.dark .p-3.rounded-lg.border.border-gray-200.bg-white .text-sm.font-medium {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark .p-3.rounded-lg.border.border-gray-200.bg-white .text-xs.text-gray-500 {
|
||||
color: hsl(240 5% 64.9%);
|
||||
}
|
||||
|
||||
/* Dark mode for selected/active design style buttons */
|
||||
.dark .p-3.rounded-lg.border.border-orange-200.bg-orange-50\/50 {
|
||||
background-color: hsl(25 95% 53%);
|
||||
border-color: hsl(25 95% 53%);
|
||||
color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .p-3.rounded-lg.border.border-orange-200.bg-orange-50\/50 .text-sm.font-medium {
|
||||
color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .p-3.rounded-lg.border.border-orange-200.bg-orange-50\/50 .text-xs.text-gray-500 {
|
||||
color: hsl(240 10% 3.9%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Dark mode for selected buttons with orange-400 border and orange-50 background */
|
||||
.dark .p-3.rounded-lg.border.border-orange-400.bg-orange-50 {
|
||||
background-color: hsl(240 3.7% 15.9%);
|
||||
border-color: hsl(25 95% 53%);
|
||||
border-width: 2px;
|
||||
color: hsl(0 0% 98%);
|
||||
box-shadow: 0 0 0 1px hsl(25 95% 53%), 0 4px 12px -1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.dark .p-3.rounded-lg.border.border-orange-400.bg-orange-50 .text-sm.font-medium {
|
||||
color: hsl(0 0% 98%);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark .p-3.rounded-lg.border.border-orange-400.bg-orange-50 .text-xs.text-gray-500 {
|
||||
color: hsl(240 5% 64.9%);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Light mode improvements for selected buttons */
|
||||
.p-3.rounded-lg.border.border-orange-400.bg-orange-50 {
|
||||
background-color: hsl(0 0% 98%);
|
||||
border-color: hsl(25 95% 53%);
|
||||
border-width: 2px;
|
||||
color: hsl(240 10% 3.9%);
|
||||
box-shadow: 0 0 0 1px hsl(25 95% 53%), 0 4px 12px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.p-3.rounded-lg.border.border-orange-400.bg-orange-50 .text-sm.font-medium {
|
||||
color: hsl(240 10% 3.9%);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.p-3.rounded-lg.border.border-orange-400.bg-orange-50 .text-xs.text-gray-500 {
|
||||
color: hsl(240 5% 40%);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Dark mode for hover states on design style buttons */
|
||||
.dark .p-3.rounded-lg.border.border-gray-200.bg-white:hover {
|
||||
background-color: hsl(25 95% 53%);
|
||||
border-color: hsl(25 95% 53%);
|
||||
color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .p-3.rounded-lg.border.border-gray-200.bg-white:hover .text-sm.font-medium {
|
||||
color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .p-3.rounded-lg.border.border-gray-200.bg-white:hover .text-xs.text-gray-500 {
|
||||
color: hsl(240 10% 3.9%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Dark mode for dropdowns and selectors */
|
||||
.dark .bg-white {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
/* Dark mode for design container */
|
||||
.dark .bg-white\/80.backdrop-blur-sm.border.border-gray-200.rounded-xl.p-4.shadow-sm {
|
||||
background-color: hsl(240 10% 3.9% / 0.9);
|
||||
border-color: hsl(240 3.7% 15.9%);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 8px 32px -4px rgba(0, 0, 0, 0.3), 0 2px 8px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Light mode improvements for design container */
|
||||
.bg-white\/80.backdrop-blur-sm.border.border-gray-200.rounded-xl.p-4.shadow-sm {
|
||||
background-color: hsl(0 0% 100% / 0.9);
|
||||
border-color: hsl(240 5.9% 90%);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 8px 32px -4px rgba(0, 0, 0, 0.1), 0 2px 8px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Dark mode for loading spinner and text */
|
||||
.dark .w-16.h-16.border-4.border-orange-200.border-t-orange-500.rounded-full.animate-spin.mx-auto {
|
||||
border-color: hsl(25 95% 53%);
|
||||
border-top-color: hsl(25 95% 70%);
|
||||
}
|
||||
|
||||
.dark .text-xl.font-semibold.text-gray-800 {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark .text-gray-600.text-sm {
|
||||
color: hsl(240 5% 64.9%);
|
||||
}
|
||||
|
||||
/* Dark mode for file explorer */
|
||||
.dark .flex-1.overflow-y-auto.p-2.scrollbar-hide {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .text-sm {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark .flex.items-center.gap-1.py-1.px-2.hover\:bg-gray-100.rounded.cursor-pointer.text-gray-700 {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark .flex.items-center.gap-1.py-1.px-2.hover\:bg-gray-100.rounded.cursor-pointer.text-gray-700:hover {
|
||||
background-color: hsl(240 3.7% 15.9%);
|
||||
}
|
||||
|
||||
/* Dark mode for file items */
|
||||
.dark .text-xs.flex.items-center.gap-1 {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark .text-xs.flex.items-center.gap-1:hover {
|
||||
color: hsl(25 95% 70%);
|
||||
background-color: hsl(240 3.7% 15.9%);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Light mode improvements for file items */
|
||||
.text-xs.flex.items-center.gap-1 {
|
||||
color: hsl(240 10% 3.9%);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Theme toggle button improvements */
|
||||
.dark .rounded-full.fixed.bottom-4.right-4.z-\[9999\].bg-white.dark\:bg-gray-800.border-gray-300.dark\:border-gray-600.text-gray-700.dark\:text-gray-200.hover\:bg-gray-50.dark\:hover\:bg-gray-700.shadow-lg.transition-all.duration-200.hover\:scale-110 {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
border-color: hsl(240 3.7% 15.9%);
|
||||
color: hsl(0 0% 98%);
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark .rounded-full.fixed.bottom-4.right-4.z-\[9999\].bg-white.dark\:bg-gray-800.border-gray-300.dark\:border-gray-600.text-gray-700.dark\:text-gray-200.hover\:bg-gray-50.dark\:hover\:bg-gray-700.shadow-lg.transition-all.duration-200.hover\:scale-110:hover {
|
||||
background-color: hsl(240 3.7% 15.9%);
|
||||
border-color: hsl(25 95% 53%);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Light mode theme toggle improvements */
|
||||
.rounded-full.fixed.bottom-4.right-4.z-\[9999\].bg-white.dark\:bg-gray-800.border-gray-300.dark\:border-gray-600.text-gray-700.dark\:text-gray-200.hover\:bg-gray-50.dark\:hover\:bg-gray-700.shadow-lg.transition-all.duration-200.hover\:scale-110 {
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.rounded-full.fixed.bottom-4.right-4.z-\[9999\].bg-white.dark\:bg-gray-800.border-gray-300.dark\:border-gray-600.text-gray-700.dark\:text-gray-200.hover\:bg-gray-50.dark\:hover\:bg-gray-700.shadow-lg.transition-all.duration-200.hover\:scale-110:hover {
|
||||
border-color: hsl(25 95% 53%);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.text-xs.flex.items-center.gap-1:hover {
|
||||
color: hsl(25 95% 53%);
|
||||
background-color: hsl(25 95% 95%);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Light mode improvements for loading elements */
|
||||
.w-16.h-16.border-4.border-orange-200.border-t-orange-500.rounded-full.animate-spin.mx-auto {
|
||||
border-color: hsl(25 95% 53%);
|
||||
border-top-color: hsl(25 95% 70%);
|
||||
box-shadow: 0 0 20px rgba(251, 146, 60, 0.3);
|
||||
}
|
||||
|
||||
.text-xl.font-semibold.text-gray-800 {
|
||||
color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.text-gray-600.text-sm {
|
||||
color: hsl(240 5% 40%);
|
||||
}
|
||||
|
||||
.dark .text-black {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark .hover\:bg-gray-50:hover {
|
||||
background-color: hsl(240 3.7% 15.9%);
|
||||
}
|
||||
|
||||
.dark .hover\:bg-gray-100:hover {
|
||||
background-color: hsl(240 3.7% 15.9%);
|
||||
}
|
||||
|
||||
/* Dark mode for select elements */
|
||||
.dark select {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
color: hsl(0 0% 98%);
|
||||
border-color: hsl(240 3.7% 15.9%);
|
||||
}
|
||||
|
||||
.dark select option {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark select option:hover {
|
||||
background-color: hsl(240 3.7% 15.9%);
|
||||
}
|
||||
|
||||
/* Dark mode for specific Open Lovable elements */
|
||||
.dark .bg-card {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .border-border {
|
||||
border-color: hsl(240 3.7% 15.9%);
|
||||
}
|
||||
|
||||
.dark .text-black {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
.dark .hover\:text-gray-700:hover {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
/* Dark mode for page background */
|
||||
.dark .bg-background {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .text-foreground {
|
||||
color: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
/* Dark mode for specific backgrounds */
|
||||
.dark .bg-gray-50 {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .bg-gray-100 {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
.dark .bg-gray-900 {
|
||||
background-color: hsl(240 10% 3.9%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* Radial gradient utilities */
|
||||
.bg-gradient-radial {
|
||||
background-image: radial-gradient(circle, var(--tw-gradient-stops));
|
||||
}
|
||||
|
||||
/* Conic gradient utilities */
|
||||
.bg-gradient-conic {
|
||||
background-image: conic-gradient(var(--tw-gradient-stops));
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
border-color: theme('colors.border');
|
||||
}
|
||||
body {
|
||||
background-color: theme('colors.background');
|
||||
color: theme('colors.foreground');
|
||||
}
|
||||
|
||||
/* Dark mode overrides for common elements */
|
||||
.dark {
|
||||
background-color: theme('colors.background');
|
||||
color: theme('colors.foreground');
|
||||
}
|
||||
|
||||
/* Ensure dark mode applies to all elements */
|
||||
.dark * {
|
||||
border-color: theme('colors.border');
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.animate-gradient-shift {
|
||||
background-size: 400% 400%;
|
||||
animation: gradient-shift 8s ease infinite;
|
||||
}
|
||||
|
||||
.animate-camera-float {
|
||||
animation: camera-float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-lens-rotate {
|
||||
animation: lens-rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.animation-delay-200 {
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
|
||||
.animate-push-up {
|
||||
animation: pushUp 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-smooth {
|
||||
opacity: 0;
|
||||
animation: fadeInSmooth 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
opacity: 0;
|
||||
animation: fadeInUp 0.5s ease-out forwards;
|
||||
}
|
||||
}
|
||||
@import "../styles/main.css";
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
// useState not currently used but kept for future interactivity
|
||||
import Link from "next/link";
|
||||
|
||||
// Import shared components
|
||||
import { HeaderProvider } from "@/components/shared/header/HeaderContext";
|
||||
// import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit"; // Not used in current implementation
|
||||
import HeaderWrapper from "@/components/shared/header/Wrapper/Wrapper";
|
||||
import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/Wrapper";
|
||||
import ButtonUI from "@/components/ui/shadcn/button";
|
||||
|
||||
// Import hero section components
|
||||
import HomeHeroBackground from "@/components/app/(home)/sections/hero/Background/Background";
|
||||
import { BackgroundOuterPiece } from "@/components/app/(home)/sections/hero/Background/BackgroundOuterPiece";
|
||||
import HomeHeroBadge from "@/components/app/(home)/sections/hero/Badge/Badge";
|
||||
import HomeHeroPixi from "@/components/app/(home)/sections/hero/Pixi/Pixi";
|
||||
import HomeHeroTitle from "@/components/app/(home)/sections/hero/Title/Title";
|
||||
import HeroInput from "@/components/app/(home)/sections/hero-input/HeroInput";
|
||||
import { Connector } from "@/components/shared/layout/curvy-rect";
|
||||
import HeroFlame from "@/components/shared/effects/flame/hero-flame";
|
||||
import FirecrawlIcon from "@/components/FirecrawlIcon";
|
||||
import FirecrawlLogo from "@/components/FirecrawlLogo";
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<HeaderProvider>
|
||||
<div className="min-h-screen bg-background-base">
|
||||
{/* Header/Navigation Section */}
|
||||
<HeaderDropdownWrapper />
|
||||
|
||||
<div className="sticky top-0 left-0 w-full z-[101] bg-background-base header">
|
||||
<div className="absolute top-0 cmw-container border-x border-border-faint h-full pointer-events-none" />
|
||||
<div className="h-1 bg-border-faint w-full left-0 -bottom-1 absolute" />
|
||||
|
||||
<div className="cmw-container absolute h-full pointer-events-none top-0">
|
||||
<Connector className="absolute -left-[10.5px] -bottom-11" />
|
||||
<Connector className="absolute -right-[10.5px] -bottom-11" />
|
||||
</div>
|
||||
|
||||
<HeaderWrapper>
|
||||
<div className="max-w-[900px] mx-auto w-full flex justify-between items-center">
|
||||
<div className="flex gap-24 items-center">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<FirecrawlIcon className="w-7 h-7 text-accent-black" />
|
||||
<FirecrawlLogo />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-8">
|
||||
<Link
|
||||
href="https://github.com/mendableai/open-lovable"
|
||||
target="_blank"
|
||||
className="contents"
|
||||
>
|
||||
<ButtonUI variant="tertiary">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<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"/>
|
||||
</svg>
|
||||
Use this Template
|
||||
</ButtonUI>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</HeaderWrapper>
|
||||
</div>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="overflow-x-clip" id="home-hero">
|
||||
<div className="pt-28 lg:pt-254 lg:-mt-100 pb-115 relative" id="hero-content">
|
||||
<HomeHeroPixi />
|
||||
<HeroFlame />
|
||||
<BackgroundOuterPiece />
|
||||
<HomeHeroBackground />
|
||||
|
||||
<div className="relative container px-16">
|
||||
<HomeHeroBadge />
|
||||
<HomeHeroTitle />
|
||||
|
||||
{/* Hero Input */}
|
||||
<div className="mt-24">
|
||||
<HeroInput />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</HeaderProvider>
|
||||
);
|
||||
}
|
||||
+27
-14
@@ -1,12 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Inter, Roboto_Mono } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/app/components/theme-provider";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter"
|
||||
});
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/GeistMonoVF.woff",
|
||||
variable: "--font-geist-mono",
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
const robotoMono = Roboto_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-roboto-mono",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Open Lovable",
|
||||
title: "Open Lovable v2",
|
||||
description: "Re-imagine any website in seconds with AI-powered website builder.",
|
||||
};
|
||||
|
||||
@@ -16,16 +36,9 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
<html lang="en">
|
||||
<body className={`${inter.variable} ${geistSans.variable} ${geistMono.variable} ${robotoMono.variable} font-sans`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+731
-3351
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user