confirm build
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
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/).
|
||||||
|
|
||||||
|
Supports both **E2B** and **Vercel** sandboxes for code execution. Choose your preferred sandbox provider in the setup below.
|
||||||
|
|
||||||
<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%"/>
|
||||||
|
|
||||||
|
|
||||||
@@ -12,7 +14,7 @@ Chat with AI to build React apps instantly. An example app made by the [Firecraw
|
|||||||
```bash
|
```bash
|
||||||
git clone https://github.com/mendableai/open-lovable.git
|
git clone https://github.com/mendableai/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`**
|
||||||
@@ -21,32 +23,73 @@ npm install
|
|||||||
# Required
|
# Required
|
||||||
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
|
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
|
||||||
|
|
||||||
# Vercel Sandbox Authentication (choose one method)
|
# =================================================================
|
||||||
# See: https://vercel.com/docs/vercel-sandbox#authentication
|
# SANDBOX PROVIDER - Choose ONE: E2B or Vercel
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
# Method 1: OIDC Token (recommended for development)
|
# Optional: Specify sandbox provider (defaults to 'e2b' if not set)
|
||||||
|
# SANDBOX_PROVIDER=e2b # or 'vercel'
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# OPTION 1: E2B Sandbox
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Get your API key from: https://e2b.dev
|
||||||
|
E2B_API_KEY=your_e2b_api_key
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# OPTION 2: Vercel Sandbox
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Method 1: OIDC Token (automatic setup)
|
||||||
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
|
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
|
||||||
# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
|
# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
|
||||||
|
|
||||||
# Method 2: Personal Access Token (for production or when OIDC unavailable)
|
# Method 2: Personal Access Token (manual setup)
|
||||||
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID
|
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID
|
||||||
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
|
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
|
||||||
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
|
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
|
||||||
|
# See: https://vercel.com/docs/vercel-sandbox#authentication
|
||||||
|
|
||||||
# Optional (need at least one AI provider)
|
# =================================================================
|
||||||
|
# AI PROVIDERS - Add at least one
|
||||||
|
# =================================================================
|
||||||
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
|
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)
|
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
|
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)
|
GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference)
|
||||||
```
|
```
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
## Sandbox Providers
|
||||||
|
|
||||||
|
Open Lovable supports two sandbox providers for code execution:
|
||||||
|
|
||||||
|
### 🔧 E2B
|
||||||
|
- **Full-featured development environment** with Node.js, npm, and pre-installed tools
|
||||||
|
- **Persistent file system** across interactions
|
||||||
|
- **Fast startup** and reliable performance
|
||||||
|
- **Advanced debugging** capabilities
|
||||||
|
- Perfect for complex applications and debugging
|
||||||
|
|
||||||
|
### ⚡ Vercel
|
||||||
|
- **Vercel-hosted sandboxes** with automatic scaling
|
||||||
|
- **Integrated with Vercel ecosystem** for seamless deployment
|
||||||
|
- **Automatic OIDC authentication** (run `vercel link` then `vercel env pull`)
|
||||||
|
- Great for **production workflows** and Vercel users
|
||||||
|
|
||||||
|
### Choosing Your Provider
|
||||||
|
|
||||||
|
- **Use E2B** if you need full development capabilities and debugging features
|
||||||
|
- **Use Vercel** if you're already in the Vercel ecosystem or prefer their hosted solution
|
||||||
|
- **Default**: E2B (if no `SANDBOX_PROVIDER` is specified)
|
||||||
|
|
||||||
|
You can switch providers anytime by updating the `SANDBOX_PROVIDER` environment variable.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -76,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+$/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -194,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
|
||||||
@@ -244,7 +244,7 @@ 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/', '');
|
||||||
// fileContent available but not used in current implementation
|
// fileContent available but not used in current implementation
|
||||||
// const fileContent = fileContents[normalizedPath]?.content || '';
|
// const fileContent = fileContents[normalizedPath]?.content || '';
|
||||||
|
|
||||||
@@ -356,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');
|
||||||
|
|
||||||
@@ -985,13 +985,15 @@ CRITICAL: When files are provided in the context:
|
|||||||
// 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/', '');
|
||||||
|
if (global.sandboxState.fileCache) {
|
||||||
global.sandboxState.fileCache.files[normalizedPath] = {
|
global.sandboxState.fileCache.files[normalizedPath] = {
|
||||||
content: content as string,
|
content: content as string,
|
||||||
lastModified: Date.now()
|
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
|
||||||
@@ -1023,7 +1025,7 @@ CRITICAL: When files are provided in the context:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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');
|
||||||
}
|
}
|
||||||
@@ -1363,7 +1365,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;
|
||||||
@@ -1729,8 +1731,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
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export async function GET() {
|
|||||||
throw new Error('Failed to list files');
|
throw new Error('Failed to list files');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileList = (await findResult.stdout()).split('\n').filter(f => f.trim());
|
const fileList = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
||||||
console.log('[get-sandbox-files] Found', fileList.length, 'files');
|
console.log('[get-sandbox-files] Found', fileList.length, 'files');
|
||||||
|
|
||||||
// Read content of each file (limit to reasonable sizes)
|
// Read content of each file (limit to reasonable sizes)
|
||||||
@@ -91,7 +91,7 @@ export async function GET() {
|
|||||||
|
|
||||||
let structure = '';
|
let structure = '';
|
||||||
if (treeResult.exitCode === 0) {
|
if (treeResult.exitCode === 0) {
|
||||||
const dirs = (await treeResult.stdout()).split('\n').filter(d => d.trim());
|
const dirs = (await treeResult.stdout()).split('\n').filter((d: string) => d.trim());
|
||||||
structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines
|
structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SandboxProvider } from '@/lib/sandbox/types';
|
|||||||
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandboxProvider: SandboxProvider | null;
|
var activeSandboxProvider: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export async function GET() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (findResult.exitCode === 0) {
|
if (findResult.exitCode === 0) {
|
||||||
const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim());
|
const logFiles = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
||||||
|
|
||||||
for (const logFile of logFiles.slice(0, 3)) {
|
for (const logFile of logFiles.slice(0, 3)) {
|
||||||
try {
|
try {
|
||||||
@@ -51,7 +51,7 @@ export async function GET() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (grepResult.exitCode === 0) {
|
if (grepResult.exitCode === 0) {
|
||||||
const errorLines = (await grepResult.stdout()).split('\n').filter(line => line.trim());
|
const errorLines = (await grepResult.stdout()).split('\n').filter((line: string) => line.trim());
|
||||||
|
|
||||||
for (const line of errorLines) {
|
for (const line of errorLines) {
|
||||||
// Extract package name from error line
|
// Extract package name from error line
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
|
|||||||
|
|
||||||
// Get active sandbox provider from global state
|
// Get active sandbox provider from global state
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandboxProvider: SandboxProvider | null;
|
var activeSandboxProvider: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
|
|||||||
@@ -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({
|
||||||
@@ -26,7 +26,7 @@ export async function GET(_request: NextRequest) {
|
|||||||
|
|
||||||
if (psResult.exitCode === 0) {
|
if (psResult.exitCode === 0) {
|
||||||
const psOutput = await psResult.stdout();
|
const psOutput = await psResult.stdout();
|
||||||
const viteProcesses = psOutput.split('\n').filter(line =>
|
const viteProcesses = psOutput.split('\n').filter((line: string) =>
|
||||||
line.toLowerCase().includes('vite') ||
|
line.toLowerCase().includes('vite') ||
|
||||||
line.toLowerCase().includes('npm run dev')
|
line.toLowerCase().includes('npm run dev')
|
||||||
);
|
);
|
||||||
@@ -49,7 +49,7 @@ export async function GET(_request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (findResult.exitCode === 0) {
|
if (findResult.exitCode === 0) {
|
||||||
const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim());
|
const logFiles = (await findResult.stdout()).split('\n').filter((f: string) => f.trim());
|
||||||
|
|
||||||
for (const logFile of logFiles.slice(0, 2)) {
|
for (const logFile of logFiles.slice(0, 2)) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -51,17 +51,17 @@ export async function POST(req: NextRequest) {
|
|||||||
screenshot: scrapeResult.screenshot,
|
screenshot: scrapeResult.screenshot,
|
||||||
metadata: scrapeResult.metadata || {}
|
metadata: scrapeResult.metadata || {}
|
||||||
});
|
});
|
||||||
} else if (scrapeResult?.data?.screenshot) {
|
} else if ((scrapeResult as any)?.data?.screenshot) {
|
||||||
// Nested data structure
|
// Nested data structure
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
screenshot: scrapeResult.data.screenshot,
|
screenshot: (scrapeResult as any).data.screenshot,
|
||||||
metadata: scrapeResult.data.metadata || {}
|
metadata: (scrapeResult as any).data.metadata || {}
|
||||||
});
|
});
|
||||||
} else if (scrapeResult?.success === false) {
|
} else if ((scrapeResult as any)?.success === false) {
|
||||||
// Explicit failure
|
// Explicit failure
|
||||||
console.error('[scrape-screenshot] Firecrawl API error:', scrapeResult.error);
|
console.error('[scrape-screenshot] Firecrawl API error:', (scrapeResult as any).error);
|
||||||
throw new Error(scrapeResult.error || 'Failed to capture screenshot');
|
throw new Error((scrapeResult as any).error || 'Failed to capture screenshot');
|
||||||
} else {
|
} else {
|
||||||
// No screenshot in response
|
// No screenshot in response
|
||||||
console.error('[scrape-screenshot] No screenshot in response. Full response:', JSON.stringify(scrapeResult, null, 2));
|
console.error('[scrape-screenshot] No screenshot in response. Full response:', JSON.stringify(scrapeResult, null, 2));
|
||||||
@@ -72,19 +72,10 @@ export async function POST(req: NextRequest) {
|
|||||||
console.error('[scrape-screenshot] Screenshot capture error:', error);
|
console.error('[scrape-screenshot] Screenshot capture error:', error);
|
||||||
console.error('[scrape-screenshot] Error stack:', error.stack);
|
console.error('[scrape-screenshot] Error stack:', error.stack);
|
||||||
|
|
||||||
// Provide fallback response for development
|
// Provide fallback response for development - removed NODE_ENV check as it doesn't work in Next.js production builds
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.warn('[scrape-screenshot] Returning placeholder screenshot for development');
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
screenshot: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
|
|
||||||
metadata: { error: 'Screenshot capture failed, using placeholder' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
error: error.message || 'Failed to capture screenshot',
|
error: error.message || 'Failed to capture screenshot'
|
||||||
details: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,23 +49,27 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle the response according to the latest SDK structure
|
// Handle the response according to the latest SDK structure
|
||||||
if (!scrapeResult.success) {
|
const result = scrapeResult as any;
|
||||||
throw new Error(scrapeResult.error || "Failed to scrape website");
|
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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
title: scrapeResult.data?.metadata?.title || "Untitled",
|
title: data?.metadata?.title || "Untitled",
|
||||||
content: scrapeResult.data?.markdown || scrapeResult.data?.html || "",
|
content: data?.markdown || data?.html || "",
|
||||||
description: scrapeResult.data?.metadata?.description || "",
|
description: data?.metadata?.description || "",
|
||||||
markdown: scrapeResult.data?.markdown || "",
|
markdown: data?.markdown || "",
|
||||||
html: scrapeResult.data?.html || "",
|
html: data?.html || "",
|
||||||
metadata: scrapeResult.data?.metadata || {},
|
metadata: data?.metadata || {},
|
||||||
screenshot: scrapeResult.data?.screenshot || null,
|
screenshot: data?.screenshot || null,
|
||||||
links: scrapeResult.data?.links || [],
|
links: data?.links || [],
|
||||||
// Include raw data for flexibility
|
// Include raw data for flexibility
|
||||||
raw: scrapeResult.data
|
raw: data
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,7 +98,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Optional: Add OPTIONS handler for CORS if needed
|
// Optional: Add OPTIONS handler for CORS if needed
|
||||||
export async function OPTIONS(_request: NextRequest) {
|
export async function OPTIONS() {
|
||||||
return new NextResponse(null, {
|
return new NextResponse(null, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const AISandboxPage = dynamic(
|
||||||
|
() => import('./page-content'),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <div className="flex items-center justify-center h-screen">Loading...</div>
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function GenerationClient() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="flex items-center justify-center h-screen">Loading...</div>}>
|
||||||
|
<AISandboxPage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
+11
-14
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef, Suspense } from 'react';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import { appConfig } from '@/config/app.config';
|
import { appConfig } from '@/config/app.config';
|
||||||
import HeroInput from '@/components/HeroInput';
|
import HeroInput from '@/components/HeroInput';
|
||||||
@@ -44,12 +44,10 @@ interface ChatMessage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AISandboxPage() {
|
function AISandboxPage() {
|
||||||
const [sandboxData, setSandboxData] = useState<SandboxData | null>(null);
|
const [sandboxData, setSandboxData] = useState<SandboxData | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [status, setStatus] = useState({ text: 'Not connected', active: false });
|
const [status, setStatus] = useState({ text: 'Not connected', active: false });
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [responseArea, setResponseArea] = useState<string[]>([]);
|
const [responseArea, setResponseArea] = useState<string[]>([]);
|
||||||
const [structureContent, setStructureContent] = useState('No sandbox created yet');
|
const [structureContent, setStructureContent] = useState('No sandbox created yet');
|
||||||
const [promptInput, setPromptInput] = useState('');
|
const [promptInput, setPromptInput] = useState('');
|
||||||
@@ -68,11 +66,8 @@ export default function AISandboxPage() {
|
|||||||
const modelParam = searchParams.get('model');
|
const modelParam = searchParams.get('model');
|
||||||
return appConfig.ai.availableModels.includes(modelParam || '') ? modelParam! : appConfig.ai.defaultModel;
|
return appConfig.ai.availableModels.includes(modelParam || '') ? modelParam! : appConfig.ai.defaultModel;
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [urlOverlayVisible, setUrlOverlayVisible] = useState(false);
|
const [urlOverlayVisible, setUrlOverlayVisible] = useState(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [urlInput, setUrlInput] = useState('');
|
const [urlInput, setUrlInput] = useState('');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [urlStatus, setUrlStatus] = useState<string[]>([]);
|
const [urlStatus, setUrlStatus] = useState<string[]>([]);
|
||||||
const [showHomeScreen, setShowHomeScreen] = useState(true);
|
const [showHomeScreen, setShowHomeScreen] = useState(true);
|
||||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(['app', 'src', 'src/components']));
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(['app', 'src', 'src/components']));
|
||||||
@@ -83,22 +78,18 @@ export default function AISandboxPage() {
|
|||||||
const [activeTab, setActiveTab] = useState<'generation' | 'preview'>('preview');
|
const [activeTab, setActiveTab] = useState<'generation' | 'preview'>('preview');
|
||||||
const [showStyleSelector, setShowStyleSelector] = useState(false);
|
const [showStyleSelector, setShowStyleSelector] = useState(false);
|
||||||
const [selectedStyle, setSelectedStyle] = useState<string | null>(null);
|
const [selectedStyle, setSelectedStyle] = useState<string | null>(null);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [showLoadingBackground, setShowLoadingBackground] = useState(false);
|
const [showLoadingBackground, setShowLoadingBackground] = useState(false);
|
||||||
const [urlScreenshot, setUrlScreenshot] = useState<string | null>(null);
|
const [urlScreenshot, setUrlScreenshot] = useState<string | null>(null);
|
||||||
const [isScreenshotLoaded, setIsScreenshotLoaded] = useState(false);
|
const [isScreenshotLoaded, setIsScreenshotLoaded] = useState(false);
|
||||||
const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false);
|
const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false);
|
||||||
const [screenshotError, setScreenshotError] = useState<string | null>(null);
|
const [screenshotError, setScreenshotError] = useState<string | null>(null);
|
||||||
const [isPreparingDesign, setIsPreparingDesign] = useState(false);
|
const [isPreparingDesign, setIsPreparingDesign] = useState(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [targetUrl, setTargetUrl] = useState<string>('');
|
const [targetUrl, setTargetUrl] = useState<string>('');
|
||||||
const [sidebarScrolled, setSidebarScrolled] = useState(false);
|
const [sidebarScrolled, setSidebarScrolled] = useState(false);
|
||||||
const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null);
|
const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null);
|
||||||
const [isStartingNewGeneration, setIsStartingNewGeneration] = useState(false);
|
const [isStartingNewGeneration, setIsStartingNewGeneration] = useState(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [sandboxFiles, setSandboxFiles] = useState<Record<string, string>>({});
|
const [sandboxFiles, setSandboxFiles] = useState<Record<string, string>>({});
|
||||||
const [hasInitialSubmission, setHasInitialSubmission] = useState<boolean>(false);
|
const [hasInitialSubmission, setHasInitialSubmission] = useState<boolean>(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [fileStructure, setFileStructure] = useState<string>('');
|
const [fileStructure, setFileStructure] = useState<string>('');
|
||||||
|
|
||||||
const [conversationContext, setConversationContext] = useState<{
|
const [conversationContext, setConversationContext] = useState<{
|
||||||
@@ -396,7 +387,6 @@ export default function AISandboxPage() {
|
|||||||
addChatMessage('Checking packages... Sandbox is ready with Vite configuration.', 'system');
|
addChatMessage('Checking packages... Sandbox is ready with Vite configuration.', 'system');
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const handleSurfaceError = (_errors: any[]) => {
|
const handleSurfaceError = (_errors: any[]) => {
|
||||||
// Function kept for compatibility but Vite errors are now handled by template
|
// Function kept for compatibility but Vite errors are now handled by template
|
||||||
|
|
||||||
@@ -407,7 +397,6 @@ export default function AISandboxPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const installPackages = async (packages: string[]) => {
|
const installPackages = async (packages: string[]) => {
|
||||||
if (!sandboxData) {
|
if (!sandboxData) {
|
||||||
addChatMessage('No active sandbox. Create a sandbox first!', 'system');
|
addChatMessage('No active sandbox. Create a sandbox first!', 'system');
|
||||||
@@ -2094,7 +2083,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
|
|||||||
addChatMessage('Waiting for sandbox to be ready...', 'system');
|
addChatMessage('Waiting for sandbox to be ready...', 'system');
|
||||||
try {
|
try {
|
||||||
const newSandboxData = await sandboxPromise;
|
const newSandboxData = await sandboxPromise;
|
||||||
if (newSandboxData) {
|
if (newSandboxData != null) {
|
||||||
activeSandboxData = newSandboxData;
|
activeSandboxData = newSandboxData;
|
||||||
// Also update the state for future use
|
// Also update the state for future use
|
||||||
setSandboxData(newSandboxData);
|
setSandboxData(newSandboxData);
|
||||||
@@ -3525,3 +3514,11 @@ Focus on the key sections and content, making it clean and modern.`;
|
|||||||
</HeaderProvider>
|
</HeaderProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="flex items-center justify-center min-h-screen">Loading...</div>}>
|
||||||
|
<AISandboxPage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -19,7 +19,7 @@ import HomeHeroBadge from "@/components/app/(home)/sections/hero/Badge/Badge";
|
|||||||
import HomeHeroPixi from "@/components/app/(home)/sections/hero/Pixi/Pixi";
|
import HomeHeroPixi from "@/components/app/(home)/sections/hero/Pixi/Pixi";
|
||||||
import HomeHeroTitle from "@/components/app/(home)/sections/hero/Title/Title";
|
import HomeHeroTitle from "@/components/app/(home)/sections/hero/Title/Title";
|
||||||
import HeroInputSubmitButton from "@/components/app/(home)/sections/hero-input/Button/Button";
|
import HeroInputSubmitButton from "@/components/app/(home)/sections/hero-input/Button/Button";
|
||||||
import Globe from "@/components/app/(home)/sections/hero-input/_svg/Globe";
|
// import Globe from "@/components/app/(home)/sections/hero-input/_svg/Globe";
|
||||||
|
|
||||||
// Import header components
|
// Import header components
|
||||||
import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit";
|
import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit";
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export const isMobileSheetOpenAtom = atom(false);
|
||||||
@@ -43,7 +43,7 @@ export default function HeroInput({
|
|||||||
} else {
|
} else {
|
||||||
setShowTiles(false);
|
setShowTiles(false);
|
||||||
}
|
}
|
||||||
}, [value, isFocused]);
|
}, [value, isFocused, showSearchFeatures]);
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ export default function ControlPanel({
|
|||||||
analysisData,
|
analysisData,
|
||||||
onReset,
|
onReset,
|
||||||
}: ControlPanelProps) {
|
}: ControlPanelProps) {
|
||||||
// const [showAIAnalysis, setShowAIAnalysis] = useState(false); // Reserved for AI analysis feature
|
// 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 [aiInsights, setAiInsights] = useState<CheckItem[]>([]);
|
||||||
const [isAnalyzingAI, setIsAnalyzingAI] = useState(false);
|
const [isAnalyzingAI, setIsAnalyzingAI] = useState(false);
|
||||||
const [combinedChecks, setCombinedChecks] = useState<CheckItem[]>([]);
|
const [combinedChecks, setCombinedChecks] = useState<CheckItem[]>([]);
|
||||||
@@ -268,7 +269,7 @@ export default function ControlPanel({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error: any) => {
|
||||||
console.error('AI analysis error:', error);
|
console.error('AI analysis error:', error);
|
||||||
// Remove loading tiles on error
|
// Remove loading tiles on error
|
||||||
setCombinedChecks(prev => prev.filter(c => !(c as any).isLoading));
|
setCombinedChecks(prev => prev.filter(c => !(c as any).isLoading));
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @stylistic/array-element-newline */
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import initCanvas from "@/utils/init-canvas";
|
import initCanvas from "@/utils/init-canvas";
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @stylistic/array-element-newline */
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { animate } from "motion";
|
import { animate } from "motion";
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
//@ts-nocheck
|
|
||||||
import { animate, AnimatePresence, cubicBezier, motion } from "motion/react";
|
import { animate, AnimatePresence, cubicBezier, motion } from "motion/react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ export default function HeroInputTabsMobile(props: {
|
|||||||
}) {
|
}) {
|
||||||
// Filter tabs based on allowedModes if provided
|
// Filter tabs based on allowedModes if provided
|
||||||
const visibleTabs = props.allowedModes
|
const visibleTabs = props.allowedModes
|
||||||
? tabs.filter((tab) => props.allowedModes.includes(tab.value))
|
? tabs.filter((tab) => props.allowedModes!.includes(tab.value))
|
||||||
: tabs;
|
: tabs;
|
||||||
|
|
||||||
const activeTab = visibleTabs.find((tab) => tab.value === props.tab)!;
|
const activeTab = visibleTabs.find((tab) => tab.value === props.tab)!;
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ const tickAscii: Ticker = async ({ app, canvas }) => {
|
|||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-expect-error - safeAdd method exists on extended ticker
|
||||||
app.ticker.safeAdd(() => {
|
app.ticker.safeAdd(() => {
|
||||||
i++;
|
i++;
|
||||||
if (i >= sprites.length) i = 0;
|
if (i >= sprites.length) i = 0;
|
||||||
|
|||||||
+2
-3
@@ -1,4 +1,3 @@
|
|||||||
//@ts-nocheck
|
|
||||||
|
|
||||||
import { AnimationOptions, cubicBezier } from "motion";
|
import { AnimationOptions, cubicBezier } from "motion";
|
||||||
import { Application, Container, Graphics, Sprite } from "pixi.js";
|
import { Application, Container, Graphics, Sprite } from "pixi.js";
|
||||||
@@ -94,11 +93,11 @@ export default function AnimatedRect(props: Props) {
|
|||||||
},
|
},
|
||||||
render,
|
render,
|
||||||
animate: (renderProps: Partial<Props>, settings?: AnimationOptions) =>
|
animate: (renderProps: Partial<Props>, settings?: AnimationOptions) =>
|
||||||
props.app.animate(p, renderProps, {
|
(props.app as any).animate(p, renderProps, {
|
||||||
...p.animationConfig,
|
...p.animationConfig,
|
||||||
...settings,
|
...settings,
|
||||||
onUpdate: render,
|
onUpdate: render,
|
||||||
}),
|
}),
|
||||||
reset: () => props.app.animate(p, props, { onUpdate: render }),
|
reset: () => (props.app as any).animate(p, props, { onUpdate: render }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-5
@@ -1,4 +1,3 @@
|
|||||||
//@ts-nocheck
|
|
||||||
|
|
||||||
import { Application, Graphics } from "pixi.js";
|
import { Application, Graphics } from "pixi.js";
|
||||||
|
|
||||||
@@ -58,22 +57,22 @@ export default function BlinkingContainer({
|
|||||||
animatedRect.animate({ scale: 1 });
|
animatedRect.animate({ scale: 1 });
|
||||||
},
|
},
|
||||||
blink: ({ delay = 0 }: { delay?: number } = {}) => {
|
blink: ({ delay = 0 }: { delay?: number } = {}) => {
|
||||||
app
|
(app as any)
|
||||||
.animate(0, 0.32, {
|
.animate(0, 0.32, {
|
||||||
repeatType: "reverse",
|
repeatType: "reverse",
|
||||||
repeat: 2,
|
repeat: 2,
|
||||||
delay,
|
delay,
|
||||||
duration: 0.065,
|
duration: 0.065,
|
||||||
ease: "linear",
|
ease: "linear",
|
||||||
onUpdate: (value) => {
|
onUpdate: (value: any) => {
|
||||||
blinkLayer.alpha = value as number;
|
blinkLayer.alpha = value as number;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
app.animate(0.32, 0, {
|
(app as any).animate(0.32, 0, {
|
||||||
duration: 0.065,
|
duration: 0.065,
|
||||||
ease: "linear",
|
ease: "linear",
|
||||||
onUpdate: (value) => {
|
onUpdate: (value: any) => {
|
||||||
blinkLayer.alpha = value as number;
|
blinkLayer.alpha = value as number;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Globe from "@/components/app/(home)/sections/hero-input/_svg/Globe";
|
import Link from "next/link";
|
||||||
import HeroInputSubmitButton from "@/components/app/(home)/sections/hero-input/Button/Button";
|
|
||||||
|
|
||||||
interface SidebarInputProps {
|
interface SidebarInputProps {
|
||||||
onSubmit: (url: string, style: string, model: string, instructions?: string) => void;
|
onSubmit: (url: string, style: string, model: string, instructions?: string) => void;
|
||||||
@@ -16,12 +15,12 @@ export default function SidebarInput({ onSubmit, disabled = false }: SidebarInpu
|
|||||||
const [additionalInstructions, setAdditionalInstructions] = useState<string>("");
|
const [additionalInstructions, setAdditionalInstructions] = useState<string>("");
|
||||||
const [isValidUrl, setIsValidUrl] = useState<boolean>(false);
|
const [isValidUrl, setIsValidUrl] = useState<boolean>(false);
|
||||||
|
|
||||||
// Simple URL validation
|
// Simple URL validation - currently unused but keeping for future use
|
||||||
const validateUrl = (urlString: string) => {
|
// const validateUrl = (urlString: string) => {
|
||||||
if (!urlString) return false;
|
// if (!urlString) return false;
|
||||||
const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
|
// const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
|
||||||
return urlPattern.test(urlString.toLowerCase());
|
// return urlPattern.test(urlString.toLowerCase());
|
||||||
};
|
// };
|
||||||
|
|
||||||
const styles = [
|
const styles = [
|
||||||
{ id: "1", name: "Glassmorphism", description: "Frosted glass effect" },
|
{ id: "1", name: "Glassmorphism", description: "Frosted glass effect" },
|
||||||
@@ -58,11 +57,11 @@ export default function SidebarInput({ onSubmit, disabled = false }: SidebarInpu
|
|||||||
<div >
|
<div >
|
||||||
<div className="p-4 border-b border-gray-100">
|
<div className="p-4 border-b border-gray-100">
|
||||||
{/* link to home page with button */}
|
{/* link to home page with button */}
|
||||||
<a href="/">
|
<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">
|
<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
|
Generate a new website
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Options Section - Show when valid URL */}
|
{/* Options Section - Show when valid URL */}
|
||||||
|
|||||||
@@ -90,10 +90,9 @@ export const SlateButton = React.forwardRef<
|
|||||||
(React.isValidElement(Icon) ? (
|
(React.isValidElement(Icon) ? (
|
||||||
Icon
|
Icon
|
||||||
) : (
|
) : (
|
||||||
//@ts-ignore
|
//@ts-expect-error - Icon component type allows JSX element
|
||||||
<Icon
|
<Icon
|
||||||
className={cn(iconSizes[size], "flex-shrink-0")}
|
className={cn(iconSizes[size], "flex-shrink-0")}
|
||||||
// @ts-ignore - Some icons support isHovered and isOpen
|
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
/>
|
/>
|
||||||
@@ -104,10 +103,9 @@ export const SlateButton = React.forwardRef<
|
|||||||
(React.isValidElement(Icon) ? (
|
(React.isValidElement(Icon) ? (
|
||||||
Icon
|
Icon
|
||||||
) : (
|
) : (
|
||||||
//@ts-ignore
|
//@ts-expect-error - Icon component type allows JSX element
|
||||||
<Icon
|
<Icon
|
||||||
className={cn(iconSizes[size], "flex-shrink-0")}
|
className={cn(iconSizes[size], "flex-shrink-0")}
|
||||||
// @ts-ignore - Some icons support isHovered and isOpen
|
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-nocheck
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, cubicBezier, motion } from "motion/react";
|
import { AnimatePresence, cubicBezier, motion } from "motion/react";
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ interface HeroFlameProps {
|
|||||||
|
|
||||||
export function HeroFlame({ className, size = "medium" }: HeroFlameProps) {
|
export function HeroFlame({ className, size = "medium" }: HeroFlameProps) {
|
||||||
const [frameIndex, setFrameIndex] = useState(0);
|
const [frameIndex, setFrameIndex] = useState(0);
|
||||||
const intervalRef = useRef<NodeJS.Timeout>();
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
intervalRef.current = setInterval(() => {
|
intervalRef.current = setInterval(() => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const pathVariants: Variants = {
|
|||||||
|
|
||||||
// Higher-order component for icon animation
|
// Higher-order component for icon animation
|
||||||
const withIconAnimation = (IconComponent: React.ComponentType<any>) => {
|
const withIconAnimation = (IconComponent: React.ComponentType<any>) => {
|
||||||
return ({
|
const AnimatedIcon = ({
|
||||||
isHovered = false,
|
isHovered = false,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
@@ -63,11 +63,13 @@ const withIconAnimation = (IconComponent: React.ComponentType<any>) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
AnimatedIcon.displayName = `Animated${IconComponent.displayName || IconComponent.name || 'Icon'}`;
|
||||||
|
return AnimatedIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Higher-order component for icon animation
|
// Higher-order component for icon animation
|
||||||
const withChartIconAnimation = (IconComponent: React.ComponentType<any>) => {
|
const withChartIconAnimation = (IconComponent: React.ComponentType<any>) => {
|
||||||
return ({
|
const AnimatedChartIcon = ({
|
||||||
isHovered = false,
|
isHovered = false,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
@@ -113,6 +115,8 @@ const withChartIconAnimation = (IconComponent: React.ComponentType<any>) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
AnimatedChartIcon.displayName = `AnimatedChart${IconComponent.displayName || IconComponent.name || 'Icon'}`;
|
||||||
|
return AnimatedChartIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Base icon components
|
// Base icon components
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ export default function LivePreviewFrame({
|
|||||||
const imgRef = useRef<HTMLImageElement>(null);
|
const imgRef = useRef<HTMLImageElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const initialPositionSetRef = useRef(false);
|
const initialPositionSetRef = useRef(false);
|
||||||
const idleStartTimerRef = useRef<NodeJS.Timeout>();
|
const idleStartTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const idleMoveTimerRef = useRef<NodeJS.Timeout>();
|
const idleMoveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(true);
|
const [isConnecting, setIsConnecting] = useState(true);
|
||||||
const [cursorPosition, setCursorPosition] = useState<{
|
const [cursorPosition, setCursorPosition] = useState<{
|
||||||
@@ -83,7 +83,7 @@ export default function LivePreviewFrame({
|
|||||||
// Only start the idle timer if not already idle and no timer is running
|
// Only start the idle timer if not already idle and no timer is running
|
||||||
idleStartTimerRef.current = setTimeout(() => {
|
idleStartTimerRef.current = setTimeout(() => {
|
||||||
setIsIdle(true);
|
setIsIdle(true);
|
||||||
idleStartTimerRef.current = undefined; // Clear ref after timer runs
|
idleStartTimerRef.current = null; // Clear ref after timer runs
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
if (animationFrameId) {
|
if (animationFrameId) {
|
||||||
@@ -96,7 +96,7 @@ export default function LivePreviewFrame({
|
|||||||
// If we were waiting to go idle, cancel it because we're moving again
|
// If we were waiting to go idle, cancel it because we're moving again
|
||||||
if (idleStartTimerRef.current) {
|
if (idleStartTimerRef.current) {
|
||||||
clearTimeout(idleStartTimerRef.current);
|
clearTimeout(idleStartTimerRef.current);
|
||||||
idleStartTimerRef.current = undefined;
|
idleStartTimerRef.current = null;
|
||||||
}
|
}
|
||||||
// Ensure idle state is false if we are moving significantly
|
// Ensure idle state is false if we are moving significantly
|
||||||
if (isIdle) setIsIdle(false);
|
if (isIdle) setIsIdle(false);
|
||||||
@@ -127,7 +127,7 @@ export default function LivePreviewFrame({
|
|||||||
const cleanupConnection = () => {
|
const cleanupConnection = () => {
|
||||||
if (reconnectTimeoutRef.current) {
|
if (reconnectTimeoutRef.current) {
|
||||||
clearTimeout(reconnectTimeoutRef.current);
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
reconnectTimeoutRef.current = undefined;
|
reconnectTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
if (wsRef.current) {
|
if (wsRef.current) {
|
||||||
wsRef.current.close();
|
wsRef.current.close();
|
||||||
@@ -171,7 +171,7 @@ export default function LivePreviewFrame({
|
|||||||
// Clear any pending reconnection attempts
|
// Clear any pending reconnection attempts
|
||||||
if (reconnectTimeoutRef.current) {
|
if (reconnectTimeoutRef.current) {
|
||||||
clearTimeout(reconnectTimeoutRef.current);
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
reconnectTimeoutRef.current = undefined;
|
reconnectTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ export default function LivePreviewFrame({
|
|||||||
// --- Interrupt Idle State ---
|
// --- Interrupt Idle State ---
|
||||||
if (idleStartTimerRef.current) {
|
if (idleStartTimerRef.current) {
|
||||||
clearTimeout(idleStartTimerRef.current);
|
clearTimeout(idleStartTimerRef.current);
|
||||||
idleStartTimerRef.current = undefined;
|
idleStartTimerRef.current = null;
|
||||||
}
|
}
|
||||||
if (isIdle) {
|
if (isIdle) {
|
||||||
setIsIdle(false);
|
setIsIdle(false);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CodeProps {
|
||||||
|
code: string;
|
||||||
|
language?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Code({ code, language = 'json', className = '' }: CodeProps) {
|
||||||
|
return (
|
||||||
|
<pre className={`overflow-auto ${className}`}>
|
||||||
|
<code className={`language-${language}`}>
|
||||||
|
{code}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface ScrambleTextProps {
|
||||||
|
text: string;
|
||||||
|
className?: string;
|
||||||
|
duration?: number;
|
||||||
|
delay?: number;
|
||||||
|
isInView?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ScrambleText({ text, className = '', duration = 1, delay = 0, isInView = true }: ScrambleTextProps) {
|
||||||
|
const [displayText, setDisplayText] = useState(text);
|
||||||
|
const [isScrambling, setIsScrambling] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isScrambling) {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
const durationMs = duration * 1000; // Convert seconds to milliseconds
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setDisplayText(
|
||||||
|
text
|
||||||
|
.split('')
|
||||||
|
.map((char) => (Math.random() > 0.5 ? chars[Math.floor(Math.random() * chars.length)] : char))
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
setDisplayText(text);
|
||||||
|
setIsScrambling(false);
|
||||||
|
}, durationMs);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [text, isScrambling, duration]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInView) {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setIsScrambling(true);
|
||||||
|
}, delay);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [text, delay, isInView]);
|
||||||
|
|
||||||
|
return <span className={className}>{displayText}</span>;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
|||||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cx } from "@/lib/app/utils";
|
import { cn } from "@/utils/cn";
|
||||||
|
|
||||||
const switchVariants = cva(
|
const switchVariants = cva(
|
||||||
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm 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-muted-foreground data-[state=unchecked]:bg-input",
|
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm 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-muted-foreground data-[state=unchecked]:bg-input",
|
||||||
@@ -46,11 +46,11 @@ const Switch = React.forwardRef<
|
|||||||
SwitchProps
|
SwitchProps
|
||||||
>(({ className, size, ...props }, ref) => (
|
>(({ className, size, ...props }, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cx(switchVariants({ size, className }))}
|
className={cn(switchVariants({ size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<SwitchPrimitives.Thumb className={cx(thumbVariants({ size }))} />
|
<SwitchPrimitives.Thumb className={cn(thumbVariants({ size }))} />
|
||||||
</SwitchPrimitives.Root>
|
</SwitchPrimitives.Root>
|
||||||
));
|
));
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface SpinnerProps {
|
||||||
|
className?: string;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
finished?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Spinner({ className = '', size = 'md', finished = false }: SpinnerProps) {
|
||||||
|
if (finished) {
|
||||||
|
// Return a checkmark or completed state
|
||||||
|
return (
|
||||||
|
<div className={`${className}`}>
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'h-4 w-4',
|
||||||
|
md: 'h-6 w-6',
|
||||||
|
lg: 'h-8 w-8'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${sizeClasses[size]} ${className}`}>
|
||||||
|
<svg
|
||||||
|
className="animate-spin"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -14,7 +14,7 @@ const eslintConfig = [
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "warn",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"react-hooks/exhaustive-deps": "warn",
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
"react/no-unescaped-entities": "off",
|
"react/no-unescaped-entities": "off",
|
||||||
"prefer-const": "warn"
|
"prefer-const": "warn"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cx(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -22,12 +22,30 @@
|
|||||||
"@mendable/firecrawl-js": "^4.3.3",
|
"@mendable/firecrawl-js": "^4.3.3",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.34.1",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
@@ -43,12 +61,16 @@
|
|||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"groq-sdk": "^0.29.0",
|
"groq-sdk": "^0.29.0",
|
||||||
|
"jotai": "^2.14.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.532.0",
|
"lucide-react": "^0.532.0",
|
||||||
"motion": "^12.23.12",
|
"motion": "^12.23.12",
|
||||||
"next": "15.4.3",
|
"next": "15.4.3",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"pixi.js": "^8.13.1",
|
"pixi.js": "^8.13.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-hook-form": "^7.62.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -60,6 +82,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ This project is configured to use Vercel Sandboxes for code execution.
|
|||||||
|
|
||||||
## Vercel Authentication
|
## Vercel Authentication
|
||||||
|
|
||||||
### Option 1: OIDC Token (Recommended for Vercel deployments)
|
### Option 1: OIDC Token (Automatic for Vercel deployments)
|
||||||
When running in a Vercel environment, authentication happens automatically via OIDC tokens. No configuration needed!
|
When running in a Vercel environment, authentication happens automatically via OIDC tokens. No configuration needed!
|
||||||
|
|
||||||
### Option 2: Personal Access Token (For local development)
|
### Option 2: Personal Access Token (For local development)
|
||||||
|
|||||||
Generated
+2280
-310
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"configs": [
|
||||||
|
{
|
||||||
|
"extension": "avif",
|
||||||
|
"quality": 80,
|
||||||
|
"effort": 4,
|
||||||
|
"scale": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extension": "webp",
|
||||||
|
"quality": 85,
|
||||||
|
"effort": 4,
|
||||||
|
"scale": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user