refine ui further
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user