Merge branch 'main' into morph-fast-apply
This commit is contained in:
+36
-14
@@ -1,24 +1,46 @@
|
|||||||
# REQUIRED - Sandboxes for code execution
|
# Required
|
||||||
# Get yours at https://e2b.dev
|
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
|
||||||
E2B_API_KEY=your_e2b_api_key_here
|
|
||||||
|
|
||||||
# REQUIRED - Web scraping for cloning websites
|
# =================================================================================
|
||||||
# Get yours at https://firecrawl.dev
|
# SANDBOX PROVIDER - Choose Option 1 OR 2
|
||||||
FIRECRAWL_API_KEY=your_firecrawl_api_key_here
|
# =================================================================================
|
||||||
|
|
||||||
# OPTIONAL - AI Providers (need at least one)
|
# Option 1: Vercel Sandbox (recommended - default)
|
||||||
# Get yours at https://console.anthropic.com
|
# Set SANDBOX_PROVIDER=vercel and choose authentication method below
|
||||||
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
SANDBOX_PROVIDER=vercel
|
||||||
|
|
||||||
# Get yours at https://platform.openai.com
|
# Vercel Authentication - Choose method a OR b
|
||||||
OPENAI_API_KEY=your_openai_api_key_here
|
# Method a: OIDC Token (recommended for development)
|
||||||
|
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
|
||||||
|
VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
|
||||||
|
|
||||||
# Get yours at https://aistudio.google.com/app/apikey
|
# Method b: Personal Access Token (for production or when OIDC unavailable)
|
||||||
GEMINI_API_KEY=your_gemini_api_key_here
|
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID
|
||||||
|
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
|
||||||
|
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
|
||||||
|
|
||||||
# Get yours at https://console.groq.com
|
# Get yours at https://console.groq.com
|
||||||
GROQ_API_KEY=your_groq_api_key_here
|
GROQ_API_KEY=your_groq_api_key_here
|
||||||
|
|
||||||
|
=======
|
||||||
|
# Option 2: E2B Sandbox
|
||||||
|
# Set SANDBOX_PROVIDER=e2b and configure E2B_API_KEY below
|
||||||
|
# SANDBOX_PROVIDER=e2b
|
||||||
|
# E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev
|
||||||
|
|
||||||
|
# =================================================================================
|
||||||
|
# AI PROVIDERS - Need at least one
|
||||||
|
# =================================================================================
|
||||||
|
|
||||||
|
# Vercel AI Gateway (recommended - provides access to multiple models)
|
||||||
|
AI_GATEWAY_API_KEY=your_ai_gateway_api_key # Get from https://vercel.com/dashboard/ai-gateway/api-keys
|
||||||
|
|
||||||
|
# Individual provider keys (used when AI_GATEWAY_API_KEY is not set)
|
||||||
|
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
|
||||||
|
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
|
||||||
|
GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey
|
||||||
|
GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended)
|
||||||
|
|
||||||
# Optional Morph Fast Apply
|
# Optional Morph Fast Apply
|
||||||
# Get yours at https://morphllm.com/
|
# Get yours at https://morphllm.com/
|
||||||
MORPH_API_KEY=your_fast_apply_key
|
MORPH_API_KEY=your_fast_apply_key
|
||||||
|
|||||||
@@ -56,3 +56,4 @@ e2b-template-*
|
|||||||
*.temp
|
*.temp
|
||||||
repomix-output.txt
|
repomix-output.txt
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
.env*.local
|
||||||
|
|||||||
@@ -1,40 +1,62 @@
|
|||||||
# Open Lovable
|
# Open Lovable
|
||||||
|
|
||||||
Chat with AI to build React apps instantly. An example app made by the [Firecrawl](https://firecrawl.dev/?ref=open-lovable-github) team. For a complete cloud solution, check out [Lovable.dev ❤️](https://lovable.dev/).
|
Chat with AI to build React apps instantly. An example app made by the [Firecrawl](https://firecrawl.dev/?ref=open-lovable-github) team. For a complete cloud solution, check out [Lovable.dev](https://lovable.dev/) ❤️.
|
||||||
|
|
||||||
<img src="https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmZtaHFleGRsMTNlaWNydGdianI4NGQ4dHhyZjB0d2VkcjRyeXBucCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZFVLWMa6dVskQX0qu1/giphy.gif" alt="Open Lovable Demo" width="100%"/>
|
<img src="https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmZtaHFleGRsMTNlaWNydGdianI4NGQ4dHhyZjB0d2VkcjRyeXBucCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZFVLWMa6dVskQX0qu1/giphy.gif" alt="Open Lovable Demo" width="100%"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. **Clone & Install**
|
1. **Clone & Install**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/mendableai/open-lovable.git
|
git clone https://github.com/firecrawl/open-lovable.git
|
||||||
cd open-lovable
|
cd open-lovable
|
||||||
npm install
|
pnpm install # or npm install / yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Add `.env.local`**
|
2. **Add `.env.local`**
|
||||||
```env
|
|
||||||
# Required
|
|
||||||
E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev (Sandboxes)
|
|
||||||
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
|
|
||||||
|
|
||||||
# Optional (need at least one AI provider)
|
```env
|
||||||
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
|
# =================================================================
|
||||||
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
|
# REQUIRED
|
||||||
GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey
|
# =================================================================
|
||||||
GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended)
|
FIRECRAWL_API_KEY=your_firecrawl_api_key # https://firecrawl.dev
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# AI PROVIDER - Choose your LLM
|
||||||
|
# =================================================================
|
||||||
|
ANTHROPIC_API_KEY=your_anthropic_api_key # https://console.anthropic.com
|
||||||
|
OPENAI_API_KEY=your_openai_api_key # https://platform.openai.com
|
||||||
|
GEMINI_API_KEY=your_gemini_api_key # https://aistudio.google.com/app/apikey
|
||||||
|
GROQ_API_KEY=your_groq_api_key # https://console.groq.com
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# SANDBOX PROVIDER - Choose ONE: Vercel (default) or E2B
|
||||||
|
# =================================================================
|
||||||
|
SANDBOX_PROVIDER=vercel # or 'e2b'
|
||||||
|
|
||||||
|
# Option 1: Vercel Sandbox (default)
|
||||||
|
# Choose one authentication method:
|
||||||
|
|
||||||
|
# Method A: OIDC Token (recommended for development)
|
||||||
|
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
|
||||||
|
VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
|
||||||
|
|
||||||
|
# Method B: Personal Access Token (for production or when OIDC unavailable)
|
||||||
|
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID
|
||||||
|
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
|
||||||
|
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
|
||||||
|
|
||||||
|
# Option 2: E2B Sandbox
|
||||||
|
# E2B_API_KEY=your_e2b_api_key # https://e2b.dev
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Run**
|
3. **Run**
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
pnpm dev # or npm run dev / yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000)
|
Open [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
@@ -5,20 +5,30 @@ import { createOpenAI } from '@ai-sdk/openai';
|
|||||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||||
import { generateObject } from 'ai';
|
import { generateObject } from 'ai';
|
||||||
import { z } from 'zod';
|
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({
|
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({
|
const anthropic = createAnthropic({
|
||||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.ANTHROPIC_API_KEY,
|
||||||
baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1',
|
baseURL: isUsingAIGateway ? aiGatewayBaseURL : (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const openai = createOpenAI({
|
const openai = createOpenAI({
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.OPENAI_API_KEY,
|
||||||
baseURL: process.env.OPENAI_BASE_URL,
|
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!
|
// 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
|
// Create a summary of available files for the AI
|
||||||
const validFiles = Object.entries(manifest.files as Record<string, any>)
|
const validFiles = Object.entries(manifest.files as Record<string, any>)
|
||||||
.filter(([path, info]) => {
|
.filter(([path]) => {
|
||||||
// Filter out invalid paths
|
// Filter out invalid paths
|
||||||
return path.includes('.') && !path.match(/\/\d+$/);
|
return path.includes('.') && !path.match(/\/\d+$/);
|
||||||
});
|
});
|
||||||
@@ -74,7 +84,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const fileSummary = validFiles
|
const fileSummary = validFiles
|
||||||
.map(([path, info]: [string, any]) => {
|
.map(([path, info]: [string, any]) => {
|
||||||
const componentName = info.componentInfo?.name || path.split('/').pop();
|
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';
|
const childComponents = info.componentInfo?.childComponents?.join(', ') || 'none';
|
||||||
return `- ${path} (${componentName}, renders: ${childComponents})`;
|
return `- ${path} (${componentName}, renders: ${childComponents})`;
|
||||||
})
|
})
|
||||||
@@ -104,7 +114,7 @@ export async function POST(request: NextRequest) {
|
|||||||
aiModel = openai(model.replace('openai/', ''));
|
aiModel = openai(model.replace('openai/', ''));
|
||||||
}
|
}
|
||||||
} else if (model.startsWith('google/')) {
|
} else if (model.startsWith('google/')) {
|
||||||
aiModel = createGoogleGenerativeAI(model.replace('google/', ''));
|
aiModel = googleGenerativeAI(model.replace('google/', ''));
|
||||||
} else {
|
} else {
|
||||||
// Default to groq if model format is unclear
|
// Default to groq if model format is unclear
|
||||||
aiModel = groq(model);
|
aiModel = groq(model);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { parseMorphEdits, applyMorphEditToFile } from '@/lib/morph-fast-apply';
|
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 { SandboxState } from '@/types/sandbox';
|
||||||
import type { ConversationState } from '@/types/conversation';
|
import type { ConversationState } from '@/types/conversation';
|
||||||
|
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var conversationState: ConversationState | null;
|
var conversationState: ConversationState | null;
|
||||||
var activeSandbox: any;
|
var activeSandboxProvider: any;
|
||||||
var existingFiles: Set<string>;
|
var existingFiles: Set<string>;
|
||||||
var sandboxState: SandboxState;
|
var sandboxState: SandboxState;
|
||||||
}
|
}
|
||||||
@@ -301,73 +302,88 @@ export async function POST(request: NextRequest) {
|
|||||||
global.existingFiles = new Set<string>();
|
global.existingFiles = new Set<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, always check the global state for active sandbox
|
// Try to get provider from sandbox manager first
|
||||||
let sandbox = global.activeSandbox;
|
let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
|
||||||
|
|
||||||
// If we don't have a sandbox in this instance but we have a sandboxId,
|
// Fall back to global state if not found in manager
|
||||||
// reconnect to the existing sandbox
|
if (!provider) {
|
||||||
if (!sandbox && sandboxId) {
|
provider = global.activeSandboxProvider;
|
||||||
console.log(`[apply-ai-code-stream] Sandbox ${sandboxId} not in this instance, attempting reconnect...`);
|
}
|
||||||
|
|
||||||
|
// 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 {
|
try {
|
||||||
// Reconnect to the existing sandbox using E2B's connect method
|
provider = await sandboxManager.getOrCreateProvider(sandboxId);
|
||||||
sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY });
|
|
||||||
console.log(`[apply-ai-code-stream] Successfully reconnected to sandbox ${sandboxId}`);
|
|
||||||
|
|
||||||
// Store the reconnected sandbox globally for this instance
|
// If we got a new provider (not reconnected), we need to create a new sandbox
|
||||||
global.activeSandbox = sandbox;
|
if (!provider.getSandboxInfo()) {
|
||||||
|
console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`);
|
||||||
// Update sandbox data if needed
|
await provider.createSandbox();
|
||||||
if (!global.sandboxData) {
|
await provider.setupViteApp();
|
||||||
const host = (sandbox as any).getHost(5173);
|
sandboxManager.registerSandbox(sandboxId, provider);
|
||||||
global.sandboxData = {
|
|
||||||
sandboxId,
|
|
||||||
url: `https://${host}`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize existingFiles if not already
|
// Update legacy global state
|
||||||
if (!global.existingFiles) {
|
global.activeSandboxProvider = provider;
|
||||||
global.existingFiles = new Set<string>();
|
console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`);
|
||||||
}
|
} catch (providerError) {
|
||||||
} catch (reconnectError) {
|
console.error(`[apply-ai-code-stream] Failed to get or create provider for sandbox ${sandboxId}:`, providerError);
|
||||||
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
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
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: {
|
results: {
|
||||||
filesCreated: [],
|
filesCreated: [],
|
||||||
packagesInstalled: [],
|
packagesInstalled: [],
|
||||||
commandsExecuted: [],
|
commandsExecuted: [],
|
||||||
errors: [`Sandbox reconnection failed: ${(reconnectError as Error).message}`]
|
errors: [`Sandbox provider creation failed: ${(providerError as Error).message}`]
|
||||||
},
|
},
|
||||||
explanation: parsed.explanation,
|
explanation: parsed.explanation,
|
||||||
structure: parsed.structure,
|
structure: parsed.structure,
|
||||||
parsedFiles: parsed.files,
|
parsedFiles: parsed.files,
|
||||||
message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox reconnection failed.`
|
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 we still don't have a provider, create a new one
|
||||||
if (!sandbox && !sandboxId) {
|
if (!provider) {
|
||||||
console.log('[apply-ai-code-stream] No sandbox available and no sandboxId provided');
|
console.log(`[apply-ai-code-stream] No active provider found, creating new sandbox...`);
|
||||||
return NextResponse.json({
|
try {
|
||||||
success: false,
|
const { SandboxFactory } = await import('@/lib/sandbox/factory');
|
||||||
error: 'No active sandbox found. Please create a sandbox first.',
|
provider = SandboxFactory.create();
|
||||||
results: {
|
const sandboxInfo = await provider.createSandbox();
|
||||||
filesCreated: [],
|
await provider.setupViteApp();
|
||||||
packagesInstalled: [],
|
|
||||||
commandsExecuted: [],
|
// Register with sandbox manager
|
||||||
errors: ['No sandbox available']
|
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
|
||||||
},
|
|
||||||
explanation: parsed.explanation,
|
// Store in legacy global state
|
||||||
structure: parsed.structure,
|
global.activeSandboxProvider = provider;
|
||||||
parsedFiles: parsed.files,
|
global.sandboxData = {
|
||||||
message: `Parsed ${parsed.files.length} files but no sandbox available to apply them.`
|
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
|
// Create a response stream for real-time updates
|
||||||
@@ -381,8 +397,8 @@ export async function POST(request: NextRequest) {
|
|||||||
await writer.write(encoder.encode(message));
|
await writer.write(encoder.encode(message));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start processing in background (pass sandbox and request to the async function)
|
// Start processing in background (pass provider and request to the async function)
|
||||||
(async (sandboxInstance, req) => {
|
(async (providerInstance, req) => {
|
||||||
const results = {
|
const results = {
|
||||||
filesCreated: [] as string[],
|
filesCreated: [] as string[],
|
||||||
filesUpdated: [] as string[],
|
filesUpdated: [] as string[],
|
||||||
@@ -447,7 +463,7 @@ export async function POST(request: NextRequest) {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
packages: uniquePackages,
|
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) {
|
if (data.type === 'success' && data.installedPackages) {
|
||||||
results.packagesInstalled = data.installedPackages;
|
results.packagesInstalled = data.installedPackages;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (parseError) {
|
||||||
// Ignore parse errors
|
console.debug('Error parsing terminal output:', parseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -589,7 +605,6 @@ export async function POST(request: NextRequest) {
|
|||||||
normalizedPath = 'src/' + normalizedPath;
|
normalizedPath = 'src/' + normalizedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = `/home/user/app/${normalizedPath}`;
|
|
||||||
const isUpdate = global.existingFiles.has(normalizedPath);
|
const isUpdate = global.existingFiles.has(normalizedPath);
|
||||||
|
|
||||||
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
|
// 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, '');
|
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the file using Python (code-interpreter SDK)
|
// Fix common Tailwind CSS errors in CSS files
|
||||||
const escapedContent = fileContent
|
if (file.path.endsWith('.css')) {
|
||||||
.replace(/\\/g, '\\\\')
|
// Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
|
||||||
.replace(/"""/g, '\\"\\"\\"')
|
fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl');
|
||||||
.replace(/\$/g, '\\$');
|
// 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(`
|
// Create directory if needed
|
||||||
import os
|
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
|
||||||
os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True)
|
if (dirPath) {
|
||||||
with open("${fullPath}", 'w') as f:
|
await providerInstance.runCommand(`mkdir -p ${dirPath}`);
|
||||||
f.write("""${escapedContent}""")
|
}
|
||||||
print(f"File written: ${fullPath}")
|
|
||||||
`);
|
// Write the file using provider
|
||||||
|
await providerInstance.writeFile(normalizedPath, fileContent);
|
||||||
|
|
||||||
// Update file cache
|
// Update file cache
|
||||||
if (global.sandboxState?.fileCache) {
|
if (global.sandboxState?.fileCache) {
|
||||||
@@ -663,27 +682,30 @@ print(f"File written: ${fullPath}")
|
|||||||
action: 'executing'
|
action: 'executing'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use E2B commands.run() for cleaner execution
|
// Use provider runCommand
|
||||||
const result = await sandboxInstance.commands.run(cmd, {
|
const result = await providerInstance.runCommand(cmd);
|
||||||
cwd: '/home/user/app',
|
|
||||||
timeout: 60,
|
// Get command output from provider result
|
||||||
on_stdout: async (data: string) => {
|
const stdout = result.stdout;
|
||||||
await sendProgress({
|
const stderr = result.stderr;
|
||||||
type: 'command-output',
|
|
||||||
command: cmd,
|
if (stdout) {
|
||||||
output: data,
|
await sendProgress({
|
||||||
stream: 'stdout'
|
type: 'command-output',
|
||||||
});
|
command: cmd,
|
||||||
},
|
output: stdout,
|
||||||
on_stderr: async (data: string) => {
|
stream: 'stdout'
|
||||||
await sendProgress({
|
});
|
||||||
type: 'command-output',
|
}
|
||||||
command: cmd,
|
|
||||||
output: data,
|
if (stderr) {
|
||||||
stream: 'stderr'
|
await sendProgress({
|
||||||
});
|
type: 'command-output',
|
||||||
}
|
command: cmd,
|
||||||
});
|
output: stderr,
|
||||||
|
stream: 'stderr'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (results.commandsExecuted) {
|
if (results.commandsExecuted) {
|
||||||
results.commandsExecuted.push(cmd);
|
results.commandsExecuted.push(cmd);
|
||||||
@@ -750,7 +772,7 @@ print(f"File written: ${fullPath}")
|
|||||||
} finally {
|
} finally {
|
||||||
await writer.close();
|
await writer.close();
|
||||||
}
|
}
|
||||||
})(sandbox, request);
|
})(provider, request);
|
||||||
|
|
||||||
// Return the stream
|
// Return the stream
|
||||||
return new Response(stream.readable, {
|
return new Response(stream.readable, {
|
||||||
@@ -760,7 +782,7 @@ print(f"File written: ${fullPath}")
|
|||||||
'Connection': 'keep-alive',
|
'Connection': 'keep-alive',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Apply AI code stream error:', error);
|
console.error('Apply AI code stream error:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
+115
-31
@@ -129,6 +129,7 @@ function parseAIResponse(response: string): ParsedResponse {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandbox: any;
|
||||||
|
var activeSandboxProvider: any;
|
||||||
var existingFiles: Set<string>;
|
var existingFiles: Set<string>;
|
||||||
var sandboxState: SandboxState;
|
var sandboxState: SandboxState;
|
||||||
}
|
}
|
||||||
@@ -157,8 +158,11 @@ export async function POST(request: NextRequest) {
|
|||||||
global.existingFiles = new Set<string>();
|
global.existingFiles = new Set<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the active sandbox or provider
|
||||||
|
const sandbox = global.activeSandbox || global.activeSandboxProvider;
|
||||||
|
|
||||||
// If no active sandbox, just return parsed results
|
// If no active sandbox, just return parsed results
|
||||||
if (!global.activeSandbox) {
|
if (!sandbox) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
results: {
|
results: {
|
||||||
@@ -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
|
// Apply to active sandbox
|
||||||
console.log('[apply-ai-code] Applying code to sandbox...');
|
console.log('[apply-ai-code] Applying code to sandbox...');
|
||||||
console.log('[apply-ai-code] Is edit mode:', isEdit);
|
console.log('[apply-ai-code] Is edit mode:', isEdit);
|
||||||
@@ -403,11 +431,28 @@ export async function POST(request: NextRequest) {
|
|||||||
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix common Tailwind CSS errors in CSS files
|
||||||
|
if (file.path.endsWith('.css')) {
|
||||||
|
// Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
|
||||||
|
fileContent = fileContent.replace(/shadow-3xl/g, 'shadow-2xl');
|
||||||
|
// Replace any other non-existent shadow utilities
|
||||||
|
fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl');
|
||||||
|
fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[apply-ai-code] Writing file using E2B files API: ${fullPath}`);
|
console.log(`[apply-ai-code] Writing file using E2B files API: ${fullPath}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the correct E2B API - sandbox.files.write()
|
// Check if we're using provider pattern (v2) or direct sandbox (v1)
|
||||||
await global.activeSandbox.files.write(fullPath, fileContent);
|
if (sandbox.writeFile) {
|
||||||
|
// V2: Provider pattern (Vercel/E2B provider)
|
||||||
|
await sandbox.writeFile(file.path, fileContent);
|
||||||
|
} else if (sandbox.files?.write) {
|
||||||
|
// V1: Direct E2B sandbox
|
||||||
|
await sandbox.files.write(fullPath, fileContent);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unsupported sandbox type');
|
||||||
|
}
|
||||||
console.log(`[apply-ai-code] Successfully wrote file: ${fullPath}`);
|
console.log(`[apply-ai-code] Successfully wrote file: ${fullPath}`);
|
||||||
|
|
||||||
// Update file cache
|
// Update file cache
|
||||||
@@ -499,15 +544,17 @@ function App() {
|
|||||||
export default App;`;
|
export default App;`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.runCode(`
|
// Use provider pattern if available
|
||||||
file_path = "/home/user/app/src/App.jsx"
|
if (sandbox.writeFile) {
|
||||||
file_content = """${appContent.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"""
|
await sandbox.writeFile('src/App.jsx', appContent);
|
||||||
|
} else if (sandbox.writeFiles) {
|
||||||
with open(file_path, 'w') as f:
|
await sandbox.writeFiles([{
|
||||||
f.write(file_content)
|
path: 'src/App.jsx',
|
||||||
|
content: Buffer.from(appContent)
|
||||||
print(f"Auto-generated: {file_path}")
|
}]);
|
||||||
`);
|
}
|
||||||
|
|
||||||
|
console.log('Auto-generated: src/App.jsx');
|
||||||
results.filesCreated.push('src/App.jsx (auto-generated)');
|
results.filesCreated.push('src/App.jsx (auto-generated)');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`);
|
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) {
|
if (!isEdit && !indexCssInParsed && !indexCssExists) {
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.runCode(`
|
const indexCssContent = `@tailwind base;
|
||||||
file_path = "/home/user/app/src/index.css"
|
|
||||||
file_content = """@tailwind base;
|
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@@ -550,15 +595,22 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}"""
|
}`;
|
||||||
|
|
||||||
with open(file_path, 'w') as f:
|
// Use provider pattern if available
|
||||||
f.write(file_content)
|
if (sandbox.writeFile) {
|
||||||
|
await sandbox.writeFile('src/index.css', indexCssContent);
|
||||||
print(f"Auto-generated: {file_path}")
|
} 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)');
|
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');
|
results.errors.push('Failed to create index.css with Tailwind');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,15 +619,47 @@ print(f"Auto-generated: {file_path}")
|
|||||||
// Execute commands
|
// Execute commands
|
||||||
for (const cmd of parsed.commands) {
|
for (const cmd of parsed.commands) {
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.runCode(`
|
// Parse command and arguments
|
||||||
import subprocess
|
const commandParts = cmd.trim().split(/\s+/);
|
||||||
os.chdir('/home/user/app')
|
const cmdName = commandParts[0];
|
||||||
result = subprocess.run(${JSON.stringify(cmd.split(' '))}, capture_output=True, text=True)
|
const args = commandParts.slice(1);
|
||||||
print(f"Executed: ${cmd}")
|
|
||||||
print(result.stdout)
|
// Execute command using sandbox
|
||||||
if result.stderr:
|
let result;
|
||||||
print(f"Errors: {result.stderr}")
|
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);
|
results.commandsExecuted.push(cmd);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
|
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
|
||||||
|
|||||||
@@ -59,10 +59,26 @@ export async function POST(request: NextRequest) {
|
|||||||
case 'clear-old':
|
case 'clear-old':
|
||||||
// Clear old conversation data but keep recent context
|
// Clear old conversation data but keep recent context
|
||||||
if (!global.conversationState) {
|
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({
|
return NextResponse.json({
|
||||||
success: false,
|
success: true,
|
||||||
error: 'No active conversation to clear'
|
message: 'New conversation state initialized',
|
||||||
}, { status: 400 });
|
state: global.conversationState
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep only recent data
|
// 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 { NextResponse } from 'next/server';
|
||||||
import { Sandbox } from '@e2b/code-interpreter';
|
import { Sandbox } from '@vercel/sandbox';
|
||||||
import type { SandboxState } from '@/types/sandbox';
|
import type { SandboxState } from '@/types/sandbox';
|
||||||
import { appConfig } from '@/config/app.config';
|
import { appConfig } from '@/config/app.config';
|
||||||
|
|
||||||
@@ -9,23 +9,74 @@ declare global {
|
|||||||
var sandboxData: any;
|
var sandboxData: any;
|
||||||
var existingFiles: Set<string>;
|
var existingFiles: Set<string>;
|
||||||
var sandboxState: SandboxState;
|
var sandboxState: SandboxState;
|
||||||
|
var sandboxCreationInProgress: boolean;
|
||||||
|
var sandboxCreationPromise: Promise<any> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST() {
|
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;
|
let sandbox: any = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[create-ai-sandbox] Creating base sandbox...');
|
console.log('[create-ai-sandbox] Creating Vercel sandbox...');
|
||||||
|
|
||||||
// Kill existing sandbox if any
|
// Kill existing sandbox if any
|
||||||
if (global.activeSandbox) {
|
if (global.activeSandbox) {
|
||||||
console.log('[create-ai-sandbox] Killing existing sandbox...');
|
console.log('[create-ai-sandbox] Stopping existing sandbox...');
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.kill();
|
await global.activeSandbox.stop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to close existing sandbox:', e);
|
console.error('Failed to stop existing sandbox:', e);
|
||||||
}
|
}
|
||||||
global.activeSandbox = null;
|
global.activeSandbox = null;
|
||||||
|
global.sandboxData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing files tracking
|
// Clear existing files tracking
|
||||||
@@ -35,81 +86,102 @@ export async function POST() {
|
|||||||
global.existingFiles = new Set<string>();
|
global.existingFiles = new Set<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create base sandbox - we'll set up Vite ourselves for full control
|
// Create Vercel sandbox with flexible authentication
|
||||||
console.log(`[create-ai-sandbox] Creating base E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`);
|
console.log(`[create-ai-sandbox] Creating Vercel sandbox with ${appConfig.vercelSandbox.timeoutMinutes} minute timeout...`);
|
||||||
sandbox = await Sandbox.create({
|
|
||||||
apiKey: process.env.E2B_API_KEY,
|
|
||||||
timeoutMs: appConfig.e2b.timeoutMs
|
|
||||||
});
|
|
||||||
|
|
||||||
const sandboxId = (sandbox as any).sandboxId || Date.now().toString();
|
// Prepare sandbox configuration
|
||||||
const host = (sandbox as any).getHost(appConfig.e2b.vitePort);
|
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 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...');
|
console.log('[create-ai-sandbox] Setting up Vite React app...');
|
||||||
|
|
||||||
// Write all files in a single Python script to avoid multiple executions
|
// First, change to the working directory
|
||||||
const setupScript = `
|
await sandbox.runCommand('pwd');
|
||||||
import os
|
// workDir is defined in appConfig - not needed here
|
||||||
import json
|
|
||||||
|
// 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 the Vite config content with the proper hostname (using string concatenation)
|
||||||
|
const viteConfigContent = `import { defineConfig } from 'vite'
|
||||||
# Create directory structure
|
|
||||||
os.makedirs('/home/user/app/src', exist_ok=True)
|
|
||||||
|
|
||||||
# Package.json
|
|
||||||
package_json = {
|
|
||||||
"name": "sandbox-app",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite --host",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
|
||||||
"vite": "^4.3.9",
|
|
||||||
"tailwindcss": "^3.3.0",
|
|
||||||
"postcss": "^8.4.31",
|
|
||||||
"autoprefixer": "^10.4.16"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with open('/home/user/app/package.json', 'w') as f:
|
|
||||||
json.dump(package_json, f, indent=2)
|
|
||||||
print('✓ package.json')
|
|
||||||
|
|
||||||
# Vite config for E2B - with allowedHosts
|
|
||||||
vite_config = """import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// E2B-compatible Vite configuration
|
// Vercel Sandbox compatible Vite configuration
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: ${appConfig.vercelSandbox.devPort},
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
hmr: false,
|
hmr: true,
|
||||||
allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1']
|
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:
|
// Create the project files (now we have the sandbox hostname)
|
||||||
f.write(vite_config)
|
const projectFiles = [
|
||||||
print('✓ vite.config.js')
|
{
|
||||||
|
path: 'package.json',
|
||||||
# Tailwind config - standard without custom design tokens
|
content: Buffer.from(JSON.stringify({
|
||||||
tailwind_config = """/** @type {import('tailwindcss').Config} */
|
"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 {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
"./index.html",
|
"./index.html",
|
||||||
@@ -119,26 +191,20 @@ export default {
|
|||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}"""
|
}`)
|
||||||
|
},
|
||||||
with open('/home/user/app/tailwind.config.js', 'w') as f:
|
{
|
||||||
f.write(tailwind_config)
|
path: 'postcss.config.js',
|
||||||
print('✓ tailwind.config.js')
|
content: Buffer.from(`export default {
|
||||||
|
|
||||||
# PostCSS config
|
|
||||||
postcss_config = """export default {
|
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}"""
|
}`)
|
||||||
|
},
|
||||||
with open('/home/user/app/postcss.config.js', 'w') as f:
|
{
|
||||||
f.write(postcss_config)
|
path: 'index.html',
|
||||||
print('✓ postcss.config.js')
|
content: Buffer.from(`<!DOCTYPE html>
|
||||||
|
|
||||||
# Index.html
|
|
||||||
index_html = """<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -149,14 +215,11 @@ index_html = """<!DOCTYPE html>
|
|||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>`)
|
||||||
|
},
|
||||||
with open('/home/user/app/index.html', 'w') as f:
|
{
|
||||||
f.write(index_html)
|
path: 'src/main.jsx',
|
||||||
print('✓ index.html')
|
content: Buffer.from(`import React from 'react'
|
||||||
|
|
||||||
# Main.jsx
|
|
||||||
main_jsx = """import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
@@ -165,19 +228,18 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)"""
|
)`)
|
||||||
|
},
|
||||||
with open('/home/user/app/src/main.jsx', 'w') as f:
|
{
|
||||||
f.write(main_jsx)
|
path: 'src/App.jsx',
|
||||||
print('✓ src/main.jsx')
|
content: Buffer.from(`function App() {
|
||||||
|
|
||||||
# App.jsx with explicit Tailwind test
|
|
||||||
app_jsx = """function App() {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
|
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
|
||||||
<div className="text-center max-w-2xl">
|
<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">
|
<p className="text-lg text-gray-400">
|
||||||
Sandbox Ready<br/>
|
|
||||||
Start building your React app with Vite and Tailwind CSS!
|
Start building your React app with Vite and Tailwind CSS!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,14 +247,11 @@ app_jsx = """function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App"""
|
export default App`)
|
||||||
|
},
|
||||||
with open('/home/user/app/src/App.jsx', 'w') as f:
|
{
|
||||||
f.write(app_jsx)
|
path: 'src/index.css',
|
||||||
print('✓ src/App.jsx')
|
content: Buffer.from(`@tailwind base;
|
||||||
|
|
||||||
# Index.css with explicit Tailwind directives
|
|
||||||
index_css = """@tailwind base;
|
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@@ -216,99 +275,53 @@ index_css = """@tailwind base;
|
|||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
background-color: rgb(17 24 39);
|
background-color: rgb(17 24 39);
|
||||||
}"""
|
}`)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
with open('/home/user/app/src/index.css', 'w') as f:
|
// Create directory structure first
|
||||||
f.write(index_css)
|
await sandbox.runCommand({
|
||||||
print('✓ src/index.css')
|
cmd: 'mkdir',
|
||||||
|
args: ['-p', 'src']
|
||||||
print('\\nAll files created successfully!')
|
});
|
||||||
`;
|
|
||||||
|
// Write all files
|
||||||
// Execute the setup script
|
await sandbox.writeFiles(projectFiles);
|
||||||
await sandbox.runCode(setupScript);
|
console.log('[create-ai-sandbox] ✓ Project files created');
|
||||||
|
|
||||||
// Install dependencies
|
// Install dependencies
|
||||||
console.log('[create-ai-sandbox] Installing dependencies...');
|
console.log('[create-ai-sandbox] Installing dependencies...');
|
||||||
await sandbox.runCode(`
|
const installResult = await sandbox.runCommand({
|
||||||
import subprocess
|
cmd: 'npm',
|
||||||
import sys
|
args: ['install', '--loglevel', 'info']
|
||||||
|
});
|
||||||
print('Installing npm packages...')
|
if (installResult.exitCode === 0) {
|
||||||
result = subprocess.run(
|
console.log('[create-ai-sandbox] ✓ Dependencies installed successfully');
|
||||||
['npm', 'install'],
|
} else {
|
||||||
cwd='/home/user/app',
|
console.log('[create-ai-sandbox] ⚠ Warning: npm install had issues but continuing...');
|
||||||
capture_output=True,
|
}
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
print('✓ Dependencies installed successfully')
|
|
||||||
else:
|
|
||||||
print(f'⚠ Warning: npm install had issues: {result.stderr}')
|
|
||||||
# Continue anyway as it might still work
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Start Vite dev server
|
// Start Vite dev server in detached mode
|
||||||
console.log('[create-ai-sandbox] Starting Vite dev server...');
|
console.log('[create-ai-sandbox] Starting Vite dev server...');
|
||||||
await sandbox.runCode(`
|
const viteProcess = await sandbox.runCommand({
|
||||||
import subprocess
|
cmd: 'npm',
|
||||||
import os
|
args: ['run', 'dev'],
|
||||||
import time
|
detached: true
|
||||||
|
});
|
||||||
os.chdir('/home/user/app')
|
|
||||||
|
console.log('[create-ai-sandbox] ✓ Vite dev server started');
|
||||||
# Kill any existing Vite processes
|
|
||||||
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Start Vite dev server
|
|
||||||
env = os.environ.copy()
|
|
||||||
env['FORCE_COLOR'] = '0'
|
|
||||||
|
|
||||||
process = subprocess.Popen(
|
|
||||||
['npm', 'run', 'dev'],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
env=env
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f'✓ Vite dev server started with PID: {process.pid}')
|
|
||||||
print('Waiting for server to be ready...')
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Wait for Vite to be fully ready
|
// Wait for Vite to be fully ready
|
||||||
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
|
await new Promise(resolve => setTimeout(resolve, appConfig.vercelSandbox.devServerStartupDelay));
|
||||||
|
|
||||||
// Force Tailwind CSS to rebuild by touching the CSS file
|
|
||||||
await sandbox.runCode(`
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
|
|
||||||
# Touch the CSS file to trigger rebuild
|
|
||||||
css_file = '/home/user/app/src/index.css'
|
|
||||||
if os.path.exists(css_file):
|
|
||||||
os.utime(css_file, None)
|
|
||||||
print('✓ Triggered CSS rebuild')
|
|
||||||
|
|
||||||
# Also ensure PostCSS processes it
|
|
||||||
time.sleep(2)
|
|
||||||
print('✓ Tailwind CSS should be loaded')
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Store sandbox globally
|
// Store sandbox globally
|
||||||
global.activeSandbox = sandbox;
|
global.activeSandbox = sandbox;
|
||||||
global.sandboxData = {
|
global.sandboxData = {
|
||||||
sandboxId,
|
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
|
// Initialize sandbox state
|
||||||
global.sandboxState = {
|
global.sandboxState = {
|
||||||
fileCache: {
|
fileCache: {
|
||||||
@@ -319,7 +332,7 @@ print('✓ Tailwind CSS should be loaded')
|
|||||||
sandbox,
|
sandbox,
|
||||||
sandboxData: {
|
sandboxData: {
|
||||||
sandboxId,
|
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('tailwind.config.js');
|
||||||
global.existingFiles.add('postcss.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,
|
success: true,
|
||||||
sandboxId,
|
sandboxId,
|
||||||
url: `https://${host}`,
|
url: sandboxUrl,
|
||||||
message: 'Sandbox created and Vite React app initialized'
|
message: 'Vercel sandbox created and Vite React app initialized'
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Store the result for reuse
|
||||||
|
global.sandboxData = {
|
||||||
|
...global.sandboxData,
|
||||||
|
...result
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[create-ai-sandbox] Error:', error);
|
console.error('[create-ai-sandbox] Error:', error);
|
||||||
@@ -348,18 +369,16 @@ print('✓ Tailwind CSS should be loaded')
|
|||||||
// Clean up on error
|
// Clean up on error
|
||||||
if (sandbox) {
|
if (sandbox) {
|
||||||
try {
|
try {
|
||||||
await sandbox.kill();
|
await sandbox.stop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to close sandbox on error:', e);
|
console.error('Failed to stop sandbox on error:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
// Clear global state on error
|
||||||
{
|
global.activeSandbox = null;
|
||||||
error: error instanceof Error ? error.message : 'Failed to create sandbox',
|
global.sandboxData = null;
|
||||||
details: error instanceof Error ? error.stack : undefined
|
|
||||||
},
|
throw error; // Throw to be caught by the outer handler
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+38
-39
@@ -1,10 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandbox: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
if (!global.activeSandbox) {
|
if (!global.activeSandbox) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -15,41 +15,37 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.log('[create-zip] Creating project zip...');
|
console.log('[create-zip] Creating project zip...');
|
||||||
|
|
||||||
// Create zip file in sandbox
|
// Create zip file in sandbox using standard commands
|
||||||
const result = await global.activeSandbox.runCode(`
|
const zipResult = await global.activeSandbox.runCommand({
|
||||||
import zipfile
|
cmd: 'bash',
|
||||||
import os
|
args: ['-c', `zip -r /tmp/project.zip . -x "node_modules/*" ".git/*" ".next/*" "dist/*" "build/*" "*.log"`]
|
||||||
import json
|
});
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
if (zipResult.exitCode !== 0) {
|
||||||
|
const error = await zipResult.stderr();
|
||||||
# Create zip file
|
throw new Error(`Failed to create zip: ${error}`);
|
||||||
with zipfile.ZipFile('/tmp/project.zip', 'w', zipfile.ZIP_DEFLATED) as zipf:
|
}
|
||||||
for root, dirs, files in os.walk('.'):
|
|
||||||
# Skip node_modules and .git
|
const sizeResult = await global.activeSandbox.runCommand({
|
||||||
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist']]
|
cmd: 'bash',
|
||||||
|
args: ['-c', `ls -la /tmp/project.zip | awk '{print $5}'`]
|
||||||
for file in files:
|
});
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
arcname = os.path.relpath(file_path, '.')
|
const fileSize = await sizeResult.stdout();
|
||||||
zipf.write(file_path, arcname)
|
console.log(`[create-zip] Created project.zip (${fileSize.trim()} bytes)`);
|
||||||
|
|
||||||
# Get file size
|
|
||||||
file_size = os.path.getsize('/tmp/project.zip')
|
|
||||||
print(f" Created project.zip ({file_size} bytes)")
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Read the zip file and convert to base64
|
// Read the zip file and convert to base64
|
||||||
const readResult = await global.activeSandbox.runCode(`
|
const readResult = await global.activeSandbox.runCommand({
|
||||||
import base64
|
cmd: 'base64',
|
||||||
|
args: ['/tmp/project.zip']
|
||||||
with open('/tmp/project.zip', 'rb') as f:
|
});
|
||||||
content = f.read()
|
|
||||||
encoded = base64.b64encode(content).decode('utf-8')
|
|
||||||
print(encoded)
|
|
||||||
`);
|
|
||||||
|
|
||||||
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
|
// Create a data URL for download
|
||||||
const dataUrl = `data:application/zip;base64,${base64Content}`;
|
const dataUrl = `data:application/zip;base64,${base64Content}`;
|
||||||
@@ -57,15 +53,18 @@ with open('/tmp/project.zip', 'rb') as f:
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
dataUrl,
|
dataUrl,
|
||||||
fileName: 'e2b-project.zip',
|
fileName: 'vercel-sandbox-project.zip',
|
||||||
message: 'Zip file created successfully'
|
message: 'Zip file created successfully'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[create-zip] Error:', error);
|
console.error('[create-zip] Error:', error);
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
success: false,
|
{
|
||||||
error: (error as Error).message
|
success: false,
|
||||||
}, { status: 500 });
|
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'];
|
const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process'];
|
||||||
if (builtins.includes(imp)) return false;
|
if (builtins.includes(imp)) return false;
|
||||||
|
|
||||||
// Extract package name (handle scoped packages and subpaths)
|
return true;
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract just the package names (without subpaths)
|
// Extract just the package names (without subpaths)
|
||||||
@@ -101,153 +93,90 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check which packages are already installed
|
// Check which packages are already installed
|
||||||
const checkResult = await global.activeSandbox.runCode(`
|
const installed: string[] = [];
|
||||||
import os
|
const missing: string[] = [];
|
||||||
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}"
|
|
||||||
|
|
||||||
if os.path.exists(package_path):
|
for (const packageName of uniquePackages) {
|
||||||
installed.append(package)
|
try {
|
||||||
else:
|
const checkResult = await global.activeSandbox.runCommand({
|
||||||
missing.append(package)
|
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 = {
|
console.log('[detect-and-install-packages] Package status:', { installed, missing });
|
||||||
'installed': installed,
|
|
||||||
'missing': missing
|
|
||||||
}
|
|
||||||
|
|
||||||
print(json.dumps(result))
|
if (missing.length === 0) {
|
||||||
`);
|
|
||||||
|
|
||||||
const status = JSON.parse(checkResult.logs.stdout.join(''));
|
|
||||||
console.log('[detect-and-install-packages] Package status:', status);
|
|
||||||
|
|
||||||
if (status.missing.length === 0) {
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
packagesInstalled: [],
|
packagesInstalled: [],
|
||||||
packagesAlreadyInstalled: status.installed,
|
packagesAlreadyInstalled: installed,
|
||||||
message: 'All packages already installed'
|
message: 'All packages already installed'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install missing packages
|
// 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(`
|
const installResult = await global.activeSandbox.runCommand({
|
||||||
import subprocess
|
cmd: 'npm',
|
||||||
import os
|
args: ['install', '--save', ...missing]
|
||||||
import json
|
});
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
const stdout = await installResult.stdout();
|
||||||
packages_to_install = ${JSON.stringify(status.missing)}
|
const stderr = await installResult.stderr();
|
||||||
|
|
||||||
# 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}"
|
|
||||||
|
|
||||||
if os.path.exists(package_path):
|
console.log('[detect-and-install-packages] Install stdout:', stdout);
|
||||||
installed.append(package)
|
if (stderr) {
|
||||||
print(f"✓ Verified installation of {package}")
|
console.log('[detect-and-install-packages] Install stderr:', stderr);
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installStatus.failed.length > 0) {
|
// Verify installation
|
||||||
console.error('[detect-and-install-packages] Failed to install:', installStatus.failed);
|
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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
packagesInstalled: installStatus.installed,
|
packagesInstalled: finalInstalled,
|
||||||
packagesFailed: installStatus.failed,
|
packagesFailed: failed,
|
||||||
packagesAlreadyInstalled: status.installed,
|
packagesAlreadyInstalled: installed,
|
||||||
message: `Installed ${installStatus.installed.length} packages`,
|
message: `Installed ${finalInstalled.length} packages`,
|
||||||
logs: installResult.logs.stdout.join('\n')
|
logs: stdout
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -11,21 +11,37 @@ import { FileManifest } from '@/types/file-manifest';
|
|||||||
import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation';
|
import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation';
|
||||||
import { appConfig } from '@/config/app.config';
|
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({
|
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({
|
const anthropic = createAnthropic({
|
||||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.ANTHROPIC_API_KEY,
|
||||||
baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1',
|
baseURL: isUsingAIGateway ? aiGatewayBaseURL : (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const googleGenerativeAI = createGoogleGenerativeAI({
|
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({
|
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
|
// Helper function to analyze user preferences from conversation history
|
||||||
@@ -142,10 +158,18 @@ export async function POST(request: NextRequest) {
|
|||||||
const stream = new TransformStream();
|
const stream = new TransformStream();
|
||||||
const writer = stream.writable.getWriter();
|
const writer = stream.writable.getWriter();
|
||||||
|
|
||||||
// Function to send progress updates
|
// Function to send progress updates with flushing
|
||||||
const sendProgress = async (data: any) => {
|
const sendProgress = async (data: any) => {
|
||||||
const message = `data: ${JSON.stringify(data)}\n\n`;
|
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
|
// Start processing in background
|
||||||
@@ -170,7 +194,7 @@ export async function POST(request: NextRequest) {
|
|||||||
if (manifest) {
|
if (manifest) {
|
||||||
await sendProgress({ type: 'status', message: '🔍 Creating search plan...' });
|
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);
|
console.log('[generate-ai-code-stream] Files available for search:', Object.keys(fileContents).length);
|
||||||
|
|
||||||
// STEP 1: Get search plan from AI
|
// 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);
|
console.log('[generate-ai-code-stream] Target selected:', target);
|
||||||
|
|
||||||
// Create surgical edit context with exact location
|
// Create surgical edit context with exact location
|
||||||
const normalizedPath = target.filePath.replace('/home/user/app/', '');
|
// normalizedPath would be: target.filePath.replace('/home/user/app/', '');
|
||||||
const fileContent = fileContents[normalizedPath]?.content || '';
|
// fileContent available but not used in current implementation
|
||||||
|
// const fileContent = fileContents[normalizedPath]?.content || '';
|
||||||
|
|
||||||
// Build enhanced context with search results
|
// Build enhanced context with search results
|
||||||
enhancedSystemPrompt = `
|
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
|
// 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
|
// This path happens when no manifest was initially available
|
||||||
let targetFiles = [];
|
let targetFiles: any[] = [];
|
||||||
if (!searchPlan || searchPlan.searchTerms.length === 0) {
|
if (!searchPlan || searchPlan.searchTerms.length === 0) {
|
||||||
console.warn('[generate-ai-code-stream] No target files after fetch, searching for relevant files');
|
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
|
- Simple style/text change = 1 file ONLY
|
||||||
- New component = 2 files MAX (component + parent)
|
- New component = 2 files MAX (component + parent)
|
||||||
- If >3 files, YOU'RE DOING TOO MUCH
|
- 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):
|
COMPONENT RELATIONSHIPS (CHECK THESE FIRST):
|
||||||
- Navigation usually lives INSIDE Header.jsx, not separate Nav.jsx
|
- Navigation usually lives INSIDE Header.jsx, not separate Nav.jsx
|
||||||
@@ -973,13 +1003,15 @@ MORPH FAST APPLY MODE (EDIT-ONLY):
|
|||||||
// Store files in cache
|
// Store files in cache
|
||||||
for (const [path, content] of Object.entries(filesData.files)) {
|
for (const [path, content] of Object.entries(filesData.files)) {
|
||||||
const normalizedPath = path.replace('/home/user/app/', '');
|
const normalizedPath = path.replace('/home/user/app/', '');
|
||||||
global.sandboxState.fileCache.files[normalizedPath] = {
|
if (global.sandboxState.fileCache) {
|
||||||
content: content as string,
|
global.sandboxState.fileCache.files[normalizedPath] = {
|
||||||
lastModified: Date.now()
|
content: content as string,
|
||||||
};
|
lastModified: Date.now()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filesData.manifest) {
|
if (filesData.manifest && global.sandboxState.fileCache) {
|
||||||
global.sandboxState.fileCache.manifest = filesData.manifest;
|
global.sandboxState.fileCache.manifest = filesData.manifest;
|
||||||
|
|
||||||
// Now try to analyze edit intent with the fetched manifest
|
// Now try to analyze edit intent with the fetched manifest
|
||||||
@@ -1011,7 +1043,7 @@ MORPH FAST APPLY MODE (EDIT-ONLY):
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update variables
|
// Update variables
|
||||||
backendFiles = global.sandboxState.fileCache.files;
|
backendFiles = global.sandboxState.fileCache?.files || {};
|
||||||
hasBackendFiles = Object.keys(backendFiles).length > 0;
|
hasBackendFiles = Object.keys(backendFiles).length > 0;
|
||||||
console.log('[generate-ai-code-stream] Updated backend cache with fetched files');
|
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
|
// Determine which provider to use based on model
|
||||||
const isAnthropic = model.startsWith('anthropic/');
|
const isAnthropic = model.startsWith('anthropic/');
|
||||||
const isGoogle = model.startsWith('google/');
|
const isGoogle = model.startsWith('google/');
|
||||||
const isOpenAI = model.startsWith('openai/gpt-5');
|
const isOpenAI = model.startsWith('openai/');
|
||||||
const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq));
|
const isKimiGroq = model === 'moonshotai/kimi-k2-instruct-0905';
|
||||||
const actualModel = isAnthropic ? model.replace('anthropic/', '') :
|
const modelProvider = isAnthropic ? anthropic :
|
||||||
(model === 'openai/gpt-5') ? 'gpt-5' :
|
(isOpenAI ? openai :
|
||||||
(isGoogle ? model.replace('google/', '') : model);
|
(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
|
// Make streaming API call with appropriate provider
|
||||||
const streamOptions: any = {
|
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
|
// Stream the response and parse in real-time
|
||||||
let generatedCode = '';
|
let generatedCode = '';
|
||||||
@@ -1287,7 +1394,7 @@ It's better to have 3 complete files than 10 incomplete files.`
|
|||||||
let tagBuffer = '';
|
let tagBuffer = '';
|
||||||
|
|
||||||
// Stream the response and parse for packages in real-time
|
// 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 || '';
|
const text = textPart || '';
|
||||||
generatedCode += text;
|
generatedCode += text;
|
||||||
currentFile += text;
|
currentFile += text;
|
||||||
@@ -1330,6 +1437,11 @@ It's better to have 3 complete files than 10 incomplete files.`
|
|||||||
raw: true
|
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)
|
// Check for package tags in buffered text (ONLY for edits, not initial generation)
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
@@ -1619,12 +1731,28 @@ Provide the complete file content without any truncation. Include all necessary
|
|||||||
completionClient = openai;
|
completionClient = openai;
|
||||||
} else if (model.includes('claude')) {
|
} else if (model.includes('claude')) {
|
||||||
completionClient = anthropic;
|
completionClient = anthropic;
|
||||||
|
} else if (model === 'moonshotai/kimi-k2-instruct-0905') {
|
||||||
|
completionClient = groq;
|
||||||
} else {
|
} else {
|
||||||
completionClient = groq;
|
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({
|
const completionResult = await streamText({
|
||||||
model: completionClient(modelMapping[model] || model),
|
model: completionClient(completionModelName),
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
@@ -1632,8 +1760,7 @@ Provide the complete file content without any truncation. Include all necessary
|
|||||||
},
|
},
|
||||||
{ role: 'user', content: completionPrompt }
|
{ role: 'user', content: completionPrompt }
|
||||||
],
|
],
|
||||||
temperature: isGPT5 ? undefined : appConfig.ai.defaultTemperature,
|
temperature: model.startsWith('openai/gpt-5') ? undefined : appConfig.ai.defaultTemperature
|
||||||
maxTokens: appConfig.ai.truncationRecoveryMaxTokens
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the full text from the stream
|
// 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, {
|
return new Response(stream.readable, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
'Connection': 'keep-alive',
|
'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 { NextResponse } from 'next/server';
|
||||||
import { parseJavaScriptFile, buildComponentTree } from '@/lib/file-parser';
|
import { parseJavaScriptFile, buildComponentTree } from '@/lib/file-parser';
|
||||||
import { FileManifest, FileInfo, RouteInfo } from '@/types/file-manifest';
|
import { FileManifest, FileInfo, RouteInfo } from '@/types/file-manifest';
|
||||||
import type { SandboxState } from '@/types/sandbox';
|
// SandboxState type used implicitly through global.activeSandbox
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandbox: any;
|
||||||
@@ -18,58 +18,82 @@ export async function GET() {
|
|||||||
|
|
||||||
console.log('[get-sandbox-files] Fetching and analyzing file structure...');
|
console.log('[get-sandbox-files] Fetching and analyzing file structure...');
|
||||||
|
|
||||||
// Get all React/JS/CSS files
|
// Get list of all relevant files
|
||||||
const result = await global.activeSandbox.runCode(`
|
const findResult = await global.activeSandbox.runCommand({
|
||||||
import os
|
cmd: 'find',
|
||||||
import json
|
args: [
|
||||||
|
'.',
|
||||||
def get_files_content(directory='/home/user/app', extensions=['.jsx', '.js', '.tsx', '.ts', '.css', '.json']):
|
'-name', 'node_modules', '-prune', '-o',
|
||||||
files_content = {}
|
'-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):
|
if (findResult.exitCode !== 0) {
|
||||||
# Skip node_modules and other unwanted directories
|
throw new Error('Failed to list files');
|
||||||
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'dist', 'build']]
|
}
|
||||||
|
|
||||||
|
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 (statResult.exitCode === 0) {
|
||||||
if any(file.endswith(ext) for ext in extensions):
|
const fileSize = parseInt(await statResult.stdout());
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
relative_path = os.path.relpath(file_path, '/home/user/app')
|
// Only read files smaller than 10KB
|
||||||
|
if (fileSize < 10000) {
|
||||||
try:
|
const catResult = await global.activeSandbox.runCommand({
|
||||||
with open(file_path, 'r') as f:
|
cmd: 'cat',
|
||||||
content = f.read()
|
args: [filePath]
|
||||||
# Only include files under 10KB to avoid huge responses
|
});
|
||||||
if len(content) < 10000:
|
|
||||||
files_content[relative_path] = content
|
if (catResult.exitCode === 0) {
|
||||||
except:
|
const content = await catResult.stdout();
|
||||||
pass
|
// 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 directory structure
|
||||||
|
const treeResult = await global.activeSandbox.runCommand({
|
||||||
# Get the files
|
cmd: 'find',
|
||||||
files = get_files_content()
|
args: ['.', '-type', 'd', '-not', '-path', '*/node_modules*', '-not', '-path', '*/.git*']
|
||||||
|
});
|
||||||
# Also get the directory structure
|
|
||||||
structure = []
|
let structure = '';
|
||||||
for root, dirs, files in os.walk('/home/user/app'):
|
if (treeResult.exitCode === 0) {
|
||||||
level = root.replace('/home/user/app', '').count(os.sep)
|
const dirs = (await treeResult.stdout()).split('\n').filter((d: string) => d.trim());
|
||||||
indent = ' ' * 2 * level
|
structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines
|
||||||
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);
|
|
||||||
|
|
||||||
// Build enhanced file manifest
|
// Build enhanced file manifest
|
||||||
const fileManifest: FileManifest = {
|
const fileManifest: FileManifest = {
|
||||||
@@ -82,12 +106,12 @@ print(json.dumps(result))
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Process each file
|
// Process each file
|
||||||
for (const [relativePath, content] of Object.entries(parsedResult.files)) {
|
for (const [relativePath, content] of Object.entries(filesContent)) {
|
||||||
const fullPath = `/home/user/app/${relativePath}`;
|
const fullPath = `/${relativePath}`;
|
||||||
|
|
||||||
// Create base file info
|
// Create base file info
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: FileInfo = {
|
||||||
content: content as string,
|
content: content,
|
||||||
type: 'utility',
|
type: 'utility',
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
relativePath,
|
relativePath,
|
||||||
@@ -96,7 +120,7 @@ print(json.dumps(result))
|
|||||||
|
|
||||||
// Parse JavaScript/JSX files
|
// Parse JavaScript/JSX files
|
||||||
if (relativePath.match(/\.(jsx?|tsx?)$/)) {
|
if (relativePath.match(/\.(jsx?|tsx?)$/)) {
|
||||||
const parseResult = parseJavaScriptFile(content as string, fullPath);
|
const parseResult = parseJavaScriptFile(content, fullPath);
|
||||||
Object.assign(fileInfo, parseResult);
|
Object.assign(fileInfo, parseResult);
|
||||||
|
|
||||||
// Identify entry point
|
// Identify entry point
|
||||||
@@ -132,9 +156,9 @@ print(json.dumps(result))
|
|||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
files: parsedResult.files,
|
files: filesContent,
|
||||||
structure: parsedResult.structure,
|
structure,
|
||||||
fileCount: Object.keys(parsedResult.files).length,
|
fileCount: Object.keys(filesContent).length,
|
||||||
manifest: fileManifest,
|
manifest: fileManifest,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +181,8 @@ function extractRoutes(files: Record<string, FileInfo>): RouteInfo[] {
|
|||||||
const routeMatches = fileInfo.content.matchAll(/path=["']([^"']+)["'].*(?:element|component)={([^}]+)}/g);
|
const routeMatches = fileInfo.content.matchAll(/path=["']([^"']+)["'].*(?:element|component)={([^}]+)}/g);
|
||||||
|
|
||||||
for (const match of routeMatches) {
|
for (const match of routeMatches) {
|
||||||
const [, routePath, componentRef] = match;
|
const [, routePath] = match;
|
||||||
|
// componentRef available in match but not used currently
|
||||||
routes.push({
|
routes.push({
|
||||||
path: routePath,
|
path: routePath,
|
||||||
component: path,
|
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 { NextRequest, NextResponse } from 'next/server';
|
||||||
import { Sandbox } from '@e2b/code-interpreter';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandbox: any;
|
||||||
|
var activeSandboxProvider: any;
|
||||||
var sandboxData: any;
|
var sandboxData: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
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) {
|
if (!packages || !Array.isArray(packages) || packages.length === 0) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -36,32 +37,17 @@ export async function POST(request: NextRequest) {
|
|||||||
console.log(`[install-packages] Cleaned:`, validPackages);
|
console.log(`[install-packages] Cleaned:`, validPackages);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get sandbox - either from global or reconnect
|
// Get active sandbox provider
|
||||||
let sandbox = global.activeSandbox;
|
const provider = global.activeSandboxProvider;
|
||||||
|
|
||||||
if (!sandbox && sandboxId) {
|
if (!provider) {
|
||||||
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) {
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'No active sandbox available'
|
error: 'No active sandbox provider available'
|
||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[install-packages] Installing packages:', packages);
|
console.log('[install-packages] Installing packages:', validPackages);
|
||||||
|
|
||||||
// Create a response stream for real-time updates
|
// Create a response stream for real-time updates
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
@@ -75,7 +61,7 @@ export async function POST(request: NextRequest) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Start installation in background
|
// Start installation in background
|
||||||
(async (sandboxInstance) => {
|
(async (providerInstance) => {
|
||||||
try {
|
try {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'start',
|
type: 'start',
|
||||||
@@ -83,23 +69,17 @@ export async function POST(request: NextRequest) {
|
|||||||
packages: validPackages
|
packages: validPackages
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kill any existing Vite process first
|
// Stop any existing development server first
|
||||||
await sendProgress({ type: 'status', message: 'Stopping development server...' });
|
await sendProgress({ type: 'status', message: 'Stopping development server...' });
|
||||||
|
|
||||||
await sandboxInstance.runCode(`
|
try {
|
||||||
import subprocess
|
// Try to kill any running dev server processes
|
||||||
import os
|
await providerInstance.runCommand('pkill -f vite');
|
||||||
import signal
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit
|
||||||
|
} catch (killError) {
|
||||||
# Try to kill any existing Vite process
|
// It's OK if no process is found
|
||||||
try:
|
console.debug('[install-packages] No existing dev server found:', killError);
|
||||||
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")
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Check which packages are already installed
|
// Check which packages are already installed
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
@@ -107,70 +87,52 @@ except:
|
|||||||
message: 'Checking installed packages...'
|
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;
|
let packagesToInstall = validPackages;
|
||||||
|
|
||||||
// Check if checkResult has the expected structure
|
try {
|
||||||
if (checkResult && checkResult.results && checkResult.results[0] && checkResult.results[0].text) {
|
// Read package.json to check existing dependencies
|
||||||
const outputLines = checkResult.results[0].text.split('\n');
|
let packageJsonContent = '';
|
||||||
for (const line of outputLines) {
|
try {
|
||||||
if (line.startsWith('NEED_INSTALL:')) {
|
packageJsonContent = await providerInstance.readFile('package.json');
|
||||||
try {
|
} catch (error) {
|
||||||
packagesToInstall = JSON.parse(line.substring('NEED_INSTALL:'.length));
|
console.log('[install-packages] Error reading package.json:', error);
|
||||||
} catch (e) {
|
}
|
||||||
console.error('Failed to parse packages to install:', e);
|
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 {
|
} catch (error) {
|
||||||
console.error('[install-packages] Invalid checkResult structure:', checkResult);
|
console.error('[install-packages] Error checking existing packages:', error);
|
||||||
// If we can't check, just try to install all packages
|
// If we can't check, just try to install all packages
|
||||||
packagesToInstall = validPackages;
|
packagesToInstall = validPackages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (packagesToInstall.length === 0) {
|
if (packagesToInstall.length === 0) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -178,164 +140,92 @@ except Exception as e:
|
|||||||
installedPackages: [],
|
installedPackages: [],
|
||||||
alreadyInstalled: validPackages
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install only packages that aren't already installed
|
// 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({
|
await sendProgress({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}`
|
message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const installResult = await sandboxInstance.runCode(`
|
// Install packages using provider method
|
||||||
import subprocess
|
const installResult = await providerInstance.installPackages(packagesToInstall);
|
||||||
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")
|
|
||||||
|
|
||||||
print(f"\\nVerified installed packages: {installed}")
|
// Get install output - ensure stdout/stderr are strings
|
||||||
`, { timeout: 60000 }); // 60 second timeout for npm install
|
const stdout = String(installResult.stdout || '');
|
||||||
|
const stderr = String(installResult.stderr || '');
|
||||||
|
|
||||||
// Send npm output
|
if (stdout) {
|
||||||
const output = installResult?.output || installResult?.logs?.stdout?.join('\n') || '';
|
const lines = stdout.split('\n').filter(line => line.trim());
|
||||||
const npmOutputLines = output.split('\n').filter((line: string) => line.trim());
|
for (const line of lines) {
|
||||||
for (const line of npmOutputLines) {
|
if (line.includes('npm WARN')) {
|
||||||
if (line.includes('STDERR:')) {
|
await sendProgress({ type: 'warning', message: line });
|
||||||
const errorMsg = line.replace('STDERR:', '').trim();
|
} else if (line.trim()) {
|
||||||
if (errorMsg && errorMsg !== 'undefined') {
|
await sendProgress({ type: 'output', message: line });
|
||||||
await sendProgress({ type: 'error', message: errorMsg });
|
|
||||||
}
|
}
|
||||||
} 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
|
if (stderr) {
|
||||||
const installedMatch = output.match(/Verified installed packages: \[(.*?)\]/);
|
const errorLines = stderr.split('\n').filter(line => line.trim());
|
||||||
let installedPackages: string[] = [];
|
for (const line of errorLines) {
|
||||||
|
if (line.includes('ERESOLVE')) {
|
||||||
if (installedMatch && installedMatch[1]) {
|
await sendProgress({
|
||||||
installedPackages = installedMatch[1]
|
type: 'warning',
|
||||||
.split(',')
|
message: `Dependency conflict resolved with --legacy-peer-deps: ${line}`
|
||||||
.map((p: string) => p.trim().replace(/'/g, ''))
|
});
|
||||||
.filter((p: string) => p.length > 0);
|
} else if (line.trim()) {
|
||||||
|
await sendProgress({ type: 'error', message: line });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installedPackages.length > 0) {
|
if (installResult.exitCode === 0) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: `Successfully installed: ${installedPackages.join(', ')}`,
|
message: `Successfully installed: ${packagesToInstall.join(', ')}`,
|
||||||
installedPackages
|
installedPackages: packagesToInstall
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'error',
|
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 sendProgress({ type: 'status', message: 'Restarting development server...' });
|
||||||
|
|
||||||
await sandboxInstance.runCode(`
|
try {
|
||||||
import subprocess
|
await providerInstance.restartViteServer();
|
||||||
import os
|
|
||||||
import time
|
// Wait a bit for the server to start
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
os.chdir('/home/user/app')
|
|
||||||
|
await sendProgress({
|
||||||
# Kill any existing Vite processes
|
type: 'complete',
|
||||||
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
|
message: 'Package installation complete and dev server restarted!',
|
||||||
time.sleep(1)
|
installedPackages: packagesToInstall
|
||||||
|
});
|
||||||
# Start Vite dev server
|
} catch (error) {
|
||||||
env = os.environ.copy()
|
await sendProgress({
|
||||||
env['FORCE_COLOR'] = '0'
|
type: 'error',
|
||||||
|
message: `Failed to restart dev server: ${(error as Error).message}`
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = (error as Error).message;
|
const errorMessage = (error as Error).message;
|
||||||
@@ -348,7 +238,7 @@ print("Vite restarted and should now recognize all packages")
|
|||||||
} finally {
|
} finally {
|
||||||
await writer.close();
|
await writer.close();
|
||||||
}
|
}
|
||||||
})(sandbox);
|
})(provider);
|
||||||
|
|
||||||
// Return the stream
|
// Return the stream
|
||||||
return new Response(stream.readable, {
|
return new Response(stream.readable, {
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandboxProvider: any;
|
||||||
var sandboxData: any;
|
var sandboxData: any;
|
||||||
var existingFiles: Set<string>;
|
var existingFiles: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
console.log('[kill-sandbox] Killing active sandbox...');
|
console.log('[kill-sandbox] Stopping active sandbox...');
|
||||||
|
|
||||||
let sandboxKilled = false;
|
let sandboxKilled = false;
|
||||||
|
|
||||||
// Kill existing sandbox if any
|
// Stop existing sandbox if any
|
||||||
if (global.activeSandbox) {
|
if (global.activeSandboxProvider) {
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.close();
|
await global.activeSandboxProvider.terminate();
|
||||||
sandboxKilled = true;
|
sandboxKilled = true;
|
||||||
console.log('[kill-sandbox] Sandbox closed successfully');
|
console.log('[kill-sandbox] Sandbox stopped successfully');
|
||||||
} catch (e) {
|
} 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;
|
global.sandboxData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,97 +15,100 @@ export async function GET() {
|
|||||||
|
|
||||||
console.log('[monitor-vite-logs] Checking Vite process logs...');
|
console.log('[monitor-vite-logs] Checking Vite process logs...');
|
||||||
|
|
||||||
// Check both the error file and recent logs
|
const errors: any[] = [];
|
||||||
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())
|
|
||||||
|
|
||||||
# Check if process is still running and get its logs
|
// Check if there's an error file from previous runs
|
||||||
# This is a bit hacky but works for our use case
|
try {
|
||||||
result = subprocess.run(['ps', '-p', str(pid)], capture_output=True, text=True)
|
const catResult = await global.activeSandbox.runCommand({
|
||||||
if result.returncode == 0:
|
cmd: 'cat',
|
||||||
# Process is running, try to check for errors in output
|
args: ['/tmp/vite-errors.json']
|
||||||
# Note: We can't easily get stdout/stderr from a running process
|
});
|
||||||
# but we can check if there are new errors
|
|
||||||
pass
|
if (catResult.exitCode === 0) {
|
||||||
except:
|
const errorFileContent = await catResult.stdout();
|
||||||
pass
|
const data = JSON.parse(errorFileContent);
|
||||||
|
errors.push(...(data.errors || []));
|
||||||
# Also scan the current console output for any HMR errors
|
}
|
||||||
# This won't catch everything but helps with recent errors
|
} catch {
|
||||||
try:
|
// No error file exists, that's OK
|
||||||
# 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))
|
|
||||||
|
|
||||||
for log_file in log_files[:5]: # Check up to 5 log files
|
// Look for any Vite-related log files that might contain errors
|
||||||
try:
|
try {
|
||||||
with open(log_file, 'r') as f:
|
const findResult = await global.activeSandbox.runCommand({
|
||||||
content = f.read()
|
cmd: 'find',
|
||||||
# Look for import errors
|
args: ['/tmp', '-name', '*vite*', '-type', 'f']
|
||||||
import_errors = re.findall(r'Failed to resolve import "([^"]+)"', content)
|
});
|
||||||
for pkg in import_errors:
|
|
||||||
if not pkg.startswith('.'):
|
if (findResult.exitCode === 0) {
|
||||||
# Extract base package name
|
const logFiles = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
||||||
if pkg.startswith('@'):
|
|
||||||
parts = pkg.split('/')
|
for (const logFile of logFiles.slice(0, 3)) {
|
||||||
final_pkg = '/'.join(parts[:2]) if len(parts) >= 2 else pkg
|
try {
|
||||||
else:
|
const grepResult = await global.activeSandbox.runCommand({
|
||||||
final_pkg = pkg.split('/')[0]
|
cmd: 'grep',
|
||||||
|
args: ['-i', 'failed to resolve import', logFile]
|
||||||
error_obj = {
|
});
|
||||||
"type": "npm-missing",
|
|
||||||
"package": final_pkg,
|
if (grepResult.exitCode === 0) {
|
||||||
"message": f"Failed to resolve import \\"{pkg}\\"",
|
const errorLines = (await grepResult.stdout()).split('\n').filter((line: string) => line.trim());
|
||||||
"file": "Unknown"
|
|
||||||
}
|
for (const line of errorLines) {
|
||||||
|
// Extract package name from error line
|
||||||
# Avoid duplicates
|
const importMatch = line.match(/"([^"]+)"/);
|
||||||
if not any(e['package'] == error_obj['package'] for e in errors):
|
if (importMatch) {
|
||||||
errors.append(error_obj)
|
const importPath = importMatch[1];
|
||||||
except:
|
|
||||||
pass
|
// Skip relative imports
|
||||||
except Exception as e:
|
if (!importPath.startsWith('.')) {
|
||||||
print(f"Error scanning logs: {e}")
|
// Extract base package name
|
||||||
|
let packageName;
|
||||||
# Deduplicate errors
|
if (importPath.startsWith('@')) {
|
||||||
unique_errors = []
|
const parts = importPath.split('/');
|
||||||
seen_packages = set()
|
packageName = parts.length >= 2 ? parts.slice(0, 2).join('/') : importPath;
|
||||||
for error in errors:
|
} else {
|
||||||
if error.get('package') and error['package'] not in seen_packages:
|
packageName = importPath.split('/')[0];
|
||||||
seen_packages.add(error['package'])
|
}
|
||||||
unique_errors.append(error)
|
|
||||||
|
const errorObj = {
|
||||||
print(json.dumps({"errors": unique_errors}))
|
type: "npm-missing",
|
||||||
`, { timeout: 5000 });
|
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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
hasErrors: data.errors.length > 0,
|
hasErrors: uniqueErrors.length > 0,
|
||||||
errors: data.errors
|
errors: uniqueErrors
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+74
-107
@@ -2,132 +2,99 @@ import { NextResponse } from 'next/server';
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
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() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
if (!global.activeSandbox) {
|
// Check both v1 and v2 global references
|
||||||
|
const provider = global.activeSandbox || global.activeSandboxProvider;
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'No active sandbox'
|
error: 'No active sandbox'
|
||||||
}, { status: 400 });
|
}, { 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
|
// Check cooldown
|
||||||
const result = await global.activeSandbox.runCode(`
|
const now = Date.now();
|
||||||
import subprocess
|
if (global.lastViteRestartTime && (now - global.lastViteRestartTime) < RESTART_COOLDOWN_MS) {
|
||||||
import os
|
const remainingTime = Math.ceil((RESTART_COOLDOWN_MS - (now - global.lastViteRestartTime)) / 1000);
|
||||||
import signal
|
console.log(`[restart-vite] Cooldown active, ${remainingTime}s remaining`);
|
||||||
import time
|
return NextResponse.json({
|
||||||
import threading
|
success: true,
|
||||||
import json
|
message: `Vite was recently restarted, cooldown active (${remainingTime}s remaining)`
|
||||||
import sys
|
});
|
||||||
|
}
|
||||||
# Kill existing Vite process
|
|
||||||
try:
|
// Set the restart flag
|
||||||
with open('/tmp/vite-process.pid', 'r') as f:
|
global.viteRestartInProgress = true;
|
||||||
pid = int(f.read().strip())
|
|
||||||
os.kill(pid, signal.SIGTERM)
|
console.log('[restart-vite] Using provider method to restart Vite...');
|
||||||
print("Killed existing Vite process")
|
|
||||||
time.sleep(1)
|
// Use the provider's restartViteServer method if available
|
||||||
except:
|
if (typeof provider.restartViteServer === 'function') {
|
||||||
print("No existing Vite process found")
|
await provider.restartViteServer();
|
||||||
|
console.log('[restart-vite] Vite restarted via provider method');
|
||||||
os.chdir('/home/user/app')
|
} else {
|
||||||
|
// Fallback to manual restart using provider's runCommand
|
||||||
# Clear error file
|
console.log('[restart-vite] Fallback to manual Vite restart...');
|
||||||
error_file = '/tmp/vite-errors.json'
|
|
||||||
with open(error_file, 'w') as f:
|
// Kill existing Vite processes
|
||||||
json.dump({"errors": [], "lastChecked": time.time()}, f)
|
try {
|
||||||
|
await provider.runCommand('pkill -f vite');
|
||||||
# Function to monitor Vite output for errors
|
console.log('[restart-vite] Killed existing Vite processes');
|
||||||
def monitor_output(proc, error_file):
|
|
||||||
while True:
|
|
||||||
line = proc.stderr.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
|
|
||||||
sys.stdout.write(line) # Also print to console
|
// Wait a moment for processes to terminate
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
# Check for import resolution errors
|
} catch {
|
||||||
if "Failed to resolve import" in line:
|
console.log('[restart-vite] No existing Vite processes found');
|
||||||
try:
|
}
|
||||||
# Extract package name from error
|
|
||||||
import_match = line.find('"')
|
// Clear any error tracking files
|
||||||
if import_match != -1:
|
try {
|
||||||
end_match = line.find('"', import_match + 1)
|
await provider.runCommand('bash -c "echo \'{\\"errors\\": [], \\"lastChecked\\": '+ Date.now() +'}\' > /tmp/vite-errors.json"');
|
||||||
if end_match != -1:
|
} catch {
|
||||||
package_name = line[import_match + 1:end_match]
|
// Ignore if this fails
|
||||||
# Skip relative imports
|
}
|
||||||
if not package_name.startswith('.'):
|
|
||||||
with open(error_file, 'r') as f:
|
// Start Vite dev server in background
|
||||||
data = json.load(f)
|
await provider.runCommand('sh -c "nohup npm run dev > /tmp/vite.log 2>&1 &"');
|
||||||
|
console.log('[restart-vite] Vite dev server restarted');
|
||||||
# Handle scoped packages correctly
|
|
||||||
if package_name.startswith('@'):
|
// Wait for Vite to start up
|
||||||
# For @scope/package, keep the scope
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
pkg_parts = package_name.split('/')
|
}
|
||||||
if len(pkg_parts) >= 2:
|
|
||||||
final_package = '/'.join(pkg_parts[:2])
|
// Update global state
|
||||||
else:
|
global.lastViteRestartTime = Date.now();
|
||||||
final_package = package_name
|
global.viteRestartInProgress = false;
|
||||||
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")
|
|
||||||
`);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Vite restarted successfully',
|
message: 'Vite restarted successfully'
|
||||||
output: result.output
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[restart-vite] Error:', error);
|
console.error('[restart-vite] Error:', error);
|
||||||
|
|
||||||
|
// Clear the restart flag on error
|
||||||
|
global.viteRestartInProgress = false;
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: (error as Error).message
|
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 { 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)
|
// Get active sandbox from global state (in production, use a proper state management solution)
|
||||||
declare global {
|
declare global {
|
||||||
@@ -26,30 +25,32 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.log(`[run-command] Executing: ${command}`);
|
console.log(`[run-command] Executing: ${command}`);
|
||||||
|
|
||||||
const result = await global.activeSandbox.runCode(`
|
// Parse command and arguments
|
||||||
import subprocess
|
const commandParts = command.trim().split(/\s+/);
|
||||||
import os
|
const cmd = commandParts[0];
|
||||||
|
const args = commandParts.slice(1);
|
||||||
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}")
|
|
||||||
`);
|
|
||||||
|
|
||||||
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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
output,
|
output,
|
||||||
message: 'Command executed successfully'
|
exitCode: result.exitCode,
|
||||||
|
message: result.exitCode === 0 ? 'Command executed successfully' : 'Command completed with non-zero exit code'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandbox: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
if (!global.activeSandbox) {
|
if (!global.activeSandbox) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -15,55 +15,70 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
console.log('[sandbox-logs] Fetching Vite dev server logs...');
|
console.log('[sandbox-logs] Fetching Vite dev server logs...');
|
||||||
|
|
||||||
// Get the last N lines of the Vite dev server output
|
// Check if Vite processes are running
|
||||||
const result = await global.activeSandbox.runCode(`
|
const psResult = await global.activeSandbox.runCommand({
|
||||||
import subprocess
|
cmd: 'ps',
|
||||||
import os
|
args: ['aux']
|
||||||
|
});
|
||||||
# Try to get the Vite process output
|
|
||||||
try:
|
|
||||||
# Read the last 100 lines of any log files
|
|
||||||
log_content = []
|
|
||||||
|
|
||||||
# Check if there are any node processes running
|
let viteRunning = false;
|
||||||
ps_result = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
|
const logContent: string[] = [];
|
||||||
vite_processes = [line for line in ps_result.stdout.split('\\n') if 'vite' in line.lower()]
|
|
||||||
|
|
||||||
if vite_processes:
|
if (psResult.exitCode === 0) {
|
||||||
log_content.append("Vite is running")
|
const psOutput = await psResult.stdout();
|
||||||
else:
|
const viteProcesses = psOutput.split('\n').filter((line: string) =>
|
||||||
log_content.append("Vite process not found")
|
line.toLowerCase().includes('vite') ||
|
||||||
|
line.toLowerCase().includes('npm run dev')
|
||||||
# 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({
|
viteRunning = viteProcesses.length > 0;
|
||||||
"hasErrors": False,
|
|
||||||
"logs": log_content,
|
if (viteRunning) {
|
||||||
"status": "running" if vite_processes else "stopped"
|
logContent.push("Vite is running");
|
||||||
}))
|
logContent.push(...viteProcesses.slice(0, 3)); // Show first 3 processes
|
||||||
except Exception as e:
|
} else {
|
||||||
print(json.dumps({
|
logContent.push("Vite process not found");
|
||||||
"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'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
console.error('[sandbox-logs] Error:', error);
|
console.error('[sandbox-logs] Error:', error);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandboxProvider: any;
|
||||||
var sandboxData: any;
|
var sandboxData: any;
|
||||||
var existingFiles: Set<string>;
|
var existingFiles: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// Check if sandbox exists
|
// Check sandbox manager first, then fall back to global state
|
||||||
const sandboxExists = !!global.activeSandbox;
|
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
|
||||||
|
const sandboxExists = !!provider;
|
||||||
|
|
||||||
let sandboxHealthy = false;
|
let sandboxHealthy = false;
|
||||||
let sandboxInfo = null;
|
let sandboxInfo = null;
|
||||||
|
|
||||||
if (sandboxExists && global.activeSandbox) {
|
if (sandboxExists && provider) {
|
||||||
try {
|
try {
|
||||||
// Since Python isn't available in the Vite template, just check if sandbox exists
|
// Check if sandbox is healthy by getting its info
|
||||||
// The sandbox object existing is enough to confirm it's healthy
|
const providerInfo = provider.getSandboxInfo();
|
||||||
sandboxHealthy = true;
|
sandboxHealthy = !!providerInfo;
|
||||||
|
|
||||||
sandboxInfo = {
|
sandboxInfo = {
|
||||||
sandboxId: global.sandboxData?.sandboxId,
|
sandboxId: providerInfo?.sandboxId || global.sandboxData?.sandboxId,
|
||||||
url: global.sandboxData?.url,
|
url: providerInfo?.url || global.sandboxData?.url,
|
||||||
filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [],
|
filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [],
|
||||||
lastHealthCheck: new Date().toISOString()
|
lastHealthCheck: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import FirecrawlApp from '@mendable/firecrawl-js';
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -8,49 +9,73 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'URL is required' }, { status: 400 });
|
return NextResponse.json({ error: 'URL is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use Firecrawl API to capture screenshot
|
// Initialize Firecrawl with API key from environment
|
||||||
const firecrawlResponse = await fetch('https://api.firecrawl.dev/v1/scrape', {
|
const apiKey = process.env.FIRECRAWL_API_KEY;
|
||||||
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();
|
|
||||||
|
|
||||||
if (!data.success || !data.data?.screenshot) {
|
if (!apiKey) {
|
||||||
throw new Error('Failed to capture screenshot');
|
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({
|
console.log('[scrape-screenshot] Attempting to capture screenshot for:', url);
|
||||||
success: true,
|
console.log('[scrape-screenshot] Using Firecrawl API key:', apiKey ? 'Present' : 'Missing');
|
||||||
screenshot: data.data.screenshot,
|
|
||||||
metadata: data.data.metadata
|
// 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) {
|
} 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({
|
return NextResponse.json({
|
||||||
error: error.message || 'Failed to capture screenshot'
|
error: error.message || 'Failed to capture screenshot'
|
||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ export async function POST(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url,
|
url,
|
||||||
formats: ['markdown', 'html'],
|
formats: ['markdown', 'html', 'screenshot'],
|
||||||
waitFor: 3000,
|
waitFor: 3000,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
blockAds: true,
|
blockAds: true,
|
||||||
@@ -52,6 +52,10 @@ export async function POST(request: NextRequest) {
|
|||||||
{
|
{
|
||||||
type: 'wait',
|
type: 'wait',
|
||||||
milliseconds: 2000
|
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');
|
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
|
// Sanitize the markdown content
|
||||||
const sanitizedMarkdown = sanitizeQuotes(markdown || '');
|
const sanitizedMarkdown = sanitizeQuotes(markdown || '');
|
||||||
@@ -91,11 +99,13 @@ ${sanitizedMarkdown}
|
|||||||
success: true,
|
success: true,
|
||||||
url,
|
url,
|
||||||
content: formattedContent,
|
content: formattedContent,
|
||||||
|
screenshot: screenshotUrl,
|
||||||
structured: {
|
structured: {
|
||||||
title: sanitizeQuotes(title),
|
title: sanitizeQuotes(title),
|
||||||
description: sanitizeQuotes(description),
|
description: sanitizeQuotes(description),
|
||||||
content: sanitizedMarkdown,
|
content: sanitizedMarkdown,
|
||||||
url
|
url,
|
||||||
|
screenshot: screenshotUrl
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
scraper: 'firecrawl-enhanced',
|
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";
|
@import "../styles/main.css";
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 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 "./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 = {
|
export const metadata: Metadata = {
|
||||||
title: "Open Lovable",
|
title: "Open Lovable v2",
|
||||||
description: "Re-imagine any website in seconds with AI-powered website builder.",
|
description: "Re-imagine any website in seconds with AI-powered website builder.",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,16 +36,9 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className={`${inter.variable} ${geistSans.variable} ${geistMono.variable} ${robotoMono.variable} font-sans`}>
|
||||||
<ThemeProvider
|
{children}
|
||||||
attribute="class"
|
|
||||||
defaultTheme="light"
|
|
||||||
enableSystem
|
|
||||||
disableTransitionOnChange
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ThemeProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+731
-3351
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
|||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export const isMobileSheetOpenAtom = atom(false);
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"heat-4": {
|
||||||
|
"hex": "fa5d190a",
|
||||||
|
"p3": "0.980392 0.364706 0.098039 / 0.039216"
|
||||||
|
},
|
||||||
|
"heat-8": {
|
||||||
|
"hex": "fa5d1914",
|
||||||
|
"p3": "0.980392 0.364706 0.098039 / 0.078431"
|
||||||
|
},
|
||||||
|
"heat-12": {
|
||||||
|
"hex": "fa5d191f",
|
||||||
|
"p3": "0.980392 0.364706 0.098039 / 0.121569"
|
||||||
|
},
|
||||||
|
"heat-16": {
|
||||||
|
"hex": "fa5d1929",
|
||||||
|
"p3": "0.980392 0.364706 0.098039 / 0.160784"
|
||||||
|
},
|
||||||
|
"heat-20": {
|
||||||
|
"hex": "fa5d1933",
|
||||||
|
"p3": "0.980392 0.364706 0.098039 / 0.200000"
|
||||||
|
},
|
||||||
|
"heat-40": {
|
||||||
|
"hex": "fa5d1966",
|
||||||
|
"p3": "0.980392 0.364706 0.098039 / 0.400000"
|
||||||
|
},
|
||||||
|
"heat-90": {
|
||||||
|
"hex": "fa5d19e6",
|
||||||
|
"p3": "0.980392 0.364706 0.098039 / 0.900000"
|
||||||
|
},
|
||||||
|
"heat-100": {
|
||||||
|
"hex": "fa5d19ff",
|
||||||
|
"p3": "0.980392 0.364706 0.098039 / 1.000000"
|
||||||
|
},
|
||||||
|
"accent-black": {
|
||||||
|
"hex": "262626ff",
|
||||||
|
"p3": "0.149020 0.149020 0.149020 / 1.000000"
|
||||||
|
},
|
||||||
|
"accent-white": {
|
||||||
|
"hex": "ffffffff",
|
||||||
|
"p3": "1.000000 1.000000 1.000000 / 1.000000"
|
||||||
|
},
|
||||||
|
"accent-amethyst": {
|
||||||
|
"hex": "9061ffff",
|
||||||
|
"p3": "0.564706 0.380392 1.000000 / 1.000000"
|
||||||
|
},
|
||||||
|
"accent-bluetron": {
|
||||||
|
"hex": "2a6dfbff",
|
||||||
|
"p3": "0.164706 0.427451 0.984314 / 1.000000"
|
||||||
|
},
|
||||||
|
"accent-crimson": {
|
||||||
|
"hex": "eb3424ff",
|
||||||
|
"p3": "0.921569 0.203922 0.141176 / 1.000000"
|
||||||
|
},
|
||||||
|
"accent-forest": {
|
||||||
|
"hex": "42c366ff",
|
||||||
|
"p3": "0.258824 0.764706 0.400000 / 1.000000"
|
||||||
|
},
|
||||||
|
"accent-honey": {
|
||||||
|
"hex": "ecb730ff",
|
||||||
|
"p3": "0.925490 0.717647 0.188235 / 1.000000"
|
||||||
|
},
|
||||||
|
"black-alpha-1": {
|
||||||
|
"hex": "00000003",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.011765"
|
||||||
|
},
|
||||||
|
"black-alpha-2": {
|
||||||
|
"hex": "00000005",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.019608"
|
||||||
|
},
|
||||||
|
"black-alpha-3": {
|
||||||
|
"hex": "00000008",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.031373"
|
||||||
|
},
|
||||||
|
"black-alpha-4": {
|
||||||
|
"hex": "0000000a",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.039216"
|
||||||
|
},
|
||||||
|
"black-alpha-5": {
|
||||||
|
"hex": "0000000d",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.050980"
|
||||||
|
},
|
||||||
|
"black-alpha-6": {
|
||||||
|
"hex": "0000000f",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.058824"
|
||||||
|
},
|
||||||
|
"black-alpha-7": {
|
||||||
|
"hex": "00000012",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.070588"
|
||||||
|
},
|
||||||
|
"black-alpha-8": {
|
||||||
|
"hex": "00000014",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.078431"
|
||||||
|
},
|
||||||
|
"black-alpha-10": {
|
||||||
|
"hex": "0000001a",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.101961"
|
||||||
|
},
|
||||||
|
"black-alpha-12": {
|
||||||
|
"hex": "0000001f",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.121569"
|
||||||
|
},
|
||||||
|
"black-alpha-16": {
|
||||||
|
"hex": "00000029",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.160784"
|
||||||
|
},
|
||||||
|
"black-alpha-20": {
|
||||||
|
"hex": "00000033",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.200000"
|
||||||
|
},
|
||||||
|
"black-alpha-24": {
|
||||||
|
"hex": "0000003d",
|
||||||
|
"p3": "0.000000 0.000000 0.000000 / 0.239216"
|
||||||
|
},
|
||||||
|
"black-alpha-32": {
|
||||||
|
"hex": "26262652",
|
||||||
|
"p3": "0.149020 0.149020 0.149020 / 0.321569"
|
||||||
|
},
|
||||||
|
"black-alpha-40": {
|
||||||
|
"hex": "26262666",
|
||||||
|
"p3": "0.149020 0.149020 0.149020 / 0.400000"
|
||||||
|
},
|
||||||
|
"black-alpha-48": {
|
||||||
|
"hex": "2626267a",
|
||||||
|
"p3": "0.149020 0.149020 0.149020 / 0.478431"
|
||||||
|
},
|
||||||
|
"black-alpha-56": {
|
||||||
|
"hex": "2626268f",
|
||||||
|
"p3": "0.149020 0.149020 0.149020 / 0.560784"
|
||||||
|
},
|
||||||
|
"black-alpha-64": {
|
||||||
|
"hex": "262626a3",
|
||||||
|
"p3": "0.149020 0.149020 0.149020 / 0.639216"
|
||||||
|
},
|
||||||
|
"black-alpha-72": {
|
||||||
|
"hex": "262626b8",
|
||||||
|
"p3": "0.149020 0.149020 0.149020 / 0.721569"
|
||||||
|
},
|
||||||
|
"black-alpha-88": {
|
||||||
|
"hex": "262626e0",
|
||||||
|
"p3": "0.149020 0.149020 0.149020 / 0.878431"
|
||||||
|
},
|
||||||
|
"white-alpha-56": {
|
||||||
|
"hex": "ffffff8f",
|
||||||
|
"p3": "1.000000 1.000000 1.000000 / 0.560784"
|
||||||
|
},
|
||||||
|
"white-alpha-72": {
|
||||||
|
"hex": "ffffffb8",
|
||||||
|
"p3": "1.000000 1.000000 1.000000 / 0.721569"
|
||||||
|
},
|
||||||
|
"border-faint": {
|
||||||
|
"hex": "edededff",
|
||||||
|
"p3": "0.929412 0.929412 0.929412 / 1.000000"
|
||||||
|
},
|
||||||
|
"border-muted": {
|
||||||
|
"hex": "e8e8e8ff",
|
||||||
|
"p3": "0.909804 0.909804 0.909804 / 1.000000"
|
||||||
|
},
|
||||||
|
"border-loud": {
|
||||||
|
"hex": "e6e6e6ff",
|
||||||
|
"p3": "0.901961 0.901961 0.901961 / 1.000000"
|
||||||
|
},
|
||||||
|
"illustrations-faint": {
|
||||||
|
"hex": "edededff",
|
||||||
|
"p3": "0.929412 0.929412 0.929412 / 1.000000"
|
||||||
|
},
|
||||||
|
"illustrations-muted": {
|
||||||
|
"hex": "e6e6e6ff",
|
||||||
|
"p3": "0.901961 0.901961 0.901961 / 1.000000"
|
||||||
|
},
|
||||||
|
"illustrations-default": {
|
||||||
|
"hex": "dbdbdbff",
|
||||||
|
"p3": "0.858824 0.858824 0.858824 / 1.000000"
|
||||||
|
},
|
||||||
|
"background-lighter": {
|
||||||
|
"hex": "fbfbfbff",
|
||||||
|
"p3": "0.984314 0.984314 0.984314 / 1.000000"
|
||||||
|
},
|
||||||
|
"background-base": {
|
||||||
|
"hex": "f9f9f9ff",
|
||||||
|
"p3": "0.976471 0.976471 0.976471 / 1.000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default function FirecrawlIcon({ className = "w-5 h-5" }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M13.7605 6.61389C13.138 6.79867 12.6687 7.21667 12.3251 7.67073C12.2513 7.76819 12.0975 7.69495 12.1268 7.57552C12.7848 4.86978 11.9155 2.6209 9.20582 1.51393C9.06836 1.4576 8.92527 1.58097 8.96132 1.72519C10.1939 6.67417 5.00941 6.25673 5.66459 11.8671C5.67585 11.9634 5.56769 12.0293 5.48882 11.973C5.2432 11.7967 4.96885 11.4288 4.78069 11.1702C4.72548 11.0942 4.60605 11.1156 4.5807 11.2063C4.43085 11.7482 4.35986 12.2586 4.35986 12.7656C4.35986 14.7373 5.37333 16.473 6.90734 17.4791C6.99522 17.5366 7.10789 17.4543 7.07804 17.3535C6.99917 17.0887 6.95466 16.8093 6.95128 16.5203C6.95128 16.3429 6.96255 16.1615 6.99015 15.9925C7.05438 15.5677 7.20197 15.1632 7.44985 14.7948C8.29995 13.5188 10.0041 12.2862 9.73199 10.6125C9.71453 10.5066 9.83959 10.4368 9.91846 10.5094C11.119 11.6063 11.3567 13.0817 11.1595 14.405C11.1426 14.5199 11.2868 14.5813 11.3595 14.4912C11.5432 14.2613 11.7674 14.0596 12.0113 13.9081C12.0722 13.8703 12.1533 13.8991 12.1764 13.9667C12.3121 14.3616 12.5138 14.7323 12.7042 15.1029C12.9318 15.5485 13.0529 16.0573 13.0338 16.5958C13.0242 16.8578 12.9808 17.1113 12.9082 17.3524C12.8772 17.4543 12.9887 17.5394 13.0783 17.4808C14.6134 16.4747 15.6275 14.739 15.6275 12.7662C15.6275 12.0806 15.5075 11.4085 15.2804 10.7787C14.8044 9.45766 13.5966 8.46561 13.9019 6.74403C13.9166 6.66178 13.8405 6.59023 13.7605 6.61389Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
export default function FirecrawlLogo() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="15"
|
||||||
|
viewBox="0 0 79 15"
|
||||||
|
width="79"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0.599609 14.4311V0.576888H9.45474V2.59564H2.87778V6.61335H8.30575V8.57272H2.87778V14.4311H0.599609Z"
|
||||||
|
fill="#262626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.9737 2.87272C11.2407 2.87272 10.6663 2.33835 10.6663 1.58626C10.6663 0.83418 11.2407 0.299805 11.9737 0.299805C12.7067 0.299805 13.2812 0.83418 13.2812 1.58626C13.2812 2.33835 12.7067 2.87272 11.9737 2.87272ZM10.8842 14.4311V4.29772H13.0237V14.4311H10.8842Z"
|
||||||
|
fill="#262626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M20.1527 4.29772H20.6281V6.29668H19.6772C17.7755 6.29668 17.1613 7.78105 17.1613 9.3446V14.4311H15.0219V4.29772H16.9236L17.1613 5.82168C17.6764 4.97064 18.4886 4.29772 20.1527 4.29772Z"
|
||||||
|
fill="#262626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M26.1788 14.5498C22.9894 14.5498 20.9886 12.4915 20.9886 9.38418C20.9886 6.2571 22.9894 4.17897 25.9807 4.17897C28.9126 4.17897 30.8738 6.03939 30.9333 9.00814C30.9333 9.26543 30.9135 9.54251 30.8738 9.8196H23.2271V9.95814C23.2865 11.68 24.3761 12.8081 26.06 12.8081C27.3674 12.8081 28.3183 12.155 28.6155 11.0269H30.755C30.3984 13.0258 28.6947 14.5498 26.1788 14.5498ZM23.3064 8.25605H28.7145C28.5362 6.75189 27.4863 5.90085 26.0005 5.90085C24.6336 5.90085 23.4648 6.81126 23.3064 8.25605Z"
|
||||||
|
fill="#262626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M37.2193 14.5498C34.1487 14.5498 32.1875 12.5508 32.1875 9.38418C32.1875 6.2571 34.2081 4.17897 37.2787 4.17897C39.8936 4.17897 41.5181 5.62376 41.9341 7.9196H39.6955C39.4182 6.7321 38.5664 5.9998 37.2391 5.9998C35.5156 5.9998 34.3864 7.38522 34.3864 9.38418C34.3864 11.3633 35.5156 12.729 37.2391 12.729C38.5465 12.729 39.4182 11.9769 39.6757 10.8092H41.9341C41.5379 13.105 39.8144 14.5498 37.2193 14.5498Z"
|
||||||
|
fill="#262626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M48.6034 4.29772H49.0789V6.29668H48.128C46.2262 6.29668 45.6121 7.78105 45.6121 9.3446V14.4311H43.4726V4.29772H45.3744L45.6121 5.82168C46.1272 4.97064 46.9394 4.29772 48.6034 4.29772Z"
|
||||||
|
fill="#262626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M54.3679 4.17897C57.0621 4.17897 58.6073 5.46543 58.6073 7.86022V14.4311H56.7451L56.5668 12.9863C55.8735 13.8967 54.9028 14.5498 53.2981 14.5498C51.0794 14.5498 49.5936 13.4613 49.5936 11.5811C49.5936 9.50293 51.0992 8.33522 53.9519 8.33522H56.4876V7.72168C56.4876 6.59355 55.6754 5.90085 54.2688 5.90085C53.001 5.90085 52.1491 6.4946 51.9907 7.38522H49.8908C50.1087 5.40605 51.8124 4.17897 54.3679 4.17897ZM53.6547 12.8873C55.4376 12.8873 56.4678 11.8383 56.4876 10.2748V9.91855H53.833C52.5057 9.91855 51.7728 10.4133 51.7728 11.4425C51.7728 12.2936 52.4859 12.8873 53.6547 12.8873Z"
|
||||||
|
fill="#262626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M62.7912 14.4311L59.4829 4.29772H61.7413L64.0591 12.0561L66.3768 4.29772H68.3381L70.5568 12.0561L72.9538 4.29772H75.1329L71.7652 14.4311H69.4672L67.3277 7.54355L65.109 14.4311H62.7912Z"
|
||||||
|
fill="#262626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M76.1601 14.4311V0.576888H78.2996V14.4311H76.1601Z"
|
||||||
|
fill="#262626"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ export default function HMRErrorDetector({ iframeRef, onErrorDetected }: HMRErro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Cross-origin errors are expected, ignore them
|
// Cross-origin errors are expected, ignore them
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, KeyboardEvent, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface HeroInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
showSearchFeatures?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isURL(str: string): boolean {
|
||||||
|
// Check if string contains a dot and looks like a URL
|
||||||
|
const urlPattern = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/;
|
||||||
|
return urlPattern.test(str.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HeroInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
placeholder = "Describe what you want to build...",
|
||||||
|
className = "",
|
||||||
|
showSearchFeatures = true
|
||||||
|
}: HeroInputProps) {
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const [showTiles, setShowTiles] = useState(false);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const isURLInput = showSearchFeatures ? isURL(value) : false;
|
||||||
|
|
||||||
|
// Reset textarea height when value changes (especially when cleared)
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.style.height = 'auto';
|
||||||
|
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show tiles animation for search terms (only if search features are enabled)
|
||||||
|
if (showSearchFeatures && value.trim() && !isURL(value) && isFocused) {
|
||||||
|
setShowTiles(true);
|
||||||
|
} else {
|
||||||
|
setShowTiles(false);
|
||||||
|
}
|
||||||
|
}, [value, isFocused, showSearchFeatures]);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`max-w-552 mx-auto w-full relative z-[11] rounded-20 ${className}`}>
|
||||||
|
<div
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<label className="p-16 flex gap-8 items-start w-full relative border-b border-black-alpha-5">
|
||||||
|
<div className="mt-2 flex-shrink-0">
|
||||||
|
{showSearchFeatures ? (
|
||||||
|
isURLInput ? (
|
||||||
|
// Link icon for URLs
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="opacity-40"
|
||||||
|
>
|
||||||
|
<path d="M9 11L11 9M11 9L15 5M11 9L5 15M15 5L13 3M15 5L17 7" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M7 13L5 15L3 13" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M13 7L15 5L17 7" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
// Search icon for search terms
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="opacity-40"
|
||||||
|
>
|
||||||
|
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" strokeWidth="1.5"/>
|
||||||
|
<path d="M12.5 12.5L16.5 16.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
// Default globe icon for generation page
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="opacity-40"
|
||||||
|
>
|
||||||
|
<circle cx="10" cy="10" r="9.5" stroke="currentColor"/>
|
||||||
|
<path d="M10 2C10 5.5 10 14.5 10 18" stroke="currentColor" strokeLinecap="round"/>
|
||||||
|
<path d="M2 10C5.5 10 14.5 10 18 10" stroke="currentColor" strokeLinecap="round"/>
|
||||||
|
<ellipse cx="10" cy="10" rx="3.5" ry="9.5" stroke="currentColor"/>
|
||||||
|
<ellipse cx="10" cy="10" rx="6" ry="9.5" stroke="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
className="w-full bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48 resize-none outline-none min-h-[24px] leading-6"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={() => setIsFocused(false)}
|
||||||
|
rows={1}
|
||||||
|
style={{
|
||||||
|
height: 'auto',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
onInput={(e) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
target.style.height = 'auto';
|
||||||
|
target.style.height = target.scrollHeight + 'px';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="p-10 flex justify-end items-center relative">
|
||||||
|
<button
|
||||||
|
onClick={onSubmit}
|
||||||
|
disabled={!value.trim()}
|
||||||
|
className={`
|
||||||
|
button relative rounded-10 px-8 py-8 text-label-medium font-medium
|
||||||
|
flex items-center justify-center gap-6
|
||||||
|
${value.trim()
|
||||||
|
? 'button-primary text-accent-white active:scale-[0.995]'
|
||||||
|
: 'bg-black-alpha-4 text-black-alpha-24 cursor-not-allowed'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{value.trim() && <div className="button-background absolute inset-0 rounded-10 pointer-events-none" />}
|
||||||
|
{value.trim() ? (
|
||||||
|
<>
|
||||||
|
<span className="px-6 relative">Re-imagine Site</span>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 8H13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M8.5 3.5L13 8L8.5 12.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="w-60 flex items-center justify-center">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 8H13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M8.5 3.5L13 8L8.5 12.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Animated tiles for search results */}
|
||||||
|
{showTiles && (
|
||||||
|
<div className="mt-16 grid grid-cols-3 gap-12 px-16">
|
||||||
|
{[0, 1, 2].map((index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="tile-animation relative aspect-[4/3] bg-black-alpha-4 rounded-12 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
animationDelay: `${index * 100}ms`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-black-alpha-4 to-transparent" />
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-8 h-8 bg-black-alpha-8 rounded-full animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
@keyframes tileSlideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-animation {
|
||||||
|
animation: tileSlideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,32 +1,24 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { Loader2, ExternalLink, RefreshCw, Terminal } from 'lucide-react';
|
import { Loader2, ExternalLink, RefreshCw, Terminal } from 'lucide-react';
|
||||||
|
|
||||||
interface SandboxPreviewProps {
|
interface SandboxPreviewProps {
|
||||||
sandboxId: string;
|
|
||||||
port: number;
|
|
||||||
type: 'vite' | 'nextjs' | 'console';
|
type: 'vite' | 'nextjs' | 'console';
|
||||||
output?: string;
|
output?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
sandboxUrl?: string; // Real URL from Vercel Sandbox API
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SandboxPreview({
|
export default function SandboxPreview({
|
||||||
sandboxId,
|
|
||||||
port,
|
|
||||||
type,
|
type,
|
||||||
output,
|
output,
|
||||||
isLoading = false
|
isLoading = false,
|
||||||
|
sandboxUrl
|
||||||
}: SandboxPreviewProps) {
|
}: SandboxPreviewProps) {
|
||||||
const [previewUrl, setPreviewUrl] = useState<string>('');
|
|
||||||
const [showConsole, setShowConsole] = useState(false);
|
const [showConsole, setShowConsole] = useState(false);
|
||||||
const [iframeKey, setIframeKey] = useState(0);
|
const [iframeKey, setIframeKey] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
// Use the real sandbox URL passed from the API
|
||||||
if (sandboxId && type !== 'console') {
|
const previewUrl = sandboxUrl || '';
|
||||||
// In production, this would be the actual E2B sandbox URL
|
|
||||||
// Format: https://{sandboxId}-{port}.e2b.dev
|
|
||||||
setPreviewUrl(`https://${sandboxId}-${port}.e2b.dev`);
|
|
||||||
}
|
|
||||||
}, [sandboxId, port, type]);
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setIframeKey(prev => prev + 1);
|
setIframeKey(prev => prev + 1);
|
||||||
@@ -50,9 +42,13 @@ export default function SandboxPreview({
|
|||||||
<span className="text-sm text-gray-400">
|
<span className="text-sm text-gray-400">
|
||||||
{type === 'vite' ? '⚡ Vite' : '▲ Next.js'} Preview
|
{type === 'vite' ? '⚡ Vite' : '▲ Next.js'} Preview
|
||||||
</span>
|
</span>
|
||||||
<code className="text-xs bg-gray-900 px-2 py-1 rounded text-blue-400">
|
{previewUrl ? (
|
||||||
{previewUrl}
|
<code className="text-xs bg-gray-900 px-2 py-1 rounded text-blue-400">
|
||||||
</code>
|
{previewUrl}
|
||||||
|
</code>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-500">Waiting for sandbox URL...</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -69,38 +65,47 @@ export default function SandboxPreview({
|
|||||||
>
|
>
|
||||||
<RefreshCw className="w-4 h-4" />
|
<RefreshCw className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<a
|
{previewUrl && (
|
||||||
href={previewUrl}
|
<a
|
||||||
target="_blank"
|
href={previewUrl}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="p-2 hover:bg-gray-700 rounded transition-colors"
|
rel="noopener noreferrer"
|
||||||
title="Open in new tab"
|
className="p-2 hover:bg-gray-700 rounded transition-colors"
|
||||||
>
|
title="Open in new tab"
|
||||||
<ExternalLink className="w-4 h-4" />
|
>
|
||||||
</a>
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Preview */}
|
{/* Main Preview */}
|
||||||
<div className="relative bg-gray-900 rounded-lg overflow-hidden border border-gray-700">
|
<div className="relative bg-gray-900 rounded-lg overflow-hidden border border-gray-700">
|
||||||
{isLoading && (
|
{(isLoading || !previewUrl) && (
|
||||||
<div className="absolute inset-0 bg-gray-900/80 flex items-center justify-center z-10">
|
<div className="absolute inset-0 bg-gray-900/80 flex items-center justify-center z-10">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2" />
|
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2" />
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-sm text-gray-400">
|
||||||
{type === 'vite' ? 'Starting Vite dev server...' : 'Starting Next.js dev server...'}
|
{!previewUrl
|
||||||
|
? 'Setting up sandbox environment...'
|
||||||
|
: type === 'vite'
|
||||||
|
? 'Starting Vite dev server...'
|
||||||
|
: 'Starting Next.js dev server...'
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<iframe
|
{previewUrl && (
|
||||||
key={iframeKey}
|
<iframe
|
||||||
src={previewUrl}
|
key={iframeKey}
|
||||||
className="w-full h-[600px] bg-white"
|
src={previewUrl}
|
||||||
title={`${type} preview`}
|
className="w-full h-[600px] bg-white"
|
||||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
title={`${type} preview`}
|
||||||
/>
|
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Console Output (Toggle) */}
|
{/* Console Output (Toggle) */}
|
||||||
|
|||||||
@@ -0,0 +1,846 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
Globe,
|
||||||
|
FileText,
|
||||||
|
Code,
|
||||||
|
Shield,
|
||||||
|
// Search, // Not used in current implementation
|
||||||
|
Zap,
|
||||||
|
Database,
|
||||||
|
// Lock, // Not used in current implementation
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Loader2,
|
||||||
|
AlertCircle,
|
||||||
|
Bot,
|
||||||
|
Sparkles,
|
||||||
|
FileCode,
|
||||||
|
Network,
|
||||||
|
Info,
|
||||||
|
Eye
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import ScoreChart from "./ScoreChart";
|
||||||
|
import RadarChart from "./RadarChart";
|
||||||
|
import MetricBars from "./MetricBars";
|
||||||
|
|
||||||
|
interface ControlPanelProps {
|
||||||
|
isAnalyzing: boolean;
|
||||||
|
showResults: boolean;
|
||||||
|
url: string;
|
||||||
|
analysisData?: any;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
icon: any;
|
||||||
|
status: 'pending' | 'checking' | 'pass' | 'fail' | 'warning';
|
||||||
|
score?: number;
|
||||||
|
details?: string;
|
||||||
|
recommendation?: string;
|
||||||
|
actionItems?: string[];
|
||||||
|
tooltip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ControlPanel({
|
||||||
|
isAnalyzing,
|
||||||
|
showResults,
|
||||||
|
url,
|
||||||
|
analysisData,
|
||||||
|
onReset,
|
||||||
|
}: ControlPanelProps) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [showAIAnalysis, setShowAIAnalysis] = useState(false); // Reserved for AI analysis feature
|
||||||
|
const [aiInsights, setAiInsights] = useState<CheckItem[]>([]);
|
||||||
|
const [isAnalyzingAI, setIsAnalyzingAI] = useState(false);
|
||||||
|
const [combinedChecks, setCombinedChecks] = useState<CheckItem[]>([]);
|
||||||
|
const [checks, setChecks] = useState<CheckItem[]>([
|
||||||
|
{
|
||||||
|
id: 'heading-structure',
|
||||||
|
label: 'Heading Hierarchy',
|
||||||
|
description: 'H1-H6 structure',
|
||||||
|
icon: FileText,
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'readability',
|
||||||
|
label: 'Readability',
|
||||||
|
description: 'Content clarity',
|
||||||
|
icon: Globe,
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meta-tags',
|
||||||
|
label: 'Metadata Quality',
|
||||||
|
description: 'Title, desc, author',
|
||||||
|
icon: FileCode,
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'semantic-html',
|
||||||
|
label: 'Semantic HTML',
|
||||||
|
description: 'Proper HTML5 tags',
|
||||||
|
icon: Code,
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'accessibility',
|
||||||
|
label: 'Accessibility',
|
||||||
|
description: 'Alt text & ARIA',
|
||||||
|
icon: Eye,
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'llms-txt',
|
||||||
|
label: 'LLMs.txt',
|
||||||
|
description: 'AI permissions',
|
||||||
|
icon: Bot,
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'robots-txt',
|
||||||
|
label: 'Robots.txt',
|
||||||
|
description: 'Crawler rules',
|
||||||
|
icon: Shield,
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sitemap',
|
||||||
|
label: 'Sitemap',
|
||||||
|
description: 'Site structure',
|
||||||
|
icon: Network,
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [overallScore, setOverallScore] = useState(0);
|
||||||
|
const [currentCheckIndex, setCurrentCheckIndex] = useState(-1);
|
||||||
|
const [selectedCheck, setSelectedCheck] = useState<string | null>(null);
|
||||||
|
const [hoveredCheck, setHoveredCheck] = useState<string | null>(null);
|
||||||
|
const [enhancedScore, setEnhancedScore] = useState(0);
|
||||||
|
const [viewMode, setViewMode] = useState<'grid' | 'chart' | 'bars'>('grid');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (analysisData && analysisData.checks && showResults) {
|
||||||
|
// Use real data from API
|
||||||
|
const mappedChecks = analysisData.checks.map((check: any) => ({
|
||||||
|
...check,
|
||||||
|
icon: checks.find(c => c.id === check.id)?.icon || FileText,
|
||||||
|
description: check.details || checks.find(c => c.id === check.id)?.description,
|
||||||
|
}));
|
||||||
|
setChecks(mappedChecks);
|
||||||
|
setCombinedChecks(mappedChecks); // Initialize with basic checks
|
||||||
|
setOverallScore(analysisData.overallScore || 0);
|
||||||
|
setCurrentCheckIndex(-1);
|
||||||
|
|
||||||
|
// If AI analysis should auto-start, handle the promise
|
||||||
|
if (analysisData.autoStartAI && analysisData.aiAnalysisPromise) {
|
||||||
|
console.log('Auto-starting AI analysis with promise');
|
||||||
|
setIsAnalyzingAI(true);
|
||||||
|
setShowAIAnalysis(true);
|
||||||
|
|
||||||
|
// Add placeholder AI tiles immediately with actual titles
|
||||||
|
const placeholderAIChecks = [
|
||||||
|
{
|
||||||
|
id: 'ai-loading-0',
|
||||||
|
label: 'Content Quality for AI',
|
||||||
|
description: 'Analyzing content signal ratio...',
|
||||||
|
icon: Sparkles,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-1',
|
||||||
|
label: 'Information Architecture',
|
||||||
|
description: 'Evaluating page structure...',
|
||||||
|
icon: Bot,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-2',
|
||||||
|
label: 'Crawlability Patterns',
|
||||||
|
description: 'Checking JavaScript usage...',
|
||||||
|
icon: Database,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-3',
|
||||||
|
label: 'AI Training Value',
|
||||||
|
description: 'Assessing training potential...',
|
||||||
|
icon: Network,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-4',
|
||||||
|
label: 'Knowledge Extraction',
|
||||||
|
description: 'Analyzing entity definitions...',
|
||||||
|
icon: FileCode,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-5',
|
||||||
|
label: 'Template Quality',
|
||||||
|
description: 'Reviewing semantic structure...',
|
||||||
|
icon: Shield,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-6',
|
||||||
|
label: 'Content Depth',
|
||||||
|
description: 'Measuring content richness...',
|
||||||
|
icon: Zap,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-7',
|
||||||
|
label: 'Machine Readability',
|
||||||
|
description: 'Testing extraction reliability...',
|
||||||
|
icon: Globe,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add loading AI tiles with staggered animation
|
||||||
|
placeholderAIChecks.forEach((check, idx) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setCombinedChecks(prev => [...prev, check]);
|
||||||
|
}, 100 * (idx + 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle the AI analysis promise
|
||||||
|
analysisData.aiAnalysisPromise
|
||||||
|
.then(async (aiResponse: any) => {
|
||||||
|
if (aiResponse) {
|
||||||
|
const data = await aiResponse.json();
|
||||||
|
if (data.success && data.insights) {
|
||||||
|
// Convert AI insights to CheckItem format
|
||||||
|
const aiChecks: CheckItem[] = data.insights.map((insight: any, idx: number) => ({
|
||||||
|
...insight,
|
||||||
|
icon: [Sparkles, Bot, Database, Network, FileCode, Shield, Zap, Globe][idx % 8],
|
||||||
|
description: insight.details?.substring(0, 60) + '...' || 'AI Analysis',
|
||||||
|
isAI: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setAiInsights(aiChecks);
|
||||||
|
|
||||||
|
// Replace loading tiles with real AI tiles
|
||||||
|
setCombinedChecks(prev => {
|
||||||
|
// Remove loading tiles
|
||||||
|
const withoutLoading = prev.filter(c => !(c as any).isLoading);
|
||||||
|
// Add real AI tiles
|
||||||
|
return [...withoutLoading, ...aiChecks];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate enhanced score
|
||||||
|
if (data.insights.length > 0) {
|
||||||
|
const aiScores = data.insights.map((i: any) => i.score || 0);
|
||||||
|
const avgAiScore = aiScores.reduce((a: number, b: number) => a + b, 0) / aiScores.length;
|
||||||
|
const combinedScore = Math.round((overallScore * 0.6) + (avgAiScore * 0.4));
|
||||||
|
setEnhancedScore(combinedScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.error('AI analysis error:', error);
|
||||||
|
// Remove loading tiles on error
|
||||||
|
setCombinedChecks(prev => prev.filter(c => !(c as any).isLoading));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsAnalyzingAI(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (isAnalyzing) {
|
||||||
|
// Reset all checks when starting analysis
|
||||||
|
const resetChecks = checks.map(check => ({ ...check, status: 'pending' as const }));
|
||||||
|
setChecks(resetChecks);
|
||||||
|
setCombinedChecks(resetChecks); // Reset combined checks too
|
||||||
|
setCurrentCheckIndex(0);
|
||||||
|
setOverallScore(0);
|
||||||
|
|
||||||
|
// Visual animation while waiting for real results
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
setCurrentCheckIndex(prev => {
|
||||||
|
if (prev >= checks.length - 1) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return prev + 1;
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return () => clearInterval(checkInterval);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isAnalyzing, showResults, analysisData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentCheckIndex >= 0 && currentCheckIndex < checks.length && isAnalyzing) {
|
||||||
|
// Mark current as checking during animation
|
||||||
|
setChecks(prev => prev.map((check, index) => {
|
||||||
|
if (index === currentCheckIndex) {
|
||||||
|
return { ...check, status: 'checking' };
|
||||||
|
}
|
||||||
|
if (index < currentCheckIndex) {
|
||||||
|
return { ...check, status: 'checking' };
|
||||||
|
}
|
||||||
|
return check;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update combinedChecks to show the animation
|
||||||
|
setCombinedChecks(prev => prev.map((check, index) => {
|
||||||
|
if (index === currentCheckIndex) {
|
||||||
|
return { ...check, status: 'checking' };
|
||||||
|
}
|
||||||
|
if (index < currentCheckIndex) {
|
||||||
|
return { ...check, status: 'checking' };
|
||||||
|
}
|
||||||
|
return check;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [currentCheckIndex, checks.length, isAnalyzing]);
|
||||||
|
|
||||||
|
const getStatusIcon = (status: CheckItem['status']) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'checking':
|
||||||
|
return <Loader2 className="w-16 h-16 text-heat-100 animate-spin" />;
|
||||||
|
case 'pass':
|
||||||
|
return <CheckCircle2 className="w-16 h-16 text-accent-black" />;
|
||||||
|
case 'fail':
|
||||||
|
return <XCircle className="w-16 h-16 text-heat-200" />;
|
||||||
|
case 'warning':
|
||||||
|
return <AlertCircle className="w-16 h-16 text-heat-100" />;
|
||||||
|
default:
|
||||||
|
return <div className="w-16 h-16 rounded-full border border-black-alpha-8" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility function available but not used in current render
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const getScoreColor = (score: number) => {
|
||||||
|
if (score >= 80) return "text-accent-black";
|
||||||
|
if (score >= 60) return "text-accent-black";
|
||||||
|
return "text-accent-black";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="w-full max-w-[1200px] mx-auto"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<motion.div
|
||||||
|
className="text-center mb-48 pt-24 md:pt-0"
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-title-h2 text-accent-black mb-12">AI Readiness Analysis</h2>
|
||||||
|
<p className="text-body-large text-black-alpha-64">Single-page snapshot of {url}</p>
|
||||||
|
|
||||||
|
{showResults && (
|
||||||
|
<>
|
||||||
|
{/* View Mode Toggle - Moved above score */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="mt-24 mb-20 flex justify-center gap-4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('grid')}
|
||||||
|
className={`px-16 py-8 rounded-8 text-label-medium font-medium transition-all ${
|
||||||
|
viewMode === 'grid'
|
||||||
|
? 'bg-accent-black text-white shadow-md'
|
||||||
|
: 'bg-black-alpha-4 text-black-alpha-64 hover:bg-black-alpha-8'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Grid View
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('chart')}
|
||||||
|
className={`px-16 py-8 rounded-8 text-label-medium font-medium transition-all ${
|
||||||
|
viewMode === 'chart'
|
||||||
|
? 'bg-accent-black text-white shadow-md'
|
||||||
|
: 'bg-black-alpha-4 text-black-alpha-64 hover:bg-black-alpha-8'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Radar Chart
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('bars')}
|
||||||
|
className={`px-16 py-8 rounded-8 text-label-medium font-medium transition-all ${
|
||||||
|
viewMode === 'bars'
|
||||||
|
? 'bg-accent-black text-white shadow-md'
|
||||||
|
: 'bg-black-alpha-4 text-black-alpha-64 hover:bg-black-alpha-8'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Bar Chart
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: "spring", delay: 0.5 }}
|
||||||
|
className="flex justify-center"
|
||||||
|
>
|
||||||
|
<ScoreChart
|
||||||
|
score={enhancedScore > 0 ? enhancedScore : overallScore}
|
||||||
|
enhanced={enhancedScore > 0}
|
||||||
|
size={180}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Conditional rendering based on view mode */}
|
||||||
|
{viewMode === 'grid' && (
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-12 mb-40 px-40 relative">
|
||||||
|
{combinedChecks.map((check, index) => {
|
||||||
|
const isActive = index === currentCheckIndex;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={check.id}
|
||||||
|
initial={(check as any).isAI ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
scale: isActive ? 1.05 : 1,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
delay: (check as any).isAI ? 0 : index * 0.1,
|
||||||
|
scale: { type: "spring", stiffness: 300 }
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
relative p-16 rounded-8 transition-all bg-accent-white border
|
||||||
|
${(check as any).isAI ? 'border-heat-100 border-opacity-40 bg-gradient-to-br from-accent-white to-heat-4' : 'border-black-alpha-8'}
|
||||||
|
${isActive ? 'border-heat-100 shadow-lg' : ''}
|
||||||
|
${check.status !== 'pending' && check.status !== 'checking' ? 'cursor-pointer hover:shadow-md' : ''}
|
||||||
|
${(check as any).isLoading ? 'animate-pulse' : ''}
|
||||||
|
`}
|
||||||
|
onClick={() => {
|
||||||
|
if (check.status !== 'pending' && check.status !== 'checking') {
|
||||||
|
setSelectedCheck(selectedCheck === check.id ? null : check.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setHoveredCheck(check.id)}
|
||||||
|
onMouseLeave={() => setHoveredCheck(null)}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex items-start justify-end mb-12">
|
||||||
|
{getStatusIcon(check.status)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-label-large mb-4 text-accent-black font-medium flex items-center gap-6">
|
||||||
|
{check.label}
|
||||||
|
{check.tooltip && !aiInsights.some(ai => ai.id === check.id) && (
|
||||||
|
<div className="relative inline-block">
|
||||||
|
<Info className="w-14 h-14 text-black-alpha-32 hover:text-black-alpha-64 transition-colors" />
|
||||||
|
<AnimatePresence>
|
||||||
|
{hoveredCheck === check.id && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 5 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 5 }}
|
||||||
|
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-8 w-200 p-8 bg-accent-black text-white text-body-x-small rounded-6 shadow-lg z-50 pointer-events-none"
|
||||||
|
>
|
||||||
|
{check.tooltip}
|
||||||
|
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-accent-black" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-body-small text-black-alpha-64">
|
||||||
|
{check.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{check.status !== 'pending' && check.status !== 'checking' && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mt-8"
|
||||||
|
>
|
||||||
|
<div className="h-2 bg-black-alpha-4 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className={`
|
||||||
|
h-full rounded-full
|
||||||
|
${check.status === 'pass' ? 'bg-accent-black' : ''}
|
||||||
|
${check.status === 'warning' ? 'bg-heat-100' : ''}
|
||||||
|
${check.status === 'fail' ? 'bg-heat-200' : ''}
|
||||||
|
`}
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${check.score}%` }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="text-label-x-small text-black-alpha-32 mt-4 text-center"
|
||||||
|
>
|
||||||
|
Click for details
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded Details */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedCheck === check.id && check.details && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="mt-12 pt-12 border-t border-black-alpha-8"
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-label-small text-black-alpha-48 mb-2">Status</div>
|
||||||
|
<div className="text-body-small text-accent-black">{check.details}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-label-small text-black-alpha-48 mb-2">Recommendation</div>
|
||||||
|
<div className="text-body-small text-black-alpha-64">{check.recommendation}</div>
|
||||||
|
{check.actionItems && check.actionItems.length > 0 && (
|
||||||
|
<ul className="mt-4 space-y-2">
|
||||||
|
{check.actionItems.map((item: string, i: number) => (
|
||||||
|
<li key={i} className="flex items-start gap-6 text-body-small text-black-alpha-64">
|
||||||
|
<span className="text-heat-100 mt-1">•</span>
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Radar Chart View */}
|
||||||
|
{viewMode === 'chart' && showResults && (
|
||||||
|
<div>
|
||||||
|
<motion.div
|
||||||
|
className="flex justify-center gap-40 mb-40"
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
{/* Basic Analysis Chart */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<h3 className="text-label-large text-accent-black mb-16 font-medium">Basic Analysis</h3>
|
||||||
|
<RadarChart
|
||||||
|
data={checks
|
||||||
|
.filter(check => check.status !== 'pending' && check.status !== 'checking')
|
||||||
|
.slice(0, 8)
|
||||||
|
.map(check => ({
|
||||||
|
label: check.label.length > 12 ? check.label.substring(0, 12) + '...' : check.label,
|
||||||
|
score: check.score || 0
|
||||||
|
}))}
|
||||||
|
size={350}
|
||||||
|
/>
|
||||||
|
<div className="mt-16 text-center">
|
||||||
|
<div className="text-title-h3 text-accent-black">{overallScore}%</div>
|
||||||
|
<div className="text-label-small text-black-alpha-48">Overall Score</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VS Indicator */}
|
||||||
|
{aiInsights.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center"
|
||||||
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.2, type: "spring" }}
|
||||||
|
>
|
||||||
|
<div className="text-label-large text-black-alpha-32 font-medium">VS</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI Analysis Chart - Only show if AI insights exist */}
|
||||||
|
{aiInsights.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-label-large text-heat-100 mb-16 font-medium">AI Enhanced Analysis</h3>
|
||||||
|
<RadarChart
|
||||||
|
data={aiInsights
|
||||||
|
.filter(check => check.status !== 'pending' && check.status !== 'checking')
|
||||||
|
.slice(0, 8)
|
||||||
|
.map(check => ({
|
||||||
|
label: check.label.length > 12 ? check.label.substring(0, 12) + '...' : check.label,
|
||||||
|
score: check.score || 0
|
||||||
|
}))}
|
||||||
|
size={350}
|
||||||
|
/>
|
||||||
|
<div className="mt-16 text-center">
|
||||||
|
<div className="text-title-h3 text-heat-100">
|
||||||
|
{Math.round(aiInsights.reduce((sum, check) => sum + (check.score || 0), 0) / aiInsights.length)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-label-small text-heat-100 opacity-60">AI Score</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Comparison Summary */}
|
||||||
|
{aiInsights.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="text-center mb-20"
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-center gap-8 px-16 py-8 bg-heat-4 rounded-8">
|
||||||
|
<span className="text-label-medium text-accent-black">
|
||||||
|
AI analysis found {aiInsights.filter(i => i.score && i.score < 50).length} additional areas for improvement
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bar Chart View */}
|
||||||
|
{viewMode === 'bars' && showResults && (
|
||||||
|
<motion.div
|
||||||
|
className="px-40 mb-40"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<MetricBars
|
||||||
|
metrics={combinedChecks
|
||||||
|
.filter(check => check.status !== 'pending' && check.status !== 'checking')
|
||||||
|
.map(check => ({
|
||||||
|
label: check.label,
|
||||||
|
score: check.score || 0,
|
||||||
|
status: check.status as 'pass' | 'warning' | 'fail',
|
||||||
|
category: (check as any).isAI ? 'ai' :
|
||||||
|
['robots-txt', 'sitemap', 'llms-txt'].includes(check.id) ? 'domain' : 'page',
|
||||||
|
details: check.details,
|
||||||
|
recommendation: check.recommendation,
|
||||||
|
actionItems: check.actionItems
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
{showResults && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
className="flex gap-12 justify-center"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onReset}
|
||||||
|
className="px-20 py-10 bg-accent-white border border-black-alpha-8 hover:bg-black-alpha-4 rounded-8 text-label-medium transition-all"
|
||||||
|
>
|
||||||
|
Analyze Another Site
|
||||||
|
</button>
|
||||||
|
{true && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
setIsAnalyzingAI(true);
|
||||||
|
setShowAIAnalysis(true);
|
||||||
|
|
||||||
|
// Add placeholder AI tiles immediately with actual titles
|
||||||
|
const placeholderAIChecks = [
|
||||||
|
{
|
||||||
|
id: 'ai-loading-0',
|
||||||
|
label: 'Content Quality for AI',
|
||||||
|
description: 'Analyzing content signal ratio...',
|
||||||
|
icon: Sparkles,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-1',
|
||||||
|
label: 'Information Architecture',
|
||||||
|
description: 'Evaluating page structure...',
|
||||||
|
icon: Bot,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-2',
|
||||||
|
label: 'Crawlability Patterns',
|
||||||
|
description: 'Checking JavaScript usage...',
|
||||||
|
icon: Database,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-3',
|
||||||
|
label: 'AI Training Value',
|
||||||
|
description: 'Assessing training potential...',
|
||||||
|
icon: Network,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-4',
|
||||||
|
label: 'Knowledge Extraction',
|
||||||
|
description: 'Analyzing entity definitions...',
|
||||||
|
icon: FileCode,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-5',
|
||||||
|
label: 'Template Quality',
|
||||||
|
description: 'Reviewing semantic structure...',
|
||||||
|
icon: Shield,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-6',
|
||||||
|
label: 'Content Depth',
|
||||||
|
description: 'Measuring content richness...',
|
||||||
|
icon: Zap,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-loading-7',
|
||||||
|
label: 'Machine Readability',
|
||||||
|
description: 'Testing extraction reliability...',
|
||||||
|
icon: Globe,
|
||||||
|
status: 'checking' as const,
|
||||||
|
score: 0,
|
||||||
|
isAI: true,
|
||||||
|
isLoading: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add loading AI tiles with staggered animation immediately
|
||||||
|
placeholderAIChecks.forEach((check, idx) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setCombinedChecks(prev => [...prev, check]);
|
||||||
|
}, 100 * (idx + 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ai-analysis', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url,
|
||||||
|
htmlContent: analysisData?.htmlContent || '',
|
||||||
|
currentChecks: checks
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.insights) {
|
||||||
|
// Convert AI insights to CheckItem format with AI flag
|
||||||
|
const aiChecks: CheckItem[] = data.insights.map((insight: any, idx: number) => ({
|
||||||
|
...insight,
|
||||||
|
icon: [Sparkles, Bot, Database, Network, FileCode, Shield, Zap, Globe][idx % 8],
|
||||||
|
description: insight.details?.substring(0, 60) + '...' || 'AI Analysis',
|
||||||
|
isAI: true, // Mark as AI-generated
|
||||||
|
}));
|
||||||
|
|
||||||
|
setAiInsights(aiChecks);
|
||||||
|
|
||||||
|
// Replace loading tiles with real AI tiles
|
||||||
|
setCombinedChecks(prev => {
|
||||||
|
// Remove loading tiles
|
||||||
|
const withoutLoading = prev.filter(c => !(c as any).isLoading);
|
||||||
|
// Add real AI tiles
|
||||||
|
return [...withoutLoading, ...aiChecks];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate enhanced score
|
||||||
|
if (data.insights.length > 0) {
|
||||||
|
const aiScores = data.insights.map((i: any) => i.score || 0);
|
||||||
|
const avgAiScore = aiScores.reduce((a: number, b: number) => a + b, 0) / aiScores.length;
|
||||||
|
const combinedScore = Math.round((overallScore * 0.6) + (avgAiScore * 0.4));
|
||||||
|
setEnhancedScore(combinedScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI analysis error:', error);
|
||||||
|
// Remove loading tiles on error
|
||||||
|
setCombinedChecks(prev => prev.filter(c => !(c as any).isLoading));
|
||||||
|
} finally {
|
||||||
|
setIsAnalyzingAI(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isAnalyzingAI}
|
||||||
|
className="px-20 py-10 bg-accent-black hover:bg-black-alpha-80 text-white rounded-8 text-label-medium transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isAnalyzingAI ? 'Analyzing...' : 'Analyze with AI'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { Check, X, FileText, Globe, Code, Sparkles, AlertCircle } from "lucide-react";
|
||||||
|
// import { Zap, Shield } from "lucide-react"; // Reserved for future features
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface InlineResultsProps {
|
||||||
|
isAnalyzing: boolean;
|
||||||
|
showResults: boolean;
|
||||||
|
analysisStep: number;
|
||||||
|
url: string;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysisSteps = [
|
||||||
|
"Fetching website content...",
|
||||||
|
"Checking for LLMs.txt...",
|
||||||
|
"Analyzing HTML structure...",
|
||||||
|
"Calculating AI readiness...",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Placeholder data for the results
|
||||||
|
const mockResults = {
|
||||||
|
score: 78,
|
||||||
|
grade: "B+",
|
||||||
|
llmsTxt: true,
|
||||||
|
robotsTxt: true,
|
||||||
|
structuredData: true,
|
||||||
|
semanticHTML: false,
|
||||||
|
metaTags: true,
|
||||||
|
accessibility: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function InlineResults({
|
||||||
|
isAnalyzing,
|
||||||
|
showResults,
|
||||||
|
analysisStep,
|
||||||
|
url: _url, // URL prop available but not used in current implementation
|
||||||
|
onReset,
|
||||||
|
}: InlineResultsProps) {
|
||||||
|
const [displayScore, setDisplayScore] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showResults) {
|
||||||
|
// Animate score counting up
|
||||||
|
const target = mockResults.score;
|
||||||
|
const duration = 1500;
|
||||||
|
const increment = target / (duration / 16);
|
||||||
|
let current = 0;
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
current += increment;
|
||||||
|
if (current >= target) {
|
||||||
|
setDisplayScore(target);
|
||||||
|
clearInterval(timer);
|
||||||
|
} else {
|
||||||
|
setDisplayScore(Math.floor(current));
|
||||||
|
}
|
||||||
|
}, 16);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}
|
||||||
|
}, [showResults]);
|
||||||
|
|
||||||
|
const getScoreColor = (score: number) => {
|
||||||
|
if (score >= 80) return "#22c55e";
|
||||||
|
if (score >= 60) return "#eab308";
|
||||||
|
return "#ef4444";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{/* Analyzing State */}
|
||||||
|
{isAnalyzing && (
|
||||||
|
<motion.div
|
||||||
|
key="analyzing"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="space-y-20"
|
||||||
|
>
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="h-2 bg-black-alpha-4 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-gradient-to-r from-heat-100 to-heat-200"
|
||||||
|
initial={{ width: "0%" }}
|
||||||
|
animate={{ width: `${((analysisStep + 1) / 4) * 100}%` }}
|
||||||
|
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Glowing dot at the end of progress */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-heat-100 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${((analysisStep + 1) / 4) * 100}%`,
|
||||||
|
boxShadow: "0 0 20px rgba(255, 77, 0, 0.8)",
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.5, 1],
|
||||||
|
opacity: [1, 0.8, 1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.8,
|
||||||
|
repeat: Infinity,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Text */}
|
||||||
|
<motion.div
|
||||||
|
key={analysisStep}
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
className="flex items-center gap-8 text-body-medium text-black-alpha-64"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||||
|
>
|
||||||
|
<Sparkles className="w-16 h-16 text-heat-100" />
|
||||||
|
</motion.div>
|
||||||
|
{analysisSteps[analysisStep]}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* ASCII Animation */}
|
||||||
|
<motion.div
|
||||||
|
className="font-mono text-xs text-black-alpha-16 overflow-hidden h-32 relative"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{ y: [0, -10, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
className="absolute inset-0 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{'< analyzing />'}
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Results State */}
|
||||||
|
{showResults && (
|
||||||
|
<motion.div
|
||||||
|
key="results"
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||||
|
className="space-y-24"
|
||||||
|
>
|
||||||
|
{/* Score Display */}
|
||||||
|
<div className="text-center">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 200,
|
||||||
|
delay: 0.2
|
||||||
|
}}
|
||||||
|
className="relative inline-block"
|
||||||
|
>
|
||||||
|
{/* Background glow */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 rounded-full blur-xl"
|
||||||
|
style={{ background: getScoreColor(mockResults.score) }}
|
||||||
|
animate={{
|
||||||
|
opacity: [0.3, 0.6, 0.3],
|
||||||
|
scale: [0.8, 1.2, 0.8],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Score circle */}
|
||||||
|
<div
|
||||||
|
className="relative w-120 h-120 rounded-full flex flex-col items-center justify-center"
|
||||||
|
style={{
|
||||||
|
background: `conic-gradient(from 0deg, ${getScoreColor(mockResults.score)} ${displayScore * 3.6}deg, #f0f0f0 ${displayScore * 3.6}deg)`,
|
||||||
|
padding: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full h-full bg-white rounded-full flex flex-col items-center justify-center">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="text-4xl font-bold"
|
||||||
|
style={{ color: getScoreColor(mockResults.score) }}
|
||||||
|
>
|
||||||
|
{displayScore}
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 5 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.7 }}
|
||||||
|
className="text-label-medium text-black-alpha-48"
|
||||||
|
>
|
||||||
|
AI Ready
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Checks Grid */}
|
||||||
|
<motion.div
|
||||||
|
className="grid grid-cols-3 gap-12"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
label: "LLMs.txt",
|
||||||
|
value: mockResults.llmsTxt,
|
||||||
|
icon: FileText,
|
||||||
|
description: "AI instructions",
|
||||||
|
detail: mockResults.llmsTxt ? "Found" : "Missing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Structured Data",
|
||||||
|
value: mockResults.structuredData,
|
||||||
|
icon: Code,
|
||||||
|
description: "Schema markup",
|
||||||
|
detail: mockResults.structuredData ? "Detected" : "Not found"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Semantic HTML",
|
||||||
|
value: mockResults.semanticHTML,
|
||||||
|
icon: Globe,
|
||||||
|
description: "HTML5 tags",
|
||||||
|
detail: mockResults.semanticHTML ? "Good" : "Needs work"
|
||||||
|
},
|
||||||
|
].map((item, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={item.label}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.9 + index * 0.1 }}
|
||||||
|
className={`
|
||||||
|
relative p-16 rounded-12 transition-all hover:shadow-md cursor-pointer
|
||||||
|
${item.value
|
||||||
|
? 'bg-gradient-to-br from-green-50 to-green-100/50 border-green-200'
|
||||||
|
: 'bg-gradient-to-br from-red-50 to-red-100/50 border-red-200'}
|
||||||
|
border
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Status indicator */}
|
||||||
|
<div className="absolute top-12 right-12">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ delay: 1 + index * 0.1, type: "spring" }}
|
||||||
|
className={`
|
||||||
|
w-24 h-24 rounded-full flex items-center justify-center
|
||||||
|
${item.value ? 'bg-green-500' : 'bg-red-500'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{item.value ? (
|
||||||
|
<Check className="w-14 h-14 text-white" strokeWidth={3} />
|
||||||
|
) : (
|
||||||
|
<X className="w-14 h-14 text-white" strokeWidth={3} />
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="mb-12">
|
||||||
|
<item.icon className={`
|
||||||
|
w-24 h-24
|
||||||
|
${item.value ? 'text-green-600' : 'text-red-600'}
|
||||||
|
`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-label-medium text-accent-black">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
<div className="text-body-small text-black-alpha-48">
|
||||||
|
{item.description}
|
||||||
|
</div>
|
||||||
|
<div className={`
|
||||||
|
text-label-small font-semibold
|
||||||
|
${item.value ? 'text-green-600' : 'text-red-600'}
|
||||||
|
`}>
|
||||||
|
{item.detail}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Quick Tip */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 1.2 }}
|
||||||
|
className="p-12 bg-heat-4 rounded-8 border border-heat-100 flex items-start gap-8"
|
||||||
|
>
|
||||||
|
<AlertCircle className="w-16 h-16 text-heat-100 mt-2" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-label-medium text-accent-black mb-4">Quick Tip</div>
|
||||||
|
<div className="text-body-small text-black-alpha-64">
|
||||||
|
{mockResults.semanticHTML
|
||||||
|
? "Your site has good semantic HTML structure for AI understanding."
|
||||||
|
: "Add semantic HTML5 elements to improve AI comprehension of your content."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.4 }}
|
||||||
|
className="flex gap-8"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onReset}
|
||||||
|
className="flex-1 px-16 py-10 bg-black-alpha-4 hover:bg-black-alpha-6 rounded-8 text-label-medium transition-all"
|
||||||
|
>
|
||||||
|
Try Another
|
||||||
|
</button>
|
||||||
|
<button className="flex-1 px-16 py-10 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-label-medium transition-all shadow-lg hover:shadow-xl">
|
||||||
|
View Details
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
interface MetricBarsProps {
|
||||||
|
metrics: {
|
||||||
|
label: string;
|
||||||
|
score: number;
|
||||||
|
status: 'pass' | 'warning' | 'fail';
|
||||||
|
category?: 'page' | 'domain' | 'ai';
|
||||||
|
details?: string;
|
||||||
|
recommendation?: string;
|
||||||
|
actionItems?: string[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MetricBars({ metrics }: MetricBarsProps) {
|
||||||
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const getBarColor = (score: number) => {
|
||||||
|
// Use brand orange colors with opacity for gradient effect
|
||||||
|
if (score >= 80) return 'bg-heat-100';
|
||||||
|
if (score >= 60) return 'bg-heat-90';
|
||||||
|
if (score >= 40) return 'bg-heat-40 opacity-80';
|
||||||
|
return 'bg-heat-20';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBulletColor = (_score: number) => {
|
||||||
|
// Always use heat-100 for all bullets for consistency
|
||||||
|
return 'bg-heat-100';
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleExpanded = (label: string) => {
|
||||||
|
const newExpanded = new Set(expandedItems);
|
||||||
|
if (newExpanded.has(label)) {
|
||||||
|
newExpanded.delete(label);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(label);
|
||||||
|
}
|
||||||
|
setExpandedItems(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort metrics by score descending
|
||||||
|
const sortedMetrics = [...metrics].sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 max-w-[800px] mx-auto">
|
||||||
|
{sortedMetrics.map((metric, index) => {
|
||||||
|
const isExpanded = expandedItems.has(metric.label);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={metric.label}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: index * 0.05, duration: 0.3 }}
|
||||||
|
className="space-y-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`grid grid-cols-12 gap-4 items-center p-8 -m-8 rounded-8 cursor-pointer transition-all hover:bg-black-alpha-2 ${
|
||||||
|
isExpanded ? 'bg-black-alpha-4' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => toggleExpanded(metric.label)}
|
||||||
|
>
|
||||||
|
{/* Bullet and Label - fixed width */}
|
||||||
|
<div className="col-span-4 flex items-center gap-8">
|
||||||
|
<div className={`w-6 h-6 rounded-full ${getBulletColor(metric.score)}`} />
|
||||||
|
<span className="text-label-medium text-accent-black truncate">{metric.label}</span>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: isExpanded ? 180 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-16 h-16 text-black-alpha-32" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bar container - flexible width */}
|
||||||
|
<div className="col-span-7 relative">
|
||||||
|
<div className="relative h-8 bg-black-alpha-8 rounded-full overflow-hidden">
|
||||||
|
{/* Animated bar */}
|
||||||
|
<motion.div
|
||||||
|
className={`absolute inset-y-0 left-0 ${getBarColor(metric.score)} rounded-full`}
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${Math.max(metric.score, 2)}%` }}
|
||||||
|
transition={{
|
||||||
|
delay: 0.2 + index * 0.05,
|
||||||
|
duration: 0.8,
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Subtle inner glow */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-transparent to-white opacity-10 rounded-full" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Score indicator lines at key thresholds */}
|
||||||
|
{[40, 60, 80].map(threshold => (
|
||||||
|
<div
|
||||||
|
key={threshold}
|
||||||
|
className="absolute top-0 bottom-0 w-px bg-black-alpha-8 opacity-30"
|
||||||
|
style={{ left: `${threshold}%` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Score value - fixed width */}
|
||||||
|
<div className="col-span-1 text-right">
|
||||||
|
<span className="text-label-medium font-medium text-heat-100">
|
||||||
|
{metric.score}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded Details */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isExpanded && metric.details && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="pl-54 pr-12 py-12 space-y-8">
|
||||||
|
<div>
|
||||||
|
<div className="text-label-small text-black-alpha-48 mb-4">Status</div>
|
||||||
|
<div className="text-body-small text-accent-black">{metric.details}</div>
|
||||||
|
</div>
|
||||||
|
{metric.recommendation && (
|
||||||
|
<div>
|
||||||
|
<div className="text-label-small text-black-alpha-48 mb-4">Recommendation</div>
|
||||||
|
<div className="text-body-small text-black-alpha-64">{metric.recommendation}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{metric.actionItems && metric.actionItems.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-label-small text-black-alpha-48 mb-4">Action Items</div>
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{metric.actionItems.map((item: string, i: number) => (
|
||||||
|
<li key={i} className="flex items-start gap-6 text-body-small text-black-alpha-64">
|
||||||
|
<span className="text-heat-100 mt-1">•</span>
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Summary stats */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="mt-20 pt-12 border-t border-black-alpha-8"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-3 gap-16 text-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-title-h3 text-heat-150">
|
||||||
|
{metrics.filter(m => m.status === 'pass').length}
|
||||||
|
</div>
|
||||||
|
<div className="text-label-small text-black-alpha-48">Passing</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-title-h3 text-heat-100">
|
||||||
|
{metrics.filter(m => m.status === 'warning').length}
|
||||||
|
</div>
|
||||||
|
<div className="text-label-small text-black-alpha-48">Warning</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-title-h3 text-heat-50">
|
||||||
|
{metrics.filter(m => m.status === 'fail').length}
|
||||||
|
</div>
|
||||||
|
<div className="text-label-small text-black-alpha-48">Failing</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface RadarChartProps {
|
||||||
|
data: {
|
||||||
|
label: string;
|
||||||
|
score: number;
|
||||||
|
maxScore?: number;
|
||||||
|
}[];
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RadarChart({ data, size = 300 }: RadarChartProps) {
|
||||||
|
const [isAnimated, setIsAnimated] = useState(false);
|
||||||
|
const center = size / 2;
|
||||||
|
const radius = (size / 2) - 60; // Increased padding for labels
|
||||||
|
const angleStep = (Math.PI * 2) / data.length;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsAnimated(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Calculate points for the polygon
|
||||||
|
const getPoint = (value: number, index: number) => {
|
||||||
|
const angle = index * angleStep - Math.PI / 2;
|
||||||
|
const r = (value / 100) * radius;
|
||||||
|
return {
|
||||||
|
x: center + r * Math.cos(angle),
|
||||||
|
y: center + r * Math.sin(angle)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create polygon points string
|
||||||
|
const polygonPoints = data
|
||||||
|
.map((item, i) => {
|
||||||
|
const point = getPoint(isAnimated ? item.score : 0, i);
|
||||||
|
return `${point.x},${point.y}`;
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
// Grid levels
|
||||||
|
const gridLevels = [20, 40, 60, 80, 100];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<svg width={size} height={size} className="overflow-visible">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="radar-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="#FF4A00" stopOpacity="0.8" />
|
||||||
|
<stop offset="100%" stopColor="#FF8533" stopOpacity="0.3" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="radar-glow">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Grid circles */}
|
||||||
|
{gridLevels.map((level) => (
|
||||||
|
<circle
|
||||||
|
key={level}
|
||||||
|
cx={center}
|
||||||
|
cy={center}
|
||||||
|
r={(level / 100) * radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(0,0,0,0.05)"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Axis lines */}
|
||||||
|
{data.map((_, i) => {
|
||||||
|
const angle = i * angleStep - Math.PI / 2;
|
||||||
|
const x2 = center + radius * Math.cos(angle);
|
||||||
|
const y2 = center + radius * Math.sin(angle);
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={i}
|
||||||
|
x1={center}
|
||||||
|
y1={center}
|
||||||
|
x2={x2}
|
||||||
|
y2={y2}
|
||||||
|
stroke="rgba(0,0,0,0.05)"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Data polygon */}
|
||||||
|
<motion.polygon
|
||||||
|
points={polygonPoints}
|
||||||
|
fill="url(#radar-gradient)"
|
||||||
|
stroke="#FF4A00"
|
||||||
|
strokeWidth="2"
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
|
filter="url(#radar-glow)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Data points */}
|
||||||
|
{data.map((item, i) => {
|
||||||
|
const point = getPoint(item.score, i);
|
||||||
|
return (
|
||||||
|
<motion.circle
|
||||||
|
key={i}
|
||||||
|
cx={point.x}
|
||||||
|
cy={point.y}
|
||||||
|
r="4"
|
||||||
|
fill="#FF4A00"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="2"
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: isAnimated ? 1 : 0 }}
|
||||||
|
transition={{ delay: 0.8 + i * 0.1, duration: 0.3 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Labels */}
|
||||||
|
{data.map((item, i) => {
|
||||||
|
const angle = i * angleStep - Math.PI / 2;
|
||||||
|
const labelRadius = radius + 40; // Increased label distance
|
||||||
|
const x = center + labelRadius * Math.cos(angle);
|
||||||
|
const y = center + labelRadius * Math.sin(angle);
|
||||||
|
|
||||||
|
// Better text anchor logic based on quadrant
|
||||||
|
let textAnchor = "middle";
|
||||||
|
let dy = 0;
|
||||||
|
|
||||||
|
// Left side
|
||||||
|
if (x < center - 20) {
|
||||||
|
textAnchor = "end";
|
||||||
|
}
|
||||||
|
// Right side
|
||||||
|
else if (x > center + 20) {
|
||||||
|
textAnchor = "start";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top
|
||||||
|
if (y < center - 20) {
|
||||||
|
dy = -5;
|
||||||
|
}
|
||||||
|
// Bottom
|
||||||
|
else if (y > center + 20) {
|
||||||
|
dy = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.g key={i}>
|
||||||
|
{/* Background for better readability */}
|
||||||
|
<motion.rect
|
||||||
|
x={x - (textAnchor === "middle" ? 30 : textAnchor === "end" ? 60 : 0)}
|
||||||
|
y={y - 10}
|
||||||
|
width={60}
|
||||||
|
height={20}
|
||||||
|
fill="white"
|
||||||
|
fillOpacity={0.9}
|
||||||
|
rx={4}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.9 + i * 0.05 }}
|
||||||
|
/>
|
||||||
|
<motion.text
|
||||||
|
x={x}
|
||||||
|
y={y + dy}
|
||||||
|
textAnchor={textAnchor as any}
|
||||||
|
dominantBaseline="middle"
|
||||||
|
className="text-xs fill-black-alpha-80 font-medium"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1 + i * 0.05 }}
|
||||||
|
style={{ pointerEvents: 'none' }}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</motion.text>
|
||||||
|
{/* Score value */}
|
||||||
|
<motion.text
|
||||||
|
x={x}
|
||||||
|
y={y + dy + 12}
|
||||||
|
textAnchor={textAnchor as any}
|
||||||
|
dominantBaseline="middle"
|
||||||
|
className="text-[10px] fill-heat-100 font-bold"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.1 + i * 0.05 }}
|
||||||
|
style={{ pointerEvents: 'none' }}
|
||||||
|
>
|
||||||
|
{item.score}%
|
||||||
|
</motion.text>
|
||||||
|
</motion.g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="mt-16 flex justify-center">
|
||||||
|
<div className="inline-flex flex-row gap-16 text-xs text-black-alpha-48 bg-white px-16 py-8 rounded-6 shadow-sm">
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-heat-200" />
|
||||||
|
<span className="whitespace-nowrap">80-100%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-heat-100" />
|
||||||
|
<span className="whitespace-nowrap">60-79%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-heat-50" />
|
||||||
|
<span className="whitespace-nowrap"><60%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface ScoreChartProps {
|
||||||
|
score: number;
|
||||||
|
enhanced?: boolean;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ScoreChart({ score, enhanced = false, size = 200 }: ScoreChartProps) {
|
||||||
|
const [animatedScore, setAnimatedScore] = useState(0);
|
||||||
|
const radius = size / 2 - 20;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setAnimatedScore(score);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [score]);
|
||||||
|
|
||||||
|
// Calculate stroke dash offset for the progress
|
||||||
|
const strokeDashoffset = circumference - (animatedScore / 100) * circumference;
|
||||||
|
|
||||||
|
// Determine color based on score
|
||||||
|
const getColor = () => {
|
||||||
|
if (score >= 80) return "#FF4A00"; // heat-200 - Excellent
|
||||||
|
if (score >= 60) return "#FF6500"; // heat-150 - Good
|
||||||
|
if (score >= 40) return "#FF8533"; // heat-100 - Warning
|
||||||
|
return "#FFA566"; // heat-50 - Poor
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGradientId = enhanced ? "enhanced-gradient" : "normal-gradient";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative inline-flex items-center justify-center">
|
||||||
|
<svg width={size} height={size} className="transform -rotate-90">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={getGradientId} x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor={getColor()} stopOpacity="1" />
|
||||||
|
<stop offset="100%" stopColor={enhanced ? "#FF8533" : getColor()} stopOpacity="0.6" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Background circle */}
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(0,0,0,0.05)"
|
||||||
|
strokeWidth="12"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Progress circle */}
|
||||||
|
<motion.circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke={`url(#${getGradientId})`}
|
||||||
|
strokeWidth="12"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
initial={{ strokeDashoffset: circumference }}
|
||||||
|
animate={{ strokeDashoffset }}
|
||||||
|
transition={{ duration: 1.5, ease: "easeOut" }}
|
||||||
|
filter="url(#glow)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Center content */}
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<motion.div
|
||||||
|
className="text-4xl font-bold text-heat-150"
|
||||||
|
initial={{ opacity: 0, scale: 0.5 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.5, duration: 0.5 }}
|
||||||
|
>
|
||||||
|
{animatedScore}%
|
||||||
|
</motion.div>
|
||||||
|
{enhanced && (
|
||||||
|
<motion.div
|
||||||
|
className="text-xs text-heat-100 mt-1"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
>
|
||||||
|
AI Enhanced
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { animate } from "motion";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
import initCanvas from "@/utils/init-canvas";
|
||||||
|
|
||||||
|
export default function EndpointsCrawl({
|
||||||
|
active,
|
||||||
|
alwaysHeat = false,
|
||||||
|
triggerOnHover = false,
|
||||||
|
size = 20,
|
||||||
|
}: {
|
||||||
|
active?: boolean;
|
||||||
|
alwaysHeat?: boolean;
|
||||||
|
triggerOnHover?: boolean;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const fnRefs = useRef<{
|
||||||
|
activate: () => void;
|
||||||
|
deactivate: () => void;
|
||||||
|
}>({ activate: () => {}, deactivate: () => {} });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = initCanvas(canvas);
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
let isActive = false;
|
||||||
|
|
||||||
|
let activeGroup = 0;
|
||||||
|
const rowAlphas = [0.2, 0.4, 1, 0.04];
|
||||||
|
|
||||||
|
const grid = [
|
||||||
|
[24],
|
||||||
|
[16, 18, 30, 32],
|
||||||
|
[8, 12, 36, 40],
|
||||||
|
[0, 3, 6, 21, 27, 42, 45, 48],
|
||||||
|
];
|
||||||
|
|
||||||
|
const scaler = size / 20;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
ctx.fillStyle = "#FF4C00";
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
for (const group of grid.slice(0, 4)) {
|
||||||
|
const groupIndex = grid.indexOf(group);
|
||||||
|
ctx.globalAlpha = rowAlphas[groupIndex];
|
||||||
|
|
||||||
|
for (const index of group) {
|
||||||
|
ctx.fillRect(
|
||||||
|
(3 + (index % 7) * 2) * scaler,
|
||||||
|
(3 + Math.floor(index / 7) * 2) * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeouts: number[] = [];
|
||||||
|
|
||||||
|
let runCount = 0;
|
||||||
|
|
||||||
|
const cycle = () => {
|
||||||
|
isRunning = true;
|
||||||
|
activeGroup = (activeGroup + 1) % 5;
|
||||||
|
|
||||||
|
rowAlphas.forEach((alpha, index) => {
|
||||||
|
let targetAlpha = alpha;
|
||||||
|
|
||||||
|
if (index === activeGroup) targetAlpha = 1;
|
||||||
|
else if (index === (activeGroup + 1) % 4) targetAlpha = 0.12;
|
||||||
|
else if (index === (activeGroup + 2) % 4) targetAlpha = 0.2;
|
||||||
|
else if (index === (activeGroup + 3) % 4) targetAlpha = 0.4;
|
||||||
|
|
||||||
|
animate(alpha, targetAlpha, {
|
||||||
|
duration: 0.05,
|
||||||
|
onUpdate: (value) => {
|
||||||
|
rowAlphas[index] = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.forEach((timeout) => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
isRunning = false;
|
||||||
|
}, 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeGroup === 3) runCount += 1;
|
||||||
|
|
||||||
|
if ((runCount === 2 || !isActive) && activeGroup === 2) return;
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
cycle();
|
||||||
|
}, 50),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
fnRefs.current = {
|
||||||
|
activate: () => {
|
||||||
|
if (isActive) return;
|
||||||
|
|
||||||
|
isActive = true;
|
||||||
|
|
||||||
|
runCount = 0;
|
||||||
|
|
||||||
|
cycle();
|
||||||
|
render();
|
||||||
|
},
|
||||||
|
deactivate: () => {
|
||||||
|
if (!isActive) return;
|
||||||
|
|
||||||
|
isActive = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
canvas.addEventListener("resize", render);
|
||||||
|
|
||||||
|
if (triggerOnHover) {
|
||||||
|
const group = canvasRef.current!.closest(".group");
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [triggerOnHover, size]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggerOnHover) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && active) {
|
||||||
|
fnRefs.current.activate();
|
||||||
|
} else {
|
||||||
|
fnRefs.current.deactivate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(canvasRef.current!);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [active, triggerOnHover]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
className={cn(
|
||||||
|
alwaysHeat
|
||||||
|
? ""
|
||||||
|
: [
|
||||||
|
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||||
|
!active && "grayscale",
|
||||||
|
],
|
||||||
|
)}
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { animate } from "motion";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
import initCanvas from "@/utils/init-canvas";
|
||||||
|
|
||||||
|
export default function EndpointsExtract({
|
||||||
|
active,
|
||||||
|
disabledCells,
|
||||||
|
alwaysHeat = false,
|
||||||
|
triggerOnHover = false,
|
||||||
|
size = 20,
|
||||||
|
}: {
|
||||||
|
active?: boolean;
|
||||||
|
disabledCells?: number[];
|
||||||
|
alwaysHeat?: boolean;
|
||||||
|
triggerOnHover?: boolean;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const fnRefs = useRef<{
|
||||||
|
activate: () => void;
|
||||||
|
deactivate: () => void;
|
||||||
|
}>({ activate: () => {}, deactivate: () => {} });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = initCanvas(canvas);
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
let isActive = false;
|
||||||
|
|
||||||
|
let activeCol = 0;
|
||||||
|
const colAlphas = [1, 0.4, 0.2, 0.12];
|
||||||
|
|
||||||
|
const scaler = size / 20;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
ctx.fillStyle = "#FF4C00";
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw Extract pattern - represents structured data extraction
|
||||||
|
// Draw columns to represent data fields
|
||||||
|
for (let col = 0; col < 4; col++) {
|
||||||
|
ctx.globalAlpha = colAlphas[col];
|
||||||
|
|
||||||
|
// Draw vertical bars of different heights to represent extracted data
|
||||||
|
const heights = [3, 2, 3, 1];
|
||||||
|
const startY = [1, 2, 1, 3];
|
||||||
|
|
||||||
|
for (let row = 0; row < heights[col]; row++) {
|
||||||
|
ctx.fillRect(
|
||||||
|
(3 + col * 4) * scaler,
|
||||||
|
(3 + startY[col] * 2 + row * 4) * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeouts: number[] = [];
|
||||||
|
|
||||||
|
let runCount = 0;
|
||||||
|
|
||||||
|
const cycle = () => {
|
||||||
|
isRunning = true;
|
||||||
|
activeCol = (activeCol + 1) % 4;
|
||||||
|
|
||||||
|
colAlphas.forEach((alpha, index) => {
|
||||||
|
let targetAlpha = alpha;
|
||||||
|
|
||||||
|
if (index === activeCol) targetAlpha = 1;
|
||||||
|
else if (index === (activeCol + 1) % 4) targetAlpha = 0.12;
|
||||||
|
else if (index === (activeCol + 2) % 4) targetAlpha = 0.2;
|
||||||
|
else if (index === (activeCol + 3) % 4) targetAlpha = 0.4;
|
||||||
|
|
||||||
|
animate(alpha, targetAlpha, {
|
||||||
|
duration: 0.05,
|
||||||
|
onUpdate: (value) => {
|
||||||
|
colAlphas[index] = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.forEach((timeout) => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
isRunning = false;
|
||||||
|
}, 400),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeCol === 3) runCount += 1;
|
||||||
|
|
||||||
|
if ((runCount === 2 || !isActive) && activeCol === 0) return;
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
cycle();
|
||||||
|
}, 50),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
fnRefs.current = {
|
||||||
|
activate: () => {
|
||||||
|
if (isActive) return;
|
||||||
|
|
||||||
|
isActive = true;
|
||||||
|
|
||||||
|
runCount = 0;
|
||||||
|
cycle();
|
||||||
|
render();
|
||||||
|
},
|
||||||
|
deactivate: () => {
|
||||||
|
if (!isActive) return;
|
||||||
|
|
||||||
|
isActive = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
canvas.addEventListener("resize", render);
|
||||||
|
|
||||||
|
if (triggerOnHover) {
|
||||||
|
const group = canvasRef.current!.closest(".group");
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [disabledCells, size, triggerOnHover]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggerOnHover) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && active) {
|
||||||
|
fnRefs.current.activate();
|
||||||
|
} else {
|
||||||
|
fnRefs.current.deactivate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(canvasRef.current!);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [active, triggerOnHover]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
className={cn(
|
||||||
|
alwaysHeat
|
||||||
|
? ""
|
||||||
|
: [
|
||||||
|
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||||
|
!active && "grayscale",
|
||||||
|
],
|
||||||
|
)}
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ComponentProps } from "react";
|
||||||
|
|
||||||
|
import EndpointsScrape from "@/components/app/(home)/sections/endpoints/EndpointsScrape/EndpointsScrape";
|
||||||
|
|
||||||
|
export default function EndpointsMap(
|
||||||
|
props: ComponentProps<typeof EndpointsScrape>,
|
||||||
|
) {
|
||||||
|
return <EndpointsScrape {...props} disabledCells={[1, 2, 3, 7, 9, 12, 15]} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { animate } from "motion";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
import initCanvas from "@/utils/init-canvas";
|
||||||
|
|
||||||
|
export default function EndpointsScrape({
|
||||||
|
active,
|
||||||
|
disabledCells,
|
||||||
|
alwaysHeat = false,
|
||||||
|
triggerOnHover = false,
|
||||||
|
size = 20,
|
||||||
|
}: {
|
||||||
|
active?: boolean;
|
||||||
|
disabledCells?: number[];
|
||||||
|
alwaysHeat?: boolean;
|
||||||
|
triggerOnHover?: boolean;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const fnRefs = useRef<{
|
||||||
|
activate: () => void;
|
||||||
|
deactivate: () => void;
|
||||||
|
}>({ activate: () => {}, deactivate: () => {} });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = initCanvas(canvas);
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
let isActive = false;
|
||||||
|
|
||||||
|
let activeRow = 2;
|
||||||
|
const rowAlphas = [0.2, 0.4, 1, 0.12];
|
||||||
|
|
||||||
|
const scaler = size / 20;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
ctx.fillStyle = "#FF4C00";
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
if (disabledCells && disabledCells.includes(i)) continue;
|
||||||
|
|
||||||
|
ctx.globalAlpha = rowAlphas[Math.floor(i / 4)];
|
||||||
|
|
||||||
|
ctx.fillRect(
|
||||||
|
(3 + (i % 4) * 4) * scaler,
|
||||||
|
(3 + Math.floor(i / 4) * 4) * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeouts: number[] = [];
|
||||||
|
|
||||||
|
let runCount = 0;
|
||||||
|
|
||||||
|
const cycle = () => {
|
||||||
|
isRunning = true;
|
||||||
|
activeRow = (activeRow + 1) % 5;
|
||||||
|
|
||||||
|
rowAlphas.forEach((alpha, index) => {
|
||||||
|
let targetAlpha = alpha;
|
||||||
|
|
||||||
|
if (index === activeRow) targetAlpha = 1;
|
||||||
|
else if (index === (activeRow + 1) % 4) targetAlpha = 0.12;
|
||||||
|
else if (index === (activeRow + 2) % 4) targetAlpha = 0.2;
|
||||||
|
else if (index === (activeRow + 3) % 4) targetAlpha = 0.4;
|
||||||
|
|
||||||
|
animate(alpha, targetAlpha, {
|
||||||
|
duration: 0.05,
|
||||||
|
onUpdate: (value) => {
|
||||||
|
rowAlphas[index] = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.forEach((timeout) => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
isRunning = false;
|
||||||
|
}, 400),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeRow === 3) runCount += 1;
|
||||||
|
|
||||||
|
if ((runCount === 2 || !isActive) && activeRow === 2) return;
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
cycle();
|
||||||
|
}, 50),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
fnRefs.current = {
|
||||||
|
activate: () => {
|
||||||
|
if (isActive) return;
|
||||||
|
|
||||||
|
isActive = true;
|
||||||
|
|
||||||
|
runCount = 0;
|
||||||
|
cycle();
|
||||||
|
render();
|
||||||
|
},
|
||||||
|
deactivate: () => {
|
||||||
|
if (!isActive) return;
|
||||||
|
|
||||||
|
isActive = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
canvas.addEventListener("resize", render);
|
||||||
|
|
||||||
|
if (triggerOnHover) {
|
||||||
|
const group = canvasRef.current!.closest(".group");
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [disabledCells, size, triggerOnHover]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggerOnHover) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && active) {
|
||||||
|
fnRefs.current.activate();
|
||||||
|
} else {
|
||||||
|
fnRefs.current.deactivate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(canvasRef.current!);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [active, triggerOnHover]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
className={cn(
|
||||||
|
alwaysHeat
|
||||||
|
? ""
|
||||||
|
: [
|
||||||
|
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||||
|
!active && "grayscale",
|
||||||
|
],
|
||||||
|
)}
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import initCanvas from "@/utils/init-canvas";
|
||||||
|
import { animate } from "motion";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export default function EndpointsSearch({
|
||||||
|
alwaysHeat,
|
||||||
|
size = 20,
|
||||||
|
}: {
|
||||||
|
alwaysHeat?: boolean;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = initCanvas(canvas);
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
let isActive = false;
|
||||||
|
|
||||||
|
let diff = 0;
|
||||||
|
const defaultRowAlphas = [
|
||||||
|
0, 0.2, 0.4, 0, 0.4, 1, 0.4, 0.2, 0.2, 0.4, 1, 0.4, 0, 0.4, 0.2, 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
const differs = Array.from({ length: 16 }, () => 0.2 + Math.random() * 0.2);
|
||||||
|
|
||||||
|
differs[5] = 0.6;
|
||||||
|
differs[6] = 0.6;
|
||||||
|
differs[9] = 0.6;
|
||||||
|
differs[10] = 0.6;
|
||||||
|
|
||||||
|
const scaler = size / 20;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
ctx.fillStyle = "#FF4C00";
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
if ([0, 3, 12, 15].includes(i)) continue;
|
||||||
|
|
||||||
|
const maxAlpha = [5, 6, 9, 10].includes(i) ? 1 : 0.4;
|
||||||
|
|
||||||
|
const alpha = defaultRowAlphas[i] + diff * differs[i];
|
||||||
|
ctx.globalAlpha = Math.min(
|
||||||
|
Math.min(alpha, maxAlpha) - Math.max(alpha - maxAlpha, 0),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.fillRect(
|
||||||
|
(3 + (i % 4) * 4) * scaler,
|
||||||
|
(3 + Math.floor(i / 4) * 4) * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeouts: number[] = [];
|
||||||
|
|
||||||
|
let runCount = 0;
|
||||||
|
|
||||||
|
const duration = 300;
|
||||||
|
|
||||||
|
const cycle = () => {
|
||||||
|
isRunning = true;
|
||||||
|
|
||||||
|
animate(diff, 1, {
|
||||||
|
duration: duration / 1000,
|
||||||
|
onUpdate: (value) => {
|
||||||
|
diff = value < 0.5 ? value * 2 : 1 - (value - 0.5) * 2;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.forEach((timeout) => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(
|
||||||
|
() => {
|
||||||
|
isRunning = false;
|
||||||
|
},
|
||||||
|
Math.max(duration, 300),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
runCount += 1;
|
||||||
|
|
||||||
|
if (runCount === 3 || !isActive) return;
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
cycle();
|
||||||
|
}, duration),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const activate = () => {
|
||||||
|
if (isActive) return;
|
||||||
|
|
||||||
|
isActive = true;
|
||||||
|
|
||||||
|
runCount = 0;
|
||||||
|
cycle();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deactivate = () => {
|
||||||
|
if (!isActive) return;
|
||||||
|
|
||||||
|
isActive = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
canvas.addEventListener("resize", render);
|
||||||
|
|
||||||
|
const group = canvasRef.current!.closest(".group");
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
group.addEventListener("mouseenter", activate);
|
||||||
|
group.addEventListener("mouseleave", deactivate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
group.removeEventListener("mouseenter", activate);
|
||||||
|
group.removeEventListener("mouseleave", deactivate);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [size]);
|
||||||
|
|
||||||
|
return <canvas ref={canvasRef} style={{ width: size, height: size }} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { animate } from "motion";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import initCanvas from "@/utils/init-canvas";
|
||||||
|
|
||||||
|
export default function EndpointsExtract({ size = 20 }: { size?: number }) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = initCanvas(canvas);
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
let isActive = false;
|
||||||
|
|
||||||
|
let diff = 0;
|
||||||
|
const defaultRowAlphas = [
|
||||||
|
0.4, 0.04, 0.2, 0.4, 0.2, 0, 0, 0.04, 0.04, 0, 0, 0.2, 0.4, 0.2, 0.04,
|
||||||
|
0.4,
|
||||||
|
];
|
||||||
|
|
||||||
|
const differs = Array.from({ length: 16 }, () => 0.2 + Math.random() * 0.2);
|
||||||
|
|
||||||
|
const scaler = size / 20;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
ctx.fillStyle = "#FF4C00";
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
if ([5, 6, 9, 10].includes(i)) continue;
|
||||||
|
|
||||||
|
ctx.globalAlpha = defaultRowAlphas[i] + diff * differs[i];
|
||||||
|
ctx.globalAlpha =
|
||||||
|
Math.min(ctx.globalAlpha, 0.4) - Math.max(ctx.globalAlpha - 0.4, 0);
|
||||||
|
|
||||||
|
ctx.fillRect(
|
||||||
|
(3 + (i % 4) * 4) * scaler,
|
||||||
|
(3 + Math.floor(i / 4) * 4) * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.fillRect(7, 7, 6, 2);
|
||||||
|
ctx.globalAlpha = 0.4;
|
||||||
|
ctx.fillRect(7, 11, 2 + diff * 4, 2);
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeouts: number[] = [];
|
||||||
|
|
||||||
|
let runCount = 0;
|
||||||
|
|
||||||
|
const duration = 300;
|
||||||
|
|
||||||
|
const cycle = () => {
|
||||||
|
isRunning = true;
|
||||||
|
|
||||||
|
animate(diff, 1, {
|
||||||
|
duration: duration / 1000,
|
||||||
|
onUpdate: (value) => {
|
||||||
|
diff = value < 0.5 ? value * 2 : 1 - (value - 0.5) * 2;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.forEach((timeout) => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(
|
||||||
|
() => {
|
||||||
|
isRunning = false;
|
||||||
|
},
|
||||||
|
Math.max(duration, 300),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
runCount += 1;
|
||||||
|
|
||||||
|
if (runCount === 3 || !isActive) return;
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
cycle();
|
||||||
|
}, duration),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const activate = () => {
|
||||||
|
if (isActive) return;
|
||||||
|
|
||||||
|
isActive = true;
|
||||||
|
|
||||||
|
runCount = 0;
|
||||||
|
cycle();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deactivate = () => {
|
||||||
|
if (!isActive) return;
|
||||||
|
|
||||||
|
isActive = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
canvas.addEventListener("resize", render);
|
||||||
|
|
||||||
|
const group = canvasRef.current!.closest(".group");
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
group.addEventListener("mouseenter", activate);
|
||||||
|
group.addEventListener("mouseleave", deactivate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
group.removeEventListener("mouseenter", activate);
|
||||||
|
group.removeEventListener("mouseleave", deactivate);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [size]);
|
||||||
|
|
||||||
|
return <canvas ref={canvasRef} style={{ width: size, height: size }} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { animate } from "motion";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
import initCanvas from "@/utils/init-canvas";
|
||||||
|
|
||||||
|
export default function EndpointsMcp({
|
||||||
|
active,
|
||||||
|
alwaysHeat = false,
|
||||||
|
triggerOnHover = false,
|
||||||
|
size = 20,
|
||||||
|
}: {
|
||||||
|
active?: boolean;
|
||||||
|
alwaysHeat?: boolean;
|
||||||
|
triggerOnHover?: boolean;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const fnRefs = useRef<{
|
||||||
|
activate: () => void;
|
||||||
|
deactivate: () => void;
|
||||||
|
}>({ activate: () => {}, deactivate: () => {} });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = initCanvas(canvas);
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
let isActive = false;
|
||||||
|
|
||||||
|
let activeIndex = 5;
|
||||||
|
const rowAlphas = [0.12, 0.2, 0.4, 0.4, 1, 1, 1, 0.4, 0.2];
|
||||||
|
|
||||||
|
const scaler = size / 20;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.fillStyle = "#FF4C00";
|
||||||
|
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
ctx.globalAlpha = rowAlphas[i];
|
||||||
|
|
||||||
|
ctx.fillRect(
|
||||||
|
(5 + (i % 3) * 4) * scaler,
|
||||||
|
(5 + Math.floor(i / 3) * 4) * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
2 * scaler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeouts: number[] = [];
|
||||||
|
|
||||||
|
let runCount = 0;
|
||||||
|
|
||||||
|
const cycle = () => {
|
||||||
|
isRunning = true;
|
||||||
|
activeIndex = (activeIndex + 1) % 9;
|
||||||
|
|
||||||
|
rowAlphas.forEach((alpha, index) => {
|
||||||
|
let targetAlpha = alpha;
|
||||||
|
|
||||||
|
if (index === activeIndex) targetAlpha = 1;
|
||||||
|
else if (index === (activeIndex - 1 + 9) % 9) targetAlpha = 1;
|
||||||
|
else if (index === (activeIndex - 2 + 9) % 9) targetAlpha = 1;
|
||||||
|
else if (index === (activeIndex - 3 + 9) % 9) targetAlpha = 0.4;
|
||||||
|
else if (index === (activeIndex - 4 + 9) % 9) targetAlpha = 0.2;
|
||||||
|
else if (index === (activeIndex - 5 + 9) % 9) targetAlpha = 0.2;
|
||||||
|
else if (index === (activeIndex - 6 + 9) % 9) targetAlpha = 0.12;
|
||||||
|
else if (index === (activeIndex - 7 + 9) % 9) targetAlpha = 0.12;
|
||||||
|
else if (index === (activeIndex - 8 + 9) % 9) targetAlpha = 0.4;
|
||||||
|
|
||||||
|
animate(alpha, targetAlpha, {
|
||||||
|
duration: 30 / 1000,
|
||||||
|
onUpdate: (value) => {
|
||||||
|
rowAlphas[index] = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.forEach((timeout) => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
isRunning = false;
|
||||||
|
}, 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeIndex === 7) runCount += 1;
|
||||||
|
|
||||||
|
if ((runCount === 2 || !isActive) && activeIndex === 6) return;
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
cycle();
|
||||||
|
}, 30),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
fnRefs.current = {
|
||||||
|
activate: () => {
|
||||||
|
if (isActive) return;
|
||||||
|
|
||||||
|
isActive = true;
|
||||||
|
|
||||||
|
runCount = 0;
|
||||||
|
|
||||||
|
cycle();
|
||||||
|
render();
|
||||||
|
},
|
||||||
|
deactivate: () => {
|
||||||
|
if (!isActive) return;
|
||||||
|
|
||||||
|
isActive = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
canvas.addEventListener("resize", render);
|
||||||
|
|
||||||
|
if (triggerOnHover) {
|
||||||
|
const group = canvasRef.current!.closest(".group");
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [size, triggerOnHover]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggerOnHover) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && active) {
|
||||||
|
fnRefs.current.activate();
|
||||||
|
} else {
|
||||||
|
fnRefs.current.deactivate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(canvasRef.current!);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [active, triggerOnHover]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
className={cn(
|
||||||
|
alwaysHeat
|
||||||
|
? ""
|
||||||
|
: [
|
||||||
|
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||||
|
!active && "grayscale",
|
||||||
|
],
|
||||||
|
)}
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||||
|
|
||||||
|
import data from "./data.json";
|
||||||
|
|
||||||
|
export default function HeroFlame() {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const ref2 = useRef<HTMLDivElement>(null);
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
const interval = setIntervalOnVisible({
|
||||||
|
element: wrapperRef.current,
|
||||||
|
callback: () => {
|
||||||
|
index++;
|
||||||
|
if (index >= data.length) index = 0;
|
||||||
|
|
||||||
|
ref.current!.innerHTML = data[index];
|
||||||
|
ref2.current!.innerHTML = data[index];
|
||||||
|
},
|
||||||
|
interval: 85,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => interval?.();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="cw-686 h-190 top-408 absolute flex gap-16 pointer-events-none select-none"
|
||||||
|
ref={wrapperRef}
|
||||||
|
>
|
||||||
|
<div className="flex-1 overflow-clip relative">
|
||||||
|
<div
|
||||||
|
className="text-black-alpha-20 font-ascii absolute bottom-0 -left-380 fc-decoration"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
whiteSpace: "pre",
|
||||||
|
fontSize: "9px",
|
||||||
|
lineHeight: "11px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-clip relative">
|
||||||
|
<div
|
||||||
|
className="text-black-alpha-20 font-ascii absolute bottom-0 -right-380 -scale-x-100 fc-decoration"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||||
|
ref={ref2}
|
||||||
|
style={{
|
||||||
|
whiteSpace: "pre",
|
||||||
|
fontSize: "9px",
|
||||||
|
lineHeight: "11px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
[
|
||||||
|
" \n \n \n \n \n . . \n .. ..+ \n .:. \n .. .. .:: \n +.. ..: :. \n .:..::. .. .. \n .--:::. .. ... .:. .. \n .. .:+=-::.:. . ...-.::. .. \n ::.... .:--+::..: ......:+....:. :.. .. \n ....... ::-=:::: ..:-:-...: .--..:: ......... \n .. . . . ..::-:-.. .-+-:::.. ...::::. .: ...::.:.. \n . -... ....: . . .--=+-::. :-=-:.... . .:..:: .:---:::::-::.... \n ..::........::=..... ...:-.. .:-=--+=-:. ..--:..=::.... . .:.. ..:---::::---=:::..:... \n ..........::::.:::::::-::.-.. ...::--==:. ..-::-+==-:... .-::....... ..--:. ..:=+==.---=-+-:::::::-.. \n . .....::......:: ::::-::.---=+-:..::-+==++X=-:. ..:-::-=-== ---.. .:.--::.. .:-==::=--X==-----====--::+:::+... \n ..-....-:..::-::=-=-:-::--===++=-==-----== X+=-:.::-==----+==+XX+=-::.:+--==--::. .:-+X=----+X=-=------===--::-:...:. .... \n ....::::...:-:-==+++=++==+++XX++==++--+-+==++++=-===+=---:-==+X:XXX+=-:-=-==++=-:. .:-=+=- -=X+X+===+---==--==--:..::...+....+ \n ..:::---.::.---=+==XXXXXXXX+XX++==++===--+===:+X+====+=--::--=+XXXXXXX+==++==+XX+=: ::::--=+++X++X+XXXX+=----==++.+=--::+::::+. ::.=... \n .:::-==-------=X+++XXXXXXXXXXX++==++.==-==-:-==+X++==+=-=--=++++X++:X:X+++X+-+X X+=---=-==+=+++XXXXX+XX=+=--=X++XXX==---::-+-::::.:..-..\n",
|
||||||
|
" \n \n \n \n .. \n . .+. \n \n .: \n : .. :. \n .. ... .. .. \n :...+. . .. :. . \n .=-::... . . ... .. \n .. .--=-::... .....=+.:. . . \n -:.... .:-=:...: .::...... .:. .. . ... \n .. .. . .:: :.:: .:-::.. . .-..:.: ........... \n ..= . .. .::-==.. .-=-::... ..:.. . ..:::.::.:... \n .+.:.:. ..-.:: . . ..:. .:=-==-::. .:--.. .. .. ... .:---:::::--::..... \n .. ..+::.......-::..: . ::--.. ..:::-==-:. ..::.. .:... ..-.. =..:=== ::::-+-=:...+.=.. \n .....= ....:::..:::::- ::=:. ..:=--==+:. .:-::-=-=--:... .:-..... .:-=:...-+==--:+:-+-=-:-:.:+... \n .....:...::.:: ::::-::----=+-::--:---+=XX=-:. ..-=-=::--==X==-:....-.:--::.. .::++-::--+=---:-:---=-=::...-.. \n ....:..:....:=:- ==--=-:--===++====---- -==+==-::--==-:-::--=XX ++=-:::---+===-:. .:-X+ ----=X==----: :=--.--::........... \n .+.:::::-..:-:-===++=++=++++X++=====-.--=X==++==----=--::+:-=+XXXXX++---=-==+++=:. ..:-+++---=+XX+++=-::-===-+=--:...:.......... \n ...::--- ::::--=+=+XXXXXXXX+++=====+===--=---==++==-=+=--::-==++XX+XXX++=+X==+XX+=-::-::--==++X++XXXXXXX=-::-+=++X+=-:::::::::-:=+..... \n ..: :-===----=-=+++++XX+XX-XX++X+==++==+=--:--==.XX+==++.===+++XX.++++XX++=X+=++XX+==--=--===+ +XXXXXXXXX+=--=X++.XX==--=-:---:::::::..:.\n",
|
||||||
|
" \n \n . \n . \n \n . : \n . .. \n . .. . \n . \n :.. . . \n .::... . \n ==-:: : . \n . . .-.=::. .:: ..+ . .=. \n . :.. ..+:..:-. .:.. .: .+. . .. \n . . .:.:-:. .-=-... ..-. ...:.. .. .. \n . . .. .. :. ..::--:.. .::-. . .: .:::-:....-...:. \n .. ......:: ::. .::+:::: .:=::==:.. ...... ... . . .::----:...::::::.... \n . .:.....:::::=:-. ..::--=-.. :-::--==.::.. . .: .. .::-:..:==--::..:::::.:.....: . \n +.. ...:-+...:.: :=---::..:::::-=+=:.. .::.:::--.++--:.. ..-:..+. .:=-::::==-:::::::-=-:.::... . \n -... .. .. .:---:+:-:-::-=+==--=-::.::-X=--::..::::.:.:--=XX+==--::::.:-+=-:. ..-=-::-::=++-::-::.:---::.. . \n ......... .:::-=+== -==+= =====--=-:::::-+=+=--:.:::..::.:-===XXX+=--::--+==+=-. ..-==-:.:-=XX=----:.:-=-=--:.. .+..... \n ...:::+:...:=:-+++XX++++.=-=-==-- ===-:.:=---=+=-:::--::..:-==++X+X+==--=+==+X+-:.......:-==++==++X:X++=-:::-==+=--:.......::::..+. \n ...:---::+:::--==++-XX++XX+=:=++=--====-:::+--==+=---== ---==++XXX++XX+.+XX===+X+=-:::-:--=====XX+-++XXX+-:--++=+X+--::::.:::::::..... \n ..::.-==---+- ++++.+++XXX-XX++:X+=-=X+==:-::.=+X+XXX+=+X++++XXXXXXXX=XX=- +++-==++X+==-==-=:=++XXXXXXXXXX++==+ ++X X+=---.--.-:.:::.:... \n",
|
||||||
|
" \n \n . \n : \n . \n . \n . \n \n \n :. . \n :-....:. .- \n ....... .. \n . .. .. :. \n .. ..:.:. .-.. . ...:... \n . .. ..... .:--:.. .::... . . .. ........ .:. . \n .. ..... ...: ..=..::-. .:.:=-:. . ... .. .:.:..:-...-:......... \n . .:: ..::.:.:.. . .:.::-.. ..=.:--==:.. . .::..+==::::+........ . \n .:.......:.:::.....:::..::==:.. :.....::-+X-:::. .-.:... ..-:..::=::-:-...:.:::... \n .. :::.:.-.:::-=---::=::...:--==-:. ..= ....:-++=--=-::.+...==-::. .--:....:=-:-:::.... ::.. \n +.... .::-==+-----------------:::..-==--::. ... ..::-=+:+++=-:::.-- =+=-:. ..=--:::::=++-=::-::.:=-::... .. \n ..:-.... :..:-+++=+=-===-------::--.::..:------:... :....::-=++XX+=--::----+X-:. ..-==-----+X+==-=-:::-=+=-::.. .....:...- \n ...:::::....:-- ==+X+====-----=+::-==-:::::----=-:.::-::::--=+XXXXXX+=-=+=-==++--:-:::.::=--=-==X++++X+=-:=:===++--::......:...... \n ...:----:::--.====+=++X+++======---=+=--:-::=+++X++=-====== ++XXXX.XXX +++-=--=+X+--::--:---==+XX+++XXXX+=--=+=++X+=-:::-::-:::....... \n .. ...:::--==:---++=+=====++ XXXXX++===+X+===+==-+X++++++++X+X++.XXXXXXXXXXXX ++======X+==-=--===+XXXXXXXXXXXX+=+++++X+XX=.--==--::-....:....\n",
|
||||||
|
" . \n . \n \n \n \n \n \n \n . ..: \n :. . .. \n ::.. \n . .. \n .: :::. :. .. \n .:..:. .:-. ::. . .: .. ..... \n ... . ... . . ... ..::-:. . . ... .:........:. \n .. =.... . ... .. .:.:-+-.. ....-=:....:.=... .. \n ... -..=::... . ......:::-: .::=+-:::+. ::-.. .:..::=:...:.......... \n ....:. ...:....:::=:.:-.. .::=--: ..:==:-::::. . .:---:. .:=:::. :::-.-...:...=. \n ..:.--::::::::::::::--:-:....--:::... ..:-==++---:.=.-::-=+-:. .:::.....--=-:.-.::.--:... \n ....... ..-=---==-=-==-::.-::+:--::..-:---:::....... ..---=+XXX+=-+:::--+X-:.. :--:.::::-++-::::::::==-:..: +.... \n ..:...:....:-=-=+:+=--==::--::-=::--:...:--.:-::....::::---+=+XXX.+=-:-----+=-::..... .::--:---=+++==--:..:-==+-:::.. ......+-. \n ..:-::::..::---=====+=== -:--:--::-==-::-::-==-==----=--::-==+:XXXXXX+==+=--==+=::..:::.::---++.+=+++X+=-:=:===+=--::...:..::...:. \n ....:-------==-=+=====XX+X+==------+=---=:==+=.==+=+++++X+==+XXXXXXXXXXX+++==--==+=-:-:-:---=X XX++XXXXX++===+==+++=------::: :....... \n .....-.:+:--==+======= ++=++X:XXXX+=:==XX==:++=-=++==+X+=+++:+XXXXX:-X.XXXXX+X++==-++++X+======++XXXXX-XXXX--XXX+==+++++X++==.---::::..=:. ..\n",
|
||||||
|
" \n \n \n \n \n \n \n . \n . . \n :. \n .. . .. \n . .. ::. . . \n . . .:+.. : .. . \n . . . ..-- .. .. ..: \n . .. .. . ...-:... . ...:-.. ... \n . ....-.-. ..:.. .. :: ...-::...... .. .. .:.:::=....:. .. \n -... .....:...:.:-. ...=.:: .:=-::::::. .::--. ....:.:. ..:.. ..: .. \n .::::::.:..+:....:..-=:-:. ..:..::. ..=------::...:..:=-.. ........::-:.. ..::.:. \n . . :::----:::::=::....+:=-:-..-::-::-... . .:-==++++++-:.-::-+-... .::.... .:==::.....:--+:.. . . \n .... .. .:-=-=+==---:-:=:::..::::-:..:-:::::::....=.:..::--=+XXX-=-:-:-:=--::.. :.::..:::-++--::...::==-:.:.. ....+. \n ...-.:.. ...:-==--=+--:-::::..::.:--:..:=:-+-:---::--:::+:-==++X:XX+-==--:--==-::.... ..::::-++==++:-::..:-==-:::.. .....+=. . \n -..:::::::--::--==--++=+==--::::::-=-:: =-=-- --=+=+++=++-==++XXXXX- X+==+-----==::..:-.::--=+.++=++XX++=-:-======:--::.:.-...:... \n ......:=:--=----=- -=-===XX+XX+=----.-+=-:-==-+==-.-=+=====-+ X+XXXXXXX:XXX+========++=------+++XXX+.+X=XXX.XX+======+++---:::-.:.=..:.... \n .......::::-=:===++ ==++=++XXXXXX:++++++=====++=+X++:=++=====++XXXXXXXXXXXXX+X++-===++XXX++=+==+X++XX++X-XXXX-XXX+==+++=+X:===-+-:::=..::....\n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n . .. \n ...... \n ..::. \n : . . . \n ... : ....+: . . .. \n . .:. .... . .:. .... . ...:. ..-:.. .. \n . . ..- ...=.: ..:. .:-::::..=.. .:-. ...... .:. :.. \n ....::. .. -.......=: :::. ...-.. .- --+::::---.. ..:.. . .. +..::. ...:. \n . .:::::-:....::-........---:-...::..:. ....:--=--===+=:....:-.... .. .. .:-:.. ..::. . \n . .:::--:--::=:::::. ..::-: ..::-....: ..:......:--===+=X=-::.---::-.. ..:....::.==-::.. :---... . .-. \n .. ...::-:-:--::-::-::. ....::-...----:::::.:=:::-:::-====+XX+=--:::=--::. .::::-=== =+=::....:=-:..-. .... \n .+...... :::::-----=.--:-::...=..:=:...:---::.:--==+--=+:--+:+XXXXXXX=-==:+:---:::...:...-:--++===+XX=-::::=----:::...:...... . \n .....:=:- :::-:::-+--++===+=-::-.::-=:::-=+=--:.::-==+-:=++ +XXXXXXXXX++==+=-=+--=--::::::---+++==++XX-X+=+ ==---===--::.+....=.... \n .=....:.::.==--:-==--====XX XXX++=++=== ==--==+X+==---------==+XXXXXXXXXXXX++==== =+==++-=.=-=+++:=++=++XXXXXXXXX+=====++---:-::..:...:.... \n ......:::--=++= ====-===++XX-X XXXXX:X+====-==+XX:+++========+XXXXXXXXXXXXXXX++=++XX +++++=+++X:X+XX.++XXXXX .XX+++=:+++X+=.---::::...:=:...\n",
|
||||||
|
" \n \n \n \n \n \n \n \n .: . \n . .... \n \n :. ... \n . . ..:. . \n . ..:.. .. .:: .. . \n . .. .:. ..: .:...... .:.. .. :.. .. \n ..-::: .:... .:=:. .. :. ..::=.:: :::=:. . .=. .. . \n ......:.. ..:-.. .. .:-:.. :.... . . .::=-------+-:......:. .. .+.:. .. \n ...::......-:.::. ..:-:....:::.. .: ....:--=--=-=+-:.::::.... .. .::-=-::. :.... \n ..:::.:.::..:..::.. ....--=....=::=:.:...-..:-:.::==--=+++--::---:.... ...::-:--=+:.. .:.:.. . \n .....:: ..:-::--:+:.:-.. .:=..:-..=.--:....::-+-::=-:-.-=++==+XX+=-=-::-::... ... :.::-=-=++X=-:..:::-::.:....... \n ....::.........---====--=-::.:=:.:--=::-=.::....::--::==== XX +-++XX++==.-=-:. :::-..:...:-- =-.==+X++------------:..... .. .. \n .....+.-:::::::-:::+--=X++ ++=====+==---:--=X+=-::.::::--:--=+XXXXXX++X++-====--==:--==--=:-=-=+==+-==+X.XX++++:=----=-::..:.. ....=.. \n . .. ...::+-==------:--==XXXXXXXXXXXX+=------+++X+===--=-----==+XX-XXXXXX:-++==+=++=--==++=++XX++==++==++XXXX:X++++=-===+=--:+::.....=-.... \n ......::--==.+++=+===.=++XXXXXXXXXXXX=++=.=--=+++X+++==+=====+X-X-XXXXXXX X+++++++++++===:==:++XX+XXX++XXXXXXXX.+++++XX+XX+=-=-:::+:::.:.:..\n",
|
||||||
|
" \n \n \n \n \n \n \n . .. \n . \n .. \n . \n .. .:. ... \n ... ...... .. . \n . .. :. . ..:. .:: :. \n +... . ..:. . . .. .:.:..:=:. . . . \n .. ..: .:: ... . ..::-::::.:.:=-. ... .. ...::... \n .:.... ..+.: . ::. .::.. . ..::.::::==::--:..:.. . . .::-:-:. . \n . ........... .-.. .. .:. .-:...... .. ....::-=-::--=-::.:::. .. ....:+:-==.. .. .. \n :...... .:.-:.+....-....:. ..:: .::-.. ..::-:..=::::--+=--==+==-=-:.:...=. ...::---+=::... :::......... \n .. ...- ......::==----:-::.::=::+:-:.::-:.... ...-:::---=+==+:====+++++=:: . ...:: .-. .=.::--=-+=--::::::=-.:::. \n .......:......:..:--X+==:=+=:--=-=-:::--==+=--:....::-:.::-=XXXX++++X+===-:-::-:::.-:::-:=::-=------==++==-=+==-::+::.. .. .. \n +:......:-- ::-::::--+XXXXX++===--=-=-::-+++++==--::::::::-=+XXXXXXXX++==-==-----::--=++++++===--=---==+X++++====----=-:..... ......... \n .....:::::-=+==-+=+--==+X-XXXXX+=X+==--=---==+XX+==+--------=++X+XXXXX+XX++=-==++==------=++++X+=+++==++XXXX=X++==+===:+== :--:............\n .......:--.-====++.X+=++XXXXXXXXXXXXX+==---.===X+.+X+X++=.++++=XXX.XXXX+XX=X++++ +XX+X+==--====+XX+XXX XXXX XXX X+=+X++++++===-=-: :::=::....\n",
|
||||||
|
" \n \n \n \n \n \n . \n \n \n . \n .. \n .. ..... .... \n . .. ..: .::. . \n . .: . .... .:-. .. \n . ::. .=.:..::.. .-=. .. .. -.. \n ..: .: :..= .: :-::+:::.:-::-... .. ..:-:-:. \n . . ..:: .. .: .-:.. . ..+..::--:::--:::=:.. . .. ..=::=-. . \n .......::... ..:.=. .. .: .:.......... .-...::-+-:--=--:-:::. .. .=..: :=-.. ..:.. \n . .:.:=::=:..::....::..::: ...:.. ..+::.::.:-:--==:--== ++==:.:...... :... ...:=:-=-::.:.:::-:.-... \n .... .. ...:=+--=--=--:::---:::::::--.... ..:::::::-+X+===--==+===:-:: :...:-:::... ..::::-:-===---:::-- ::::. . \n .......:....::..::-XX+:+++=---------::=++===--:..-::.:.::-=XXXX+.++++=----.::::::+--===---:---:::::==+=======--::+:::.. .. .. \n =.....+::--:::=::+:-=+XXXXXX+=-=::-:-=:--==+X+==--::.:=:::-=+X=XXXX+X+==--=--=- -.::--==XX+==+===---+==+++:-=====--.-=-::.:.. ...-....= \n ....=.::+::---===X==-===XXXXXXXX+=X+---------=++++==++---==+=++XXXXXXXXXXX++====++==-------===+X+==+++++XXXXXX+=+=++====+=-=-=-::..-.::.... \n .... .:.---=:====+X++.+XXXXXXX+.+++++=-- -==++XX++X+XX+++XXXXXXXXXXXX+=+XX.++++=XXXXX+==--=-==++X.XXXXX XX= XXXX++.X+++ += =--=-::::::::.-..\n",
|
||||||
|
" \n \n \n \n \n \n . \n . \n \n . . \n . . . ..: \n . .:. :: \n .: .+. :: \n .. ... .. . .. ...= .:.:: .. .:. \n .: .. .:.. ....:..::..-.:..:.. ..:::+ \n ..: .. .:. ... .. :-..::..::. . . ...:. . \n .::. :. . : . .. . ...+::--:.:----+.... . . ...:: .. \n ..-:..... .-.....-..::... .::. .:..::..::----.:.:--+-=+:.::. .. .: ...-::... ....:+.. \n . .. ..:=-:--: :-:....:----....:::.. :.:.....:-=++=-:::----:=:.. :...::..-.. . ...::- -+::::::=:.....+ \n ...........:=X=++++==-:..::::=:-+=--:-::.....+..=.:-X.++X+=-===-:--:....:.:: :-=-:-:.-::..::---------.::.:..... .. . \n ......-:::=-:.::-=+XXXXX++--::..::-:==-==+=--:::...:..:-++XXXX++++=--::-:::::.:..::-++=---+---::::--==.==-----::::+:..::. ..... \n ......-::::--==-:-=-=+XX=XX+=--=-:::::::-=:=++=--:=:.:-====+XX:X-XXXX+==----+=.--: ::::====++==-----+=+++++++==------.:----:. . ...... \n ...:..:::::-----==+===++XX XXXX+=+==--:::---=++X+X++==--=++XX X++XXXXXXXXX+++=+XXX++=--:::--==-= ++++ XXXXXXXX:+++X+ ===----=--.:::..::..: \n .....:::--===--==+XX++XXXXXXXXX+++===------==+X+++XXXX:++XXXXXX+++++++=+XXXXX+++XXX-+X+= ----=.=+XXXXXXXXXXXXXX++XXXXX+++=.---==-:--:::-... \n",
|
||||||
|
" \n \n \n \n . \n . \n \n . . \n . \n : \n . . . \n .. . .: : \n .. . . . .. ... . . \n .. . . ....+ .. ... . . \n .. . . ... . ..::. :...:-. . . \n ... .. .. .:. :.. . .::.. ::.:::.::-. . \n ......... ..: ::..:-. .:. ... .. :-::.....::-.-:.... .: .... .. \n .:--::::..::. .::-:.=....:. ........:.:-=-:...:::::: .:.-....:. . ..... ....+.=...: \n .:.. ..:++=-=X=--:.......::=:---:::+.. ..=.:-==:-==++=:::::::. ....::::-::::.... ..:::=.::-::.... .. \n ...::-: ..:-+X+XX +==--.. ..:-==-----:-:.: .:---++++++++++=-::..:.. .......:====--:::::...::-:--:-::.::+..::..... +.. \n ..+....::--:::.:-=+XXXX++=--:.....::-=---=+--:.:....:-==+++X++=++=-::::::--:::+....-==---=--=:.:::--===+==-:-:::::::::::.. ..... \n :.. ......::+::-=--- ==+XX:XX+=----:-:.:::-==+=++=-:::+:-==+X:X+++X++X++==-==+X++==-:...:-=---=========++X-XXX+=+==----:+:::::......==.. \n ...+.::::---.::-=+XX=+X=:XXXXX+===--=--:=:-- ====X+===-=++=+XX+X++++XXXXX+X.+++++X++==-::=:-+-==++=+++XXXXXXXXX. :XX++++=-:- -==:::::::... \n .....::--=====--=+XXXXXXXXXXXX-+++++X+=---=++++-+XX++++XX++XXXX+++++++=+XX=XXX+XXXXXX++=------==+XXXXXXXXXXXXXXX+XXXXXXX+=---==----::=::... \n",
|
||||||
|
" \n \n \n \n \n \n \n . \n . \n . \n . . \n . .. \n . . .. .. .. \n : .. ....... . ..:. \n . .-. .. .:. ....:.::. \n . . .. .. .. .. +..:.. ..-::...- . ..= \n .::....:+..: .:. ....... ...........:::::...:.:.. ..- . . ..:... \n . . .-=-:-===--:. .:+::--=.... .:-==---:------::..... .....-.+.:::. . .......... . \n ...... .=.-++==+X++=-.. .:.--:::::.:.. ..-X==-===+==-- ::.... ...-------...... :..:.::::-.+... ..... \n .+..::....:-=++++X++---.. ...--.:-:--:-: .. ..-== +X++====--:..::::........:--:::::::-:.. :::--==++-.::.......... . \n .............::-::: ==+=+XX++--::::....::--==:=--:.=...:-==++:++++====-::-=X+=-=-.-:..+--+:.:-::----=--==++XXX ==-::::::...::.. ...... \n ....::::::-=::=-===-=+:XXXXX+=-=-:::: :..::-=+=+==-.::========++.+XXX:+X++==+==X+==--....:-:---=-=--=+:++X=XXXXXX.+====+::::-::..:+:.... \n .+..::::-==----:+X++++XXXXXX+== ==++=-::--=++X++X+==++X++-++==+++++XXXXXXXX++++X.X++=--::.:::-=++++++=X=XXXXX+XXX+X++XX+--::=--::::::... \n ....:: --=====---=+++XX.X=XXX++=+++X+=====+XX XX+XXXXXXXXXXX++= ++++X+=+XXXXXXXXX+X-XX..=-- =-==XXXXXXXXXXXXXXX+.XXXX-XXX=--------:::::.-. \n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n \n . . \n . . .. .. \n .: . . .. \n . . .:.. .. .. \n .. . .. .+... . \n .. .::..: .. ... ..............:.+. . .. . \n .:..::::---=. ..:---:. . .=--=-:.-::..=:-:. . .. ..... . . .-. \n . .:==-----+=-:. :--:.=.-..... . .-===::---:-:::::. ..::-:--:. . .:.=.::--.. . \n ..... ..:-=== ===.-:: .::::..:: .:.... ..:==-=+=+=-=-=::.+.:.... .:-:::.. ...+... .:.:::-=++::.. .=.. \n . .. ..-...:..::+====.++=-:+::.... .::::---=::.. ..:::-=+==-+X+===-:.:-+=--::=:: ..::.:. .:..:+::=:::--=+XX+---:...::.... .. \n ..:.+....:--.::-:---=+X++++==-:::::......::==++--:..:==--=-=--==+X==++=---+-=+==--::. .:::: :-::::--+=--=+XXXXX++=---:=-:.:....=.... \n .. ..::::-=-::=++==+XX.XX+X+----==.--::.:-=+X+X+===-=X+===----== XXXX++++==++++++==-:....:::.==-=--==XXX=XXXX-X:++==-++=:::--:::::....= \n ...:.-::-====-.-=++:+XXXXXX+==-==++=----- =XXXXXXXXXXXXX++=---:==+X.XX.+XXXXXX+X+XX++++-:.::--=X++:X =XXXXX++.XX++X++X+++--::--:-::::.:. \n .-..::.-=====-=-=+++XXXXXXXX+:+++XX+=+===+XXXXXXXXXXXXXXX+======++XXX++XX XX.XXXXXX=++X=--====+XXXXXX+XX+X X++++XXXXXX++=--- ---- :::.: .. \n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n . \n \n .. \n . \n . . .. \n ... : .. ......... .:.:. \n .:..+....-:. .:::-:. .:-:--=:.+.... .. . .:... \n .:==-::::-=:: .:::..... . .:==--:--::.-::.... .-.-:::. ... .::-:. \n . ..--=-------:.. ..::....:::.-. ......==--:--=-:=-+.+....... .:-:....- . .....:-==-:. \n .:......::-==--==--:..........::::=---.. ..:==--==---=-----...:=-+--:......:.... .. ........::--=++=-:.....:.. \n .....-.:::::::::-==+==--:-:.:.:... ...::-++=--:...:=---.-=---+=-==-::+==----.:-::....::..-:....::==::--=+X++=-::::+:-:.... -.. \n .......::-=-:=+=--++X++==-=-::-==::::.:.:-+XX+==---+==--=::::--+X=====--====-=+:--... ....:-=-:-::-=++++++XX++==.----==-::-:...=... \n ..:.::::------====+XX+X.++=---.==-:-::.:-=XXXXXXXX+++=- --:::--+XXXX+++.==++++++==-=-:....--++:===+XXX++XX+++X++==-===--:::::::::.... \n ....::---------==+=+X+++XX+=====+++==-:--=+XXXX+XXXX-++==--::--=+XXXXXXXXXXXXXXXXX+= =-:=:-- +XXXXXXXXXXXXX++=++++++++=--::-- :::....+ \n ....::-=====--=-++++X++++XXXXXXXXXX+++==+XXX:X+XXXXXXXXX=--.-==+XX-+XXXXXXXX+XX+XXX+++++=+++=+XXXXXXXXXXXX+++==+.++XX++=-=--=--- ::..:. . \n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n . \n \n \n .. .. \n .. . ...:. \n . .:. ...... .:=:-=-:. . . \n .:--::..-.::. ........ .. :-=-::::::.-..... ....:. .....:. \n .::--:=:::::.. ..- ...:.:. .::---:::::::--+.... .... .. .... ::-.. \n .... ...:--:-::-::.::.. .......--::.. ..:=.---:::--:::....:--:-=. . ..... .. ...:..:.:+:--- .. .. \n +...::.=..::-==--:::::::::.... .::=+=-::.....:::--.---=----:..:--::::.:.::...-+...=-.:. ..:==:. ::- ===-::....-:.. \n . .-.:-:--=--:=+X+=--=:::::---:....::-=+X+=--::.--:+:..:+:=+==---:--=--::-::-:... ..-=:::::::-===--=+++==-:::::::::::....... \n .......:: ------==++X+== ---::==-+::.:.:=+XX=X+++==--::....-:-XX+=++++========---:-:. .:-+=---=++++ +=+X+=====-------:::........ \n ... .:::-:---==:===++XX+==---+++X==-:.:-=+XX=X+XXX+==-::.:+:-=XXXXXXXXXXXXXX-++==--:....:--X+XXX-XXXX-XXX+==+====-=---::::::..-=.... \n ..::=.-------=+-===+++XX++==+X.XX+:+---=+XX:XXXXX:++:=-:::-:==+XXXXXXXXXXXX++ X+=====--===:+XXXXXXXXXXXX+==--=++X++.=--::--=:::...... \n .... ::-==--=-=+X+XX+== ++XXXXXXXXXX+++++++X++:XXXXXXX+X+==---=++=XXXXXX:XXX++ ==XXXXXXXXXXX++XXXXXXXXXXXXX++====++XX:+==-= ===--:.....:.:.\n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n \n \n . . \n . .....: \n . .:-:--::. . \n .::=-... .. .......... .:-::....=. . . . . \n ..:::::....... . ... . :.:-: :.::. .... . . ... . . ... ...... \n .. ..::=:::.......... .-=..:. .:::-:::.::..... .::---:. :. .:.+.:....:::. \n .-....::-=--::.:....:.:. .-.---==-.. ...:..:::::=-:.::..:--::..... =.. ...=-. ..::.....::--+-:... .. \n ..:---::--=X=--:.:.:.:-:-:.....:--=+X=::..=:::.=...:-+=---------::-::..::.......:-:....=.:----::-==-=--:......:::.. . \n .-..:--:::-=--++=-:+::::---=::...:--=+XX+=++=--::.....::+X+++=+X+=====-.-:::::. .:--:::-==-+-=:==+.+---:.::.::::.....- .. \n .......::-::------=+++==-.--=-+X+==:..:-=+XXXX++X==-::..:::-+XXX-XX+XXXX+===+-=-:.. ..:::=X++.XX++++++++++--=--::::::-:-:. ...:... \n ..:::::--::-+==-==-=+++=====++XX=++::.:-++XXX.++++==-:...:--=+XXXXXX X+XX===X+==-+--:--:-=+XXXXXXXXXX+++= ----+==--=-:::--:........ \n ..:---=---==X+++=-===+XX++==+XXX===.===+++ +XX++====+--::--=+XXXXXXXXXX++.==+X-XXX+++===-=XXXXXXXXXXXXX+=----=+X++=-----==-::........ \n ....=:--======+XX+X+=----=+.XX+.XXXXX+=+=++X+=+XX XX+==--=---=+XXXX-X=XXXX++===.=+XXXXX.X.++++XXXXXXXXXXXXX-+++=++X XX++==++===-:..........\n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n \n \n .. \n :::::.. \n .... . -=...... .::..:...+ \n ..::.... .. .: .. .--:::.+... .. .. . .. . \n . ...:..-... .. .. .=.. .. .:=:::::.:. ... .:::.. .: .. . .... \n . ..:--::...:...::..: .:-:..-:... ....:::::-:....+..:-:.::. .=. .. .:....::::. \n ..:.:..::-=--:..+....::::. ...: --+-:.-. .......::+-::::: ---::::... ... ..:::. . ..::::.. ::--- -.. .. \n ..::-:::---+=-:..::..:::-:....::--=++=-:-:-:::-....:-+=-=+==++=-.-+::..:::..= ..::.....:::-:-=-----=-:......+:...= . \n .-.:::::::--.+===--:::-=:.==-:.+.::-+XX+++++-- :.. ..:-XX++++XX++++==-=-::.. ..=:=+--:++=:=-=====:=-:::...=.::......+ .. \n ......::-:--=----=++==---==-=+XX=-:..:-=+XXX+++:==--:...:--+XX:XXX+XX++=-==+-+-:=...::: +X++XX++=+++ +=+=----=:.::::-::-..... .. \n .:::::-::--+=--=---+X+===-=-=+XX++=:::-=+XXX.+++===-:-..:-=+X:X:XXXXX XX===XX-++=--+-::-=+XX++:XXXXXX++=-::--====---:::--:...+ ... \n ..:-----:-==X++==-.--+XX++==+X=X++==+==+XXXXX+++==----: :-=+XXXXXXXXXX+=====+XXXXX+++=---+XXXXXXXXXXXXX+=----=+X++==--=-=--:.+..=.... \n .+.::--==-==++XXX+=----+=+XXXXXXXXX++==+++++=XXX=XXX==------=+XXXXXXX XX.+===== +X-XXX+XX===+XXX=XX=XXX=XX+.++++XXXXX:+++++==-:.... .....-\n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n \n \n . ...... \n . .. .. ...=::.. \n .+.. . .:. . .:.. .=... : \n ...:.-.. .: . .:. =.. ..:.....:. . . . \n ...:::..:... ..... .:.....-: ........-+:... ..:.:::. .: .... \n ......-:--::. +.. ... . ..:::::=:...... -..:==:.:::..:=-:::. . .. ... ......: ::... . \n ..:...:-=--::...:.:+:..-::..::.---==-=-:.:.:.. ..:==----:-=+==--::.. ... .::. ...+:::---=:::-:.. .... \n ...:::=::-=--:--=::--::-==::..:.-+++===+=-::.. ..:-X++++++++++=--+-:.. ...:-:::-==------==----::.-... ..... \n .. -..::-:: ::-=------------X=-:....-+XX++====:--: .:=X =XXXXXX+==--=+-::::..:....-+====+===-= ===---::-::....+.....= \n ..+:..::::-=--::::-++==--:::--+X+=-:..:-=+XX+=------:. ..-=+XXXXXXXX+=+=--=X==:=-=::..::+++==+++++++++=--:::==---::::.::.... .. \n .:::::::--=== -:-:-=X:X==::-=++++=-=====+-XX++==-::-:..+:-=+.X+XXXXX+=-=--+XXX-X+==-:::-=XXXXXXXXXXX++=-=:::-++===-:::--::.. ..+ \n ..:---===-==+++=-.:::-++XX+==+XX++==-====+XXXXX+++=---::::-=+XXXXXXXX++=-:-==++XX+++=+=--=X:XXXXXXXX=X+++=-=-=+XXXXX+===--+:... ...= \n ...::.--+==++X+ +++=--.:=++XXXXXXXX+.+==++====++XXXXX+==+=-==+X:X-XXXXXXX+==--+==+XX.+=-=+==++XXX ++XXX-XXX: XX X-XXXXXX+ +=--+:...:......=\n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n \n . \n . .. . ........ \n .. . . .. ..:.. :. \n .... . .. .. +...-=.. .. \n ....... .. . .. .-. .. ....--:.. . ..: :... \n .. . .:::.. . : .+...::. ..:-:.........::-........:-=-::-.. . .. -... :..... \n ..=...--::-=.....=+....-:..:.....:----:.:... ..:=-:::::--=-==--:. ... ......:.:--....... \n ...:...:--=:..:::-...:::+-...:.:--=-----:::. ..:+X+==++ ++== -=-:... ...:-..---:--:.:---:::=:.. . \n ...-::..::-=:+::=::::::=-+-:....-+X+===----:.. .-+ XXXXX++X==-:-=-::..... ..-=---=---::::---- :=::.:.. ..... \n . ......:::--:.::.-+=--+:..:::-=+--::.:-=X++==----:::...:-=+XXXXX+++=-=-:-==---:.+. .:=+X=:===-.-===--::: :-:::-:........ \n ...:::.::-=---::::-=X+=--::::-==+=--==--+XX++=--=:::....:-=+XXXXX+=+---:--XX-XX+=-::..:-=++++++--XX+=--:..-:-==---:-::-:=.. . \n ..::----:-=-== -::.:=+=++=---===+= --===+XX+++===X---:..:-=+XXXXXX+==--::--=+X++==-=-::-=+XX+XX+X XXX+==-:::-=X++++=----:::.. .+. \n ...:: -----=====+=-::::-=++X++++XXX+=--==+=+:++XX:+++ ==-:--=+=XX=XXXX++==-:-==+X++==--==--+XXXX+=++XXX XX+++++XXXX ++====-.--... +.... \n ...-.:: ----:===.+==X+=-===++X.XXXXX++X+XX+=-==-==+XXXX++====+XX=X XXXX+XXX+======XXX++=-=-===++XXXX++X XXXX+XXX=XXXXX ++++===-:--:..........\n",
|
||||||
|
" \n \n \n \n \n \n \n \n . \n . \n . ..:: \n . .. ::. . \n . . . .. .: ::. \n +... . .:.. .: .:::. .. ::...:.. . \n ::.. :.... ..::.:.. ..:-::.. .:: --:.:. . . \n =....-::.... . :-::. ..::.::-.::. .:--::.:-::-----+-.. .. .. .....:. . \n .... ::-.:....:.. . ..:=:.:..::::::..::-.. ..=+=--=+X+--=--==:... . ...... :::.. ... ::..... \n .-... ..:-=:...:::......--:=..:=++=--:::::. ..=XX++XX++===:----::.. ..-::.--:........::::-:.. . \n .....-::-...:-+:::::..-...:--::-::==++=--:.::......:-=+XX-++==-=-:::-+-::-:.:. ..:+X+==---:-::-::::.:::............ \n .. .:..---:::...:=+--::...:.:-=-::-==-=+++==-:-=::....:-=XX:X++=-=-::-:=X+=++=-:....:-=========++=-:. ...:-:::::::.:: .. \n .::-:.:::--=--::..:-=+=--:+:----=-=::-+XX.X+==---=-- ..:-+XXXXX++=--:..:--+X+==----:..:-+:+++++++=X+---:..::=X====-::.:::. \n ..:::-:::- ---+=:...:--=++=+=.==++=-:---++X+X+===-=--=:::-+XXX==XXX+=-- ::--=+:+==-:-=:-=+XXXX+=++++XX++== ==++.X+=----:::::. \n ..+...::-----------+=::--=+X+++++==:++====----===X+=+=+=+==-=XXXXXXXXXXXX+===+==+XX+==-:-=-=+++XXX+=:++XXXXXX+X-X+++++=+=---:::+:......... \n .....::::-----------=++==+:XXXX-X.XXXX+X+=-::-:--==XX+X+XX+ ++XX+:+XXXXX-X:+++++.+XXXX=--=.====+XXXX++XXXXXXXXXXXXXX+XX++==--::::--...::.....\n",
|
||||||
|
" \n \n \n \n \n \n \n . \n .: \n . :. \n . . .. . \n . . .. \n .. ... .. .:. .::- \n :.. .:. ......: .::. ...::-:. \n ::.. .. .-. . . ..... ..:::.. ---:: =---:. .. \n . ..:::. . . ..:-.. .:..:........ .:----=+-+=----::-:. . ..... . ..:. \n .. ....::-:. ..... ..::-:.-====-:.:.... .:+X+=:++=-----:--:.. .:.:.::.... .. .:.. \n .. :.. ....--:..: .. .::..:-=--===::..::: .=.:==X+X+==--- -:.:-:...-: .:-++==--:.:..::........ . . .. \n ..::-::...:.:--:.....:. ..: ::.:-=.===-=-::--:.=.:-=+XXXX+=- -+:::-+=--+-::. .:--=-=-----=+=-:.. .=.::..- .....+. \n ...::....::--:+....:--:::::.::+::::..:-+XX+=-:-::--::..:+XXXXX=+=--::..:-=++==-::.:...:-=+=+=====++=::.....:==-:-::....... \n ....:..::::::--:....:-=---===:--==-:+::=++X++-:-:-:=--::=XXXXX++++=-:::=-:-=++=--::-=::=+XX+X+=-==+X+==-----=++==-:::::..... \n ....-::::::::::::=-..:-==X++=====-==---.::-=+=X+--- -==--:-+ ++XX+X+X+=---==-=+XX==-::-=--=++XXX+==+=+XXXXXX++++==+=--::::...:. ... \n ...-..::::----:::-:-=---=+XX-XX++++====+-:..: ---++=-+=+====+X++=++XXXXXX+===+++X=++=-::--=-==-+X++=+-+XXXXXXXXX++++ +=+=-::.:..:.-...-... \n -.....::----==-------=== +XXXXXXXXXX++-++-::.::====XX++++==+++XX ++++XX X XXXXXX++++++=--==+==-+++XXXXXXXXXXXXXXX++-XXX+++=--:::: :.:-::::.:.\n",
|
||||||
|
" \n \n \n \n \n \n \n : \n . \n \n . . ... .. \n . ... . :. ::. \n .. .. . .... .::.:..:-.:.. \n .:... .:. . . +..=::===--::-:.. \n ..::. .::......... . . ..:--:--X+=-::::- .. \n .. ..:. . .-...:===--:.... .: ..-XX+====:-::::::.. ... :...:. \n .:.:. .::. .. . .....-==:- -:...:.:....:-+X++++=----::..-:...:.. .-=+=--:..::::... .. . \n .:::... .::. .. .. ::....--+=--::: ..-:...:=+:++X++=-+:::::-=-::.-... .---=----::-+=::.. .... .. \n .. ...::.. :: .=.::...........:=+X+=-:.:..--:::-+XXXX+===-::..:--+X=--::... ..:-+==+=---==--::.....:+:....+ .... \n ...... ..:.:.::. .:----::-=:.:=:::::.-=+X=-:. .::::..:-+=XXX+==-::.=.--:-++=--::.::.:-=++ X=----=+=--:::+-==-:::.+.. . \n ..............:-....:--=++=---==---:::::--=X=-:.:::---:::-X+++XX+==-::::==-=++==-:.:- --==+X++=---==XX++XX+==-==--=:::... . \n .-....:-:-:-::+::::-::--=+XX++=======-=::..:---+=--:==-----.+X+==+XX+X+=--===.XX:==::..:-=-===X+==-= =+XXXXXXXX+==+==+--:........ .... \n .... ::: . ------:=::--=+XXXXX-X++X==---:....:-=+++=-==-----++X+==++XXXXXXXXXXX++++=-::.:-=--==+XX+=++++XXXXXXXX++XX+:++=--:.:......:::.... \n ......::--======.----==+XXXX-XXXXX-====--:::::===+XX ++====+++X++XX++-+X=XX+XXX+:++X+=- -=.====+XX:XX+XXXXXXXXXX++++.XXX+==--::::::::::::.+.\n",
|
||||||
|
" \n \n \n \n \n . \n . \n . \n \n .. ..+ \n . . . .::.::..:: \n . . :. ..:--=-:::.. \n .. . . ..-..:--=-:::::. \n . .. .....-. . .-::+::--=:::.:.. \n . .. .:-=--:...... .:...-=XX+=-==-:.:+:.. .. :+...... \n .. .. . ..:-:==-:::. .. :..:-=++======-:.:. ....: :--=-::.-..::. . \n .... :. ..= .....:--==--:... ..:..::-+++=====-...:::--:.. .-:=--:-=-:--:::.. \n .... ::....+:. .. ...:-+==-..... ..:..:-++++==--:...:--=X+=-::. .. ..--=---=----.:::. -....:.. \n . .. ..::==::.::=........:=++=-... .:....-+XXX+=--:....:---== =-:....:..:-+===-::-:--.:.:::::-=::+.. \n ......... .:. ..::-===-:::-=--:..:.--++--:..:.::::.:=+X++X+---:...:=--===:--..::--:-=++=---::-=X=-=++======--:..:. \n .......::.:.::...::..:-===-++=-:===---:..:.:-=+-::.--:.:::-=++==+X===--::-==++X+=-....:----=.++-::.-==XX+X++XX+=====-::....: \n ....::::::-:::-:.::--+XXXXXX++==+=--:-...:.::-=X=-::-:::::-==+===++XX++==X++X+-++-:..=.------++=======+XX.XXXX++++==--=::.:. ....:..... \n ...:.:::-==- -=.::--=+XXXXXXXX+++:---:. .::=-:-+X+=-----:=.====++++XXXX+++.X++==++=:::-= --==+XXXX.+++XXX:XXX-XX++++=++=-:- .....::::.... \n ......::--==+.==+=--=++XXXXXXXXXXX==---::: ==X+===+XX++==+++.==++++XX+=+.XX++:+==++XX+=-=+=.===++XXXXX-X.XX=XX++==-++X+XX+==.-::: ::-:::=...\n",
|
||||||
|
" \n \n \n \n \n \n \n \n . .. ..- .... \n .::-:::. . \n . :..::-:.. \n .. .......::.::... \n .... .. ..::==--::-:.... \n .:---.-.. ...-=+:+==-::::..+.. .. .. \n . :::=-::... .=.---+==----+:..:. .. ::=-:....::.. . \n . . . . .:--=--:: .-..--=+===---:..::.::.. :::=-:.::-::.... \n . ....= . . ..-=--:. . .:-=X+==--:.. .:-=+++=-.. .:=:--:.:::::.. . . \n ..:--:=.....=. .-.== -:.. -... .-=+XX=--::. :-+===--:.. .-. ..-==--::.:=:...-.::::-::.. \n . . ...::-----.. :--. +..:====-..:..:.+..:-==+X=-:::..=.:=--==--:...::-:: +===-::..:--:: =---==--:.-.. \n ........... .=...::---==--:::=-:-:.....:-+=-:..::.....:==++.+=---::.:==-++X=-:. ..-::-:=++=-=:::-++======X= =--::..... \n +:........::::..-.:=XXX+XXX=--::--:=:. .=..:-++--..::...:---====+==+--:=:=+ +++=:. .-----=X+==----==X++++=+++==--:::::... ..... \n .=.:=::::=-:--::.:-=+XXXXX=+==--:::.. .:.:--=+X=-::::.------==+XXXX++==+ ====++-:...-=:-=--=XX++====+XXXX+++X+++==-==-:::.. ....::.... \n ....::.--==--=-----=+XXXXX-X++=+--:::::..--=X+-=+X+====--=--===++XXXXXX++=-++= =++=: :-----==++XX+=++XXXXXXX++ ++=+++++=-- ::...:::::.=. \n ......::-===+=+++===++XXXXXXXXX+++===-:::.-=+XX+==+XXX===:+X==+XX+XXXXX+XX++======++X+=---== ++-=+XXXXXXXXXXXX++==+.+XXXX+=+==--::--::::....\n",
|
||||||
|
" \n \n \n \n \n \n \n . .... . \n ..:--::.. \n ....::.. \n .. . ..-:... \n ... ..:--:--:.::: . \n .::::.. . ...:---+=--:..... . . \n .::-+-.... ...==++=----::.... .--:.. . .. \n .-::---:.. ..:--==.---::..::=..... .=.--:..=.::=.. \n .. . .::--::. . .:-=+==--::=.. ::--:-::. .:::-:....::.. \n ..::. .. . .:-=--... .:-=X+=--::. .:-:++=--.. . .::--:::..::. ...=... \n ..:::-:.:..::-. ..:+=--:.-. .. .-==X+=-::.. .::====--:... ::.::==--::..:::....::::-=-:.. \n .. ........::-=---:.:-:::. .:-=+--:..:+.....: ==+X=-:: ....=--==+=-:=....::--+==-::..::=--:-::-=+--::.... \n .....+.::... ..:-== =-==--:..:::-:. ...:=+-::..... :-=-=.=+=--+.::-=--++X=-:. .:----+X=-:+:---++====.++==--:..:::.. . \n ........:-:::...:-+X XXXXX+--: :::.. .-.::-=X=-:..:..---:--==+X++.=----:== ++-:. .-:-:--+X++=---=++++++==++++--: :-::.. ...... \n .=..:::-:------..:-+XXXXXXX+==-::.... ..:-=+=++==---::.--:-=:=+.XXX++==-===--++-:..:-::----=+X+==-==XXXXX+=++ ++==--==::::. ...+:..... \n ....::-:-== ==-=--==+XXXXXX-+++=--::.:..:--+X+.==++=======- -=+++XXXXXXX+=======++=-::-:--.==++.X+=++XXXXXX++=+++=X+-++=-=--::..:::::... \n .....::-=-=+=++++++++XXXXXXXX++X+ +=--::--=+XX+==+XX+===.=+=++XX.++++XXXXX+====:==+X+= --==-+X==++XXXXXXXXXX++==.++:XXXX++===--.:--::::-.. \n",
|
||||||
|
" \n \n \n \n \n ... \n ......::.. \n .:..= \n .. \n . ...... .. \n ....=..--:+:. . .. \n .:--:.. ..-=--=-:.... . . \n ..::--:. .---==--:: .. .: .:-::.. \n ..:--.: ..---=-=--::....-=.... . .:::....:.. \n .:--::. .:-===-:::=.. ..:-::-:.. ..:::- ..... \n ..- . . ..==-:... ..-=+=-:-... ..==++-::. . ::-::..-... ..... \n .:::: .. .:+=-::. .. .-=++=-:... .:---=-::+......:.:X--:..:.....= ....:--::. \n .........-::::..:+::.. ..-==::.-.. .:===++=- ... :-:--==-::... .::-+==::...::-:..-..:----:.. \n .:.=. ..:--+=-----:::....:. .::=+-.=. ::::--+=-:: ..:-::-==+-:. :.::-++= --::-+X=-=----=---:.. .... \n .-... .:::: ..:=+XX++==+=:.:..... ..:-++::::.-. .:::--=XX==-::::.:--+X-:. .:.: :-=X=--:::-=+= ==--==+--:.-..::.. ...=. \n ........::=-::..:-++XX++-+=--:.-. .:-++++-:.:--..::::--=+XXX+=--::-+--==-:. .::.::: :-=+--::-=+X+-+==:==+=-::::-::... . .... \n .+..::::-:---:-:::-+ XXXX++==+-::...=. .::-+X ===-::::-------==++X.XX+==--=---=+=:..:..::::--==+=--+XXXXX+=====++=-==----::...:.::.:. \n .:.::--=====++====XXXXXXX+==+==+=-:::::-==++==++==--=::--==++==+XXX-XX++.======X+-::.::=-=++==++++XXXXXXX+==-=+=++ ++==----::::::::.=. \n .. ..::-===+==++XX+XXXXXXXXX++=++X+X+=-=== +=++=++XX:==---=+++++++=+++XXXX++=====-+XX==-===-+X==++XXXXXXXXXX+=-==++X+X+-+===+=-----::.... \n",
|
||||||
|
" \n \n \n .:. \n \n ...=. \n \n \n .:.... \n .-..::... \n .:---::. .:-=--:=.... .. . \n .:=::. ::.:=-- ::.-. ..::-:.. \n ..-:.. .-::=--:.... ..:... ..-... . \n ..=::. .::---::.... :--::.. ...-:... . \n .. ..+-:.. .-=--::. :--+-:... .::+:...... ... \n :..:.-. ..==:.. .. :-++==-::. .---:=-:.=. .::+-:..=. .:::... \n .:::..:.... ... .-==-. . .::-=+=--:. .:: :-=--... ..::=+--::...:........:::-:. \n .......::::+-:.::.... . ::==:..: .::--=X=-::...::.::-==-:. ...:-=+=---::-+==-:::::--::.. ... \n . ..-:.=.:-++++=--+-:-.. .:-+-:...... ...::-=X+=--:...:.:=+=-:. ...:.::==-:::+-==:----::--=-:. ...... .. \n . . ...:: =...-==+X++===-:-.... .:=++=:...::. ..:---=+XX+=-::..::--==:. .:... ...:=-::.::=+=====--==+-:.....:+.. ..+.. \n .......::::::..:-=+XX+=.=-==:::... ..:-==+=-:.....:..:---==++XXX=-:---::-+=::. . ..=:+:-.-::.=+XXXX====-=+=-:-:::::.. ...... \n ..:::------=--- =+X:X++==-== =++=:.:.:::--=:===-::....:+:-= ==++XXXX+==--===.X+-:.. ...:-=---=--=+XXX.XX+=--====+=+=----:.......... \n . .=.:------==+X++++XXXX++.==-==+XX+-+-=-===-=.=+++==::::-=X++++=++XXXXXX+=--==-=+X=-::--:-=X===+=++XXXXX=X+=-=+=+XX+=====-=--:::::=... \n . . ...:--======+++XXXXXXXXXX++++++XXX++===:+++==+XXX.++=---=+XXXXX+-+++XXXXX+=======+X+====++=+.++XXXXXXXXXXXX==+++XXXX++ ======---:.::..- .\n",
|
||||||
|
" . \n .. \n \n \n \n \n . \n .... \n . ..--....... . \n ..:==+::. .::--+.. . . .. \n .+.-.+. ...:--.:. .. =..-:... \n ..-:. .:-:---.. . . . .:... \n :+-. ..:--+... .:-::.. ..:-:. \n .==:.. ..-+--:... .::-=-.. .:=-.. \n .:-.... .-+-:. ..:++--::.:. .::----:. ..==:::. ....: \n .::...... .--=:. .:-=+--::.. .....::--:.. ..-+-::.. .. .. ...+::.. \n .. ...:-::.... . .:-:.. . .. ..:-=XX-::. ......:=--.. ..:-=:-:..:==-:::.=.-:::.. \n +..:..:-=:=+--:::.... .:==:... .. ...--=X+--:.. ..:==--:. ..-:..::--=-- ::-:-::-:. . \n .......:--=+==-:-:::... .:--==-.. . ..:--=+XX+=-::...:-=+-:. .. . ..:-::..:--==-==::.:=-:... ..=. \n .........:....::=:=X+=-----:--:.:. ..:-=.=:.-. -...:--==+XXX=-::--::+=+-:. .:::+:...:-=X++X=----==-:::::..-. ..+. \n .+..:::::-::::.:-=+X+==-:----+==+=:+..::=--==---::. ..:-=:--==:++XX-=-:--==-=X=:. .:--::-:::-=+=XXX++=--=======- :::....=:... \n .. :--:-:--==+++=+X+X+==-- :--=+XX=-:---=--= ====--...::-+==+XX++XXX:X=--::- -=+=:..:-:::-==--=-==+XXX:++=+-===+XX==.-----::::=...: \n . ..+:----=--==+++=XXXXX+==++===-+++=---=+==-+XXXXX+=-::--==+ XX+XXXXXXX++=---=--=++-::--==-=X=:+++XXXXXXXX++-== +X-X++=---- -:::+::.. \n . ..:::--=++= =+=+=XX+XXX:X++XX++++++==++X++==+XXXX-X++=-==++XXXX:X X-XXXXXX+=+===+-++==+-===+X+X:XXXXXXXX.X+===+XX++XXX+==.--=--:::+:+....\n",
|
||||||
|
" \n \n \n \n \n \n . \n .-:. .:----:.. \n .:--:=.. ...::=-:.. .:. \n . ::.. .:--::. ..... \n .-=:. ..:--::. . ..::. \n :=:.. ..==::. .:--::. .:-:.. \n ... .-=:. :..++-:.. . .:.:-::. .-:.. \n ... :=:. ..=+-::... . ...::--: .--:+.. .. \n ...=... .::. ..--+==-.. .:. ..::-::. :-::.. . . .... \n .. ::-.. . .::. .:-=++-:.. ..==::. .::... :--::.....::.-.. \n . ...+:::--:::..= ..---.. ..:-=++-::. ..==-:.. ..::. .=::-:::::-.. ::.. \n . . ..::::----.::::.-. . .- --:. ..:--=++=-:....+.-=+-.. ...:. ..::-----+-:.:-:: .... \n -...... ..:---==-::.::-:::.::. +..:==--:.. .:=::--=+++X--:::::::=+-.. .:..:.-...:=+===+=-:.--:::...... \n ...::..:.:: ---=++=-::.::-++--:==:..::-:-+--:-.. ..:-::--++++ +=:..::--:=+-.. . ..:-..:::::-+X=++=--::-==-==-:=........=:. \n ..:::::::--:-=-++X+=--:::::--=====-::-= --=--=-:. ..:-=---=+.++XX+=::...-::-=:. ..:::.:-::=::--=+XX+====-=--=++=-+::::.. ....- \n ..:=------:-=-=+X+X+=--=-----=.=--:--X--=+X+++==-:..::-==++XXXXXXXX+=-::::-:--=-.. .:-:--+--=-=++X+XX+++==---=++==---=:::=:::...+ \n +..::--=---===+==+++++++=-++========-=XX+==+XXXXXX+=:::--=+XXXXXXXXXXXXX+-----====-::-:-==+XXX-XXXXX.XX X+=---==+++-=------:::::..=. \n ..: :..:::-==.=--=+X++++XX+-XXXXXX+:==++++XX+++=+XXXXXXXX=--=+XX-XXXX XXX.XX+++ ++==+XX++==-=-=+ X-XXXX+XXXXXX+==.++=++XXX=----=--::=:::: ...\n",
|
||||||
|
" \n \n \n \n \n .. ...... . \n .::.. ..::=+-::. \n .--... .. ...-:.. ..- \n ..:-. ..-:.+. .:. \n .-..- ..=-:. .. ..: \n -:. ..+-:.. .:---::. .:.. \n ..- ::. ..-+:::. .:=::. :... \n .-. ..-+=--:. . .+.-:.. ::.. \n . .. .:. .--====:.. ..=-:. .:+.. ... \n ...... ..-. .:-==-: : .+=:.. ... ::- .....:.... . \n ... ..... . ..:::. .:.:-===-:. ...-+-:. .. .:.:-:....:...:.. \n .......::........ .:=-:: .::.::==++=:=..:..--=-. ... . .::-:: :--:.:-.:. .. \n . . ..::::---:. ..:.:....--:.....::--:--: ..:+-:-:-==++=::...:::-=:. . ........:-+-:-=-:::::...=.. \n -...-......-::--+=-:....:---.-=--=:+:-=--:-::.. . .:-::::=====+-:. .: :--. .. . . ....+..-++===--::::--::-:..+. \n .-.::...:...::-=+=-::.-.:.:---.=--::-+-:---:-:.. . .:-=--== == ==:.. ..:--. ..:..:......:-+++===--::::-=-:-:... . :.. \n .::::::::.::--==+=--:::+ :::-----::-++==++=---:......:-+X++X+++X++=-:......--:. ..::=-:.:--=++++==--=: :-==---:::......... \n ..:::-::+:--=--====+===+==--------:-=X++ XX+++X++=-:..:-=+ XXXXXXX X++-:::: -.--:..:.:-=+X+++ +=+XX+X++==:::--= ==--.:::::..... . \n ....::---.::-++===.+XX:XXXX+++--=====+++==++.XXXX++==-::-==+XXXXXXXXXXXX+=.==-==+==+--:--=+XXXXX-XX.XXXX=+=--==-==++=-:.:--:: ..:.:... \n .+...::-:--===--=++=+++++XXXXXXXXX+===+X+++++=+=+XX=XX++====++XXXXXXXXXXXX=++==+=+XX-XX++=-:==++XXXXXXXXXXXXXX+==+X=++XX+=-:-=-:::: :::=..:.\n",
|
||||||
|
" \n \n \n \n ... ..+-:.... \n ::. ...::-::.. \n .::. . ..-... . \n .. .-=:: .. \n : .:=-:.. ... .. \n :. .-+:::. .::-=::. . \n . .:X-::-. .. ...:... . \n .. .:=--:-.. ..-:. . \n . ::=-=--.. .+-: . . \n .... ...:---=-:.. . .-=:.. . ..::. .. \n . ..:... .-:..:--==-.. ..+.:+-:. ... ...::.. ..... \n ..... . .:::=-:. .::...-=-==:. ..::-=:. ... .:.::.:..:...:. \n ....--:..... .. ..-:.. :=::.::. . :--:.:----=-:. ...-:. . . . ..-::.:.::..::-.. . \n .. ..::-::. ........-:-=::==::..:..: .:--::--=----.. ..::. .. . .-=----::....::.::.. \n .... .. .:::-=-:.. ...::-:-=-:-::=-:=::.. .:-=---=====-. . .-.. . .. ..-==---:.:-....::::... \n ...........::--=-:::..-::.:-::--:.:-+==--:..-... .:-++XX++++==-:. . .::.. ..:.=. ...:-=++- -::::..:--:-:=.. . . . \n ....::.:..:-::--=:=----:+:-=--:-:.:-++XXX+=-=+=---:...:-=+XXXX=XX+==:....+:::::. ....:=+==-::--==+XX==--:..::----::. ......=.. \n ....:-:::::+=----==++X++====+-:----.==+=+++X++=+==--=:.:--=+X=XXX=XXX++-:+::--=-:--:..:-+XXX+:++==+XX XX=-:-.----+=-::..::......... \n .. .. :::----:::-=--= =+XXXXXXXXX+=--===+-====++XX++=+=-----==+++XXXXXXXX+++==+=XXXX++=-::--=+X+X++X++XXXXXXX=-==+===++=-::::::::...::.... \n ......::-===+=-=-====++-+XXXXXXX..++==+X+======+XXX-+=+=--=+X++++XXXXXXXX++======XXXXXX++-===+++=XXXXXX=XXXXX++==-++XX+XX+= ---::::::::::. .\n",
|
||||||
|
" \n \n .. :+:. \n ... .--::. . \n . ..--:. \n .. .--:. \n .. .:=:+.. \n . .--...:. .-:.. \n :=:..:... .::-:. . \n .=:. .. . :-:. \n .::::::. .:=:. \n .. ::-::::--: .. .--.. .. \n .. .-. .:..::+:--.. . .:+:. . ..... \n . .:...: .. :.:=::: ..:=:. . .... . .. \n .. .. .--::.:. :.:. ::.:::.. .::. ..:.. ..... \n .:...+ ::...:=-.....:. ..-=..::-:=::. ..: .:-....... .-... \n . ..:.. ..:-:::..-:...=.. .:--:::::=-:. :. .-=::::... .:... .. \n .. ....:::. .+.:+:::-.:-::-:. .. .:-=--:::--.. ... .. .-=-:-:.... ........ \n ..... ..:..::---:+.. ..:==:::- ..:==-::.. .-.:. ..:-=+=====--:. . ..:.. ..:. :--++-::.... .::..- \n ..:..- :...::---=-::...:-+:::-:..:-++X++-::--:-::. ..-+XXXXXXXX+=-. . ....::. .:--=-:..::--=++-:::....:-:-:.. . \n :..::......:.::-=X++==--:-++-::-::---=+++X==-----:-:..:-++X=XXXXX:+=-.-..:::::::.....=+XX+=---=--=+X++=-:: :-:-=-:.. .. ..+. \n ..+::-::..:::=:--=+XXXXX+=+X=-:--:=:---==+XX+===--::-----==XXXXXXXXX+=-+-=+X++++--:..:=-++X+=====.=+XXXX+=---=--==-:....: ........ \n .. ..:=---==::-:--====+XXXXXXXX+==-=- ==+---=+XX.+==--:=--===++XXXXXXXXX+==-==+XXXXX+=--:--.++XX++++.X+XXXXX+=--=++++++=-:.:::::...+:..-. \n ....::--=++=+=---.==++.+XXXXXXXXX++=+==+-====:XXXX+++=--====+XX-XX:XXXXX+====:==+=XXXXX+==-=++XXX-X==XXXXXXXX++==+X=XXX++=-:::::::::.::...-\n",
|
||||||
|
" \n . .:: \n .+. .-::. \n .:--:. . \n ..--.. \n . .:-:.. \n .-:...::. .:. \n :-....... .--:.. \n .-.. .. ..:-.. \n .:+:=.. .:-:. \n . ::+::..:=: .-:.. \n . . ..:.:::-. :-.. . .:.. \n .:..-. . ..:.... .:.-. .... \n .. .:-:... . .. :.::... .-:. ..... . \n .. -.. ..-::.... ..:-..-.::+.. .:. .::.. . .. \n .. ..:..:..-::.. .. +:::.=.:.:::. . .--:. .... ... \n . +.... ....:: :-:.:..-:=.. .::-:::..:-:. :. .-=::::... .. .. \n .. ..:::=.. ..-=:::::.:.:--... . .:--------::. ..:. .. .--=--:. ....-. \n . .. ..+..=:::.... ..:+-:-::.+.:=+=--:. .:.:.. ..:=+++++++=--. ..... :...:. ..:-=+-::.-.. .:::.. \n ..::.. ....:-==--::.=.:-=-=:::..:-=++XX=---=:-::. .:-+X+XX.XXX+=-.:. ....... . .-++==:..=::--==--::..:..::-:.. \n ..-.::... ..::::-=XX X+=---+=-::::-- -=+++X++==---::-:::-++:XXXXXX-+==:..:-=--:-:.. .:==+X+=------== +++=:::--::--:.. .... ..-. \n . ...:::-:...::-- ==+X-XXX:+++=-:---.-::-==+X+X+=---: ----==+ XXXXXXX:==---+XXXX-X=-:..:-++ X+====-=++XXX+X=::--= =-=-:.............. \n .. .:.--====--::---==-++XXXXXXX+==-=--==----+XXXX+==--:==--=++XX.XXX-XX++=---=+XXXX-++=-:--+++X+++ ++XX=XXX++=--+XXX+++=-:.....::.+.:..+. \n ......:--==+===-:--=++XX+XXXXXXXXX++++=====++== XXX-++=---===++XX+:XXXXXX+=======+XXXX++=====+XXX++XXXXXXXXXXX++==+XXXXX++=-::::::-:::.....=\n",
|
||||||
|
" ::.. \n .-:. \n .::. \n ..: .. \n : .:.: .. \n :.. . .::. \n :. .--.. \n .::. . .::. \n . :. . .. .:. \n -...:...:: :.. \n ..: . .:. .... \n .:-::. . . ..-. .:. . \n .::. .. ..:.. .. .. \n ... .:.... ....:....... .. :.. \n .:..:...: .:. ..=:.. ..... . .:-...+. \n .. .:=::::... .:. .::::. ..... .. . .=:.. \n .=... .-=:....... ::.. . .::::::--:.. .. .:-=-:.. ... \n . ...::.+. .-::......::-.--:.......... .-==+==--=+=:. .... ..:--:.. . :..:. \n .. ..::-+=-:.. ..=-::-...-==+==+-:--::-.....:++XX+:+ XX=-:. . .:==-=:. ...::---.. ..:...::. \n ..:.. ..+:-+XX++=::::-=-:....:---==++===--:::::::-=+XXXXXXXX+=-:... :=:-.. . .--==+=-::..:::-===+=:.:::::+-:. ... \n . ....::.. .:.::=++XX++==-==- ::-::--::==++=+=--:-=: ::--=XX=XX-XX++=--:-XX+===+-....:=+=++--::----=++=.++::::---:-:. ..... \n . ..::-----:..-::--==+X-X+++==--::--:-::-:-+X+=+=--::-=:::-==XX X-XXX++=+--=+XX+ ++=--..:-=++ ==--==+XXX++-+=--+++++==-:.............. \n ....::--====--::==++XXXXXXXXX++==-=--::-=++=X++=+.==-::-:--==+X+XXX:XX+===--=++XXXX++==----+XX+====+XXXXX=X++= =+XX+XX+=-:..:::::::..-.. \n .....+::--======--= XXXXXX.X-XX+XXXX+=---===+XX+=+XX.++=--===-++XXXXXX=X.-++====++XXX.X++---=++X=X=++XXXXX-XX.XX++++XXXXX+==-::===-- :::.....\n",
|
||||||
|
" -.. \n .. . \n .. .. \n . ... \n . .:: \n .:. :. \n ..: .. \n ... : \n .:.. .: .. \n ..:.. .. . \n .:::.. . . . \n . .. .. . :. \n .. ... .. . .. .. . :. \n .:-:... .. :.. . .-. \n .-::.... ...-..:=:. .-:. \n . ..::.. . . ..-.. .. ..:....::-=:. .::.+.. . \n ..:-:. .-:. . .::=:--:..::.+.....:-=-----::-=:.. =.. ..::.. . . \n .. .:-++=-.. .-:.. ::-.-:-::=-:......:=+X:X+===+X+-:. .::--. . .:::-:.........: \n ... ...:=XX+==-:.:..-::.. .:--------=-:::...::-+XXXX++++X+--... ... .-----:......-: --+=:.......:. . \n .:... ..::-=+++=--:.:-::..:::::--==+=---:::-..=:=:=++XXX+.++==--::=+==-::.:. .:-+-=--:..:::.-==-==+=::.:::.:. . \n ....:::::....:::--=+X++====-::..::...:-:-+X=--=:-:-==:::--=+XXXX+X+==--:-++XX====-:....-==+=-:::--++++====-====-==--:. . ....... \n ..::::-:--:::=X=== =+X++:+++--:.::.-.:-==+-+=-==-::.::::--=+X-XX++++==--:-=++XX+====-:.:-=XX=--::-=+XXX++===.=XX+==+=-:..:-::.....-. \n ......::-----==--+X+=+X:XXX+++XXX==--:::.-==+XX++==++.-:: -- ==+X+XX+XX++=+=-==++.XX+=+=-:--=+++=--==+XXXXXXXX+===++X+++==:::-=-:::: :.... \n ...+..:::-=+=----===+++XXXX X+XXX+X+=---- ++=++X++XXXXX+=====+X+++++XX+XXX+++===+XXXXX+==---=+.++X++XXXXXXXXXXXX++++XXX++=--------::-- ::....\n",
|
||||||
|
" \n . \n . .-. \n . \n . . \n .. . \n .. . \n .:. . \n .:.... \n .. \n . . . \n .:::.-.. . .. \n .:.. .:.. . \n ...... . . ....::. .:. \n .. .:. . .:..= ..... ...:.+..:=+. . ... \n .::-:. ..: .:..=:..:--:......::-:::-+:::-:.. ..- ..= \n ..--=--. . . .::..:..::.. . .:=++XX+==-=-=:. ..... :..-:. . \n .:==== -::.. ....-. .::.:-::.+::.-....:-=++XX=====--:. .. .::..: . .:::---:. . \n . . . .:=====-::.::++......:--+-=-:.:::......::=++XX+=+==--::.:==--:. ..-.:-::. . ..:--------:..+:... \n .. .........-:.:::-====-=:--:.. .....:::-==-:.::::::::::--++XX+==-=+--::==+XX=--::.. .:--==-:+..::--==------=-::--:.. .. \n ......::::..--=-:---=+==-===-:.. .. ..:====+-::-::..=:+:::-+XXX===+==-::-=+++X+=---::..--=+=-:...:-=.+++===-=+++=--=-:..:::.-.. .. \n ...:::::---:-==--=++X+-++=+==-::.:..::-=+X++=-=--=-..::-::-=+XX+=++=+=--:-=++XX+=-=-::-:=+==-::::-=+XXXXX+===++X+=:=---::-::...:.... \n ......::-.---:----==++XXX XXX++:++=--:::----=++XX.++++=:::-:-==+==+XX+XX++-=-==+XX++===-::=== -======+XXXXXXXXX+==+XX++=---=::-::=-::::=.. \n .. ..::-=----::-:===++.XXXX-XXX=XXXX==--.====+=++XXX.X++====+.++++ +X++XXX:+=.=++XX+:++==--==+ +:XX-XX-XXXXXX:XX++XXXX++=- ::-:-::::::::..=.\n",
|
||||||
|
" \n \n . \n \n \n .: \n \n ... \n . \n -.... \n ::.. \n .. .... . \n . .. . ...:--: \n . . .. ..-.::.. ......:::-: .. \n .::=. .. . .::.. -...:-:--::..... . . ..: \n .::=:--:. . ...... ... .:-===+=:: :::. . . ....::. \n .::::::.. ... .. ...::=. .:. .:----+++--=::-.. ... . ...::::.. . \n . .:------....... . .::::-:...:. .:-==++==------::..-.--:. ...:::. . .:::::--:. ..-. \n .. :....:.:-----:.... .:-::--:.:. ... . .::-=+X+=------::+=+==+-::. .::--:.. ..:::-:::-:-:+..... \n ......:-..:::::::---=---=---:. . .:=+-=-:::....-.:.:::-+X+=----- -::-++=++--:::.+..::-.-:. .::-==++-=-====-::-::...... \n ...:...+::+::::::-+++ +=----::.......::-=+==-=-::-:. ...::-+++=- -=+=-:::-==++=-::-::=:.-=--::...:-++XXX++=-==++=--:::............ \n .. .+.:::::.::+----=XXXXXX++======:::..:--+-++.++-==--:.:---:-===++==++.=----=+X+==+--::-=-----------+XXXX:XX+====+-+=--::.:::..::::..= \n ......::---::.::-- -=+X=XXXX+:++++XX+-::-=====++XXXXX+=----==:====+XXX:XX X==-=++X==-+=-::=--=-=+X++++XX-XXX:XX+==+XX+==-::::::::::::::.. \n ......::--==---::-==+.+++XXXXXXXXXXXXX+==++X+ ==++XXXXXX+-==.+++XX ++XX+XXXXX++=+=+X++X+=-: --=++XXXXXXXXXXXXXXX+-+XXXXX+=--::-.-::-::::.....\n",
|
||||||
|
" \n \n \n . \n . \n \n \n .. \n ..... \n . \n .=. \n . .. ..::: \n .:.. . .::. .... .: . \n .... . .. .:::::.. .. . \n . ..... . . . .-::---:..+. .... \n ......... . .:+: . ..-::-----:::... .. . . ..+.:. \n .....:.. ...::...+ .::--=+=-:::::-:...:::-: ::. ...::-:.. . \n ....:::::::- . . .:=:.::.... .::-=X+--:-::-=-==:--:-:.. ...::.. ..:-:::::...... \n :.. .. -...:.:---::::.::. ..----:::: ........:-=X=-::::.::.:==-----:.+. .:-::. ...: ------.--:.::. . \n ...:...:.....:-=+X+=--::.:.. .- -=-.-:..-::. . ..:=== --::-=-:..:-==+=-:..::-...=::::.. ..-====+==-----=-:::::... . . \n .......::.::.::=++XX-X+=-+--=-::. ..:-=-=+===--:::.. ::-:-=====- ===-:.::-+X=-- ::::-=:::--.:..:-+XXXXXXX+= ===--::::............. \n .....:: :::..--::-==++XX:X====+++==:.=.:-==.=+XXX+=--::::-----==+XXXX-++=-::=++=.---::..::.--=++=--=+XXXXXXX++===++=--::.::........... \n ....:::----::-:--.===-++XXX++X++X+X++-:-=++:===+XX-X+==--===++++++XXXXXXXX+=--==+==++=-:.-::==+XX.-XX:XXXXXXX++=++X++X+-:::::::::::..+... \n .....::---===---=+XX:+++X+++XXXXXXXX++===+XX+-==+XXXXXX=+++XXX:XX.X+XX+:XXX++=++==+XXX+=--:--=+++XX:XX-+XXXXX:++=+XXX+XX+=-:-===-:::::.=....\n",
|
||||||
|
" \n \n \n \n \n .. \n \n \n \n :. .. \n :. . .... \n . . .... . \n .. ...:.. . . \n . .. ....=::.. . .. \n . .. . .:-:-:....... :.. .. ..:. \n ..: ... .:..... .::+:-=-::..:::+.....=:. .. .=.:. \n ...::.+... ..--:.. .. .::--=-::....::=-:--:.:. ... -.....:..-. .. \n .. ..:-=--:: .. .. ...--::+:. .. ..:-=--:.. ..:.:--::-::. . ..::.. ....::..::..-.... \n ..........:+==--.:.:..:. .:.:-+-::.. ... .:--=--+:::::. .:--+=-:...:. ..::.. ..::--==--::::::..... \n :......:..:...:-+=:==+--:::+=:.. ...-==+==-:.-.. ...:---===---=-:-..:-=+-:...=.:::::::::....-=+X++++.+=--+-::...... .. \n :...+....::-::-=====:++=======+-:. ..:-==+.XX++-:....:+::--==+XX++-=-::.:==+--::+......:--++=-::-++XXX.++X ==+=--::..::=:+....... . \n ...+.::--:::+===+= ==+++X+=+.+==++=:.::-====+XX++==--::--==++=+XX.XX=XX+-::--==-==--.....:-=XXX +=++XXXX.+X+++++=== =::.::-::........ \n ....::+:-=----=+ ====+:++=+++:++++= =---+X+==++XX++====-=+XXXXX+XXX.X-XXX++--+==+X++=-:..::-++XX++XX+XXXXXX++==++++=+X=--:-:-::..:...... \n . ....::--=+++==+X=++=+===+++XXXXX++:==+++++XX++XXXX+++++-+X.XXXXXXXX+-XXX:++++X+=XXXX-+=-:--=++ XXXXXXXXXXXX++=-+++XXXXXX=-==---+::...:. ..\n",
|
||||||
|
" \n \n \n \n \n \n \n . . \n .. \n .. \n .. \n . . . \n . . .:.. . \n .. . .--:...:. .. =.. .. \n . .. .::... . ..-::....-: .. . . . \n .:-::...: --::. ..-:-:.... .:.:...=. . . -.... \n .--:::::.. .:--:. .::-:.::.. .....:-::-:. . .. ........ \n . ..:- --:--....=. .:-+-:.. .::--:-:..... =:::-:.. . .. =..:-=-:.:..... \n .. ...... -----=--:::--.. .:-=+++-:.. ..:-----+::-:. .::--:.. ......:::.. .:=:===--=+-::::.. ... \n .+.. :...:--:::---+==+=--==--=-. ..:-=+XX+==:. .::.-=-=XX++=--:.. .: -- ::.. .+.:-+X=-:.:--=++-===+==-=--:..+...::.. \n ......::..::=-------====:-+===-==-.. .:---=+X+==-::::: --==X=XX XXXXX=-:.:------:::. ..::+X+ ==--=++X++=++==+=--:--:..:::..... . \n .....::=--::=------==-= ====++==:--:::=+===++X+==--:-::=++XX -:XXXXXXX+-::---=++=-:.. .:-=++==+===+XXXXX+===++=--=+=-::-::.+.....: \n ....::- ===--======-+-===++XXXX+=---+====+X++:XX++++=--=+XXXXXXXXXXXXXX++==+X++XXXX+-::.:--==+X++++=XXXX.++=====++ XX++=:--::::..+.. .. .\n ....:::-:==++==== ==+-===+=+XXXXX+==-= =+==XXXXXXXX++XX===+XXXXXXXXXXXXXXX+==+++-XXXXX =--===++XXX=X.X.XXXXX++:==++XX+XX+=.--:::.:.........\n",
|
||||||
|
" \n \n \n \n \n \n \n \n . \n \n \n ..- \n .. .::. . . \n .:. .::.......... \n .:... -:..+ . .:...=. .. \n .::..:.. .:-:. ..=::.:... ..........: . \n ..: -:::::....-. .:--:.. ..::---:..... ::..-:. . .::...:. \n ....-::--:-=:::.::. ..:.==::. .::----::: .. .:.::..: .... .::--:::-:... . \n ...::::..::=-=---.--:--:. .:--XX+=-:.. ..::-=====--:=:. ..:-::.+. ...:==-:-..:- -=----==-:::...-.. .. \n . ......::-:::::::- -=---.---=-:. ..:--+X+==--::. ..::-=+X+XXXXX+=:....:-----:.. ..:-XX==-::---====-=+=---:::......:... \n .-....:-::-:--:--: --==---.--==-- .. :-=--==++=--::::::--==+X XXXXX-X+-:::=--:=-:::. ..:-=+=--=--==+++==++==+=-+--=-:.::..... \n ....:::---:-----=--=====-===-+==----+--=++==+X====-::--=++XXXXXXXXXXXX+=+--==+XX+=-:....:-==+====+=+XX+X+==+==-==.===-::::::.:.... \n ...+:---===---.======-===+X:XX++==-----==+XXXXXX+==----==+XXXXXXXXXXXXX++=+X+ +XXXX+=-:+:--=+++++X++XXXXX+++=====+X++.==-::-::......... .\n .....::-==++.==-==-++=+==+++XXXXXX+========XXXXXXXX-+++++XX-X:XXX+XXXXXX ++=++==+XXXX:X+==.=++XXXXXXXXXXXXXXX+===:+XX=XX+=--:-::::.........\n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n \n . .. \n . .. \n . ..+ \n .. ..:.. ....... \n ....-..- . :-:. . ...::.=. .. ... \n ...=..+.::. .::. : :.. ..::-::....... . ..: .... \n .. ........:::::..::. ..:-=-:.. ..:-----::..-... .... :... .:.:.....:. .. \n ........:.+.::.::.:-:. ...:=+==-:..- ..:-=+X+=--::..+ . ....:. .:.:=-:.. .:..::::::-:. ... . \n ............::::::--::-=-.. ..:-=++=--:::... ..::-=+:XX+-+=-:.....::-::.. .:==-:-::+::::----.-=-::..:.....+. \n .....:..::::::::::--:+::::--.=:..-:-:-==:=+=-:..+.:.::-==+XXX++XX+-:..::-==--:.. ..:-==--:::-=-==-=--=--+-::::::.... . \n ......: ::.::::.---.-=---.=-+-----:::::--++====--:..-::-==++XXXXXXXX+=--::-=+XX+=-:.....:--=------==X==:==--=-==-----:......... \n .:.:::.---::.--+:--+-==++======-:--::---=+X-+++==-.::-=++++:XXX:XXXXX+======++XXX+=-::.::-========++X++++==+====++==--::... -..... \n .....:--:===+------=--====+XX.++X+=-==---=+XXXXXXX+==----=++:X:X=XXXXXXX+=++===+X.X++==---=++XX+++ XXXX==XX++=-==+XXX++=-::::::.......... \n ......::-=.=+ ==--=++++====+X=XX:XXX+ +X==++++X XXXXX++===-+XXX.XXXXX.-XX++=+==.=+XX-+++:+=++XXXXXX.XX.XXXXX+.X+=+++XXX X+=--::--::.........\n",
|
||||||
|
" \n \n \n \n \n \n \n \n .. \n \n \n \n .: .-.. \n . .... .. ::.. \n ..... :.. .::. ...::::......... \n . ...:.. ..:. ..= ::.. ....:--==-- ::.. ... ... . .. \n ... .......::. ..:----::=..= ..:-+XX= =--:... ..=.::. .=.:.:.. .. ..=...::. \n . +......:.........:: . ..:-----=::.. ..:--=XX+====::.=....::::. ..:--::=....-....:::::::.... .. \n .... .. .=.::-::::::....:::..=.::-------::....:.:---=++===++==-:..:-==--::.. .:----::::::::--::--+:::::....=.. \n ...............::----- -:: ::::.:=:::-===----:.=..::-=-=+X+X++++==-:::=-+XX+=--. ...:::--:::=::-=--=---:----.::.::. . \n ....::::....::::+:---:==---=-=-::+::::--=X+===-:+:..::-==++X XXXXX+==:--=-=+X+=+=-::..::------:--==+=++==---====---::.-.... \n ...:::--:-:::::------===++====++-----++-=+X+X+=:=-:.:::-=+-+XXXXXXX++++-==-=+-+===---:: -=+++== =:+++XX+++===+=+X+==--::... .-... \n ..+...:----------=========XX=X++-+X+===---==++=XX+==+==--==+++XXXXXXXXX+++==--==+X++===---++XXXX+.++XXXXXXXXX++===+XX++==--:::::.-.......- \n ....=..::-========-=:++++ +++XXXXXXXX++=====+-=++XXXXX+++++XX++XX:XXXXXX.X+======+XX.+++++++==+X:XXXXXXXX+X:XXX-X+++XXXX++==--:--:::::.......\n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n \n . \n .. ..... \n . .::..:. \n .. .:.. .. ..-:-:::.. .. \n . .-. ....... . ..:::=---::::.. ... . ... \n ... . ... .:::...... .. ..:--++=-----:. ...:.. .. ...... . . \n :..: ..:. ... ..:=--::.... . .:: -=++==-=---:=....:::... .:::..... . +....... \n ..:-:.::.. .. ..-...:-:-- :::.....::---+====- ----::..:--=:-:.. .:--::.......+=:.:::........ \n . . ..:-:::::+.....+=.::-...-=--::::....:.::- -+=====+=+-::+:-=-++=--:. ..:-::.:.. ..::=::-::=-:::::..... \n ...............::------+::::::..::::::-=+==-::... .::--=+X+++ +X+=-: :--++==---::...::=--::.=.::--====--:--==-::.... \n ..:-::::..-.::-:-=-- =+==--:--=-:::::---=++=-::::....::-=++XXXX++++=+=-.--===.---: ::.:-=++=--:-==+++++-==-=-=+==--::.......... \n ...:::.:::::-:----- ==+XX+=====++---.:--=++++=---=-+::---=+XXXXXXXX+==--:---=++===- ::---+X++=-===++XXXX++=-==+++==--::::.:...... .:. \n ....:.::----------===-===+XXX X++ +X+===--+-==:+XX+==:====++=+++XX.XXXXX+===--==+XX+= ==--===+XX+++++X=XX XXXXX+++++X++==-.-:+:::.......... \n ..=...::---.-==+=+==++++++++-=XXXXXX X++++:+==:=++XXX++ +XXXX+XXXXXXX+.+XX++===+XXXX=X+.==--:==+XXXXXXXXXXXXXXXXX+++XX=++++-=-----.::........\n",
|
||||||
|
" \n \n \n \n \n \n \n \n \n :... \n .:..: \n .. .:.:..+. \n .. . .. ...::.:.. . .. \n . .:-:-:--:...:. . . \n .. . .:..... . .::+===--:.::-:=.. ...:.. . \n .. . .:=-:::.. -.. .::-+===--::---:.....::+.. .:.... . ... \n .:...... ...::--::-:..... ..::==-=--::-:-.::.::--:::... .--::.. . .... . \n .::::........ ......----... .. .:+::==-------=-::::-===-::::. .::--:. .. ......::........ \n .. ..+.::-::--:..:.:...+..:..::==-:....: .::::-++========::.:-+=.=-::::.-..:-.-:::. ...::-----:: ---::.. \n .....-.+..:.:.:::--+==--::::-:-:..:::::-=--:+...+...:+:-=++X++== =--:::-+--=-:::..:..:-==--::.:--=+++==-----==--::.. . \n .....-.=..:::-:::--=-=+=-----:-=::.: ::--==--:::::..::+--+XXXX-====--:.::-=-==---::.::::=+==-----=+XX++-==:-===.--::........ \n ....-:::::::.------= -=+X++===+=.==-- --:--==++=-+--::-:====+XXXX+-+==-::=--=++==---:.:--==++==--=++ XXXXX++===+++===-::-:::....+:... \n ....-.::-:------====+=.=++:XX+X++++ ===+ ======+XX+==-== ++++++X+XXXXX=+==-==++XXXX==--::+--==XX++-+X XXXXXX:++++++++====-+-::::::........ \n ...=.::--=-=== ===++XXX++XXXXXXXXXX+XXX:++======+X XX+.++X++XX+++X+XX++XXX++=++ XXXX+===--:--=++XXXXXXXXXXXXXX++++XX+X+++.=-------::::......\n",
|
||||||
|
" \n \n \n \n \n \n \n :.... \n . . \n . ....- \n .:..:::::.. \n .:..+.::... . \n =::::.::... . +...::.. \n .. .:-==-:-:....::... .::. \n .:-::... .:-+=---::..:::......::.. ....+ . \n . . :..:-:::.... .:--=--::::::::-:.::+:-:.. :=-::. .. \n ....... ..::-:::.. .:::=--::::::.-:---=---:...:.:.:--:...- ..::..=. ..... \n . ..:::::::..... .. ...-:-::. ...-::-+=-=-::::::.:-+--=--::.....::-::... -..:------:.:.:... \n .. . ....:=::-::-:-...:..:..::..:--::. . .:::--+ +=----::.=.:-=:--::.+....+.-=::.=...:-=:==--+- ---::=... \n ............::::::--:-+-::=:::-:..=:...:--:::.... ..+---=++X+=-=-::..:.:==:=--:....::::==--::---=++.====--=---=:-:..: ... \n .....::::-::.:--::---====-------::.:::.---==--:.: ..-==--==-++====-::...::==----=::.:.::-==-::--=+=++++++======----: :. .... \n ....-:=::::: --:==----=+==++=+==+=- --=.=+=.==X+=--:: -===+==+++X+-+==-::--==.++=-:::. ..:-==++===+++XX+++X====+===----:::::.:...:.. \n ....::---------==:++==+++++++++X+=++XX+=======+XX++ ===+==-++==++X XXX++==++++++++=--:.:.::-==XX++.XXXXXXXX++=++:XX++==--::+: :::.... . \n .. .::---=========+X++XXX+XXXXXXX++:XXXX+=+=== =+X=X++X+-+.+++++XXXX++XXXX+++==++XX+==----- -=+XXXXXXXXXXXXX+++++X-XXX++=--------.:::......\n",
|
||||||
|
" \n \n \n \n \n \n . \n . . \n :. .:.. \n .. .. ..::. \n . .....:: ..:. \n .-:...-.:.. . .. .. \n . .:-=-::... ...+. :+.. \n ... .:-+=-::::. ........:--.. ..-.. \n . .=-:.... .::==::::......-::::..-:. .::... \n .... ..::=. .:-==:=:.......::---::-. .:-::::. ........ \n ........ . ..=.:. .=::==--:+......:-+.--::. .+. ..::-:. .:-::::-:::.:. \n .....:-:.::-.. .. .. :. ..:.. .::--+=--:. .. ..:=::=::.......::-:.. ..:-----:::-=-:.:.:. \n ...... .::::::::.::--...... .....:-:.. .::--=+=.=::.... ..:=::.:-..:....::--+....:-=+=--==-==-::::::... \n ....-:.....::::::::::--- :::::.=....::--==-:.. . ..:=--+====--::.......-+-:.::+.. ...:=-:..::-.=======+-==----:::...: .. \n ......::...:-::--::--------==-=-:::---=++==++=-::-..:--:-=:==++===-:..::::==- ::.... .--=+.---=+X===+==+==+= ----::........ \n ...=:::-::::--=-==-=+==--====+++-:===-=-==.++X+=----=---======+XXXX+=-:-===++==--::. . .:--X+===.:XX+.+=+===+++====-::.::-:.+.... \n ...:::------+=-==+.++X+=++XXXXXX++=+X+===+=-==+XX++=+=++ =====+XXXXXX +===+==++X+==::....::-++X ++XXXXXXX+===-=++-+++=-=--=::::::=.-.... \n ...:-----=----==+X++XXXXXXXXXXX+XX+++++==+==-==+XX.+=++-=====+++XX:XXX-X+=+++=+XXX+=--======+++XXXXXXXXXX+===+++XX++++=+------::::=:.....\n"
|
||||||
|
]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
|
||||||
|
import AnimatedWidth from "@/components/shared/layout/animated-width";
|
||||||
|
import ArrowRight from "@/components/app/(home)/sections/hero-input/_svg/ArrowRight";
|
||||||
|
import Button from "@/components/shared/button/Button";
|
||||||
|
|
||||||
|
export default function HeroInputSubmitButton({
|
||||||
|
dirty,
|
||||||
|
buttonText = "Re-imagine Site",
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
dirty: boolean;
|
||||||
|
buttonText?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={`hero-input-button !p-0 ${disabled ? 'bg-gray-400 hover:bg-gray-400 cursor-wait' : 'bg-heat-100 hover:bg-heat-200'}`}
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<AnimatedWidth>
|
||||||
|
<AnimatePresence initial={false} mode="popLayout">
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
|
||||||
|
exit={{ opacity: 0, x: -10, filter: "blur(2px)" }}
|
||||||
|
initial={{ opacity: 0, x: 10, filter: "blur(2px)" }}
|
||||||
|
key={dirty ? "dirty" : "clean"}
|
||||||
|
>
|
||||||
|
{dirty ? (
|
||||||
|
<div className="py-8 w-126 text-center text-white">
|
||||||
|
{buttonText}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-60 py-8 flex-center">
|
||||||
|
<ArrowRight />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</AnimatedWidth>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import Globe from "./_svg/Globe";
|
||||||
|
import HeroInputSubmitButton from "./Button/Button";
|
||||||
|
import HeroInputTabsMobile from "./Tabs/Mobile/Mobile";
|
||||||
|
import HeroInputTabs from "./Tabs/Tabs";
|
||||||
|
import AsciiExplosion from "@/components/shared/effects/flame/ascii-explosion";
|
||||||
|
import { Endpoint } from "@/components/shared/Playground/Context/types";
|
||||||
|
|
||||||
|
export default function HeroInput() {
|
||||||
|
const [tab, setTab] = useState<Endpoint>(Endpoint.Scrape);
|
||||||
|
const [url, setUrl] = useState<string>("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-552 mx-auto w-full z-[11] lg:z-[2] rounded-20 lg:-mt-76">
|
||||||
|
<div
|
||||||
|
className="overlay bg-accent-white"
|
||||||
|
style={{
|
||||||
|
boxShadow:
|
||||||
|
"0px 0px 44px 0px rgba(0, 0, 0, 0.02), 0px 88px 56px -20px rgba(0, 0, 0, 0.03), 0px 56px 56px -20px rgba(0, 0, 0, 0.02), 0px 32px 32px -20px rgba(0, 0, 0, 0.03), 0px 16px 24px -12px rgba(0, 0, 0, 0.03), 0px 0px 0px 1px rgba(0, 0, 0, 0.05), 0px 0px 0px 10px #F9F9F9",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label className="p-16 flex gap-8 items-center w-full relative border-b border-black-alpha-5">
|
||||||
|
<Globe />
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="w-full bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48"
|
||||||
|
placeholder="https://example.com"
|
||||||
|
type="text"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
(
|
||||||
|
document.querySelector(
|
||||||
|
".hero-input-button",
|
||||||
|
) as HTMLButtonElement
|
||||||
|
)?.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="p-10 flex justify-between items-center relative">
|
||||||
|
<HeroInputTabs
|
||||||
|
setTab={setTab}
|
||||||
|
tab={tab}
|
||||||
|
allowedModes={[
|
||||||
|
Endpoint.Scrape,
|
||||||
|
Endpoint.Search,
|
||||||
|
Endpoint.Map,
|
||||||
|
Endpoint.Crawl,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HeroInputTabsMobile
|
||||||
|
setTab={setTab}
|
||||||
|
tab={tab}
|
||||||
|
allowedModes={[
|
||||||
|
Endpoint.Scrape,
|
||||||
|
Endpoint.Search,
|
||||||
|
Endpoint.Map,
|
||||||
|
Endpoint.Crawl,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className="contents"
|
||||||
|
href={`/playground?endpoint=${tab}&url=${url}&autorun=true`}
|
||||||
|
>
|
||||||
|
<HeroInputSubmitButton dirty={url.length > 0} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-248 top-84 cw-768 pointer-events-none absolute overflow-clip -z-10">
|
||||||
|
<AsciiExplosion className="-top-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { animate, AnimatePresence, cubicBezier, motion } from "motion/react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { tabs } from "@/components/app/(home)/sections/hero-input/Tabs/Tabs";
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
import { Endpoint } from "@/components/shared/Playground/Context/types";
|
||||||
|
|
||||||
|
export default function HeroInputTabsMobile(props: {
|
||||||
|
setTab: (tab: Endpoint) => void;
|
||||||
|
tab: Endpoint;
|
||||||
|
allowedModes?: Endpoint[];
|
||||||
|
}) {
|
||||||
|
// Filter tabs based on allowedModes if provided
|
||||||
|
const visibleTabs = props.allowedModes
|
||||||
|
? tabs.filter((tab) => props.allowedModes!.includes(tab.value))
|
||||||
|
: tabs;
|
||||||
|
|
||||||
|
const activeTab = visibleTabs.find((tab) => tab.value === props.tab)!;
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.innerWidth > 996) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (ref.current && e.composedPath().includes(ref.current)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsOpen(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="py-8 px-10 flex items-center rounded-10 inside-border before:border-black-alpha-4 relative lg:hidden gap-4"
|
||||||
|
ref={ref}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
<activeTab.icon size={24} alwaysHeat />
|
||||||
|
<div className="px-6 text-label-medium">{activeTab.label}</div>
|
||||||
|
<svg
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-200",
|
||||||
|
isOpen ? "rotate-180 text-accent-black" : "text-black-alpha-48",
|
||||||
|
)}
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.4001 10.2L12.0001 13.8L15.6001 10.2"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: 1, filter: "blur(0px)" }}
|
||||||
|
className="absolute z-[1001] top-[calc(100%-4px)] left-[calc(50%-(50vw-6px))] w-[calc(100vw-12px)]"
|
||||||
|
exit={{ opacity: 0, filter: "blur(2px)" }}
|
||||||
|
initial={{ opacity: 0, filter: "blur(2px)" }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
ease: cubicBezier(0.25, 0.1, 0.25, 1.0),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx-auto w-full p-4 max-w-366 rounded-16 bg-accent-white"
|
||||||
|
style={{
|
||||||
|
boxShadow:
|
||||||
|
"0 32px 40px 6px rgba(0, 0, 0, 0.02), 0 12px 32px 0 rgba(0, 0, 0, 0.02), 0 24px 32px -8px rgba(0, 0, 0, 0.02), 0 8px 16px -2px rgba(0, 0, 0, 0.02), 0 0 0 1px rgba(0, 0, 0, 0.04)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="py-10 px-12 text-label-small text-black-alpha-48">
|
||||||
|
Output
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MenuItems
|
||||||
|
setTab={props.setTab}
|
||||||
|
tab={props.tab}
|
||||||
|
visibleTabs={visibleTabs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItems(props: {
|
||||||
|
tab: Endpoint;
|
||||||
|
setTab: (tab: Endpoint) => void;
|
||||||
|
visibleTabs: typeof tabs;
|
||||||
|
}) {
|
||||||
|
const backgroundRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className="absolute top-0 opacity-0 left-0 bg-black-alpha-4 rounded-12 w-full pointer-events-none"
|
||||||
|
ref={backgroundRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{props.visibleTabs.map((tab) => (
|
||||||
|
<div
|
||||||
|
className="text-label-small select-none cursor-pointer flex gap-12 py-12 px-16"
|
||||||
|
key={tab.value}
|
||||||
|
onClick={() => {
|
||||||
|
animate(
|
||||||
|
backgroundRef.current!,
|
||||||
|
{
|
||||||
|
scaleX: [1, 0.99, 1],
|
||||||
|
scaleY: [1, 0.96, 1],
|
||||||
|
opacity: [1, 0.9, 1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||||
|
duration: 0.15,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
props.setTab(tab.value);
|
||||||
|
}}
|
||||||
|
onMouseEnter={async (e) => {
|
||||||
|
const child = e.currentTarget as HTMLElement;
|
||||||
|
|
||||||
|
if (backgroundRef.current?.getBoundingClientRect().height === 0) {
|
||||||
|
backgroundRef.current!.style.height = child.offsetHeight + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getComputedStyle(backgroundRef.current!).opacity === "0") {
|
||||||
|
await animate(
|
||||||
|
backgroundRef.current!,
|
||||||
|
{
|
||||||
|
y: child.offsetTop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||||
|
duration: 0.01,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
animate(backgroundRef.current!, { scale: 0.995 }).then(() =>
|
||||||
|
animate(backgroundRef.current!, { scale: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
animate(
|
||||||
|
backgroundRef.current!,
|
||||||
|
{
|
||||||
|
y: child.offsetTop,
|
||||||
|
opacity: 1,
|
||||||
|
height: child.offsetHeight + "px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||||
|
duration: 0.2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
animate(
|
||||||
|
backgroundRef.current!,
|
||||||
|
{
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||||
|
duration: 0.2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="size-24 p-2">
|
||||||
|
<tab.icon size={20} alwaysHeat />
|
||||||
|
</div>
|
||||||
|
<div className="px-6 text-label-medium">{tab.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { animate } from "motion";
|
||||||
|
import { Fragment, useRef } from "react";
|
||||||
|
|
||||||
|
import EndpointsSearch from "@/components/app/(home)/sections/endpoints/EndpointsSearch/EndpointsSearch";
|
||||||
|
import EndpointsCrawl from "@/components/app/(home)/sections/endpoints/EndpointsCrawl/EndpointsCrawl";
|
||||||
|
import EndpointsMap from "@/components/app/(home)/sections/endpoints/EndpointsMap/EndpointsMap";
|
||||||
|
import EndpointsScrape from "@/components/app/(home)/sections/endpoints/EndpointsScrape/EndpointsScrape";
|
||||||
|
import EndpointsExtract from "@/components/app/(home)/sections/endpoints/EndpointsExtract/EndpointsExtract";
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
import Tooltip from "@/components/ui/shadcn/tooltip";
|
||||||
|
import { Endpoint } from "@/components/shared/Playground/Context/types";
|
||||||
|
|
||||||
|
export const tabs = [
|
||||||
|
{
|
||||||
|
label: "Scrape",
|
||||||
|
value: Endpoint.Scrape,
|
||||||
|
action: "scraping",
|
||||||
|
description:
|
||||||
|
"Scrapes only the specified URL without crawling subpages. Outputs the content from the page.",
|
||||||
|
icon: EndpointsScrape,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Search",
|
||||||
|
value: Endpoint.Search,
|
||||||
|
description: "Search the web and get full content from results",
|
||||||
|
action: "searching",
|
||||||
|
icon: EndpointsSearch,
|
||||||
|
new: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Map",
|
||||||
|
value: Endpoint.Map,
|
||||||
|
action: "mapping",
|
||||||
|
description: "Attempts to output all website's urls in a few seconds.",
|
||||||
|
icon: EndpointsMap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Crawl",
|
||||||
|
value: Endpoint.Crawl,
|
||||||
|
action: "crawling",
|
||||||
|
description:
|
||||||
|
"Crawls a URL and all its accessible subpages, outputting the content from each page.",
|
||||||
|
icon: EndpointsCrawl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Extract",
|
||||||
|
value: Endpoint.Extract,
|
||||||
|
action: "extracting",
|
||||||
|
description:
|
||||||
|
"Extract structured data from pages using LLMs. Provide URLs and a schema to get organized data.",
|
||||||
|
icon: EndpointsExtract,
|
||||||
|
new: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function HeroInputTabs(props: {
|
||||||
|
setTab: (tab: Endpoint) => void;
|
||||||
|
tab: Endpoint;
|
||||||
|
disabled?: boolean;
|
||||||
|
allowedModes?: Endpoint[];
|
||||||
|
}) {
|
||||||
|
// Filter tabs based on allowedModes if provided
|
||||||
|
const visibleTabs = props.allowedModes
|
||||||
|
? tabs.filter((tab) => props.allowedModes!.includes(tab.value))
|
||||||
|
: tabs;
|
||||||
|
|
||||||
|
const activeIndex = visibleTabs.findIndex((tab) => tab.value === props.tab);
|
||||||
|
|
||||||
|
const backgroundRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-black-alpha-4 flex items-center rounded-10 p-2 relative lg-max:hidden"
|
||||||
|
style={{
|
||||||
|
boxShadow:
|
||||||
|
"0px 6px 12px 0px rgba(0, 0, 0, 0.02) inset, 0px 0.75px 0.75px 0px rgba(0, 0, 0, 0.02) inset, 0px 0.25px 0.25px 0px rgba(0, 0, 0, 0.04) inset",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute top-2 left-2 h-32 bg-accent-white rounded-8 w-89"
|
||||||
|
ref={backgroundRef}
|
||||||
|
style={{
|
||||||
|
boxShadow:
|
||||||
|
"0px 6px 12px -3px rgba(0, 0, 0, 0.04), 0px 3px 6px -1px rgba(0, 0, 0, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.04), 0px 0.5px 0.5px 0px rgba(0, 0, 0, 0.06)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{visibleTabs.map((tab, index) => (
|
||||||
|
<Fragment key={tab.value}>
|
||||||
|
{index > 0 && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"px-2 transition-all",
|
||||||
|
!(index !== activeIndex && index !== activeIndex + 1) &&
|
||||||
|
"opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="w-1 h-12 bg-black-alpha-5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"text-label-medium p-6 relative transition-all group flex items-center",
|
||||||
|
tab.value === props.tab
|
||||||
|
? "text-accent-black"
|
||||||
|
: "text-black-alpha-56",
|
||||||
|
!tab.new && "pr-4",
|
||||||
|
)}
|
||||||
|
key={tab.value}
|
||||||
|
ref={(element) => {
|
||||||
|
if (element && backgroundRef.current) {
|
||||||
|
if (activeIndex === index) {
|
||||||
|
animate(
|
||||||
|
backgroundRef.current,
|
||||||
|
{
|
||||||
|
x: element.offsetLeft - 2,
|
||||||
|
width: element.offsetWidth - 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 23,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
props.setTab(tab.value);
|
||||||
|
|
||||||
|
const t = e.target as HTMLElement;
|
||||||
|
|
||||||
|
const target =
|
||||||
|
t instanceof HTMLButtonElement
|
||||||
|
? t
|
||||||
|
: (t.closest("button") as HTMLButtonElement);
|
||||||
|
|
||||||
|
if (backgroundRef.current) {
|
||||||
|
animate(backgroundRef.current, { scale: 0.975 }).then(() =>
|
||||||
|
animate(backgroundRef.current!, { scale: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
animate(
|
||||||
|
backgroundRef.current,
|
||||||
|
{
|
||||||
|
x: target.offsetLeft - 2,
|
||||||
|
width: target.offsetWidth - 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 250,
|
||||||
|
damping: 25,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.icon && <tab.icon active={tab.value === props.tab} />}
|
||||||
|
|
||||||
|
<span className="px-6"> {tab.label}</span>
|
||||||
|
|
||||||
|
{tab.new && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"py-2 px-6 rounded-4 text-[12px]/[16px] font-[450] transition-all",
|
||||||
|
tab.value === props.tab
|
||||||
|
? "bg-heat-12 text-heat-100"
|
||||||
|
: "bg-black-alpha-4 text-black-alpha-56",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip delay={0.25} description={tab.description} offset={-8} />
|
||||||
|
</button>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export default function ArrowRight() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11.6667 4.79163L16.875 9.99994M16.875 9.99994L11.6667 15.2083M16.875 9.99994H3.125"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export default function Globe() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 19.7083C16.2572 19.7083 19.7083 16.2572 19.7083 12C19.7083 7.74276 16.2572 4.29163 12 4.29163M12 19.7083C7.74276 19.7083 4.29163 16.2572 4.29163 12C4.29163 7.74276 7.74276 4.29163 12 4.29163M12 19.7083C10.044 19.7083 8.45829 16.2572 8.45829 12C8.45829 7.74276 10.044 4.29163 12 4.29163M12 19.7083C13.956 19.7083 15.5416 16.2572 15.5416 12C15.5416 7.74276 13.956 4.29163 12 4.29163M19.5 12H4.49996"
|
||||||
|
stroke="#262626"
|
||||||
|
strokeLinecap="square"
|
||||||
|
strokeOpacity="0.32"
|
||||||
|
strokeWidth="1.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import CurvyRect, { Connector } from "@/components/shared/layout/curvy-rect";
|
||||||
|
import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
|
||||||
|
|
||||||
|
import HeroScrapingCodeLoading from "./Loading/Loading";
|
||||||
|
import Code from "@/components/ui/code";
|
||||||
|
|
||||||
|
const URL = {
|
||||||
|
value: "https://example.com",
|
||||||
|
encrypted: "h=t*A:!/z!aap?A-cZz",
|
||||||
|
};
|
||||||
|
const MARKDOWN = {
|
||||||
|
value: "# Getting Started...",
|
||||||
|
encrypted: "# ?0z-ang S*a-Z-a0*9",
|
||||||
|
};
|
||||||
|
const TITLE = {
|
||||||
|
value: "Guide",
|
||||||
|
encrypted: "G!=*?",
|
||||||
|
};
|
||||||
|
const SCREENSHOT = {
|
||||||
|
value: "https://example.com/hero",
|
||||||
|
encrypted: "ht-=*:/?*Za!zl=-?a9?h0-!",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HeroScrapingCode({ step }: { step: number }) {
|
||||||
|
const [url, setUrl] = useState(URL.encrypted);
|
||||||
|
const [markdown, setMarkdown] = useState(MARKDOWN.encrypted);
|
||||||
|
const [title, setTitle] = useState(TITLE.encrypted);
|
||||||
|
const [screenshot, setScreenshot] = useState(SCREENSHOT.encrypted);
|
||||||
|
|
||||||
|
const reveal = useCallback((value: string, setter: (v: string) => void) => {
|
||||||
|
let progress = 0;
|
||||||
|
let increaseProgress = -10;
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
increaseProgress = (increaseProgress + 1) % 5;
|
||||||
|
|
||||||
|
if (increaseProgress === 4) {
|
||||||
|
progress += 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress > 1) {
|
||||||
|
progress = 1;
|
||||||
|
setter(encryptText(value, progress, { randomizeChance: 0.3 }));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setter(encryptText(value, progress, { randomizeChance: 0.3 }));
|
||||||
|
|
||||||
|
const interval = 70 + progress * 30;
|
||||||
|
setTimeout(animate, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (step >= 0 && url === URL.encrypted) reveal(URL.value, setUrl);
|
||||||
|
|
||||||
|
if (step >= 3 && title === TITLE.encrypted) reveal(TITLE.value, setTitle);
|
||||||
|
if (step >= 4 && markdown === MARKDOWN.encrypted)
|
||||||
|
reveal(MARKDOWN.value, setMarkdown);
|
||||||
|
|
||||||
|
if (step >= 5 && screenshot === SCREENSHOT.encrypted)
|
||||||
|
reveal(SCREENSHOT.value, setScreenshot);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (step < 0) {
|
||||||
|
URL.encrypted = encryptText(URL.value, 0, { randomizeChance: 0.3 });
|
||||||
|
setUrl(URL.encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step < 3) {
|
||||||
|
TITLE.encrypted = encryptText(TITLE.value, 0, { randomizeChance: 0.3 });
|
||||||
|
setTitle(TITLE.encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step < 4) {
|
||||||
|
MARKDOWN.encrypted = encryptText(MARKDOWN.value, 0, {
|
||||||
|
randomizeChance: 0.3,
|
||||||
|
});
|
||||||
|
setMarkdown(MARKDOWN.encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step < 5) {
|
||||||
|
SCREENSHOT.encrypted = encryptText(SCREENSHOT.value, 0, {
|
||||||
|
randomizeChance: 0.3,
|
||||||
|
});
|
||||||
|
setScreenshot(SCREENSHOT.encrypted);
|
||||||
|
}
|
||||||
|
}, 70);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [step, reveal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-280 lg:h-310 flex z-[1] w-full relative -top-1 bg-background-base">
|
||||||
|
<Connector className="lg:hidden absolute -top-10 -left-[10.5px]" />
|
||||||
|
<Connector className="lg:hidden absolute -top-10 -right-[10.5px]" />
|
||||||
|
<div className="lg:hidden absolute top-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint" />
|
||||||
|
|
||||||
|
<Connector className="lg:hidden absolute -bottom-10 -left-[10.5px]" />
|
||||||
|
<Connector className="lg:hidden absolute -bottom-10 -right-[10.5px]" />
|
||||||
|
<div className="lg:hidden absolute bottom-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint" />
|
||||||
|
|
||||||
|
<div className="flex-1 lg-max:min-w-0 h-full relative lg:inside-border before:border-border-faint">
|
||||||
|
<CurvyRect className="overlay" allSides />
|
||||||
|
<CurvyRect
|
||||||
|
className="size-32 absolute bottom-0 -left-31 lg-max:hidden"
|
||||||
|
bottomRight
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="pl-15 border-b border-border-faint p-13 flex justify-between items-center">
|
||||||
|
<div className="flex gap-10 items-center">
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
className="w-12 h-12 rounded-full relative inside-border before:border-border-muted"
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-mono-x-small font-mono text-black-alpha-20">
|
||||||
|
[ .JSON ]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-scroll hide-scrollbar lg:contents relative">
|
||||||
|
<Code
|
||||||
|
code={`[
|
||||||
|
{
|
||||||
|
"url": "${url}",
|
||||||
|
"markdown": "${markdown}",
|
||||||
|
"json": { "title": "${title}", "docs": "..." },
|
||||||
|
"screenshot": "${screenshot}.png"
|
||||||
|
}
|
||||||
|
]`}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HeroScrapingCodeLoading finished={step >= 6} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-28 lg-max:hidden -ml-1 relative">
|
||||||
|
<div className="h-1 w-[calc(100%-1px)] top-0 left-0 absolute bg-border-faint" />
|
||||||
|
<CurvyRect className="overlay" topLeft />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-53 lg-max:hidden -right-37 bottom-0 absolute w-65">
|
||||||
|
<CurvyRect className="overlay" bottom topRight />
|
||||||
|
<div className="overlay border-y border-border-faint" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
|
||||||
|
import AnimatedWidth from "@/components/shared/layout/animated-width";
|
||||||
|
import Spinner from "@/components/ui/spinner";
|
||||||
|
|
||||||
|
export default function HeroScrapingCodeLoading({
|
||||||
|
finished,
|
||||||
|
}: {
|
||||||
|
finished: boolean;
|
||||||
|
}) {
|
||||||
|
const [scrapingText, setScrapingText] = useState("Scraping...");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (finished) return;
|
||||||
|
|
||||||
|
let timeout = 0;
|
||||||
|
let tick = 0;
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
tick += 1;
|
||||||
|
|
||||||
|
if (tick % 3 !== 0) {
|
||||||
|
setScrapingText(
|
||||||
|
encryptText("Scraping", 0, {
|
||||||
|
randomizeChance: 0.6 + Math.random() * 0.3,
|
||||||
|
}) + "...",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setScrapingText("Scraping...");
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = 80;
|
||||||
|
timeout = window.setTimeout(animate, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
}, [finished]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-6 p-6 pr-0 rounded-full inside-border before:border-border-faint absolute right-20 bottom-20 text-mono-small font-mono text-accent-black">
|
||||||
|
<Spinner finished={finished} />
|
||||||
|
|
||||||
|
<AnimatedWidth initial={{ width: "auto" }}>
|
||||||
|
<AnimatePresence initial={false} mode="popLayout">
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
className="pr-12"
|
||||||
|
exit={{ opacity: 0, x: 10 }}
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
>
|
||||||
|
{finished ? "Scrape Completed" : scrapingText}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</AnimatedWidth>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default function Check() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 2.5C5.85786 2.5 2.5 5.85786 2.5 10C2.5 14.1421 5.85786 17.5 10 17.5C14.1421 17.5 17.5 14.1421 17.5 10C17.5 5.85786 14.1421 2.5 10 2.5ZM12.8305 8.59995C13.0928 8.27937 13.0455 7.80685 12.7249 7.54455C12.4043 7.28226 11.9318 7.32951 11.6695 7.65009L8.81932 11.1337L7.90533 10.2197C7.61244 9.9268 7.13756 9.9268 6.84467 10.2197C6.55178 10.5126 6.55178 10.9875 6.84467 11.2804L8.34467 12.7804C8.4945 12.9302 8.70073 13.0096 8.91236 12.9991C9.12399 12.9885 9.32129 12.8889 9.45547 12.725L12.8305 8.59995Z"
|
||||||
|
fill="#FA5D19"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.hero-scraping-highlight::before {
|
||||||
|
animation: hero-scraping-highlight-before 1s linear infinite;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hero-scraping-highlight-before {
|
||||||
|
0% {
|
||||||
|
border-color: var(--border-loud);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
opacity: 0.25;
|
||||||
|
border-color: var(--heat-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
border-color: var(--border-loud);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { animate } from "motion";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import CurvyRect from "@/components/shared/layout/curvy-rect";
|
||||||
|
import { sleep } from "@/utils/sleep";
|
||||||
|
|
||||||
|
import BrowserMobile from "./_svg/BrowserMobile";
|
||||||
|
import BrowserTab from "./_svg/BrowserTab";
|
||||||
|
import HeroScrapingCode from "./Code/Code";
|
||||||
|
import HeroScrapingTag from "./Tag/Tag";
|
||||||
|
|
||||||
|
import "./HeroScraping.css";
|
||||||
|
|
||||||
|
export default function HeroScraping() {
|
||||||
|
const [step, setStep] = useState(-1);
|
||||||
|
|
||||||
|
const navigationRef = useRef<HTMLDivElement>(null);
|
||||||
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
|
const h1Ref = useRef<HTMLDivElement>(null);
|
||||||
|
const descriptionRef = useRef<HTMLDivElement>(null);
|
||||||
|
const ctaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const highlightRef = useRef<HTMLDivElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const wrapElement = async (
|
||||||
|
element: HTMLElement,
|
||||||
|
{ borderRadius }: { borderRadius?: number } = {},
|
||||||
|
) => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const containerBnds = containerRef.current.getBoundingClientRect();
|
||||||
|
const elementBnds = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (!highlightRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (highlightRef.current) {
|
||||||
|
await animate(highlightRef.current, { opacity: 0 }, { duration: 0.3 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error animating highlight:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!highlightRef.current) return;
|
||||||
|
|
||||||
|
Object.assign(highlightRef.current.style, {
|
||||||
|
left: elementBnds.left - containerBnds.left - 4 + "px",
|
||||||
|
top: elementBnds.top - containerBnds.top - 4 + "px",
|
||||||
|
width: elementBnds.width + 8 + "px",
|
||||||
|
height: elementBnds.height + 8 + "px",
|
||||||
|
borderRadius: borderRadius ? `${borderRadius}px` : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await animate(
|
||||||
|
highlightRef.current,
|
||||||
|
{ opacity: [1, 0.5, 0.3, 0.8, 0.6, 0.9, 0.7, 1] },
|
||||||
|
{ duration: 0.4 },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error animating highlight:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
setStep(0);
|
||||||
|
if (!highlightRef.current) return;
|
||||||
|
|
||||||
|
await animate(highlightRef.current, {
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(700);
|
||||||
|
|
||||||
|
setTimeout(() => setStep(1), 300);
|
||||||
|
if (navigationRef.current) {
|
||||||
|
await wrapElement(navigationRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(1200);
|
||||||
|
|
||||||
|
setTimeout(() => setStep(2), 300);
|
||||||
|
if (buttonRef.current) {
|
||||||
|
await wrapElement(buttonRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(1200);
|
||||||
|
|
||||||
|
setTimeout(() => setStep(3), 300);
|
||||||
|
if (h1Ref.current) {
|
||||||
|
await wrapElement(h1Ref.current, { borderRadius: 12 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(1200);
|
||||||
|
|
||||||
|
setTimeout(() => setStep(4), 300);
|
||||||
|
if (descriptionRef.current) {
|
||||||
|
await wrapElement(descriptionRef.current, { borderRadius: 8 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(1200);
|
||||||
|
|
||||||
|
setTimeout(() => setStep(5), 300);
|
||||||
|
if (ctaRef.current) {
|
||||||
|
await wrapElement(ctaRef.current, { borderRadius: 24 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(1500);
|
||||||
|
setTimeout(() => setStep(6), 300);
|
||||||
|
|
||||||
|
if (highlightRef.current) {
|
||||||
|
await animate(highlightRef.current, { opacity: 0 }, { duration: 0.3 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
if (started) return;
|
||||||
|
|
||||||
|
if (window.scrollY > 100) {
|
||||||
|
started = true;
|
||||||
|
start();
|
||||||
|
window.removeEventListener("scroll", onScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (started) return;
|
||||||
|
|
||||||
|
started = true;
|
||||||
|
start();
|
||||||
|
window.removeEventListener("scroll", onScroll);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
window.addEventListener("scroll", onScroll);
|
||||||
|
|
||||||
|
return () => window.removeEventListener("scroll", onScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="pt-56 lg:pt-25 lg:px-25 container -mt-36 relative"
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
<div className="h-53 absolute top-[calc(100%-1px)] w-full left-0">
|
||||||
|
<div className="h-1 bg-border-faint bottom-0 left-0 w-full absolute" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="left-61 top-89 rounded-[16px] size-32 absolute hero-scraping-highlight inside-border before:border-border-loud opacity-0 scale-[0.9]"
|
||||||
|
ref={highlightRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="overlay lg-max:hidden">
|
||||||
|
<div className="h-1 absolute bottom-0 w-full left-0 bg-border-faint" />
|
||||||
|
<CurvyRect className="overlay" bottom />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:h-370 rounded-t-16 lg-max:pt-70 relative">
|
||||||
|
<div className="overlay mask-border lg-max:hidden p-1 bg-gradient-to-b from-black/7 to-transparent" />
|
||||||
|
|
||||||
|
<div className="top-17 left-17 flex gap-8 items-center absolute lg-max:hidden">
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
className="w-10 h-10 rounded-full relative inside-border before:border-border-muted"
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-42 lg:px-6">
|
||||||
|
<BrowserMobile className="absolute top-0 cw-316 lg:hidden" />
|
||||||
|
|
||||||
|
<BrowserTab className="absolute top-[7.5px] left-70 lg-max:hidden bg-background-base z-[1]" />
|
||||||
|
<div className="absolute size-18 top-17 left-89 lg-max:hidden inside-border before:border-border-muted z-[2] rounded-full" />
|
||||||
|
|
||||||
|
<div className="rounded-t-16 relative lg:h-330 lg:p-6">
|
||||||
|
<div className="overlay mask-border lg-max:hidden p-1 bg-gradient-to-b from-black/7 to-transparent" />
|
||||||
|
|
||||||
|
<div className="lg:h-322 rounded-t-10 relative">
|
||||||
|
<div className="overlay mask-border lg-max:hidden p-1 bg-gradient-to-b z-[2] from-black/7 to-transparent" />
|
||||||
|
|
||||||
|
<div className="px-28 lg-max:hidden py-20 flex justify-between items-center relative border-b border-border-faint">
|
||||||
|
<div className="flex gap-8 items-center relative">
|
||||||
|
<div className="size-24 rounded-full relative inside-border before:border-border-muted" />
|
||||||
|
<div className="w-64 h-12 rounded-full relative inside-border before:border-border-muted" />
|
||||||
|
|
||||||
|
{step >= 0 && (
|
||||||
|
<HeroScrapingTag
|
||||||
|
active={step === 0}
|
||||||
|
className="absolute left-[calc(100%+24px)] top-0"
|
||||||
|
initial={{ x: -12, opacity: 0 }}
|
||||||
|
label="Logo"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute top-24 center-x flex gap-8"
|
||||||
|
ref={navigationRef}
|
||||||
|
>
|
||||||
|
{step >= 1 && (
|
||||||
|
<HeroScrapingTag
|
||||||
|
active={step === 1}
|
||||||
|
className="absolute right-[calc(100%+20px)] -top-4"
|
||||||
|
initial={{ x: 12, opacity: 0 }}
|
||||||
|
label="Navigation"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
className="w-64 h-16 rounded-full relative inside-border before:border-border-muted"
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="w-72 h-24 rounded-full relative inside-border before:border-border-muted"
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
{step >= 2 && (
|
||||||
|
<HeroScrapingTag
|
||||||
|
active={step === 2}
|
||||||
|
className="absolute right-[calc(100%+20px)] top-0"
|
||||||
|
initial={{ x: 12, opacity: 0 }}
|
||||||
|
label="Button"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:grid grid-cols-2">
|
||||||
|
<div className="pt-40 pl-151 flex gap-16 relative lg-max:hidden">
|
||||||
|
<CurvyRect
|
||||||
|
className="size-32 -top-1 -right-1 absolute"
|
||||||
|
topRight
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="h-53 lg-max:hidden -left-37 bottom-1 absolute w-65">
|
||||||
|
<CurvyRect className="overlay" left />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex gap-16 mb-16 flex-wrap w-300 relative"
|
||||||
|
ref={h1Ref}
|
||||||
|
>
|
||||||
|
{step >= 3 && (
|
||||||
|
<HeroScrapingTag
|
||||||
|
active={step === 3}
|
||||||
|
className="absolute right-[calc(100%+16px)] top-0"
|
||||||
|
initial={{ x: 12, opacity: 0 }}
|
||||||
|
label="H1 Title"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="w-144 h-32 rounded-8 relative inside-border before:border-border-muted" />
|
||||||
|
<div className="w-82 h-32 rounded-8 relative inside-border before:border-border-muted" />
|
||||||
|
<div className="w-100 h-32 rounded-8 relative inside-border before:border-border-muted" />
|
||||||
|
<div className="w-180 h-32 rounded-8 relative inside-border before:border-border-muted" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex gap-6 mb-32 flex-wrap w-300 relative"
|
||||||
|
ref={descriptionRef}
|
||||||
|
>
|
||||||
|
{step >= 4 && (
|
||||||
|
<HeroScrapingTag
|
||||||
|
active={step === 4}
|
||||||
|
className="absolute top-0 right-[calc(100%+16px)]"
|
||||||
|
initial={{ x: 12, opacity: 0 }}
|
||||||
|
label="Description"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="w-131 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||||
|
<div className="w-72 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||||
|
<div className="w-34 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||||
|
<div className="w-56 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||||
|
<div className="w-116 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||||
|
<div className="w-116 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="w-64 h-24 rounded-full relative inside-border before:border-border-muted"
|
||||||
|
ref={ctaRef}
|
||||||
|
>
|
||||||
|
{step >= 5 && (
|
||||||
|
<HeroScrapingTag
|
||||||
|
active={step === 5}
|
||||||
|
className="absolute top-0 right-[calc(100%+16px)]"
|
||||||
|
initial={{ x: 12, opacity: 0 }}
|
||||||
|
label="CTA Button"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HeroScrapingCode step={step} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { motion } from "motion/react";
|
||||||
|
import { ComponentProps, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
|
||||||
|
export default function HeroScrapingTag({
|
||||||
|
active,
|
||||||
|
label,
|
||||||
|
...attrs
|
||||||
|
}: ComponentProps<typeof motion.div> & { active?: boolean; label: string }) {
|
||||||
|
const [value, setValue] = useState(
|
||||||
|
encryptText(label, 0, { randomizeChance: 0 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let progress = 0;
|
||||||
|
let increaseProgress = -10;
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
increaseProgress = (increaseProgress + 1) % 5;
|
||||||
|
|
||||||
|
if (increaseProgress === 4) {
|
||||||
|
progress += 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress > 1) {
|
||||||
|
progress = 1;
|
||||||
|
setValue(encryptText(label, progress, { randomizeChance: 0 }));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(encryptText(label, progress, { randomizeChance: 0 }));
|
||||||
|
|
||||||
|
const interval = 40 + progress * 20;
|
||||||
|
setTimeout(animate, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
{...attrs}
|
||||||
|
animate={{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"py-4 h-max font-mono w-max px-6 text-mono-x-small rounded-6 transition-colors",
|
||||||
|
active
|
||||||
|
? "bg-heat-12 text-heat-100"
|
||||||
|
: "bg-black-alpha-4 text-black-alpha-56",
|
||||||
|
attrs.className,
|
||||||
|
)}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 18,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
export default function BrowserMobile(props: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="112"
|
||||||
|
viewBox="0 0 316 112"
|
||||||
|
width="316"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_2254_6088)">
|
||||||
|
<rect
|
||||||
|
height="370"
|
||||||
|
rx="15.5"
|
||||||
|
stroke="url(#paint0_linear_2254_6088)"
|
||||||
|
strokeOpacity="0.07"
|
||||||
|
width="315"
|
||||||
|
x="0.5"
|
||||||
|
y="0.5"
|
||||||
|
/>
|
||||||
|
<mask fill="white" id="path-2-inside-1_2254_6088">
|
||||||
|
<path d="M240 32C240 37.5228 244.477 42 250 42H294C302.837 42 310 49.1634 310 58V361C310 366.523 305.523 371 300 371H16C10.4772 371 6 366.523 6 361V58C6 49.1634 13.1634 42 22 42H70C75.5228 42 80 37.5228 80 32V18C80 12.4772 84.4772 8 90 8H230C235.523 8 240 12.4772 240 18V32Z" />
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
d="M310 58L311 58L310 58ZM22 42L22 41L22 42ZM250 42V43H294V42V41H250V42ZM294 42V43C302.284 43 309 49.7157 309 58L310 58L311 58C311 48.6112 303.389 41 294 41V42ZM310 58H309V361H310H311V58H310ZM300 371V370H16V371V372H300V371ZM6 361H7V58H6H5V361H6ZM6 58H7C7 49.7157 13.7157 43 22 43L22 42L22 41C12.6112 41 5 48.6112 5 58H6ZM22 42V43H70V42V41H22V42ZM80 32H81V18H80H79V32H80ZM90 8V9H230V8V7H90V8ZM240 18H239V32H240H241V18H240ZM230 8V9C234.971 9 239 13.0294 239 18H240H241C241 11.9249 236.075 7 230 7V8ZM70 42V43C76.0751 43 81 38.0751 81 32H80H79C79 36.9706 74.9706 41 70 41V42ZM16 371V370C11.0294 370 7 365.971 7 361H6H5C5 367.075 9.92487 372 16 372V371ZM80 18H81C81 13.0294 85.0294 9 90 9V8V7C83.9249 7 79 11.9249 79 18H80ZM310 361H309C309 365.971 304.971 370 300 370V371V372C306.075 372 311 367.075 311 361H310ZM250 42V41C245.029 41 241 36.9706 241 32H240H239C239 38.0751 243.925 43 250 43V42Z"
|
||||||
|
fill="url(#paint1_linear_2254_6088)"
|
||||||
|
fillOpacity="0.07"
|
||||||
|
mask="url(#path-2-inside-1_2254_6088)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
height="310"
|
||||||
|
rx="9.5"
|
||||||
|
stroke="url(#paint2_linear_2254_6088)"
|
||||||
|
strokeOpacity="0.07"
|
||||||
|
width="291"
|
||||||
|
x="12.5"
|
||||||
|
y="48.5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
height="9"
|
||||||
|
rx="4.5"
|
||||||
|
stroke="#E8E8E8"
|
||||||
|
width="9"
|
||||||
|
x="17.5"
|
||||||
|
y="17.5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
height="9"
|
||||||
|
rx="4.5"
|
||||||
|
stroke="#E8E8E8"
|
||||||
|
width="9"
|
||||||
|
x="35.5"
|
||||||
|
y="17.5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
height="9"
|
||||||
|
rx="4.5"
|
||||||
|
stroke="#E8E8E8"
|
||||||
|
width="9"
|
||||||
|
x="53.5"
|
||||||
|
y="17.5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
height="17"
|
||||||
|
rx="8.5"
|
||||||
|
stroke="#E8E8E8"
|
||||||
|
width="17"
|
||||||
|
x="89.5"
|
||||||
|
y="17.5"
|
||||||
|
/>
|
||||||
|
<mask fill="white" id="path-10-inside-2_2254_6088">
|
||||||
|
<path d="M12 48H304V112H12V48Z" />
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
d="M304 112V111H12V112V113H304V112Z"
|
||||||
|
fill="#EDEDED"
|
||||||
|
mask="url(#path-10-inside-2_2254_6088)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
height="23"
|
||||||
|
rx="11.5"
|
||||||
|
stroke="#E8E8E8"
|
||||||
|
width="71"
|
||||||
|
x="212.5"
|
||||||
|
y="68.5"
|
||||||
|
/>
|
||||||
|
<circle cx="44" cy="80" r="11.5" stroke="#E8E8E8" />
|
||||||
|
<rect
|
||||||
|
height="11"
|
||||||
|
rx="5.5"
|
||||||
|
stroke="#E8E8E8"
|
||||||
|
width="63"
|
||||||
|
x="64.5"
|
||||||
|
y="74.5"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
id="paint0_linear_2254_6088"
|
||||||
|
x1="158"
|
||||||
|
x2="158"
|
||||||
|
y1="0"
|
||||||
|
y2="371"
|
||||||
|
>
|
||||||
|
<stop />
|
||||||
|
<stop offset="1" stopOpacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
id="paint1_linear_2254_6088"
|
||||||
|
x1="529.5"
|
||||||
|
x2="529.5"
|
||||||
|
y1="8"
|
||||||
|
y2="324"
|
||||||
|
>
|
||||||
|
<stop offset="0.4" />
|
||||||
|
<stop offset="1" stopOpacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
id="paint2_linear_2254_6088"
|
||||||
|
x1="158"
|
||||||
|
x2="158"
|
||||||
|
y1="48"
|
||||||
|
y2="359"
|
||||||
|
>
|
||||||
|
<stop />
|
||||||
|
<stop offset="1" stopOpacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_2254_6088">
|
||||||
|
<rect fill="white" height="112" width="316" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
export default function BrowserTab(attrs: HTMLAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 226 36"
|
||||||
|
width="226"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...attrs}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 35C5.52285 35 10 30.5228 10 25V11C10 5.47715 14.4772 1 20 1H206C211.523 1 216 5.47715 216 11V25C216 30.5228 220.477 35 226 35"
|
||||||
|
stroke="#E8E8E8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import CurvyRect from "@/components/shared/layout/curvy-rect";
|
||||||
|
|
||||||
|
import CenterStar from "./_svg/CenterStar";
|
||||||
|
|
||||||
|
export default function HomeHeroBackground() {
|
||||||
|
return (
|
||||||
|
<div className="overlay contain-layout pointer-events-none lg-max:hidden">
|
||||||
|
<div className="top-100 h-[calc(100%-99px)] border-border-faint border-y w-full left-0 absolute" />
|
||||||
|
|
||||||
|
<div className="cw-[1314px] z-[105] absolute top-0 border-x border-border-faint h-full">
|
||||||
|
<div className="text-mono-x-small font-mono text-black-alpha-12 select-none">
|
||||||
|
<div className="absolute top-111 -left-1 w-102 text-center">
|
||||||
|
{" "}
|
||||||
|
[ 200 OK ]{" "}
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-10 -left-1 w-102 text-center">
|
||||||
|
{" "}
|
||||||
|
[ .JSON ]{" "}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-111 -right-1 w-102 text-center">
|
||||||
|
{" "}
|
||||||
|
[ SCRAPE ]{" "}
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-10 -right-1 w-102 text-center">
|
||||||
|
{" "}
|
||||||
|
[ .MD ]{" "}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="top-302 h-1 left-0 bg-border-faint w-303 absolute" />
|
||||||
|
<div className="top-403 h-1 left-0 bg-border-faint w-303 absolute" />
|
||||||
|
<div className="top-504 h-1 left-100 bg-border-faint w-203 absolute" />
|
||||||
|
|
||||||
|
<div className="top-302 h-1 right-0 bg-border-faint w-303 absolute" />
|
||||||
|
<div className="top-403 h-1 right-0 bg-border-faint w-303 absolute" />
|
||||||
|
<div className="top-504 h-1 right-100 bg-border-faint w-203 absolute" />
|
||||||
|
|
||||||
|
{Array.from({ length: 2 }, (_, i) => (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<CurvyRect
|
||||||
|
bottomLeft={i === 1}
|
||||||
|
bottomRight={i === 0}
|
||||||
|
className="w-101 h-[calc(100%-99px)] top-100 absolute"
|
||||||
|
style={{ [i === 0 ? "left" : "right"]: -101 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CurvyRect
|
||||||
|
className="w-102 h-203 top-100 absolute"
|
||||||
|
style={{ [i === 0 ? "left" : "right"]: -1 }}
|
||||||
|
allSides
|
||||||
|
/>
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 top-302 absolute"
|
||||||
|
style={{ [i === 0 ? "left" : "right"]: -1 }}
|
||||||
|
allSides
|
||||||
|
/>
|
||||||
|
<CurvyRect
|
||||||
|
className="w-102 h-203 top-403 absolute"
|
||||||
|
style={{ [i === 0 ? "left" : "right"]: -1 }}
|
||||||
|
allSides
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="cw-[910px] absolute top-100 border-x border-border-faint h-[calc(100%-99px)]" />
|
||||||
|
<div className="cw-[708px] absolute top-100 border-x border-border-faint h-[calc(100%-99px)]">
|
||||||
|
<CenterStar className="absolute top-77 -right-24 z-[1]" />
|
||||||
|
<CenterStar className="absolute top-77 -left-24 z-[1]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CurvyRect
|
||||||
|
className="cw-[708px] absolute top-100 h-[calc(100%-99px)]"
|
||||||
|
bottom
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="cw-[506px] absolute top-100 border-x border-border-faint h-102" />
|
||||||
|
<div className="cw-[304px] absolute top-100 border-x border-border-faint h-102" />
|
||||||
|
<div className="cw-[102px] absolute top-100 border-x border-border-faint h-102" />
|
||||||
|
|
||||||
|
<div className="top-201 h-1 bg-border-faint cw-[1112px] absolute" />
|
||||||
|
|
||||||
|
<div className="cw-[1112px] absolute top-0 h-full">
|
||||||
|
<CurvyRect className="w-full absolute top-full h-100 left-0" top />
|
||||||
|
<CurvyRect
|
||||||
|
className="w-100 absolute top-full h-100 -left-99"
|
||||||
|
topRight
|
||||||
|
/>
|
||||||
|
<CurvyRect
|
||||||
|
className="w-100 absolute top-full h-100 -right-99"
|
||||||
|
topLeft
|
||||||
|
/>
|
||||||
|
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute left-0"
|
||||||
|
style={{
|
||||||
|
top: 100 + i * 101,
|
||||||
|
}}
|
||||||
|
allSides
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute right-0"
|
||||||
|
style={{
|
||||||
|
top: 100 + i * 101,
|
||||||
|
}}
|
||||||
|
allSides
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute left-101 top-100"
|
||||||
|
bottomLeft
|
||||||
|
top
|
||||||
|
/>
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute left-101 top-201"
|
||||||
|
bottom
|
||||||
|
topLeft
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute right-101 top-100"
|
||||||
|
bottomRight
|
||||||
|
top
|
||||||
|
/>
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute right-101 top-201"
|
||||||
|
bottom
|
||||||
|
topRight
|
||||||
|
/>
|
||||||
|
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute left-101"
|
||||||
|
style={{
|
||||||
|
top: 302 + i * 101,
|
||||||
|
}}
|
||||||
|
allSides
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute right-101"
|
||||||
|
style={{
|
||||||
|
top: 302 + i * 101,
|
||||||
|
}}
|
||||||
|
allSides
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute top-100 left-202"
|
||||||
|
bottomRight
|
||||||
|
top
|
||||||
|
/>
|
||||||
|
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute top-100"
|
||||||
|
key={i}
|
||||||
|
style={{ left: 303 + i * 101 }}
|
||||||
|
allSides
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<CurvyRect
|
||||||
|
className="size-102 absolute top-100 right-202"
|
||||||
|
bottomLeft
|
||||||
|
top
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Connector } from "@/components/shared/layout/curvy-rect";
|
||||||
|
import {
|
||||||
|
useHeaderContext,
|
||||||
|
useHeaderHeight,
|
||||||
|
} from "@/components/shared/header/HeaderContext";
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
|
||||||
|
export const BackgroundOuterPiece = () => {
|
||||||
|
const [noRender, setNoRender] = useState(false);
|
||||||
|
const { dropdownContent } = useHeaderContext();
|
||||||
|
const { headerHeight } = useHeaderHeight();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const heroContent = document.getElementById("hero-content");
|
||||||
|
if (!heroContent) {
|
||||||
|
// If hero-content doesn't exist, don't render the background piece
|
||||||
|
setNoRender(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heroContentHeight = heroContent.clientHeight;
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
setNoRender(window.scrollY > heroContentHeight - 120);
|
||||||
|
};
|
||||||
|
|
||||||
|
onScroll();
|
||||||
|
|
||||||
|
window.addEventListener("scroll", onScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", onScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"cw-[1335px] transition-all z-[105] absolute top-0 flex justify-between h-[calc(100%+21px)] duration-[200ms] pointer-events-none",
|
||||||
|
{ "opacity-0": noRender || dropdownContent || !headerHeight },
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
paddingTop: headerHeight - 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="h-[3000px] w-[calc(100%-21px)] left-[10.5px] absolute bottom-21 border-x border-border-faint" />
|
||||||
|
|
||||||
|
<Connector className="sticky" style={{ top: headerHeight - 10 }} />
|
||||||
|
<Connector className="sticky" style={{ top: headerHeight - 10 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export default function CenterStar({
|
||||||
|
...props
|
||||||
|
}: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="47"
|
||||||
|
viewBox="0 0 47 47"
|
||||||
|
width="47"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M24 18C24 21.3137 26.6863 24 30 24H34V25H30C26.6863 25 24 27.6863 24 31V35H23V31C23 27.6863 20.3137 25 17 25H13V24H17C20.3137 24 23 21.3137 23 18V14H24V18Z"
|
||||||
|
fill="var(--heat-100)"
|
||||||
|
fillOpacity="1"
|
||||||
|
/>
|
||||||
|
<circle cx="23.5" cy="23.5" r="23" stroke="#EDEDED" strokeOpacity="1" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function HomeHeroBadge() {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="p-4 rounded-full flex w-max mx-auto mb-12 lg:mb-16 items-center relative inside-border before:border-border-faint group"
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<div className="px-8 text-label-x-small">Website Builder</div>
|
||||||
|
|
||||||
|
<div className="p-1">
|
||||||
|
<div className="size-18 bg-accent-black flex-center rounded-full group-hover:bg-heat-100 transition-all group-hover:w-30">
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 10 8"
|
||||||
|
width="10"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
className="transition-all -translate-x-2 group-hover:translate-x-0"
|
||||||
|
d="M6 1L9 4L6 7"
|
||||||
|
stroke="white"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.25"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
className="transition-all -translate-x-3 group-hover:translate-x-0 scale-x-[0] group-hover:scale-x-[1] origin-right"
|
||||||
|
d="M1 4L9 4"
|
||||||
|
stroke="white"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { Connector } from "@/components/shared/layout/curvy-rect";
|
||||||
|
import HeroFlame from "@/components/shared/effects/flame/hero-flame";
|
||||||
|
|
||||||
|
import HomeHeroBackground from "./Background/Background";
|
||||||
|
import { BackgroundOuterPiece } from "./Background/BackgroundOuterPiece";
|
||||||
|
import HomeHeroBadge from "./Badge/Badge";
|
||||||
|
import HomeHeroPixi from "./Pixi/Pixi";
|
||||||
|
import HomeHeroTitle from "./Title/Title";
|
||||||
|
import HeroInput from "../hero-input/HeroInput";
|
||||||
|
import HeroScraping from "../hero-scraping/HeroScraping";
|
||||||
|
|
||||||
|
export default function HomeHero() {
|
||||||
|
return (
|
||||||
|
<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 />
|
||||||
|
|
||||||
|
<p className="text-center text-body-large">
|
||||||
|
Power your AI apps with clean data crawled
|
||||||
|
<br className="lg-max:hidden" />
|
||||||
|
from any website.
|
||||||
|
<Link
|
||||||
|
className="bg-black-alpha-4 hover:bg-black-alpha-6 lg:ml-4 rounded-6 px-8 lg:px-6 text-label-large lg-max:py-2 h-30 lg:h-24 block lg-max:mt-8 lg-max:mx-auto lg-max:w-max lg:inline-block gap-4 transition-all"
|
||||||
|
href="https://github.com/firecrawl/firecrawl"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
It's also open source.
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container lg:contents !p-16 relative -mt-90">
|
||||||
|
<div className="absolute top-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
|
||||||
|
<div className="absolute bottom-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
|
||||||
|
|
||||||
|
<Connector className="-top-10 -left-[10.5px] lg:hidden" />
|
||||||
|
<Connector className="-top-10 -right-[10.5px] lg:hidden" />
|
||||||
|
<Connector className="-bottom-10 -left-[10.5px] lg:hidden" />
|
||||||
|
<Connector className="-bottom-10 -right-[10.5px] lg:hidden" />
|
||||||
|
|
||||||
|
<HeroInput />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HeroScraping />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Suspense, lazy, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const Pixi = lazy(() => import("@/components/shared/pixi/Pixi"));
|
||||||
|
import features from "./tickers/features";
|
||||||
|
|
||||||
|
function PixiContent() {
|
||||||
|
return (
|
||||||
|
<Pixi
|
||||||
|
canvasAttrs={{
|
||||||
|
className: "cw-[1314px] h-506 absolute top-100 lg-max:hidden",
|
||||||
|
}}
|
||||||
|
fps={Infinity}
|
||||||
|
initOptions={{ backgroundAlpha: 0 }}
|
||||||
|
smartStop={false}
|
||||||
|
tickers={[features]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomeHeroPixi() {
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleError = (e: ErrorEvent) => {
|
||||||
|
if (e.message.includes('pixi') || e.message.includes('ChunkLoadError')) {
|
||||||
|
setHasError(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('error', handleError);
|
||||||
|
return () => window.removeEventListener('error', handleError);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
// Return empty div as fallback if Pixi fails to load
|
||||||
|
return <div className="cw-[1314px] h-506 absolute top-100 lg-max:hidden" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="cw-[1314px] h-506 absolute top-100 lg-max:hidden" />}>
|
||||||
|
<PixiContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||||
|
import PixiAssetManager from "@/components/shared/pixi/PixiAssetManager";
|
||||||
|
import { RenderTexture, Sprite, Text } from "pixi.js";
|
||||||
|
|
||||||
|
// Add more contrast to the ASCII_CHARS and ensure 'X' is used
|
||||||
|
// const ASCII_CHARS = [' ', '.', ':', '-', '=', '+', 'X'];
|
||||||
|
const ASCII_CHARS = ' .":,-_^=+';
|
||||||
|
|
||||||
|
function getAsciiChar(luminance: number) {
|
||||||
|
if (luminance < 50) return " ";
|
||||||
|
|
||||||
|
const norm = Math.max(0, Math.min(1, (luminance - 16) / (250 - 16)));
|
||||||
|
const skewed = Math.pow(norm, 1.5);
|
||||||
|
|
||||||
|
const minIdx = 1;
|
||||||
|
const maxIdx = ASCII_CHARS.length - 1;
|
||||||
|
const idx = minIdx + Math.floor(skewed * (maxIdx - minIdx + 1));
|
||||||
|
const safeIdx = Math.max(minIdx, Math.min(maxIdx, idx));
|
||||||
|
|
||||||
|
return ASCII_CHARS[safeIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprinkle logic is now a no-op, as getAsciiChar handles the randomness
|
||||||
|
function sprinkleAscii(line: string) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tickAscii: Ticker = async ({ app, canvas }) => {
|
||||||
|
const textures = await Promise.all(
|
||||||
|
Array.from({ length: 150 }, async (_, i) => {
|
||||||
|
const texture = await PixiAssetManager.load(
|
||||||
|
`/Arşiv/FAQ Demo/FAQ_${i.toString().padStart(5, "0")}.png`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return texture!;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const width = canvas.clientWidth;
|
||||||
|
const height = canvas.clientHeight;
|
||||||
|
|
||||||
|
const sprites = textures.map((texture) => new Sprite(texture));
|
||||||
|
|
||||||
|
sprites.forEach((sprite) => {
|
||||||
|
sprite.width = width;
|
||||||
|
sprite.height = height;
|
||||||
|
sprite.x = 0;
|
||||||
|
sprite.y = 0;
|
||||||
|
|
||||||
|
app.stage.addChild(sprite);
|
||||||
|
sprite.alpha = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render the texture to a renderTexture to extract pixels
|
||||||
|
const renderTexture = RenderTexture.create({ width, height });
|
||||||
|
|
||||||
|
let ascii = "";
|
||||||
|
|
||||||
|
const asciiText = new Text({
|
||||||
|
text: ascii,
|
||||||
|
style: {
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 8,
|
||||||
|
fill: 0x000,
|
||||||
|
align: "left",
|
||||||
|
lineHeight: 8,
|
||||||
|
whiteSpace: "pre",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
asciiText.alpha = 0.2;
|
||||||
|
asciiText.x = 0;
|
||||||
|
asciiText.y = 0;
|
||||||
|
|
||||||
|
const variants: string[] = [];
|
||||||
|
|
||||||
|
const render = (index: number) => {
|
||||||
|
ascii = "";
|
||||||
|
const sprite = sprites[index];
|
||||||
|
|
||||||
|
sprites.forEach((sprite) => {
|
||||||
|
sprite.alpha = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
sprite.alpha = 1;
|
||||||
|
app.renderer.render({ container: sprite, target: renderTexture });
|
||||||
|
sprite.alpha = 0;
|
||||||
|
|
||||||
|
const pixels = app.renderer.extract.pixels(renderTexture).pixels;
|
||||||
|
|
||||||
|
const charWidth = 4.81640625;
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y += 8) {
|
||||||
|
let line = "";
|
||||||
|
|
||||||
|
for (let x = 0; x < width; x += charWidth) {
|
||||||
|
let totalLum = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (let dy = 0; dy < 8; dy++) {
|
||||||
|
for (let dx = 0; dx < 4; dx++) {
|
||||||
|
const px = Math.floor(x + dx);
|
||||||
|
const py = Math.floor(y + dy);
|
||||||
|
if (px >= width || py >= height) continue;
|
||||||
|
const idx = (py * width + px) * 4;
|
||||||
|
const r = pixels[idx];
|
||||||
|
const g = pixels[idx + 1];
|
||||||
|
const b = pixels[idx + 2];
|
||||||
|
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
totalLum += lum;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const avgLum = count ? totalLum / count : 0;
|
||||||
|
line += getAsciiChar(avgLum);
|
||||||
|
}
|
||||||
|
ascii += sprinkleAscii(line) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
variants[index] = ascii;
|
||||||
|
|
||||||
|
asciiText.text = ascii;
|
||||||
|
};
|
||||||
|
|
||||||
|
app.stage.addChild(asciiText);
|
||||||
|
|
||||||
|
for (let i = 0; i < sprites.length; i++) {
|
||||||
|
render(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
//@ts-expect-error - safeAdd method exists on extended ticker
|
||||||
|
app.ticker.safeAdd(() => {
|
||||||
|
i++;
|
||||||
|
if (i >= sprites.length) i = 0;
|
||||||
|
|
||||||
|
render(i);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tickAscii;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||||
|
|
||||||
|
import AnimatedRect from "./components/AnimatedRect";
|
||||||
|
import BlinkingContainer from "./components/BlinkingContainer";
|
||||||
|
import crawl from "./crawl";
|
||||||
|
import mapping from "./mapping";
|
||||||
|
import scrape from "./scrape";
|
||||||
|
import search from "./search";
|
||||||
|
|
||||||
|
type Props = Parameters<Ticker>[0] & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CELL_SIZE = 80;
|
||||||
|
|
||||||
|
export const MAIN_COLOR = 0xe6e6e6;
|
||||||
|
|
||||||
|
const animations = [scrape, mapping, search, crawl];
|
||||||
|
|
||||||
|
let lastActive = -1;
|
||||||
|
|
||||||
|
export default function cell(props: Props) {
|
||||||
|
const blinkingContainer = BlinkingContainer({
|
||||||
|
x: props.x + 10,
|
||||||
|
y: props.y + 10,
|
||||||
|
app: props.app,
|
||||||
|
});
|
||||||
|
|
||||||
|
const anchorGraphic = AnimatedRect({
|
||||||
|
app: props.app,
|
||||||
|
x: CELL_SIZE / 2,
|
||||||
|
y: CELL_SIZE / 2,
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
radius: 10,
|
||||||
|
color: MAIN_COLOR,
|
||||||
|
});
|
||||||
|
|
||||||
|
blinkingContainer.container.addChild(anchorGraphic.graphic);
|
||||||
|
|
||||||
|
props.app.stage.addChild(blinkingContainer.container);
|
||||||
|
|
||||||
|
let running = false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
trigger: async () => {
|
||||||
|
if (running) return;
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
lastActive = (lastActive + 1) % animations.length;
|
||||||
|
|
||||||
|
const fn = animations[lastActive];
|
||||||
|
|
||||||
|
await fn({
|
||||||
|
...props,
|
||||||
|
blinkingContainer,
|
||||||
|
anchorGraphic,
|
||||||
|
});
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||||
|
import AnimatedRect from "./components/AnimatedRect";
|
||||||
|
|
||||||
|
type Props = Parameters<Ticker>[0] & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function cellReveal(props: Props) {
|
||||||
|
const graphic = AnimatedRect({
|
||||||
|
app: props.app,
|
||||||
|
x: props.x + 0.5,
|
||||||
|
y: props.y + 0.5,
|
||||||
|
width: 101,
|
||||||
|
height: 101,
|
||||||
|
radius: 0,
|
||||||
|
alpha: 0,
|
||||||
|
color: 0x000,
|
||||||
|
centering: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
props.app.stage.addChild(graphic.graphic);
|
||||||
|
|
||||||
|
return {
|
||||||
|
trigger: async () => {
|
||||||
|
let cycleCount = 0;
|
||||||
|
|
||||||
|
const cycle = async () => {
|
||||||
|
await graphic.animate(
|
||||||
|
{
|
||||||
|
alpha: Math.random() * 0.04,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ease: "linear",
|
||||||
|
duration: 0.03,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cycleCount < 5) {
|
||||||
|
cycleCount += 1;
|
||||||
|
cycle();
|
||||||
|
} else {
|
||||||
|
await graphic.animate({ alpha: 0 });
|
||||||
|
graphic.graphic.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cycle();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
|
||||||
|
import { AnimationOptions, cubicBezier } from "motion";
|
||||||
|
import { Application, Container, Graphics, Sprite } from "pixi.js";
|
||||||
|
|
||||||
|
import { isDestroyed } from "@/components/shared/pixi/utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
app: Application;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
radius: number;
|
||||||
|
color: number;
|
||||||
|
scale?: number;
|
||||||
|
rotation?: number;
|
||||||
|
type?: "rect" | "arc" | "container" | Sprite;
|
||||||
|
animationConfig?: AnimationOptions;
|
||||||
|
alpha?: number;
|
||||||
|
|
||||||
|
centering?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IAnimatedRect = ReturnType<typeof AnimatedRect>;
|
||||||
|
|
||||||
|
export default function AnimatedRect(props: Props) {
|
||||||
|
const graphic = (() => {
|
||||||
|
if (props.type === "container") return new Container();
|
||||||
|
if (props.type instanceof Sprite) return props.type;
|
||||||
|
|
||||||
|
return new Graphics();
|
||||||
|
})();
|
||||||
|
|
||||||
|
props.alpha ??= 1;
|
||||||
|
props.scale ??= 1;
|
||||||
|
props.centering ??= true;
|
||||||
|
props.rotation ??= 0;
|
||||||
|
|
||||||
|
const p = {
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
if (isDestroyed(props.app) || graphic.destroyed) return;
|
||||||
|
|
||||||
|
graphic.scale.set(p.scale!);
|
||||||
|
graphic.alpha = p.alpha!;
|
||||||
|
graphic.rotation = p.rotation!;
|
||||||
|
|
||||||
|
if (!(graphic instanceof Graphics)) {
|
||||||
|
if (graphic instanceof Sprite) {
|
||||||
|
graphic.x = p.x;
|
||||||
|
graphic.y = p.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const g = graphic as Graphics;
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
|
||||||
|
if (p.type !== "arc") {
|
||||||
|
g.roundRect(
|
||||||
|
p.centering ? p.x - p.width / 2 : p.x,
|
||||||
|
p.centering ? p.y - p.height / 2 : p.y,
|
||||||
|
p.width,
|
||||||
|
p.height,
|
||||||
|
p.radius,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
g.arc(p.x, p.y, p.width / 2, 0, Math.PI * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.fill({ color: p.color });
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
|
||||||
|
p.animationConfig ??= {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: cubicBezier(0.83, 0, 0.17, 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultProps: props,
|
||||||
|
currentProps: p,
|
||||||
|
graphic,
|
||||||
|
setStyle: (style: Partial<Props>) => {
|
||||||
|
Object.assign(p, style);
|
||||||
|
|
||||||
|
render();
|
||||||
|
},
|
||||||
|
render,
|
||||||
|
animate: (renderProps: Partial<Props>, settings?: AnimationOptions) =>
|
||||||
|
(props.app as any).animate(p, renderProps, {
|
||||||
|
...p.animationConfig,
|
||||||
|
...settings,
|
||||||
|
onUpdate: render,
|
||||||
|
}),
|
||||||
|
reset: () => (props.app as any).animate(p, props, { onUpdate: render }),
|
||||||
|
};
|
||||||
|
}
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
|
||||||
|
import { Application, Graphics } from "pixi.js";
|
||||||
|
|
||||||
|
import { CELL_SIZE } from "@/components/app/(home)/sections/hero/Pixi/tickers/features/cell";
|
||||||
|
|
||||||
|
import AnimatedRect from "./AnimatedRect";
|
||||||
|
|
||||||
|
export type IBlinkingContainer = ReturnType<typeof BlinkingContainer>;
|
||||||
|
|
||||||
|
export default function BlinkingContainer({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
app,
|
||||||
|
}: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
app: Application;
|
||||||
|
}) {
|
||||||
|
const animatedRect = AnimatedRect({
|
||||||
|
app,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: CELL_SIZE,
|
||||||
|
height: CELL_SIZE,
|
||||||
|
radius: 0,
|
||||||
|
color: 0xededed,
|
||||||
|
type: "container",
|
||||||
|
});
|
||||||
|
|
||||||
|
animatedRect.graphic.pivot.set(CELL_SIZE / 2, CELL_SIZE / 2);
|
||||||
|
|
||||||
|
animatedRect.graphic.x = x + CELL_SIZE / 2;
|
||||||
|
animatedRect.graphic.y = y + CELL_SIZE / 2;
|
||||||
|
|
||||||
|
animatedRect.graphic.addChild(
|
||||||
|
new Graphics()
|
||||||
|
.rect(0, 0, CELL_SIZE, CELL_SIZE)
|
||||||
|
.fill({ color: "#EDEDED", alpha: 0 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const blinkLayer = new Graphics()
|
||||||
|
.rect(0, 0, CELL_SIZE, CELL_SIZE)
|
||||||
|
.fill({ color: "#F9F9F9" });
|
||||||
|
|
||||||
|
blinkLayer.zIndex = 1;
|
||||||
|
blinkLayer.alpha = 0;
|
||||||
|
|
||||||
|
animatedRect.graphic.addChild(blinkLayer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
container: animatedRect.graphic,
|
||||||
|
animate: animatedRect.animate,
|
||||||
|
reset: animatedRect.reset,
|
||||||
|
shrink: async () => {
|
||||||
|
await animatedRect.animate({ scale: 0.92 });
|
||||||
|
|
||||||
|
animatedRect.animate({ scale: 1 });
|
||||||
|
},
|
||||||
|
blink: ({ delay = 0 }: { delay?: number } = {}) => {
|
||||||
|
(app as any)
|
||||||
|
.animate(0, 0.32, {
|
||||||
|
repeatType: "reverse",
|
||||||
|
repeat: 2,
|
||||||
|
delay,
|
||||||
|
duration: 0.065,
|
||||||
|
ease: "linear",
|
||||||
|
onUpdate: (value: any) => {
|
||||||
|
blinkLayer.alpha = value as number;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
(app as any).animate(0.32, 0, {
|
||||||
|
duration: 0.065,
|
||||||
|
ease: "linear",
|
||||||
|
onUpdate: (value: any) => {
|
||||||
|
blinkLayer.alpha = value as number;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MAIN_COLOR } from "@/components/app/(home)/sections/hero/Pixi/tickers/features/cell";
|
||||||
|
|
||||||
|
import AnimatedRect from "./AnimatedRect";
|
||||||
|
|
||||||
|
export default function Dot(
|
||||||
|
props: Pick<
|
||||||
|
Parameters<typeof AnimatedRect>[0],
|
||||||
|
"x" | "y" | "app" | "animationConfig"
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return AnimatedRect({
|
||||||
|
...props,
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
radius: 10,
|
||||||
|
color: MAIN_COLOR,
|
||||||
|
type: "arc",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { animate } from "motion";
|
||||||
|
|
||||||
|
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||||
|
import { sleep } from "@/utils/sleep";
|
||||||
|
|
||||||
|
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||||
|
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||||
|
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||||
|
import Dot from "./components/Dot";
|
||||||
|
|
||||||
|
type Props = Parameters<Ticker>[0] & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
blinkingContainer: IBlinkingContainer;
|
||||||
|
anchorGraphic: IAnimatedRect;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function crawl(props: Props) {
|
||||||
|
const rects = Array.from({ length: 6 }, () => {
|
||||||
|
return AnimatedRect({
|
||||||
|
app: props.app,
|
||||||
|
x: CELL_SIZE / 2,
|
||||||
|
y: CELL_SIZE / 2,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
radius: 0,
|
||||||
|
color: MAIN_COLOR,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dots = Array.from({ length: 16 }, () => {
|
||||||
|
return Dot({
|
||||||
|
x: CELL_SIZE / 2,
|
||||||
|
y: CELL_SIZE / 2,
|
||||||
|
app: props.app,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dots.forEach((dot) =>
|
||||||
|
props.blinkingContainer.container.addChild(dot.graphic),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
/* Step 1: Reveal the main square, reveal the corner dots */
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
dots[0].animate({ x: 30, y: 30 }, { delay: 0.2 }),
|
||||||
|
dots[1].animate({ x: CELL_SIZE - 30, y: 30 }, { delay: 0.2 }),
|
||||||
|
dots[2].animate({ x: 30, y: CELL_SIZE - 30 }, { delay: 0.2 }),
|
||||||
|
dots[3].animate({ x: CELL_SIZE - 30, y: CELL_SIZE - 30 }, { delay: 0.2 }),
|
||||||
|
|
||||||
|
props.anchorGraphic.animate({
|
||||||
|
radius: 0,
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
}),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.forEach((rect) =>
|
||||||
|
props.blinkingContainer.container.addChild(rect.graphic),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.unshift(props.anchorGraphic);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
props.blinkingContainer.blink({ delay: 0.3 });
|
||||||
|
await props.blinkingContainer.shrink();
|
||||||
|
|
||||||
|
let spriteOverlay: IAnimatedRect | null = null;
|
||||||
|
|
||||||
|
// Use fallback rectangle instead of trying to load missing image
|
||||||
|
spriteOverlay = AnimatedRect({
|
||||||
|
x: 13,
|
||||||
|
y: 39,
|
||||||
|
color: MAIN_COLOR,
|
||||||
|
width: 54,
|
||||||
|
height: 34,
|
||||||
|
app: props.app,
|
||||||
|
radius: 4,
|
||||||
|
centering: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
spriteOverlay.graphic.zIndex = -1;
|
||||||
|
props.blinkingContainer.container.addChild(spriteOverlay.graphic);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
spriteOverlay?.animate({ height: 23, y: 50 }),
|
||||||
|
|
||||||
|
rects[0].animate({ width: 16, height: 16, y: 34 }),
|
||||||
|
rects.slice(1, 4).map((rect) => rect.animate({ x: 24, y: 50 })),
|
||||||
|
rects.slice(4, 8).map((rect) => rect.animate({ x: 56, y: 50 })),
|
||||||
|
|
||||||
|
dots[0].animate({ x: 28, y: 22 }),
|
||||||
|
dots[1].animate({ x: 52, y: 22 }),
|
||||||
|
dots[2].animate({ x: 16, y: 58 }),
|
||||||
|
dots[3].animate({ x: 64, y: 58 }),
|
||||||
|
|
||||||
|
dots[4].animate({ x: 16, y: 42 }),
|
||||||
|
dots[5].animate({ x: 64, y: 42 }),
|
||||||
|
dots[6].animate({ x: 32, y: 58 }),
|
||||||
|
dots[7].animate({ x: 48, y: 58 }),
|
||||||
|
|
||||||
|
dots.slice(8, 12).map((dot) => dot.animate({ x: 24, y: 50 })),
|
||||||
|
dots.slice(12, 16).map((dot) => dot.animate({ x: 56, y: 50 })),
|
||||||
|
].flat().filter(Boolean),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
props.blinkingContainer.blink({ delay: 0.3 });
|
||||||
|
await props.blinkingContainer.shrink();
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
spriteOverlay?.animate({ height: 8, y: 58 }),
|
||||||
|
|
||||||
|
rects[0].animate({ y: 28 }),
|
||||||
|
[1, 4].map((i) => rects[i].animate({ y: 44 })),
|
||||||
|
[2, 3].map((i) => rects[i].animate({ x: 12, y: 56 })),
|
||||||
|
[5, 6].map((i) => rects[i].animate({ x: 68, y: 56 })),
|
||||||
|
|
||||||
|
dots[0].animate({ y: 16 }),
|
||||||
|
dots[1].animate({ y: 16 }),
|
||||||
|
|
||||||
|
dots[2].animate({ x: 4, y: 64 }),
|
||||||
|
dots[3].animate({ x: 76, y: 64 }),
|
||||||
|
|
||||||
|
dots[4].animate({ x: 4, y: 48 }),
|
||||||
|
dots[5].animate({ x: 76, y: 48 }),
|
||||||
|
dots[6].animate({ x: 20, y: 64 }),
|
||||||
|
dots[7].animate({ x: 60, y: 64 }),
|
||||||
|
|
||||||
|
dots[8].animate({ x: 16, y: 36 }),
|
||||||
|
dots[12].animate({ x: 64, y: 36 }),
|
||||||
|
|
||||||
|
dots[9].animate({ x: 32, y: 52 }),
|
||||||
|
dots[13].animate({ x: 48, y: 52 }),
|
||||||
|
|
||||||
|
[10, 11].map((i) => dots[i].animate({ x: 12, y: 56 })),
|
||||||
|
[14, 15].map((i) => dots[i].animate({ x: 68, y: 56 })),
|
||||||
|
].flat().filter(Boolean),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
props.blinkingContainer.blink({ delay: 0.3 });
|
||||||
|
await props.blinkingContainer.shrink();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
spriteOverlay.animate({ height: 0, y: 66 }),
|
||||||
|
|
||||||
|
rects[0].animate({ y: 20 }),
|
||||||
|
[1, 4].map((i) => rects[i].animate({ y: 36 })),
|
||||||
|
[2, 5].map((i) => rects[i].animate({ y: 48 })),
|
||||||
|
[3, 6].map((i) => rects[i].animate({ y: 60, x: i === 3 ? 24 : 56 })),
|
||||||
|
|
||||||
|
[0, 1, 4, 5, 8, 9, 12, 13].map((i) =>
|
||||||
|
dots[i].animate({ y: dots[i].currentProps.y - 8 }),
|
||||||
|
),
|
||||||
|
|
||||||
|
dots[2].animate({ x: 4, y: 56 }),
|
||||||
|
dots[3].animate({ x: 76, y: 56 }),
|
||||||
|
|
||||||
|
dots[6].animate({ x: 32, y: 68 }),
|
||||||
|
dots[7].animate({ x: 48, y: 68 }),
|
||||||
|
|
||||||
|
dots[10].animate({ x: 32, y: 52 }),
|
||||||
|
dots[11].animate({ x: 16, y: 68 }),
|
||||||
|
dots[14].animate({ x: 48, y: 52 }),
|
||||||
|
dots[15].animate({ x: 64, y: 68 }),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
rects.map((rect) =>
|
||||||
|
rect.animate(props.anchorGraphic.defaultProps, {
|
||||||
|
delay: Math.random() * 0.3,
|
||||||
|
duration: 0.3,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
dots.map((dot) =>
|
||||||
|
dot.animate(dot.defaultProps, { delay: Math.random() * 0.3 }),
|
||||||
|
),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.shift();
|
||||||
|
|
||||||
|
rects.forEach((rect) => rect.graphic.destroy());
|
||||||
|
dots.forEach((dot) => dot.graphic.destroy());
|
||||||
|
spriteOverlay.graphic.destroy();
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||||
|
import setTimeoutOnVisible from "@/utils/set-timeout-on-visible";
|
||||||
|
|
||||||
|
import cell from "./cell";
|
||||||
|
import cellReveal from "./cellReveal";
|
||||||
|
|
||||||
|
const CELL_GRID = [
|
||||||
|
"-ooooooooooo-",
|
||||||
|
"-oo-------oo-",
|
||||||
|
"ooo-------ooo",
|
||||||
|
"-oo-------oo-",
|
||||||
|
"-oo-------oo-",
|
||||||
|
];
|
||||||
|
|
||||||
|
const REVEAL_ANIMATION_GRID = [
|
||||||
|
[
|
||||||
|
"---ooooooo---",
|
||||||
|
"--o-------o--",
|
||||||
|
"--o-------o--",
|
||||||
|
"--o-------o--",
|
||||||
|
"--o-------o--",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"--o-------o--",
|
||||||
|
"-o---------o-",
|
||||||
|
"-o---------o-",
|
||||||
|
"-o---------o-",
|
||||||
|
"-o---------o-",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"-o---------o-",
|
||||||
|
"-------------",
|
||||||
|
"o-----------o",
|
||||||
|
"-------------",
|
||||||
|
"-------------",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"-------------",
|
||||||
|
"-------------",
|
||||||
|
"o-----------o",
|
||||||
|
"-------------",
|
||||||
|
"-------------",
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const features: Ticker = (params) => {
|
||||||
|
const cells: ReturnType<typeof cell>[] = [];
|
||||||
|
const cellReveals: {
|
||||||
|
cell: ReturnType<typeof cellReveal>;
|
||||||
|
row: number;
|
||||||
|
column: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < CELL_GRID.length; i++) {
|
||||||
|
const row = CELL_GRID[i];
|
||||||
|
|
||||||
|
for (let j = 0; j < row.length; j++) {
|
||||||
|
if (row[j] === "o") {
|
||||||
|
cells.push(
|
||||||
|
cell({
|
||||||
|
...params,
|
||||||
|
x: j * 101,
|
||||||
|
y: i * 101,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
cellReveals.push({
|
||||||
|
cell: cellReveal({
|
||||||
|
...params,
|
||||||
|
x: j * 101,
|
||||||
|
y: i * 101,
|
||||||
|
}),
|
||||||
|
row: i,
|
||||||
|
column: j,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cycle = () =>
|
||||||
|
setTimeoutOnVisible({
|
||||||
|
element: params.canvas,
|
||||||
|
callback: () => {
|
||||||
|
const cell = cells[Math.floor(Math.random() * cells.length)];
|
||||||
|
|
||||||
|
if (cell) {
|
||||||
|
cell.trigger().then(() => cycle());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeout: 3000 * Math.random(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
cycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
let revealIndex = -1;
|
||||||
|
|
||||||
|
const revealCycle = () => {
|
||||||
|
revealIndex += 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < REVEAL_ANIMATION_GRID[revealIndex].length; i++) {
|
||||||
|
const row = REVEAL_ANIMATION_GRID[revealIndex][i];
|
||||||
|
|
||||||
|
for (let j = 0; j < row.length; j++) {
|
||||||
|
if (row[j] === "o") {
|
||||||
|
cellReveals
|
||||||
|
.find((cell) => cell.row === i && cell.column === j)
|
||||||
|
?.cell.trigger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (revealIndex < REVEAL_ANIMATION_GRID.length - 1) {
|
||||||
|
setTimeout(() => {
|
||||||
|
revealCycle();
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
revealCycle();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default features;
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||||
|
import { sleep } from "@/utils/sleep";
|
||||||
|
|
||||||
|
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||||
|
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||||
|
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||||
|
import Dot from "./components/Dot";
|
||||||
|
|
||||||
|
type Props = Parameters<Ticker>[0] & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
blinkingContainer: IBlinkingContainer;
|
||||||
|
anchorGraphic: IAnimatedRect;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function mapping(props: Props) {
|
||||||
|
const rects = Array.from({ length: 8 }, () => {
|
||||||
|
return AnimatedRect({
|
||||||
|
app: props.app,
|
||||||
|
x: CELL_SIZE / 2,
|
||||||
|
y: CELL_SIZE / 2,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
radius: 0,
|
||||||
|
color: MAIN_COLOR,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dots = Array.from({ length: 20 }, () => {
|
||||||
|
return Dot({
|
||||||
|
x: CELL_SIZE / 2,
|
||||||
|
y: CELL_SIZE / 2,
|
||||||
|
app: props.app,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dots.forEach((dot) =>
|
||||||
|
props.blinkingContainer.container.addChild(dot.graphic),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
await props.anchorGraphic.animate({
|
||||||
|
radius: 0,
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
rects.forEach((rect) =>
|
||||||
|
props.blinkingContainer.container.addChild(rect.graphic),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.unshift(props.anchorGraphic);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
props.blinkingContainer.blink({ delay: 0.1 });
|
||||||
|
await props.blinkingContainer.shrink();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
dots.slice(0, 16).map((dot, index) => {
|
||||||
|
const x = 13 + (index % 4) * 18;
|
||||||
|
const y = 13 + Math.floor(index / 4) * 18;
|
||||||
|
|
||||||
|
return dot.animate({ x, y });
|
||||||
|
}),
|
||||||
|
|
||||||
|
rects[0].animate({ width: 10, height: 10 }),
|
||||||
|
|
||||||
|
rects.map((rect, index) => {
|
||||||
|
const x = 22 + (index % 3) * 18;
|
||||||
|
const y = 22 + Math.floor(index / 3) * 18;
|
||||||
|
|
||||||
|
return rect.animate({ x, y });
|
||||||
|
}),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
|
props.blinkingContainer.blink({ delay: 0.1 });
|
||||||
|
await props.blinkingContainer.shrink();
|
||||||
|
|
||||||
|
const baseDotPositions = [
|
||||||
|
[13, 13],
|
||||||
|
[31, 31],
|
||||||
|
[49, 31],
|
||||||
|
[13, 31],
|
||||||
|
[49, 49],
|
||||||
|
[67, 49],
|
||||||
|
[13, 49],
|
||||||
|
[31, 67],
|
||||||
|
[67, 67],
|
||||||
|
];
|
||||||
|
|
||||||
|
const dotPositions: string[] = [];
|
||||||
|
|
||||||
|
for (const [x, y] of baseDotPositions) {
|
||||||
|
const positions = [
|
||||||
|
{ x: x - 9, y: y - 9 },
|
||||||
|
{ x: x + 9, y: y - 9 },
|
||||||
|
{ x: x - 9, y: y + 9 },
|
||||||
|
{ x: x + 9, y: y + 9 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const position of positions) {
|
||||||
|
if (!dotPositions.includes(`${position.x},${position.y}`)) {
|
||||||
|
dotPositions.push(`${position.x},${position.y}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
rects[0].animate({ x: 13, y: 13 }),
|
||||||
|
rects[1].animate({ x: 31, y: 31 }),
|
||||||
|
rects[2].animate({ x: 49, y: 31 }),
|
||||||
|
rects[3].animate({ x: 13, y: 31 }),
|
||||||
|
|
||||||
|
rects[4].animate({ x: 49, y: 49 }),
|
||||||
|
rects[5].animate({ x: 67, y: 49 }),
|
||||||
|
|
||||||
|
rects[6].animate({ x: 13, y: 49 }),
|
||||||
|
rects[7].animate({ x: 31, y: 67 }),
|
||||||
|
rects[8].animate({ x: 67, y: 67 }),
|
||||||
|
|
||||||
|
dots.map((dot, index) => {
|
||||||
|
const position = dotPositions[index].split(",").map(Number);
|
||||||
|
|
||||||
|
return dot.animate({ x: position[0], y: position[1] });
|
||||||
|
}),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const lines = Array.from({ length: 8 }, () => {
|
||||||
|
return AnimatedRect({
|
||||||
|
app: props.app,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
radius: 0,
|
||||||
|
color: MAIN_COLOR,
|
||||||
|
centering: false,
|
||||||
|
animationConfig: {
|
||||||
|
duration: 0.25,
|
||||||
|
ease: "linear",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
lines.forEach((graphic) =>
|
||||||
|
props.blinkingContainer.container.addChild(graphic.graphic),
|
||||||
|
);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
lines[0].setStyle({ width: 1, height: 0, y: 18, x: 12.5 });
|
||||||
|
await lines[0].animate({ height: 9 });
|
||||||
|
|
||||||
|
lines[1].setStyle({ width: 0, height: 1, y: 30.5, x: 18 });
|
||||||
|
await lines[1].animate({ width: 9 });
|
||||||
|
lines[2].setStyle({ width: 0, height: 1, y: 30.5, x: 36 });
|
||||||
|
await lines[2].animate({ width: 9 });
|
||||||
|
|
||||||
|
lines[3].setStyle({ width: 1, height: 3, y: 36, x: 48.5 });
|
||||||
|
await lines[3].animate({ height: 9 });
|
||||||
|
lines[4].setStyle({ width: 0, height: 1, y: 48.5, x: 54 });
|
||||||
|
await lines[4].animate({ width: 9 });
|
||||||
|
})();
|
||||||
|
|
||||||
|
lines[5].setStyle({ width: 0, height: 1, y: 66.5, x: 62 });
|
||||||
|
await lines[5].animate({ width: 28, x: 62 - 28 }, { duration: 0.4 });
|
||||||
|
lines[6].setStyle({ width: 0, height: 1, y: 66.5, x: 26 });
|
||||||
|
await lines[6].animate({ width: 13.5, x: 26 - 13.5 });
|
||||||
|
lines[7].setStyle({ width: 1, height: 0, y: 66.5, x: 12.5 });
|
||||||
|
await lines[7].animate({ height: 14.5, y: 66.5 - 13.5 });
|
||||||
|
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
props.blinkingContainer.blink({ delay: 0.1 });
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
lines.map((line) => line.animate({ alpha: 0 })),
|
||||||
|
|
||||||
|
rects.map((rect) =>
|
||||||
|
rect.animate(props.anchorGraphic.defaultProps, {
|
||||||
|
delay: Math.random() * 0.3,
|
||||||
|
duration: 0.3,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
dots.map((dot) =>
|
||||||
|
dot.animate(dot.defaultProps, { delay: Math.random() * 0.3 }),
|
||||||
|
),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.shift();
|
||||||
|
|
||||||
|
lines.forEach((line) => line.graphic.destroy());
|
||||||
|
rects.forEach((rect) => rect.graphic.destroy());
|
||||||
|
dots.forEach((dot) => dot.graphic.destroy());
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||||
|
import { sleep } from "@/utils/sleep";
|
||||||
|
|
||||||
|
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||||
|
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||||
|
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||||
|
import Dot from "./components/Dot";
|
||||||
|
|
||||||
|
type Props = Parameters<Ticker>[0] & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
blinkingContainer: IBlinkingContainer;
|
||||||
|
anchorGraphic: IAnimatedRect;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function scrape(props: Props) {
|
||||||
|
const rects = Array.from({ length: 15 }, () => {
|
||||||
|
return AnimatedRect({
|
||||||
|
app: props.app,
|
||||||
|
x: CELL_SIZE / 2,
|
||||||
|
y: CELL_SIZE / 2,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
radius: 0,
|
||||||
|
color: MAIN_COLOR,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dots = Array.from({ length: 25 }, () => {
|
||||||
|
return Dot({
|
||||||
|
x: CELL_SIZE / 2,
|
||||||
|
y: CELL_SIZE / 2,
|
||||||
|
app: props.app,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dots.forEach((dot) =>
|
||||||
|
props.blinkingContainer.container.addChild(dot.graphic),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
[0, 12, 13, 14].map((index) =>
|
||||||
|
dots[index].animate({ x: 30, y: 30 }, { delay: 0.2 }),
|
||||||
|
),
|
||||||
|
[1, 15, 16, 17].map((index) =>
|
||||||
|
dots[index].animate({ x: CELL_SIZE - 30, y: 30 }, { delay: 0.2 }),
|
||||||
|
),
|
||||||
|
[2, 18, 19, 20].map((index) =>
|
||||||
|
dots[index].animate({ x: 30, y: CELL_SIZE - 30 }, { delay: 0.2 }),
|
||||||
|
),
|
||||||
|
[3, 21, 22, 23].map((index) =>
|
||||||
|
dots[index].animate(
|
||||||
|
{ x: CELL_SIZE - 30, y: CELL_SIZE - 30 },
|
||||||
|
{ delay: 0.2 },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
props.anchorGraphic.animate({
|
||||||
|
radius: 0,
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
}),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.forEach((rect) =>
|
||||||
|
props.blinkingContainer.container.addChild(rect.graphic),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.unshift(props.anchorGraphic);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
props.blinkingContainer.blink({ delay: 0.1 });
|
||||||
|
await props.blinkingContainer.shrink();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
[0, 12, 13, 14].map((index) => dots[index].animate({ x: 22, y: 22 })),
|
||||||
|
[1, 15, 16, 17].map((index) =>
|
||||||
|
dots[index].animate({ x: CELL_SIZE - 22, y: 22 }),
|
||||||
|
),
|
||||||
|
[2, 18, 19, 20].map((index) =>
|
||||||
|
dots[index].animate({ x: 22, y: CELL_SIZE - 22 }),
|
||||||
|
),
|
||||||
|
[3, 21, 22, 23].map((index) =>
|
||||||
|
dots[index].animate({ x: CELL_SIZE - 22, y: CELL_SIZE - 22 }),
|
||||||
|
),
|
||||||
|
|
||||||
|
dots[4].animate({ x: 40, y: 22 }),
|
||||||
|
dots[5].animate({ x: 22, y: 40 }),
|
||||||
|
dots[6].animate({ x: CELL_SIZE - 22, y: 40 }),
|
||||||
|
dots[7].animate({ x: 40, y: 58 }),
|
||||||
|
|
||||||
|
dots[8].animate({ x: 40, y: 22 }),
|
||||||
|
dots[9].animate({ x: 22, y: 40 }),
|
||||||
|
dots[10].animate({ x: CELL_SIZE - 22, y: 40 }),
|
||||||
|
dots[11].animate({ x: 40, y: 58 }),
|
||||||
|
|
||||||
|
rects[0].animate({ width: 10, height: 10 }),
|
||||||
|
rects.slice(0, 4).map((rect) => rect.animate({ x: 31, y: 31 })),
|
||||||
|
rects
|
||||||
|
.slice(4, 8)
|
||||||
|
.map((rect) => rect.animate({ x: CELL_SIZE - 31, y: 31 })),
|
||||||
|
rects
|
||||||
|
.slice(8, 12)
|
||||||
|
.map((rect) => rect.animate({ x: 31, y: CELL_SIZE - 31 })),
|
||||||
|
rects
|
||||||
|
.slice(12, 16)
|
||||||
|
.map((rect) => rect.animate({ x: CELL_SIZE - 31, y: CELL_SIZE - 31 })),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
|
props.blinkingContainer.blink({ delay: 0.1 });
|
||||||
|
await props.blinkingContainer.shrink();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
dots[0].animate({ x: 4, y: 4 }),
|
||||||
|
dots[1].animate({ x: CELL_SIZE - 4, y: 4 }),
|
||||||
|
dots[2].animate({ x: 4, y: CELL_SIZE - 4 }),
|
||||||
|
dots[3].animate({ x: CELL_SIZE - 4, y: CELL_SIZE - 4 }),
|
||||||
|
dots[4].animate({ x: 40, y: 4 }),
|
||||||
|
dots[5].animate({ x: 4, y: 40 }),
|
||||||
|
dots[6].animate({ x: 76, y: 40 }),
|
||||||
|
dots[7].animate({ x: 40, y: 76 }),
|
||||||
|
|
||||||
|
dots[13].animate({ x: 22, y: 4 }),
|
||||||
|
dots[14].animate({ x: 4, y: 22 }),
|
||||||
|
dots[16].animate({ x: 58, y: 4 }),
|
||||||
|
dots[17].animate({ x: 76, y: 22 }),
|
||||||
|
dots[19].animate({ x: 4, y: 58 }),
|
||||||
|
dots[20].animate({ x: 22, y: 76 }),
|
||||||
|
dots[22].animate({ x: 58, y: 76 }),
|
||||||
|
dots[23].animate({ x: 76, y: 58 }),
|
||||||
|
|
||||||
|
rects.map((rect, index) => {
|
||||||
|
const quadrant = Math.floor(index / 4);
|
||||||
|
const position = index % 4;
|
||||||
|
|
||||||
|
const col = (position % 2 === 0 ? 1 : 2) + (quadrant % 2 === 0 ? 0 : 2);
|
||||||
|
const row = Math.floor(position / 2) + (quadrant < 2 ? 1 : 3);
|
||||||
|
|
||||||
|
return rect.animate({
|
||||||
|
x: 13 + (col - 1) * 18,
|
||||||
|
y: 13 + (row - 1) * 18,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(1200);
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
dots.map((dot) =>
|
||||||
|
dot.animate({ alpha: 0 }, { delay: Math.random() * 0.3 }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
props.blinkingContainer.blink({ delay: 0.2 });
|
||||||
|
|
||||||
|
const newWidths: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < rects.length; i++) {
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
newWidths.push(20 + Math.random() * 28);
|
||||||
|
} else {
|
||||||
|
const remainingSpace = 62 - newWidths[i - 1];
|
||||||
|
newWidths.push(10 + Math.random() * remainingSpace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
rects.map((rect, index) => {
|
||||||
|
const y = 8 + Math.floor(index / 2) * 6 + Math.floor(index / 4) * 8;
|
||||||
|
|
||||||
|
return rect.animate(
|
||||||
|
{
|
||||||
|
y,
|
||||||
|
x:
|
||||||
|
(index % 2 === 0 ? 8 : newWidths[index - 1] + 10) +
|
||||||
|
newWidths[index] / 2,
|
||||||
|
height: 4,
|
||||||
|
width: newWidths[index],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: Math.random() * 0.1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
props.blinkingContainer.blink({ delay: 0.1 });
|
||||||
|
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
rects.map((rect) =>
|
||||||
|
rect.animate(props.anchorGraphic.defaultProps, {
|
||||||
|
delay: Math.random() * 0.3,
|
||||||
|
duration: 0.3,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.shift();
|
||||||
|
|
||||||
|
rects.forEach((rect) => rect.graphic.destroy());
|
||||||
|
dots.forEach((dot) => dot.graphic.destroy());
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||||
|
import { sleep } from "@/utils/sleep";
|
||||||
|
|
||||||
|
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||||
|
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||||
|
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||||
|
import Dot from "./components/Dot";
|
||||||
|
|
||||||
|
type Props = Parameters<Ticker>[0] & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
blinkingContainer: IBlinkingContainer;
|
||||||
|
anchorGraphic: IAnimatedRect;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function search(props: Props) {
|
||||||
|
const rects = Array.from({ length: 8 }, () => {
|
||||||
|
return AnimatedRect({
|
||||||
|
app: props.app,
|
||||||
|
x: CELL_SIZE / 2,
|
||||||
|
y: CELL_SIZE / 2,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
radius: 0,
|
||||||
|
color: MAIN_COLOR,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dots = Array.from({ length: 16 }, () => {
|
||||||
|
return Dot({
|
||||||
|
x: CELL_SIZE / 2,
|
||||||
|
y: CELL_SIZE / 2,
|
||||||
|
app: props.app,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dots.forEach((dot) =>
|
||||||
|
props.blinkingContainer.container.addChild(dot.graphic),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
await props.anchorGraphic.animate({
|
||||||
|
radius: 0,
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
rects.forEach((rect) =>
|
||||||
|
props.blinkingContainer.container.addChild(rect.graphic),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.unshift(props.anchorGraphic);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
props.blinkingContainer.blink({ delay: 0.1 });
|
||||||
|
await props.blinkingContainer.shrink();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
dots.map((dot, index) => {
|
||||||
|
const x = 13 + (index % 4) * 18;
|
||||||
|
const y = 13 + Math.floor(index / 4) * 18;
|
||||||
|
|
||||||
|
return dot.animate({ x, y });
|
||||||
|
}),
|
||||||
|
|
||||||
|
rects[0].animate({ width: 10, height: 10 }),
|
||||||
|
|
||||||
|
rects.map((rect, index) => {
|
||||||
|
const x = 22 + (index % 3) * 18;
|
||||||
|
const y = 22 + Math.floor(index / 3) * 18;
|
||||||
|
|
||||||
|
return rect.animate({ x, y });
|
||||||
|
}),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
[
|
||||||
|
rects.map((rect) => rect.animate({ alpha: 0.68 })),
|
||||||
|
dots.map((dot) => dot.animate({ alpha: 0.68 })),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
props.blinkingContainer.blink();
|
||||||
|
await sleep(400);
|
||||||
|
|
||||||
|
for await (const rect of rects) {
|
||||||
|
// Get the surrounding dots of this rect
|
||||||
|
const rectX = rect.currentProps.x;
|
||||||
|
const rectY = rect.currentProps.y;
|
||||||
|
const surroundingDots = dots.filter((dot) => {
|
||||||
|
const dx = Math.abs(dot.currentProps.x - rectX);
|
||||||
|
const dy = Math.abs(dot.currentProps.y - rectY);
|
||||||
|
|
||||||
|
// Consider "surrounding" as adjacent horizontally, vertically, or diagonally (distance 18)
|
||||||
|
return (
|
||||||
|
(dx === 0 && dy === 9) ||
|
||||||
|
(dx === 9 && dy === 0) ||
|
||||||
|
(dx === 9 && dy === 9)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
surroundingDots.map((dot) =>
|
||||||
|
dot.animate({ alpha: 1 }, { duration: 0.75 }),
|
||||||
|
),
|
||||||
|
rect.animate({ alpha: 1, width: 14, height: 14 }, { duration: 0.75 }),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rect.animate({ alpha: 0.68, width: 10, height: 10 }, { duration: 0.75 });
|
||||||
|
Promise.all(
|
||||||
|
surroundingDots.map((dot) =>
|
||||||
|
dot.animate({ alpha: 0.68 }, { duration: 0.75 }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
rects.map((rect) =>
|
||||||
|
rect.animate(props.anchorGraphic.defaultProps, {
|
||||||
|
delay: Math.random() * 0.3,
|
||||||
|
duration: 0.3,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
dots.map((dot) =>
|
||||||
|
dot.animate(dot.defaultProps, { delay: Math.random() * 0.3 }),
|
||||||
|
),
|
||||||
|
].flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rects.shift();
|
||||||
|
|
||||||
|
rects.forEach((rect) => rect.graphic.destroy());
|
||||||
|
dots.forEach((dot) => dot.graphic.destroy());
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// import dynamic from "next/dynamic";
|
||||||
|
// import { useRef, useEffect, forwardRef } from "react";
|
||||||
|
|
||||||
|
// const originalText =
|
||||||
|
// "";
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
randomizeChance?: number;
|
||||||
|
reversed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptText = (
|
||||||
|
text: string,
|
||||||
|
progress: number,
|
||||||
|
_options?: Options,
|
||||||
|
) => {
|
||||||
|
const options = {
|
||||||
|
randomizeChance: 0.7,
|
||||||
|
..._options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptionChars = "a-zA-Z0-9*=?!";
|
||||||
|
const skipTags = ["<br class='lg-max:hidden'>", "<span>", "</span>"];
|
||||||
|
|
||||||
|
// Calculate how many characters should be encrypted
|
||||||
|
const totalChars = text.length;
|
||||||
|
const encryptedCount = Math.floor(totalChars * (1 - progress));
|
||||||
|
|
||||||
|
let result = "";
|
||||||
|
let charIndex = 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const char = text[i];
|
||||||
|
|
||||||
|
// Check if we're at the start of a tag to skip
|
||||||
|
let shouldSkip = false;
|
||||||
|
|
||||||
|
for (const tag of skipTags) {
|
||||||
|
if (text.substring(i, i + tag.length) === tag) {
|
||||||
|
result += tag;
|
||||||
|
i += tag.length - 1; // -1 because loop will increment
|
||||||
|
shouldSkip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldSkip) continue;
|
||||||
|
|
||||||
|
// Skip spaces - keep them as is
|
||||||
|
if (char === " ") {
|
||||||
|
result += char;
|
||||||
|
charIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this character should be encrypted
|
||||||
|
if (
|
||||||
|
options.reversed
|
||||||
|
? charIndex < encryptedCount
|
||||||
|
: text.length - charIndex < encryptedCount
|
||||||
|
) {
|
||||||
|
// 40% chance to show original character, 60% chance to encrypt
|
||||||
|
if (Math.random() < options.randomizeChance) {
|
||||||
|
result += char;
|
||||||
|
} else {
|
||||||
|
// Use random character from encryption set
|
||||||
|
const randomIndex = Math.floor(Math.random() * encryptionChars.length);
|
||||||
|
result += encryptionChars[randomIndex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Keep original character
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
charIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// const Wrapper = forwardRef<
|
||||||
|
// HTMLDivElement,
|
||||||
|
// React.HTMLAttributes<HTMLDivElement>
|
||||||
|
// >((props, ref) => {
|
||||||
|
// return (
|
||||||
|
// <div className="text-title-h1 mx-auto text-center [&_span]:text-heat-100 mb-12 lg:mb-16">
|
||||||
|
// <div {...props} className="hidden lg:contents" ref={ref} />
|
||||||
|
// <div
|
||||||
|
// className="lg:hidden contents"
|
||||||
|
// dangerouslySetInnerHTML={{ __html: originalText }}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Wrapper.displayName = "Wrapper";
|
||||||
|
|
||||||
|
// export default dynamic(() => Promise.resolve(HomeHeroTitle), {
|
||||||
|
// ssr: false,
|
||||||
|
// loading: () => (
|
||||||
|
// <Wrapper
|
||||||
|
// dangerouslySetInnerHTML={{ __html: encryptText(originalText, 0) }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// function HomeHeroTitle() {
|
||||||
|
// const textRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (window.innerWidth < 996) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let progress = 0;
|
||||||
|
// let increaseProgress = -10;
|
||||||
|
|
||||||
|
// const animate = () => {
|
||||||
|
// increaseProgress = (increaseProgress + 1) % 5;
|
||||||
|
|
||||||
|
// if (increaseProgress === 4) {
|
||||||
|
// progress += 0.3;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (progress > 1) {
|
||||||
|
// progress = 1;
|
||||||
|
// textRef.current!.innerHTML = encryptText(originalText, progress);
|
||||||
|
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// textRef.current!.innerHTML = encryptText(originalText, progress);
|
||||||
|
|
||||||
|
// const interval = 50 + progress * 20;
|
||||||
|
// setTimeout(animate, interval);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// animate();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <Wrapper
|
||||||
|
// dangerouslySetInnerHTML={{ __html: encryptText(originalText, 0) }}
|
||||||
|
// ref={textRef}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// import dynamic from "next/dynamic";
|
||||||
|
// import { useRef, useEffect, forwardRef } from "react";
|
||||||
|
|
||||||
|
// const originalText =
|
||||||
|
// "Turn websites into <br class='lg-max:hidden'><span>LLM-ready</span> data";
|
||||||
|
|
||||||
|
// type Options = {
|
||||||
|
// randomizeChance?: number;
|
||||||
|
// reversed?: boolean;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export const encryptText = (
|
||||||
|
// text: string,
|
||||||
|
// progress: number,
|
||||||
|
// _options?: Options,
|
||||||
|
// ) => {
|
||||||
|
// const options = {
|
||||||
|
// randomizeChance: 0.7,
|
||||||
|
// ..._options,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const encryptionChars = "a-zA-Z0-9*=?!";
|
||||||
|
// const skipTags = ["<br class='lg-max:hidden'>", "<span>", "</span>"];
|
||||||
|
|
||||||
|
// // Calculate how many characters should be encrypted
|
||||||
|
// const totalChars = text.length;
|
||||||
|
// const encryptedCount = Math.floor(totalChars * (1 - progress));
|
||||||
|
|
||||||
|
// let result = "";
|
||||||
|
// let charIndex = 1;
|
||||||
|
|
||||||
|
// for (let i = 0; i < text.length; i++) {
|
||||||
|
// const char = text[i];
|
||||||
|
|
||||||
|
// // Check if we're at the start of a tag to skip
|
||||||
|
// let shouldSkip = false;
|
||||||
|
|
||||||
|
// for (const tag of skipTags) {
|
||||||
|
// if (text.substring(i, i + tag.length) === tag) {
|
||||||
|
// result += tag;
|
||||||
|
// i += tag.length - 1; // -1 because loop will increment
|
||||||
|
// shouldSkip = true;
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (shouldSkip) continue;
|
||||||
|
|
||||||
|
// // Skip spaces - keep them as is
|
||||||
|
// if (char === " ") {
|
||||||
|
// result += char;
|
||||||
|
// charIndex++;
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // If this character should be encrypted
|
||||||
|
// if (
|
||||||
|
// options.reversed
|
||||||
|
// ? charIndex < encryptedCount
|
||||||
|
// : text.length - charIndex < encryptedCount
|
||||||
|
// ) {
|
||||||
|
// // 40% chance to show original character, 60% chance to encrypt
|
||||||
|
// if (Math.random() < options.randomizeChance) {
|
||||||
|
// result += char;
|
||||||
|
// } else {
|
||||||
|
// // Use random character from encryption set
|
||||||
|
// const randomIndex = Math.floor(Math.random() * encryptionChars.length);
|
||||||
|
// result += encryptionChars[randomIndex];
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // Keep original character
|
||||||
|
// result += char;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// charIndex++;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return result;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const Wrapper = forwardRef<
|
||||||
|
// HTMLDivElement,
|
||||||
|
// React.HTMLAttributes<HTMLDivElement>
|
||||||
|
// >((props, ref) => {
|
||||||
|
// return (
|
||||||
|
// <div className="text-title-h1 mx-auto text-center [&_span]:text-heat-100 mb-12 lg:mb-16">
|
||||||
|
// <div {...props} className="hidden lg:contents" ref={ref} />
|
||||||
|
// <div
|
||||||
|
// className="lg:hidden contents"
|
||||||
|
// dangerouslySetInnerHTML={{ __html: originalText }}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Wrapper.displayName = "Wrapper";
|
||||||
|
|
||||||
|
// export default dynamic(() => Promise.resolve(HomeHeroTitle), {
|
||||||
|
// ssr: false,
|
||||||
|
// loading: () => (
|
||||||
|
// <Wrapper
|
||||||
|
// dangerouslySetInnerHTML={{ __html: encryptText(originalText, 0) }}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// });
|
||||||
|
|
||||||
|
export default function HomeHeroTitle() {
|
||||||
|
return (
|
||||||
|
<h1 className="text-title-h1 mx-auto text-center [&_span]:text-heat-100 mb-12 lg:mb-16">
|
||||||
|
Open Lovable <span>v2</span>
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Home Page Components Rules
|
||||||
|
|
||||||
|
When working with home/landing page components in components-new/app/(home):
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
```
|
||||||
|
home/
|
||||||
|
├── sections/ # Major page sections
|
||||||
|
│ ├── hero/ # Hero section with flames
|
||||||
|
│ ├── features/ # Feature showcase
|
||||||
|
│ ├── testimonials/# Customer testimonials
|
||||||
|
│ ├── pricing/ # Pricing cards
|
||||||
|
│ └── faq/ # FAQ section
|
||||||
|
├── navbar/ # Landing page navbar
|
||||||
|
└── footer/ # Landing page footer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
These components will be migrated from `marketing/` when beginning home page migration after Dashboard v2.
|
||||||
|
|
||||||
|
### Priority Sections to Migrate:
|
||||||
|
1. **Hero** - Main landing with HeroFlame effect
|
||||||
|
2. **Features** - Feature grid with animations
|
||||||
|
3. **Testimonials** - Social proof section
|
||||||
|
4. **Pricing** - Pricing tiers with heat buttons
|
||||||
|
5. **FAQ** - Collapsible FAQ items
|
||||||
|
|
||||||
|
### Usage Pattern:
|
||||||
|
```tsx
|
||||||
|
// app/page.tsx (future)
|
||||||
|
import { Hero } from '@/components/home/sections/hero';
|
||||||
|
import { Features } from '@/components/home/sections/features';
|
||||||
|
import { Testimonials } from '@/components/home/sections/testimonials';
|
||||||
|
import { Pricing } from '@/components/home/sections/pricing';
|
||||||
|
import { FAQ } from '@/components/home/sections/faq';
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero />
|
||||||
|
<Features />
|
||||||
|
<Testimonials />
|
||||||
|
<Pricing />
|
||||||
|
<FAQ />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
- **Fire theme**: Subtle flame effects in hero
|
||||||
|
- **Performance**: Lazy load below-fold sections
|
||||||
|
- **Responsive**: Mobile-first approach
|
||||||
|
- **Animations**: Intersection observer for scroll effects
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface SidebarInputProps {
|
||||||
|
onSubmit: (url: string, style: string, model: string, instructions?: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SidebarInput({ onSubmit, disabled = false }: SidebarInputProps) {
|
||||||
|
const [url, setUrl] = useState<string>("");
|
||||||
|
const [selectedStyle, setSelectedStyle] = useState<string>("1");
|
||||||
|
const [selectedModel, setSelectedModel] = useState<string>("moonshotai/kimi-k2-instruct-0905");
|
||||||
|
const [additionalInstructions, setAdditionalInstructions] = useState<string>("");
|
||||||
|
const [isValidUrl, setIsValidUrl] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Simple URL validation - currently unused but keeping for future use
|
||||||
|
// const validateUrl = (urlString: string) => {
|
||||||
|
// if (!urlString) return false;
|
||||||
|
// const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
|
||||||
|
// return urlPattern.test(urlString.toLowerCase());
|
||||||
|
// };
|
||||||
|
|
||||||
|
const styles = [
|
||||||
|
{ id: "1", name: "Glassmorphism", description: "Frosted glass effect" },
|
||||||
|
{ id: "2", name: "Neumorphism", description: "Soft 3D shadows" },
|
||||||
|
{ id: "3", name: "Brutalism", description: "Bold and raw" },
|
||||||
|
{ id: "4", name: "Minimalist", description: "Clean and simple" },
|
||||||
|
{ id: "5", name: "Dark Mode", description: "Dark theme design" },
|
||||||
|
{ id: "6", name: "Gradient Rich", description: "Vibrant gradients" },
|
||||||
|
{ id: "7", name: "3D Depth", description: "Dimensional layers" },
|
||||||
|
{ id: "8", name: "Retro Wave", description: "80s inspired" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const models = [
|
||||||
|
{ id: "moonshotai/kimi-k2-instruct-0905", name: "Kimi K2 0905 on Groq" },
|
||||||
|
{ id: "openai/gpt-5", name: "GPT-5" },
|
||||||
|
{ id: "anthropic/claude-sonnet-4-20250514", name: "Sonnet 4" },
|
||||||
|
{ id: "google/gemini-2.0-flash-exp", name: "Gemini 2.0" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSubmit = (e?: React.FormEvent) => {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
if (!url.trim() || disabled) return;
|
||||||
|
|
||||||
|
onSubmit(url.trim(), selectedStyle, selectedModel, additionalInstructions || undefined);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setUrl("");
|
||||||
|
setAdditionalInstructions("");
|
||||||
|
setIsValidUrl(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div >
|
||||||
|
<div className="p-4 border-b border-gray-100">
|
||||||
|
{/* link to home page with button */}
|
||||||
|
<Link href="/">
|
||||||
|
<button className="w-full px-3 py-2 text-xs font-medium text-gray-700 bg-white rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500">
|
||||||
|
Generate a new website
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Options Section - Show when valid URL */}
|
||||||
|
{isValidUrl && (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* Style Selector */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-2">Style</label>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{styles.map((style) => (
|
||||||
|
<button
|
||||||
|
key={style.id}
|
||||||
|
onClick={() => setSelectedStyle(style.id)}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`
|
||||||
|
py-2 px-2 rounded text-xs font-medium border transition-all text-center
|
||||||
|
${selectedStyle === style.id
|
||||||
|
? 'border-orange-500 bg-orange-50 text-orange-900'
|
||||||
|
: 'border-gray-200 hover:border-gray-300 bg-white text-gray-700'
|
||||||
|
}
|
||||||
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{style.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model Selector */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-2">AI Model</label>
|
||||||
|
<select
|
||||||
|
value={selectedModel}
|
||||||
|
onChange={(e) => setSelectedModel(e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-full px-3 py-2 text-xs font-medium text-gray-700 bg-white rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500"
|
||||||
|
>
|
||||||
|
{models.map((model) => (
|
||||||
|
<option key={model.id} value={model.id}>
|
||||||
|
{model.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Additional Instructions */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-2">Additional Instructions (optional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={additionalInstructions}
|
||||||
|
onChange={(e) => setAdditionalInstructions(e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-full px-3 py-2 text-xs text-gray-700 bg-gray-50 rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 placeholder:text-gray-400"
|
||||||
|
placeholder="e.g., make it more colorful, add animations..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="pt-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!isValidUrl || disabled}
|
||||||
|
className={`
|
||||||
|
w-full py-2.5 px-4 rounded-lg text-sm font-medium transition-all
|
||||||
|
${isValidUrl && !disabled
|
||||||
|
? 'bg-orange-500 hover:bg-orange-600 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{disabled ? 'Scraping...' : 'Scrape Site'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import HeroInputSubmitButton from "@/components/app/(home)/sections/hero-input/Button/Button";
|
||||||
|
|
||||||
|
interface SidebarQuickInputProps {
|
||||||
|
onSubmit: (url: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SidebarQuickInput({ onSubmit, disabled = false }: SidebarQuickInputProps) {
|
||||||
|
const [url, setUrl] = useState<string>("");
|
||||||
|
|
||||||
|
const handleSubmit = (e?: React.FormEvent) => {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
if (!url.trim() || disabled) return;
|
||||||
|
|
||||||
|
onSubmit(url.trim());
|
||||||
|
setUrl("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<div className="p-3 flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
className="flex-1 bg-transparent text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none"
|
||||||
|
placeholder="Enter a new URL to regenerate..."
|
||||||
|
type="text"
|
||||||
|
value={url}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div onClick={handleSubmit}>
|
||||||
|
<HeroInputSubmitButton dirty={url.length > 0} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
export enum Endpoint {
|
||||||
|
Scrape = "scrape",
|
||||||
|
Crawl = "crawl",
|
||||||
|
Search = "search",
|
||||||
|
Map = "map",
|
||||||
|
Extract = "extract",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AgentModel {
|
||||||
|
FIRE_1 = "FIRE-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FormatType {
|
||||||
|
Markdown = "markdown",
|
||||||
|
Summary = "summary",
|
||||||
|
Json = "json",
|
||||||
|
RawHtml = "rawHtml",
|
||||||
|
Html = "html",
|
||||||
|
Screenshot = "screenshot",
|
||||||
|
ScreenshotFullPage = "screenshot@fullPage",
|
||||||
|
Links = "links",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SearchFormatType {
|
||||||
|
Web = "web",
|
||||||
|
Images = "images",
|
||||||
|
News = "news",
|
||||||
|
}
|
||||||
|
|
||||||
|
type Prev = [never, 0, 1, 2, 3, 4, 5];
|
||||||
|
|
||||||
|
type Join<K, P> = K extends string | number
|
||||||
|
? P extends string | number
|
||||||
|
? `${K}.${P}`
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type Paths<T, D extends number = 5> = [D] extends [never]
|
||||||
|
? never
|
||||||
|
: T extends object
|
||||||
|
? {
|
||||||
|
[K in keyof T]-?: K extends string | number
|
||||||
|
? T[K] extends object
|
||||||
|
? K | Join<K, Paths<T[K], Prev[D]>>
|
||||||
|
: K
|
||||||
|
: never;
|
||||||
|
}[keyof T]
|
||||||
|
: "";
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { animate } from "framer-motion";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
|
||||||
|
interface AnimatedDotIconProps {
|
||||||
|
active?: boolean;
|
||||||
|
alwaysHeat?: boolean;
|
||||||
|
triggerOnHover?: boolean;
|
||||||
|
size?: number;
|
||||||
|
className?: string;
|
||||||
|
pattern?:
|
||||||
|
| "usage"
|
||||||
|
| "api-keys"
|
||||||
|
| "settings"
|
||||||
|
| "overview"
|
||||||
|
| "team"
|
||||||
|
| "billing"
|
||||||
|
| "account-settings"
|
||||||
|
| "admin"
|
||||||
|
| "domain-checker"
|
||||||
|
| "extract-playground"
|
||||||
|
| "extract"
|
||||||
|
| "logs"
|
||||||
|
| "playground"
|
||||||
|
| "teams";
|
||||||
|
}
|
||||||
|
|
||||||
|
const initCanvas = (canvas: HTMLCanvasElement) => {
|
||||||
|
const { width, height } = canvas.getBoundingClientRect();
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
canvas.style.width = `${width}px`;
|
||||||
|
canvas.style.height = `${height}px`;
|
||||||
|
|
||||||
|
const upscaleCanvas = () => {
|
||||||
|
const scale = window.visualViewport?.scale || 1;
|
||||||
|
const dpr = (window.devicePixelRatio || 1) * scale;
|
||||||
|
|
||||||
|
canvas.width = width * dpr;
|
||||||
|
canvas.height = height * dpr;
|
||||||
|
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
canvas.dispatchEvent(new Event("resize"));
|
||||||
|
};
|
||||||
|
|
||||||
|
upscaleCanvas();
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
setTimeout(upscaleCanvas, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
window.visualViewport?.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pattern definitions for different pages
|
||||||
|
const patterns = {
|
||||||
|
usage: {
|
||||||
|
grid: [
|
||||||
|
[10, 11, 12, 14, 15, 16],
|
||||||
|
[3, 7, 19, 23],
|
||||||
|
[0, 2, 24, 26],
|
||||||
|
[27, 28, 29, 31, 32, 33],
|
||||||
|
],
|
||||||
|
gridSize: 7,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
"api-keys": {
|
||||||
|
grid: [[12], [10, 14], [8, 16], [6, 18], [4, 5, 19, 20]],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
grid: [
|
||||||
|
[0, 1, 2, 3, 4],
|
||||||
|
[5, 9],
|
||||||
|
[10, 14],
|
||||||
|
[15, 19],
|
||||||
|
[20, 21, 22, 23, 24],
|
||||||
|
],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
overview: {
|
||||||
|
grid: [
|
||||||
|
[24],
|
||||||
|
[16, 18, 30, 32],
|
||||||
|
[8, 12, 36, 40],
|
||||||
|
[0, 3, 6, 21, 27, 42, 45, 48],
|
||||||
|
],
|
||||||
|
gridSize: 7,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
grid: [
|
||||||
|
[6, 7, 8],
|
||||||
|
[11, 12, 13],
|
||||||
|
[16, 17, 18],
|
||||||
|
[0, 4, 20, 24],
|
||||||
|
],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
grid: [
|
||||||
|
[6, 7, 8],
|
||||||
|
[11, 12, 13],
|
||||||
|
[16, 17, 18],
|
||||||
|
[0, 4, 20, 24],
|
||||||
|
],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
billing: {
|
||||||
|
grid: [
|
||||||
|
[0, 4],
|
||||||
|
[5, 6, 8, 9],
|
||||||
|
[10, 11, 13, 14],
|
||||||
|
[15, 19],
|
||||||
|
],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
"account-settings": {
|
||||||
|
grid: [
|
||||||
|
[2, 7, 12, 17, 22],
|
||||||
|
[5, 10, 15, 20],
|
||||||
|
[8, 13, 18],
|
||||||
|
[11, 16],
|
||||||
|
],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
grid: [
|
||||||
|
[0, 1, 2, 3, 4],
|
||||||
|
[5, 14],
|
||||||
|
[10, 11, 12, 13],
|
||||||
|
[15, 24],
|
||||||
|
[20, 21, 22, 23, 24],
|
||||||
|
],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
"domain-checker": {
|
||||||
|
grid: [
|
||||||
|
[12, 13, 14],
|
||||||
|
[7, 11, 15, 19],
|
||||||
|
[2, 6, 20, 24],
|
||||||
|
[0, 1, 25, 26],
|
||||||
|
],
|
||||||
|
gridSize: 6,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
"extract-playground": {
|
||||||
|
grid: [
|
||||||
|
[5, 10, 15, 20],
|
||||||
|
[6, 11, 16, 21],
|
||||||
|
[7, 12, 17, 22],
|
||||||
|
[8, 13, 18, 23],
|
||||||
|
],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
extract: {
|
||||||
|
grid: [[12], [7, 17], [2, 6, 18, 22], [0, 1, 3, 4, 20, 21, 23, 24]],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
logs: {
|
||||||
|
grid: [
|
||||||
|
[0, 5, 10, 15, 20],
|
||||||
|
[1, 6, 11, 16, 21],
|
||||||
|
[2, 7, 12, 17, 22],
|
||||||
|
[3, 8, 13, 18, 23],
|
||||||
|
],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
playground: {
|
||||||
|
grid: [
|
||||||
|
[6, 8, 16, 18],
|
||||||
|
[10, 11, 12, 13, 14],
|
||||||
|
[5, 9, 15, 19],
|
||||||
|
[0, 4, 20, 24],
|
||||||
|
],
|
||||||
|
gridSize: 5,
|
||||||
|
cellSize: 2,
|
||||||
|
spacing: 2,
|
||||||
|
offset: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AnimatedDotIcon({
|
||||||
|
active = true,
|
||||||
|
alwaysHeat = false,
|
||||||
|
triggerOnHover = false,
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
pattern = "usage",
|
||||||
|
}: AnimatedDotIconProps) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const fnRefs = useRef<{
|
||||||
|
activate: () => void;
|
||||||
|
deactivate: () => void;
|
||||||
|
}>({ activate: () => {}, deactivate: () => {} });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = initCanvas(canvas);
|
||||||
|
const config = patterns[pattern];
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
let isActive = false;
|
||||||
|
|
||||||
|
let activeGroup = 0;
|
||||||
|
const rowAlphas = [0.2, 0.4, 1, 0.04];
|
||||||
|
|
||||||
|
const scaler = size / 20;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
ctx.fillStyle = "#fa5d19";
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
for (const group of config.grid.slice(0, 4)) {
|
||||||
|
const groupIndex = config.grid.indexOf(group);
|
||||||
|
ctx.globalAlpha = rowAlphas[groupIndex];
|
||||||
|
|
||||||
|
for (const index of group) {
|
||||||
|
ctx.fillRect(
|
||||||
|
(config.offset + (index % config.gridSize) * config.spacing) *
|
||||||
|
scaler,
|
||||||
|
(config.offset +
|
||||||
|
Math.floor(index / config.gridSize) * config.spacing) *
|
||||||
|
scaler,
|
||||||
|
config.cellSize * scaler,
|
||||||
|
config.cellSize * scaler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeouts: number[] = [];
|
||||||
|
let runCount = 0;
|
||||||
|
|
||||||
|
const cycle = () => {
|
||||||
|
isRunning = true;
|
||||||
|
activeGroup = (activeGroup + 1) % 5;
|
||||||
|
|
||||||
|
rowAlphas.forEach((alpha, index) => {
|
||||||
|
let targetAlpha = alpha;
|
||||||
|
|
||||||
|
if (index === activeGroup) targetAlpha = 1;
|
||||||
|
else if (index === (activeGroup + 1) % 4) targetAlpha = 0.12;
|
||||||
|
else if (index === (activeGroup + 2) % 4) targetAlpha = 0.2;
|
||||||
|
else if (index === (activeGroup + 3) % 4) targetAlpha = 0.4;
|
||||||
|
|
||||||
|
animate(alpha, targetAlpha, {
|
||||||
|
duration: 0.05,
|
||||||
|
onUpdate: (value) => {
|
||||||
|
rowAlphas[index] = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.forEach((timeout) => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
isRunning = false;
|
||||||
|
}, 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeGroup === 3) runCount += 1;
|
||||||
|
|
||||||
|
if ((runCount === 2 || !isActive) && activeGroup === 2) return;
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
cycle();
|
||||||
|
}, 50),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
fnRefs.current = {
|
||||||
|
activate: () => {
|
||||||
|
if (isActive) return;
|
||||||
|
|
||||||
|
isActive = true;
|
||||||
|
runCount = 0;
|
||||||
|
cycle();
|
||||||
|
render();
|
||||||
|
},
|
||||||
|
deactivate: () => {
|
||||||
|
if (!isActive) return;
|
||||||
|
isActive = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
canvas.addEventListener("resize", render);
|
||||||
|
|
||||||
|
if (triggerOnHover) {
|
||||||
|
const group = canvasRef.current!.closest(".group");
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||||
|
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [triggerOnHover, size, pattern]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggerOnHover) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && active) {
|
||||||
|
fnRefs.current.activate();
|
||||||
|
} else {
|
||||||
|
fnRefs.current.deactivate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(canvasRef.current!);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [active, triggerOnHover]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
className={cn(
|
||||||
|
alwaysHeat
|
||||||
|
? ""
|
||||||
|
: [
|
||||||
|
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||||
|
!active && "grayscale",
|
||||||
|
],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useRef, useEffect, ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
// Smoothly animates its container to match the natural height of its content.
|
||||||
|
// Fixes previous behavior where the component observed itself, causing height 0
|
||||||
|
// with overflow hidden (content clipped) or visible overflow that overlapped
|
||||||
|
// following sections like the footer.
|
||||||
|
export default function AnimatedHeight({
|
||||||
|
children,
|
||||||
|
overflow = true,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
overflow?: boolean;
|
||||||
|
}) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [measuredHeight, setMeasuredHeight] = useState<number | null>(null);
|
||||||
|
const hasAnimatedOnceRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const contentEl = contentRef.current;
|
||||||
|
const containerEl = containerRef.current;
|
||||||
|
if (!contentEl || !containerEl) return;
|
||||||
|
|
||||||
|
const updateHeight = () => {
|
||||||
|
// Use scrollHeight to capture full natural height, including overflowed content
|
||||||
|
const height = contentEl.scrollHeight;
|
||||||
|
setMeasuredHeight((prev) => (prev === height ? prev : height));
|
||||||
|
|
||||||
|
// Enable transition after the first measurement to avoid initial jank
|
||||||
|
if (!hasAnimatedOnceRef.current) {
|
||||||
|
containerEl.style.transition = "height 300ms ease-in-out";
|
||||||
|
hasAnimatedOnceRef.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial measure
|
||||||
|
updateHeight();
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => updateHeight());
|
||||||
|
resizeObserver.observe(contentEl);
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
overflow: overflow ? "hidden" : "visible",
|
||||||
|
height: measuredHeight === null ? undefined : `${measuredHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div ref={contentRef}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
|
||||||
|
const asciiPatterns = [
|
||||||
|
`· · · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · · · · · · · · · · · · · · · · · · ·`,
|
||||||
|
`· · · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · · · ▪ · · · · · · · · ▪ · · · · ·
|
||||||
|
· · · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · · · ▪ · · · · · · · · ▪ · · · · · ·`,
|
||||||
|
`· · · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · · ▪ ▄ ▪ · · · · · · ▪ ▄ ▪ · · · ·
|
||||||
|
· · · · ▪ · · · · · · · · ▪ · · · · · ·
|
||||||
|
· · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · · ▪ ▄ ▪ · · · · · · ▪ ▄ ▪ · · · · ·`,
|
||||||
|
`· · · · · · · · · · · · · · · · · · · ·
|
||||||
|
· · ▪ ▄ █ ▄ ▪ · · · · ▪ ▄ █ ▄ ▪ · · ·
|
||||||
|
· · · ▪ ▄ ▪ · · · · · · ▪ ▄ ▪ · · · · ·
|
||||||
|
· · · ▪ · · · · · · · · · ▪ · · · · ·
|
||||||
|
· · ▪ ▄ █ ▄ ▪ · · · · ▪ ▄ █ ▄ ▪ · · · ·`,
|
||||||
|
];
|
||||||
|
|
||||||
|
interface AsciiBackgroundProps {
|
||||||
|
className?: string;
|
||||||
|
variant?: "dots" | "grid" | "flame";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsciiBackground({
|
||||||
|
className,
|
||||||
|
variant = "dots",
|
||||||
|
}: AsciiBackgroundProps) {
|
||||||
|
const [frameIndex, setFrameIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setFrameIndex((prev) => (prev + 1) % asciiPatterns.length);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 pointer-events-none select-none overflow-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<pre className="text-heat-100/3 font-mono text-[10px] leading-tight whitespace-pre absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||||
|
{asciiPatterns[frameIndex]}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user