refine ui further

This commit is contained in:
Developers Digest
2025-09-05 15:56:14 -04:00
parent 836b085f75
commit 9d71ae77e7
8 changed files with 285 additions and 232 deletions
+52 -19
View File
@@ -1294,26 +1294,59 @@ It's better to have 3 complete files than 10 incomplete files.`
} }
let result; let result;
try { let retryCount = 0;
result = await streamText(streamOptions); const maxRetries = 2;
} catch (streamError) {
console.error('[generate-ai-code-stream] Error calling streamText:', streamError); while (retryCount <= maxRetries) {
try {
// Send specific error for debugging result = await streamText(streamOptions);
await sendProgress({ break; // Success, exit retry loop
type: 'error', } catch (streamError: any) {
message: `Failed to initialize ${isGoogle ? 'Gemini' : isAnthropic ? 'Claude' : isOpenAI ? 'GPT-5' : 'Groq'} streaming: ${(streamError as Error).message}` console.error(`[generate-ai-code-stream] Error calling streamText (attempt ${retryCount + 1}/${maxRetries + 1}):`, streamError);
});
// Check if this is a Groq service unavailable error
// If this is a Google model error, provide helpful info const isGroqServiceError = isKimiGroq && streamError.message?.includes('Service unavailable');
if (isGoogle) { const isRetryableError = streamError.message?.includes('Service unavailable') ||
await sendProgress({ streamError.message?.includes('rate limit') ||
type: 'info', streamError.message?.includes('timeout');
message: 'Tip: Make sure your GEMINI_API_KEY is set correctly and has proper permissions.'
}); 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 // Stream the response and parse in real-time
+25 -4
View File
@@ -21,8 +21,11 @@ export async function POST(req: NextRequest) {
const app = new FirecrawlApp({ apiKey }); const app = new FirecrawlApp({ apiKey });
// Use Firecrawl SDK to capture screenshot with the latest API console.log('[scrape-screenshot] Attempting to capture screenshot for:', url);
const scrapeResult = await app.scrapeUrl(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 formats: ['screenshot'], // Request screenshot format
waitFor: 3000, // Wait for page to fully load waitFor: 3000, // Wait for page to fully load
timeout: 30000, 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) { 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'); throw new Error(scrapeResult.error || 'Failed to capture screenshot');
} }
@@ -50,9 +58,22 @@ export async function POST(req: NextRequest) {
}); });
} 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
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 });
} }
} }
+12 -3
View File
@@ -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,10 @@ 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, 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 // Sanitize the markdown content
const sanitizedMarkdown = sanitizeQuotes(markdown || ''); const sanitizedMarkdown = sanitizeQuotes(markdown || '');
@@ -91,11 +98,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',
+1 -1
View File
@@ -40,7 +40,7 @@ export async function POST(request: NextRequest) {
// Scrape the website using the latest SDK patterns // Scrape the website using the latest SDK patterns
// Include screenshot if requested in formats // Include screenshot if requested in formats
const scrapeResult = await app.scrapeUrl(url, { const scrapeResult = await app.scrape(url, {
formats: formats, formats: formats,
onlyMainContent: options.onlyMainContent !== false, // Default to true for cleaner content onlyMainContent: options.onlyMainContent !== false, // Default to true for cleaner content
waitFor: options.waitFor || 2000, // Wait for dynamic content waitFor: options.waitFor || 2000, // Wait for dynamic content
+181 -199
View File
@@ -5,7 +5,6 @@ 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';
import SidebarInput from '@/components/app/generation/SidebarInput'; import SidebarInput from '@/components/app/generation/SidebarInput';
import SidebarQuickInput from '@/components/app/generation/SidebarQuickInput';
import HeaderBrandKit from '@/components/shared/header/BrandKit/BrandKit'; import HeaderBrandKit from '@/components/shared/header/BrandKit/BrandKit';
import { HeaderProvider } from '@/components/shared/header/HeaderContext'; import { HeaderProvider } from '@/components/shared/header/HeaderContext';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
@@ -85,10 +84,12 @@ export default function AISandboxPage() {
const [screenshotError, setScreenshotError] = useState<string | null>(null); const [screenshotError, setScreenshotError] = useState<string | null>(null);
const [isPreparingDesign, setIsPreparingDesign] = useState(false); const [isPreparingDesign, setIsPreparingDesign] = useState(false);
const [targetUrl, setTargetUrl] = useState<string>(''); const [targetUrl, setTargetUrl] = useState<string>('');
const [sidebarScrolled, setSidebarScrolled] = useState(false);
const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null); const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null);
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);
const [fileStructure, setFileStructure] = useState<string>(''); const [fileStructure, setFileStructure] = useState<string>('');
const [isApplyingCode, setIsApplyingCode] = useState(false);
const [conversationContext, setConversationContext] = useState<{ const [conversationContext, setConversationContext] = useState<{
scrapedWebsites: Array<{ url: string; content: any; timestamp: Date }>; 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 element:', iframeRef.current);
console.log('[applyGeneratedCode] Current iframe src:', iframeRef.current?.src); 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) { if (results.filesCreated?.length > 0) {
setConversationContext(prev => ({ setConversationContext(prev => ({
...prev, ...prev,
@@ -929,6 +935,8 @@ Tip: I automatically detect and install npm packages from your code imports (lik
} catch (e) { } catch (e) {
console.log('[home] Could not reload iframe (cross-origin):', e); console.log('[home] Could not reload iframe (cross-origin):', e);
} }
// Clear applying code state after reload
setIsApplyingCode(false);
}, 1000); }, 1000);
} }
}, refreshDelay); }, refreshDelay);
@@ -1113,7 +1121,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
{/* File Explorer - Hide during edits */} {/* File Explorer - Hide during edits */}
{!generationProgress.isEdit && ( {!generationProgress.isEdit && (
<div className="w-[250px] border-r border-gray-200 bg-white flex flex-col flex-shrink-0"> <div className="w-[250px] border-r border-gray-200 bg-white flex flex-col flex-shrink-0">
<div className="p-3 bg-gray-100 text-gray-900 flex items-center justify-between"> <div className="p-4 bg-gray-100 text-gray-900 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BsFolderFill style={{ width: '16px', height: '16px' }} /> <BsFolderFill style={{ width: '16px', height: '16px' }} />
<span className="text-sm font-medium">Explorer</span> <span className="text-sm font-medium">Explorer</span>
@@ -1121,11 +1129,11 @@ Tip: I automatically detect and install npm packages from your code imports (lik
</div> </div>
{/* File Tree */} {/* File Tree */}
<div className="flex-1 overflow-y-auto p-2 scrollbar-hide"> <div className="flex-1 overflow-y-auto p-4 scrollbar-hide">
<div className="text-sm"> <div className="text-sm">
{/* Root app folder */} {/* Root app folder */}
<div <div
className="flex items-center gap-2 py-1 px-2 hover:bg-gray-100 rounded cursor-pointer text-gray-700" className="flex items-center gap-2 py-1.5 px-3 hover:bg-gray-100 rounded cursor-pointer text-gray-700"
onClick={() => toggleFolder('app')} onClick={() => toggleFolder('app')}
> >
{expandedFolders.has('app') ? ( {expandedFolders.has('app') ? (
@@ -1142,7 +1150,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
</div> </div>
{expandedFolders.has('app') && ( {expandedFolders.has('app') && (
<div className="ml-4"> <div className="ml-6">
{/* Group files by directory */} {/* Group files by directory */}
{(() => { {(() => {
const fileTree: { [key: string]: Array<{ name: string; edited?: boolean }> } = {}; 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
<div key={dir} className="mb-1"> <div key={dir} className="mb-1">
{dir && ( {dir && (
<div <div
className="flex items-center gap-2 py-1 px-2 hover:bg-gray-100 rounded cursor-pointer text-gray-700" className="flex items-center gap-2 py-1.5 px-3 hover:bg-gray-100 rounded cursor-pointer text-gray-700"
onClick={() => toggleFolder(dir)} onClick={() => toggleFolder(dir)}
> >
{expandedFolders.has(dir) ? ( {expandedFolders.has(dir) ? (
@@ -1188,7 +1196,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
</div> </div>
)} )}
{(!dir || expandedFolders.has(dir)) && ( {(!dir || expandedFolders.has(dir)) && (
<div className={dir ? 'ml-6' : ''}> <div className={dir ? 'ml-8' : ''}>
{files.sort((a, b) => a.name.localeCompare(b.name)).map(fileInfo => { {files.sort((a, b) => a.name.localeCompare(b.name)).map(fileInfo => {
const fullPath = dir ? `${dir}/${fileInfo.name}` : fileInfo.name; const fullPath = dir ? `${dir}/${fileInfo.name}` : fileInfo.name;
const isSelected = selectedFile === fullPath; const isSelected = selectedFile === fullPath;
@@ -1196,7 +1204,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik
return ( return (
<div <div
key={fullPath} key={fullPath}
className={`flex items-center gap-2 py-1 px-2 rounded cursor-pointer transition-all ${ className={`flex items-center gap-2 py-1.5 px-3 rounded cursor-pointer transition-all ${
isSelected isSelected
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'text-gray-700 hover:bg-gray-100' : 'text-gray-700 hover:bg-gray-100'
@@ -1491,21 +1499,32 @@ Tip: I automatically detect and install npm packages from your code imports (lik
</div> </div>
); );
} else if (activeTab === 'preview') { } else if (activeTab === 'preview') {
// Show screenshot when we have one and (loading OR generating OR no sandbox yet) // Only show loading state for initial generation, not for edits
if (urlScreenshot && (loading || generationProgress.isGenerating || !sandboxData?.url || isPreparingDesign)) { const isInitialGeneration = !sandboxData?.url && (urlScreenshot || isCapturingScreenshot || isPreparingDesign || loadingStage);
const shouldShowLoadingOverlay = isInitialGeneration && (loading || generationProgress.isGenerating || isPreparingDesign || loadingStage || isCapturingScreenshot);
if (isInitialGeneration) {
return ( return (
<div className="relative w-full h-full bg-gray-100"> <div className="relative w-full h-full bg-gray-900">
<img {/* Screenshot as background when available */}
src={urlScreenshot} {urlScreenshot && (
alt="Website preview" <img
className="w-full h-full object-contain" src={urlScreenshot}
/> alt="Website preview"
{(generationProgress.isGenerating || isPreparingDesign) && ( className="absolute inset-0 w-full h-full object-cover"
<div className="absolute inset-0 bg-black/40 flex items-center justify-center"> />
<div className="text-center bg-black/70 rounded-lg p-6 backdrop-blur-sm"> )}
<div className="w-12 h-12 border-3 border-gray-300 border-t-white rounded-full animate-spin mx-auto mb-3" />
<p className="text-white text-sm font-medium"> {/* Loading overlay - only show when actively processing initial generation */}
{generationProgress.isGenerating ? 'Generating code...' : `Preparing your design for ${targetUrl}...`} {shouldShowLoadingOverlay && (
<div className="absolute inset-0 bg-black/70 flex items-center justify-center backdrop-blur-sm">
<div className="text-center">
<div className="w-12 h-12 border-3 border-white/30 border-t-white rounded-full animate-spin mx-auto mb-4" />
<p className="text-white text-lg font-medium">
{isCapturingScreenshot ? 'Analyzing website...' :
isPreparingDesign ? 'Preparing design...' :
generationProgress.isGenerating ? 'Generating code...' :
'Loading...'}
</p> </p>
</div> </div>
</div> </div>
@@ -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 // Show sandbox iframe - keep showing during edits, only hide during initial loading
// Don't show loading overlay for edits if (sandboxData?.url) {
if (loadingStage || (generationProgress.isGenerating && !generationProgress.isEdit)) {
return (
<div className="relative w-full h-full bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="mb-8">
<div className="w-16 h-16 border-4 border-orange-200 border-t-orange-500 rounded-full animate-spin mx-auto"></div>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-2">
{loadingStage === 'gathering' && 'Gathering website information...'}
{loadingStage === 'planning' && 'Planning your design...'}
{(loadingStage === 'generating' || generationProgress.isGenerating) && 'Generating your application...'}
</h3>
<p className="text-gray-600 text-sm">
{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'}
</p>
</div>
</div>
);
}
// Show sandbox iframe only when not in any loading state
if (sandboxData?.url && !loading) {
return ( return (
<div className="relative w-full h-full"> <div className="relative w-full h-full">
<iframe <iframe
@@ -1550,6 +1545,26 @@ Tip: I automatically detect and install npm packages from your code imports (lik
allow="clipboard-write" allow="clipboard-write"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
/> />
{/* Show loading overlay after applying code during edits */}
{isApplyingCode && (
<div className="absolute inset-0 bg-black/60 flex items-center justify-center backdrop-blur-sm">
<div className="text-center">
<div className="w-12 h-12 border-3 border-white/30 border-t-white rounded-full animate-spin mx-auto mb-4" />
<p className="text-white text-lg font-medium">Applying changes...</p>
<p className="text-white/70 text-sm mt-2">Reloading environment</p>
</div>
</div>
)}
{/* Show a subtle indicator when code is being edited/generated (before applying) */}
{generationProgress.isGenerating && generationProgress.isEdit && !isApplyingCode && (
<div className="absolute top-4 right-4 inline-flex items-center gap-2 px-3 py-1.5 bg-black/80 backdrop-blur-sm rounded-lg">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
<span className="text-white text-xs font-medium">Generating code...</span>
</div>
)}
{/* Refresh button */} {/* Refresh button */}
<button <button
onClick={() => { onClick={() => {
@@ -1570,18 +1585,6 @@ Tip: I automatically detect and install npm packages from your code imports (lik
); );
} }
// Show loading animation when capturing screenshot
if (isCapturingScreenshot) {
return (
<div className="flex items-center justify-center h-full bg-gray-900">
<div className="text-center">
<div className="w-12 h-12 border-3 border-gray-600 border-t-white rounded-full animate-spin mx-auto mb-4" />
<h3 className="text-lg font-medium text-white">Gathering website information</h3>
</div>
</div>
);
}
// Default state when no sandbox and no screenshot // Default state when no sandbox and no screenshot
return ( return (
<div className="flex items-center justify-center h-full bg-gray-50 text-gray-600 text-lg"> <div className="flex items-center justify-center h-full bg-gray-50 text-gray-600 text-lg">
@@ -3130,7 +3133,7 @@ Focus on the key sections and content, making it clean and modern.`;
</div> </div>
)} )}
<div className="bg-white p-[15px] border-b border-border-faint flex items-center justify-between shadow-sm"> <div className="bg-white py-[15px] py-[8px] border-b border-border-faint flex items-center justify-between shadow-sm">
<HeaderBrandKit /> <HeaderBrandKit />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Model Selector - Left side */} {/* Model Selector - Left side */}
@@ -3183,10 +3186,7 @@ Focus on the key sections and content, making it clean and modern.`;
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg> </svg>
</button> </button>
<div className={`inline-flex items-center gap-1.5 ${status.active ? 'bg-gray-50 border border-gray-200 text-gray-700' : 'bg-gray-100 border border-gray-200 text-gray-600'} px-4 py-1.5 rounded-lg text-sm font-medium`}>
<span id="status-text">{status.text}</span>
<div className={`w-1.5 h-1.5 rounded-full ${status.active ? 'bg-green-500' : 'bg-gray-400'}`} />
</div>
</div> </div>
</div> </div>
@@ -3194,8 +3194,8 @@ Focus on the key sections and content, making it clean and modern.`;
{/* Center Panel - AI Chat (1/3 of remaining width) */} {/* Center Panel - AI Chat (1/3 of remaining width) */}
<div className="flex-1 max-w-[400px] flex flex-col border-r border-border bg-background"> <div className="flex-1 max-w-[400px] flex flex-col border-r border-border bg-background">
{/* Sidebar Input Component */} {/* Sidebar Input Component */}
<div className="p-4 border-b border-border"> {!hasInitialSubmission ? (
{!hasInitialSubmission ? ( <div className="p-4 border-b border-border">
<SidebarInput <SidebarInput
onSubmit={(url, style, model, instructions) => { onSubmit={(url, style, model, instructions) => {
// Mark that we've had an initial submission // Mark that we've had an initial submission
@@ -3217,90 +3217,62 @@ Focus on the key sections and content, making it clean and modern.`;
}} }}
disabled={loading || generationProgress.isGenerating} disabled={loading || generationProgress.isGenerating}
/> />
) : ( </div>
<SidebarQuickInput ) : null}
onSubmit={(url) => {
// Clear all previous generation data for a fresh start
setChatMessages([{
content: 'Starting fresh generation...',
type: 'system',
timestamp: new Date()
}]);
setGenerationProgress({
isGenerating: false,
isThinking: false,
thinkingText: '',
thinkingDuration: 0,
files: [],
currentFile: undefined,
streamedCode: '',
components: [],
currentComponent: 0,
status: '',
isEdit: false
});
setConversationContext({
scrapedWebsites: [],
generatedComponents: [],
appliedCode: [],
currentProject: '',
lastGeneratedCode: undefined
});
setSandboxFiles({});
setFileStructure('');
setSelectedFile(null);
setUrlScreenshot(null);
setScreenshotError(null);
// For subsequent submissions, use the previously selected style and model
const prevStyle = sessionStorage.getItem('selectedStyle') || '1';
const prevModel = sessionStorage.getItem('selectedModel') || appConfig.ai.defaultModel;
// Store the configuration in sessionStorage
sessionStorage.setItem('targetUrl', url);
sessionStorage.setItem('selectedStyle', prevStyle);
sessionStorage.setItem('selectedModel', prevModel);
sessionStorage.setItem('autoStart', 'true');
// Start generation using the existing logic
setHomeUrlInput(url);
setHomeContextInput('');
startGeneration();
}}
disabled={loading || generationProgress.isGenerating}
/>
)}
</div>
{conversationContext.scrapedWebsites.length > 0 && ( {conversationContext.scrapedWebsites.length > 0 && (
<div className="p-4 bg-card"> <div className="p-4 bg-card border-b border-gray-200">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-4">
{conversationContext.scrapedWebsites.map((site, idx) => { {conversationContext.scrapedWebsites.map((site, idx) => {
// Extract favicon and site info from the scraped data // Extract favicon and site info from the scraped data
const metadata = site.content?.metadata || {}; const metadata = site.content?.metadata || {};
const sourceURL = metadata.sourceURL || site.url; const sourceURL = metadata.sourceURL || site.url;
const favicon = metadata.favicon || `https://www.google.com/s2/favicons?domain=${new URL(sourceURL).hostname}&sz=32`; const favicon = metadata.favicon || `https://www.google.com/s2/favicons?domain=${new URL(sourceURL).hostname}&sz=128`;
const siteName = metadata.ogSiteName || metadata.title || new URL(sourceURL).hostname; const siteName = metadata.ogSiteName || metadata.title || new URL(sourceURL).hostname;
const screenshot = site.content?.screenshot || sessionStorage.getItem('websiteScreenshot');
return ( return (
<div key={idx} className="flex items-center gap-2 text-sm"> <div key={idx} className="flex flex-col gap-3">
<img {/* Site info with favicon */}
src={favicon} <div className="flex items-center gap-4 text-sm">
alt={siteName} <img
className="w-5 h-5 rounded" src={favicon}
onError={(e) => { alt={siteName}
e.currentTarget.src = `https://www.google.com/s2/favicons?domain=${new URL(sourceURL).hostname}&sz=32`; className="w-16 h-16 rounded"
}} onError={(e) => {
/> e.currentTarget.src = `https://www.google.com/s2/favicons?domain=${new URL(sourceURL).hostname}&sz=128`;
<a }}
href={sourceURL} />
target="_blank" <a
rel="noopener noreferrer" href={sourceURL}
className="text-black hover:text-gray-700 truncate max-w-[250px]" target="_blank"
title={sourceURL} rel="noopener noreferrer"
> className="text-black hover:text-gray-700 truncate max-w-[250px] font-medium"
{siteName} title={sourceURL}
</a> >
{siteName}
</a>
</div>
{/* Pinned screenshot */}
{screenshot && (
<div
className="w-full rounded-lg overflow-hidden border border-gray-200 transition-all duration-300"
style={{
opacity: sidebarScrolled ? 0 : 1,
transform: sidebarScrolled ? 'translateY(-20px)' : 'translateY(0)',
pointerEvents: sidebarScrolled ? 'none' : 'auto',
maxHeight: sidebarScrolled ? '0' : '200px'
}}
>
<img
src={screenshot}
alt={`${siteName} preview`}
className="w-full h-auto object-cover"
style={{ maxHeight: '200px' }}
/>
</div>
)}
</div> </div>
); );
})} })}
@@ -3308,7 +3280,13 @@ Focus on the key sections and content, making it clean and modern.`;
</div> </div>
)} )}
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-1 scrollbar-hide" ref={chatMessagesRef}> <div
className="flex-1 overflow-y-auto p-6 flex flex-col gap-4 scrollbar-hide"
ref={chatMessagesRef}
onScroll={(e) => {
const scrollTop = e.currentTarget.scrollTop;
setSidebarScrolled(scrollTop > 50);
}}>
{chatMessages.map((msg, idx) => { {chatMessages.map((msg, idx) => {
// Check if this message is from a successful generation // Check if this message is from a successful generation
const isGenerationComplete = msg.content.includes('Successfully recreated') || const isGenerationComplete = msg.content.includes('Successfully recreated') ||
@@ -3320,9 +3298,9 @@ Focus on the key sections and content, making it clean and modern.`;
return ( return (
<div key={idx} className="block"> <div key={idx} className="block">
<div className={`flex ${msg.type === 'user' ? 'justify-end' : 'justify-start'} mb-1`}> <div className={`flex ${msg.type === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className="block"> <div className="block">
<div className={`block rounded-[10px] px-12 py-6 ${ <div className={`block rounded-[10px] px-14 py-8 ${
msg.type === 'user' ? 'bg-[#36322F] text-white ml-auto max-w-[80%]' : msg.type === 'user' ? 'bg-[#36322F] text-white ml-auto max-w-[80%]' :
msg.type === 'ai' ? 'bg-gray-100 text-gray-900 mr-auto max-w-[80%]' : msg.type === 'ai' ? 'bg-gray-100 text-gray-900 mr-auto max-w-[80%]' :
msg.type === 'system' ? 'bg-[#36322F] text-white text-sm' : msg.type === 'system' ? 'bg-[#36322F] text-white text-sm' :
@@ -3364,7 +3342,7 @@ Focus on the key sections and content, making it clean and modern.`;
{/* Show applied files if this is an apply success message */} {/* Show applied files if this is an apply success message */}
{msg.metadata?.appliedFiles && msg.metadata.appliedFiles.length > 0 && ( {msg.metadata?.appliedFiles && msg.metadata.appliedFiles.length > 0 && (
<div className="mt-4 inline-block bg-gray-100 rounded-[10px] p-4"> <div className="mt-3 inline-block bg-gray-100 rounded-[10px] p-5">
<div className="text-xs font-medium mb-3 text-gray-700"> <div className="text-xs font-medium mb-3 text-gray-700">
{msg.content.includes('Applied') ? 'Files Updated:' : 'Generated Files:'} {msg.content.includes('Applied') ? 'Files Updated:' : 'Generated Files:'}
</div> </div>
@@ -3518,76 +3496,80 @@ Focus on the key sections and content, making it clean and modern.`;
{/* Right Panel - Preview or Generation (2/3 of remaining width) */} {/* Right Panel - Preview or Generation (2/3 of remaining width) */}
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<div className="px-3 py-[9.5px] bg-white border-b border-gray-200 flex justify-between items-center"> <div className="px-3 pt-4 pb-4 bg-white border-b border-gray-200 flex justify-between items-center">
<div className="flex items-center gap-4"> <div className="flex items-center gap-2">
<div className="flex gap-1"> {/* Toggle-style Code/View switcher */}
<div className="inline-flex bg-gray-100 border border-gray-200 rounded-md p-0.5">
<button <button
onClick={() => setActiveTab('generation')} onClick={() => setActiveTab('generation')}
className={`p-8 rounded-lg transition-colors ${ className={`px-3 py-1 rounded transition-all text-xs font-medium ${
activeTab === 'generation' activeTab === 'generation'
? 'bg-white border border-gray-200 text-gray-900 shadow-sm' ? 'bg-white text-gray-900 shadow-sm'
: 'bg-gray-50 border border-gray-200 text-gray-500 hover:text-gray-700 hover:bg-gray-100' : 'bg-transparent text-gray-600 hover:text-gray-900'
}`} }`}
title="Code"
> >
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div className="flex items-center gap-1.5">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /> <svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</svg> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
<span>Code</span>
</div>
</button> </button>
<button <button
onClick={() => setActiveTab('preview')} onClick={() => setActiveTab('preview')}
className={`p-8 rounded-lg transition-colors ${ className={`px-3 py-1 rounded transition-all text-xs font-medium ${
activeTab === 'preview' activeTab === 'preview'
? 'bg-white border border-gray-200 text-gray-900 shadow-sm' ? 'bg-white text-gray-900 shadow-sm'
: 'bg-gray-50 border border-gray-200 text-gray-500 hover:text-gray-700 hover:bg-gray-100' : 'bg-transparent text-gray-600 hover:text-gray-900'
}`} }`}
title="Preview"
> >
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div className="flex items-center gap-1.5">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<span>View</span>
</div>
</button> </button>
</div> </div>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{/* Live Code Generation Status - Moved to far right */} {/* Files generated count */}
{activeTab === 'generation' && (generationProgress.isGenerating || generationProgress.files.length > 0) && ( {activeTab === 'generation' && !generationProgress.isEdit && generationProgress.files.length > 0 && (
<div className="flex items-center gap-3"> <div className="text-gray-500 text-xs font-medium">
{!generationProgress.isEdit && ( {generationProgress.files.length} files generated
<div className="text-gray-600 text-sm">
{generationProgress.files.length} files generated
</div>
)}
<div className={`inline-flex items-center gap-1.5 ${generationProgress.isGenerating ? 'bg-gray-50 border border-gray-200 text-gray-700' : 'bg-gray-100 text-gray-600'} px-3 py-1.5 rounded-lg text-sm font-medium`}>
{generationProgress.isGenerating ? (
<>
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse" />
{generationProgress.isEdit ? 'Editing code' : 'Live code generation'}
</>
) : (
<>
<div className="w-1.5 h-1.5 bg-gray-400 rounded-full" />
Complete
</>
)}
</div>
</div> </div>
)} )}
{sandboxData && !generationProgress.isGenerating && (
<> {/* Live Code Generation Status */}
<a {activeTab === 'generation' && generationProgress.isGenerating && (
href={sandboxData.url} <div className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 border border-gray-200 rounded-md text-xs font-medium text-gray-700">
target="_blank" <div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse" />
rel="noopener noreferrer" {generationProgress.isEdit ? 'Editing code' : 'Live generation'}
title="Open in new tab" </div>
className="p-8 rounded-lg transition-colors bg-gray-50 border border-gray-200 text-gray-700 hover:bg-gray-100 inline-block" )}
>
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"> {/* Sandbox Status Indicator */}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> {sandboxData && (
</svg> <div className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 border border-gray-200 rounded-md text-xs font-medium text-gray-700">
</a> <div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
</> Sandbox active
</div>
)}
{/* Open in new tab button */}
{sandboxData && (
<a
href={sandboxData.url}
target="_blank"
rel="noopener noreferrer"
title="Open in new tab"
className="p-1.5 rounded-md transition-all text-gray-600 hover:text-gray-900 hover:bg-gray-100"
>
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
)} )}
</div> </div>
</div> </div>
+1 -1
View File
@@ -154,7 +154,7 @@ export default function HomePage() {
}} }}
> >
<div className="p-16 flex gap-12 items-center w-full relative"> <div className="p-16 flex gap-12 items-center w-full relative bg-white rounded-20">
<Globe /> <Globe />
<input <input
className="flex-1 bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48 focus:outline-none focus:ring-0 focus:border-transparent" className="flex-1 bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48 focus:outline-none focus:ring-0 focus:border-transparent"
+11 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, KeyboardEvent } from "react"; import { useState, KeyboardEvent, useEffect, useRef } from "react";
interface HeroInputProps { interface HeroInputProps {
value: string; value: string;
@@ -18,6 +18,15 @@ export default function HeroInput({
className = "" className = ""
}: HeroInputProps) { }: HeroInputProps) {
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// 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';
}
}, [value]);
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
@@ -52,6 +61,7 @@ export default function HeroInput({
</div> </div>
<textarea <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" 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} placeholder={placeholder}
value={value} value={value}
+2 -4
View File
@@ -55,12 +55,10 @@ export default function SidebarInput({ onSubmit, disabled = false }: SidebarInpu
return ( return (
<div className="w-full"> <div className="w-full">
<div className="bg-white rounded-20 border border-gray-200 shadow-sm"> <div >
<div className="p-4 border-b border-gray-100"> <div className="p-4 border-b border-gray-100">
<h3 className="text-sm font-medium text-gray-900 mb-3">Generate New Website</h3>
{/* URL Input */} {/* URL Input */}
<div className="flex gap-3 items-center mb-3"> <div className="flex gap-3 items-center">
<Globe /> <Globe />
<input <input
className="flex-1 bg-transparent text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none" className="flex-1 bg-transparent text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none"