diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index 049f533..a97c11a 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -1294,26 +1294,59 @@ It's better to have 3 complete files than 10 incomplete files.` } let result; - try { - result = await streamText(streamOptions); - } catch (streamError) { - console.error('[generate-ai-code-stream] Error calling streamText:', streamError); - - // Send specific error for debugging - await sendProgress({ - type: 'error', - message: `Failed to initialize ${isGoogle ? 'Gemini' : isAnthropic ? 'Claude' : isOpenAI ? 'GPT-5' : 'Groq'} streaming: ${(streamError as Error).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.' - }); + 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; + } } - - throw streamError; } // Stream the response and parse in real-time diff --git a/app/api/scrape-screenshot/route.ts b/app/api/scrape-screenshot/route.ts index feccddf..55fc9ea 100644 --- a/app/api/scrape-screenshot/route.ts +++ b/app/api/scrape-screenshot/route.ts @@ -21,8 +21,11 @@ export async function POST(req: NextRequest) { const app = new FirecrawlApp({ apiKey }); - // Use Firecrawl SDK to capture screenshot with the latest API - const scrapeResult = await app.scrapeUrl(url, { + console.log('[scrape-screenshot] Attempting to capture screenshot for:', url); + console.log('[scrape-screenshot] Using Firecrawl API key:', apiKey ? 'Present' : 'Missing'); + + // Use the new v4 scrape method (not scrapeUrl) + const scrapeResult = await app.scrape(url, { formats: ['screenshot'], // Request screenshot format waitFor: 3000, // Wait for page to fully load timeout: 30000, @@ -35,7 +38,12 @@ export async function POST(req: NextRequest) { ] }); + console.log('[scrape-screenshot] Scrape result success:', scrapeResult.success); + console.log('[scrape-screenshot] Scrape result data:', scrapeResult.data ? Object.keys(scrapeResult.data) : 'No data'); + if (!scrapeResult.success) { + console.error('[scrape-screenshot] Firecrawl API error:', scrapeResult.error); + console.error('[scrape-screenshot] Full scrapeResult:', JSON.stringify(scrapeResult, null, 2)); throw new Error(scrapeResult.error || 'Failed to capture screenshot'); } @@ -50,9 +58,22 @@ export async function POST(req: NextRequest) { }); } 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 + 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({ - 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 }); } } \ No newline at end of file diff --git a/app/api/scrape-url-enhanced/route.ts b/app/api/scrape-url-enhanced/route.ts index 2c74278..699fd3a 100644 --- a/app/api/scrape-url-enhanced/route.ts +++ b/app/api/scrape-url-enhanced/route.ts @@ -43,7 +43,7 @@ export async function POST(request: NextRequest) { }, body: JSON.stringify({ url, - formats: ['markdown', 'html'], + formats: ['markdown', 'html', 'screenshot'], waitFor: 3000, timeout: 30000, blockAds: true, @@ -52,6 +52,10 @@ export async function POST(request: NextRequest) { { type: 'wait', milliseconds: 2000 + }, + { + type: 'screenshot', + fullPage: false // Just visible viewport for performance } ] }) @@ -68,7 +72,10 @@ export async function POST(request: NextRequest) { throw new Error('Failed to scrape content'); } - const { markdown, html, metadata } = data.data; + const { markdown, html, metadata, screenshot, actions } = data.data; + + // Get screenshot from either direct field or actions result + const screenshotUrl = screenshot || actions?.screenshots?.[0] || null; // Sanitize the markdown content const sanitizedMarkdown = sanitizeQuotes(markdown || ''); @@ -91,11 +98,13 @@ ${sanitizedMarkdown} success: true, url, content: formattedContent, + screenshot: screenshotUrl, structured: { title: sanitizeQuotes(title), description: sanitizeQuotes(description), content: sanitizedMarkdown, - url + url, + screenshot: screenshotUrl }, metadata: { scraper: 'firecrawl-enhanced', diff --git a/app/api/scrape-website/route.ts b/app/api/scrape-website/route.ts index 73e9e48..a1d95a6 100644 --- a/app/api/scrape-website/route.ts +++ b/app/api/scrape-website/route.ts @@ -40,7 +40,7 @@ export async function POST(request: NextRequest) { // Scrape the website using the latest SDK patterns // Include screenshot if requested in formats - const scrapeResult = await app.scrapeUrl(url, { + 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 diff --git a/app/generation/page.tsx b/app/generation/page.tsx index 6dec2ab..0b6ab1a 100644 --- a/app/generation/page.tsx +++ b/app/generation/page.tsx @@ -5,7 +5,6 @@ import { useSearchParams, useRouter } from 'next/navigation'; import { appConfig } from '@/config/app.config'; import HeroInput from '@/components/HeroInput'; import SidebarInput from '@/components/app/generation/SidebarInput'; -import SidebarQuickInput from '@/components/app/generation/SidebarQuickInput'; import HeaderBrandKit from '@/components/shared/header/BrandKit/BrandKit'; import { HeaderProvider } from '@/components/shared/header/HeaderContext'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; @@ -85,10 +84,12 @@ export default function AISandboxPage() { const [screenshotError, setScreenshotError] = useState(null); const [isPreparingDesign, setIsPreparingDesign] = useState(false); const [targetUrl, setTargetUrl] = useState(''); + const [sidebarScrolled, setSidebarScrolled] = useState(false); const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null); const [sandboxFiles, setSandboxFiles] = useState>({}); const [hasInitialSubmission, setHasInitialSubmission] = useState(false); const [fileStructure, setFileStructure] = useState(''); + const [isApplyingCode, setIsApplyingCode] = useState(false); const [conversationContext, setConversationContext] = useState<{ scrapedWebsites: Array<{ url: string; content: any; timestamp: Date }>; @@ -860,6 +861,11 @@ Tip: I automatically detect and install npm packages from your code imports (lik console.log('[applyGeneratedCode] Current iframe element:', iframeRef.current); console.log('[applyGeneratedCode] Current iframe src:', iframeRef.current?.src); + // Set applying code state for edits to show loading overlay + if (isEdit && sandboxData) { + setIsApplyingCode(true); + } + if (results.filesCreated?.length > 0) { setConversationContext(prev => ({ ...prev, @@ -929,6 +935,8 @@ Tip: I automatically detect and install npm packages from your code imports (lik } catch (e) { console.log('[home] Could not reload iframe (cross-origin):', e); } + // Clear applying code state after reload + setIsApplyingCode(false); }, 1000); } }, refreshDelay); @@ -1113,7 +1121,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik {/* File Explorer - Hide during edits */} {!generationProgress.isEdit && (
-
+
Explorer @@ -1121,11 +1129,11 @@ Tip: I automatically detect and install npm packages from your code imports (lik
{/* File Tree */} -
+
{/* Root app folder */}
toggleFolder('app')} > {expandedFolders.has('app') ? ( @@ -1142,7 +1150,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
{expandedFolders.has('app') && ( -
+
{/* Group files by directory */} {(() => { const fileTree: { [key: string]: Array<{ name: string; edited?: boolean }> } = {}; @@ -1171,7 +1179,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
{dir && (
toggleFolder(dir)} > {expandedFolders.has(dir) ? ( @@ -1188,7 +1196,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
)} {(!dir || expandedFolders.has(dir)) && ( -
+
{files.sort((a, b) => a.name.localeCompare(b.name)).map(fileInfo => { const fullPath = dir ? `${dir}/${fileInfo.name}` : fileInfo.name; const isSelected = selectedFile === fullPath; @@ -1196,7 +1204,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik return (
); } else if (activeTab === 'preview') { - // Show screenshot when we have one and (loading OR generating OR no sandbox yet) - if (urlScreenshot && (loading || generationProgress.isGenerating || !sandboxData?.url || isPreparingDesign)) { + // Only show loading state for initial generation, not for edits + const isInitialGeneration = !sandboxData?.url && (urlScreenshot || isCapturingScreenshot || isPreparingDesign || loadingStage); + const shouldShowLoadingOverlay = isInitialGeneration && (loading || generationProgress.isGenerating || isPreparingDesign || loadingStage || isCapturingScreenshot); + + if (isInitialGeneration) { return ( -
- Website preview - {(generationProgress.isGenerating || isPreparingDesign) && ( -
-
-
-

- {generationProgress.isGenerating ? 'Generating code...' : `Preparing your design for ${targetUrl}...`} +

+ {/* Screenshot as background when available */} + {urlScreenshot && ( + Website preview + )} + + {/* Loading overlay - only show when actively processing initial generation */} + {shouldShowLoadingOverlay && ( +
+
+
+

+ {isCapturingScreenshot ? 'Analyzing website...' : + isPreparingDesign ? 'Preparing design...' : + generationProgress.isGenerating ? 'Generating code...' : + 'Loading...'}

@@ -1514,32 +1533,8 @@ Tip: I automatically detect and install npm packages from your code imports (lik ); } - // Check loading stage FIRST to prevent showing old sandbox - // Don't show loading overlay for edits - if (loadingStage || (generationProgress.isGenerating && !generationProgress.isEdit)) { - return ( -
-
-
-
-
-

- {loadingStage === 'gathering' && 'Gathering website information...'} - {loadingStage === 'planning' && 'Planning your design...'} - {(loadingStage === 'generating' || generationProgress.isGenerating) && 'Generating your application...'} -

-

- {loadingStage === 'gathering' && 'Analyzing the website structure and content'} - {loadingStage === 'planning' && 'Creating the optimal React component architecture'} - {(loadingStage === 'generating' || generationProgress.isGenerating) && 'Writing clean, modern code for your app'} -

-
-
- ); - } - - // Show sandbox iframe only when not in any loading state - if (sandboxData?.url && !loading) { + // Show sandbox iframe - keep showing during edits, only hide during initial loading + if (sandboxData?.url) { return (