diff --git a/app/api/conversation-state/route.ts b/app/api/conversation-state/route.ts index 1a37468..969692c 100644 --- a/app/api/conversation-state/route.ts +++ b/app/api/conversation-state/route.ts @@ -59,10 +59,26 @@ export async function POST(request: NextRequest) { case 'clear-old': // Clear old conversation data but keep recent context if (!global.conversationState) { + // Initialize conversation state if it doesn't exist + global.conversationState = { + conversationId: `conv-${Date.now()}`, + startedAt: Date.now(), + lastUpdated: Date.now(), + context: { + messages: [], + edits: [], + projectEvolution: { majorChanges: [] }, + userPreferences: {} + } + }; + + console.log('[conversation-state] Initialized new conversation state for clear-old'); + return NextResponse.json({ - success: false, - error: 'No active conversation to clear' - }, { status: 400 }); + success: true, + message: 'New conversation state initialized', + state: global.conversationState + }); } // Keep only recent data diff --git a/app/api/create-ai-sandbox-v2/route.ts b/app/api/create-ai-sandbox-v2/route.ts index 93410a1..6545fe8 100644 --- a/app/api/create-ai-sandbox-v2/route.ts +++ b/app/api/create-ai-sandbox-v2/route.ts @@ -5,7 +5,7 @@ import type { SandboxState } from '@/types/sandbox'; // Store active sandbox globally declare global { - var activeSandboxProvider: SandboxProvider | null; + var activeSandboxProvider: any; var sandboxData: any; var existingFiles: Set; var sandboxState: SandboxState; diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index af86837..049f533 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -18,6 +18,12 @@ export const dynamic = 'force-dynamic'; const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY; const aiGatewayBaseURL = 'https://ai-gateway.vercel.sh/v1'; +console.log('[generate-ai-code-stream] AI Gateway config:', { + isUsingAIGateway, + hasGroqKey: !!process.env.GROQ_API_KEY, + hasAIGatewayKey: !!process.env.AI_GATEWAY_API_KEY +}); + const groq = createGroq({ apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GROQ_API_KEY, baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined, @@ -152,10 +158,18 @@ export async function POST(request: NextRequest) { const stream = new TransformStream(); const writer = stream.writable.getWriter(); - // Function to send progress updates + // Function to send progress updates with flushing const sendProgress = async (data: any) => { const message = `data: ${JSON.stringify(data)}\n\n`; - await writer.write(encoder.encode(message)); + try { + await writer.write(encoder.encode(message)); + // Force flush by writing a keep-alive comment + if (data.type === 'stream' || data.type === 'conversation') { + await writer.write(encoder.encode(': keepalive\n\n')); + } + } catch (error) { + console.error('[generate-ai-code-stream] Error writing to stream:', error); + } }; // Start processing in background @@ -1169,15 +1183,22 @@ CRITICAL: When files are provided in the context: // Determine which provider to use based on model const isAnthropic = model.startsWith('anthropic/'); const isGoogle = model.startsWith('google/'); - const isOpenAI = model.startsWith('openai/gpt-5'); - const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq)); + const isOpenAI = model.startsWith('openai/'); + const isKimiGroq = model === 'moonshotai/kimi-k2-instruct-0905'; + const modelProvider = isAnthropic ? anthropic : + (isOpenAI ? openai : + (isGoogle ? googleGenerativeAI : + (isKimiGroq ? groq : groq))); // Fix model name transformation for different providers let actualModel: string; if (isAnthropic) { actualModel = model.replace('anthropic/', ''); - } else if (model === 'openai/gpt-5') { - actualModel = 'gpt-5'; + } 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/', ''); @@ -1186,6 +1207,8 @@ CRITICAL: When files are provided in the context: } console.log(`[generate-ai-code-stream] Using provider: ${isAnthropic ? 'Anthropic' : isGoogle ? 'Google' : isOpenAI ? 'OpenAI' : 'Groq'}, model: ${actualModel}`); + console.log(`[generate-ai-code-stream] AI Gateway enabled: ${isUsingAIGateway}`); + console.log(`[generate-ai-code-stream] Model string: ${model}`); // Make streaming API call with appropriate provider const streamOptions: any = { @@ -1349,6 +1372,11 @@ It's better to have 3 complete files than 10 incomplete files.` raw: true }); + // Debug: Log every 100 characters streamed + if (generatedCode.length % 100 < text.length) { + console.log(`[generate-ai-code-stream] Streamed ${generatedCode.length} chars`); + } + // Check for package tags in buffered text (ONLY for edits, not initial generation) let lastIndex = 0; if (isEdit) { @@ -1638,12 +1666,28 @@ Provide the complete file content without any truncation. Include all necessary completionClient = openai; } else if (model.includes('claude')) { completionClient = anthropic; + } else if (model === 'moonshotai/kimi-k2-instruct-0905') { + completionClient = groq; } else { completionClient = groq; } + // Determine the correct model name for the completion + let completionModelName: string; + if (model === 'moonshotai/kimi-k2-instruct-0905') { + completionModelName = 'moonshotai/kimi-k2-instruct-0905'; + } else if (model.includes('openai')) { + completionModelName = model.replace('openai/', ''); + } else if (model.includes('anthropic')) { + completionModelName = model.replace('anthropic/', ''); + } else if (model.includes('google')) { + completionModelName = model.replace('google/', ''); + } else { + completionModelName = model; + } + const completionResult = await streamText({ - model: completionClient(modelMapping[model] || model), + model: completionClient(completionModelName), messages: [ { role: 'system', diff --git a/app/api/scrape-screenshot/route.ts b/app/api/scrape-screenshot/route.ts index b77820a..feccddf 100644 --- a/app/api/scrape-screenshot/route.ts +++ b/app/api/scrape-screenshot/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import FirecrawlApp from '@mendable/firecrawl-js'; export async function POST(req: NextRequest) { try { @@ -8,43 +9,44 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'URL is required' }, { status: 400 }); } - // Use Firecrawl API to capture screenshot - const firecrawlResponse = await fetch('https://api.firecrawl.dev/v1/scrape', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${process.env.FIRECRAWL_API_KEY}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - url, - formats: ['screenshot'], // Regular viewport screenshot, not full page - waitFor: 3000, // Wait for page to fully load - timeout: 30000, - blockAds: true, - actions: [ - { - type: 'wait', - milliseconds: 2000 // Additional wait for dynamic content - } - ] - }) + // Initialize Firecrawl with API key from environment + const apiKey = process.env.FIRECRAWL_API_KEY; + + if (!apiKey) { + console.error("FIRECRAWL_API_KEY not configured"); + return NextResponse.json({ + error: 'Firecrawl API key not configured' + }, { status: 500 }); + } + + const app = new FirecrawlApp({ apiKey }); + + // Use Firecrawl SDK to capture screenshot with the latest API + const scrapeResult = await app.scrapeUrl(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 + } + ] }); - if (!firecrawlResponse.ok) { - const error = await firecrawlResponse.text(); - throw new Error(`Firecrawl API error: ${error}`); + if (!scrapeResult.success) { + throw new Error(scrapeResult.error || 'Failed to capture screenshot'); } - - const data = await firecrawlResponse.json(); - if (!data.success || !data.data?.screenshot) { - throw new Error('Failed to capture screenshot'); + if (!scrapeResult.data?.screenshot) { + throw new Error('Screenshot not available in response'); } return NextResponse.json({ success: true, - screenshot: data.data.screenshot, - metadata: data.data.metadata + screenshot: scrapeResult.data.screenshot, + metadata: scrapeResult.data.metadata || {} }); } catch (error: any) { diff --git a/app/api/scrape-website/route.ts b/app/api/scrape-website/route.ts new file mode 100644 index 0000000..73e9e48 --- /dev/null +++ b/app/api/scrape-website/route.ts @@ -0,0 +1,106 @@ +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: `

Example Website

This is mock content for demonstration purposes.

`, + 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.scrapeUrl(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 + if (!scrapeResult.success) { + throw new Error(scrapeResult.error || "Failed to scrape website"); + } + + return NextResponse.json({ + success: true, + data: { + title: scrapeResult.data?.metadata?.title || "Untitled", + content: scrapeResult.data?.markdown || scrapeResult.data?.html || "", + description: scrapeResult.data?.metadata?.description || "", + markdown: scrapeResult.data?.markdown || "", + html: scrapeResult.data?.html || "", + metadata: scrapeResult.data?.metadata || {}, + screenshot: scrapeResult.data?.screenshot || null, + links: scrapeResult.data?.links || [], + // Include raw data for flexibility + raw: scrapeResult.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: `

Error

${error instanceof Error ? error.message : 'Unknown error occurred'}

`, + metadata: { + title: "Error", + description: "Failed to scrape website", + statusCode: 500 + } + } + }, { status: 500 }); + } +} + +// Optional: Add OPTIONS handler for CORS if needed +export async function OPTIONS(request: NextRequest) { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +} \ No newline at end of file diff --git a/app/builder/page.tsx b/app/builder/page.tsx new file mode 100644 index 0000000..9135b63 --- /dev/null +++ b/app/builder/page.tsx @@ -0,0 +1,285 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; + +export default function BuilderPage() { + const [targetUrl, setTargetUrl] = useState(""); + const [selectedStyle, setSelectedStyle] = useState("modern"); + const [isLoading, setIsLoading] = useState(true); + const [previewUrl, setPreviewUrl] = useState(""); + const [progress, setProgress] = useState("Initializing..."); + const [generatedCode, setGeneratedCode] = useState(""); + 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"); + }, [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 = ` + + + + + + ${style} Website - Reimagined + + + +
+ +
+ +
+
+

Welcome to Your ${style === 'modern' ? 'Modern' : style === 'playful' ? 'Playful' : style === 'professional' ? 'Professional' : 'Artistic'} Website

+

Reimagined from ${url}

+ Get Started +
+ +
+
+

Fast

+

Lightning-fast performance optimized for modern web standards.

+
+
+

Responsive

+

Looks great on all devices, from mobile to desktop.

+
+
+

Beautiful

+

Stunning design that captures attention and drives engagement.

+
+
+
+ +`; + + 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 ( +
+
+ {/* Sidebar */} +
+

Building Your Website

+ +
+
+
Target URL
+
{targetUrl}
+
+ +
+
Style
+
{selectedStyle}
+
+ +
+
Status
+
{progress}
+
+
+ +
+ {!isLoading && ( + + )} + + +
+
+ + {/* Preview */} +
+ {isLoading ? ( +
+
+
+

{progress}

+
+
+ ) : ( + previewUrl && ( +