From 599e2098882ea56e78bd049e72ad87eb5a86306a Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:14:54 -0400 Subject: [PATCH] add in firecrawl search --- app/api/install-packages/route.ts | 6 +- app/api/search/route.ts | 51 +++ app/generation/page.tsx | 47 ++- app/page.tsx | 359 +++++++++++++++++- components/HeroInput.tsx | 121 +++++- .../sections/hero-input/Button/Button.tsx | 15 +- components/app/generation/SidebarInput.tsx | 16 +- lib/sandbox/factory.ts | 1 - lib/sandbox/providers/e2b-provider.ts | 16 - lib/sandbox/providers/vercel-provider.ts | 41 -- lib/sandbox/sandbox-manager.ts | 5 - 11 files changed, 559 insertions(+), 119 deletions(-) create mode 100644 app/api/search/route.ts diff --git a/app/api/install-packages/route.ts b/app/api/install-packages/route.ts index 90a9c5d..6e36da4 100644 --- a/app/api/install-packages/route.ts +++ b/app/api/install-packages/route.ts @@ -164,9 +164,9 @@ export async function POST(request: NextRequest) { // Install packages using provider method const installResult = await providerInstance.installPackages(packagesToInstall); - // Get install output - const stdout = installResult.stdout; - const stderr = installResult.stderr; + // Get install output - ensure stdout/stderr are strings + const stdout = String(installResult.stdout || ''); + const stderr = String(installResult.stderr || ''); if (stdout) { const lines = stdout.split('\n').filter(line => line.trim()); diff --git a/app/api/search/route.ts b/app/api/search/route.ts new file mode 100644 index 0000000..b93b1a0 --- /dev/null +++ b/app/api/search/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(req: NextRequest) { + try { + const { query } = await req.json(); + + if (!query) { + return NextResponse.json({ error: 'Query is required' }, { status: 400 }); + } + + // Use Firecrawl search to get top 10 results with screenshots + const searchResponse = await fetch('https://api.firecrawl.dev/v1/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.FIRECRAWL_API_KEY}`, + }, + body: JSON.stringify({ + query, + limit: 10, + scrapeOptions: { + formats: ['markdown', 'screenshot'], + onlyMainContent: true, + }, + }), + }); + + if (!searchResponse.ok) { + throw new Error('Search failed'); + } + + const searchData = await searchResponse.json(); + + // Format results with screenshots and markdown + const results = searchData.data?.map((result: any) => ({ + url: result.url, + title: result.title || result.url, + description: result.description || '', + screenshot: result.screenshot || null, + markdown: result.markdown || '', + })) || []; + + return NextResponse.json({ results }); + } catch (error) { + console.error('Search error:', error); + return NextResponse.json( + { error: 'Failed to perform search' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/generation/page.tsx b/app/generation/page.tsx index b2d5aad..c6728fc 100644 --- a/app/generation/page.tsx +++ b/app/generation/page.tsx @@ -174,11 +174,12 @@ export default function AISandboxPage() { // Mark that we have an initial submission since we're loading with a URL setHasInitialSubmission(true); - // Clear sessionStorage after reading + // Clear sessionStorage after reading sessionStorage.removeItem('targetUrl'); sessionStorage.removeItem('selectedStyle'); sessionStorage.removeItem('selectedModel'); sessionStorage.removeItem('additionalInstructions'); + // Note: Don't clear siteMarkdown here, it will be cleared when used // Set the values in the component state setHomeUrlInput(storedUrl); @@ -2638,20 +2639,37 @@ Tip: I automatically detect and install npm packages from your code imports (lik // Screenshot is already being captured in parallel above - const scrapeResponse = await fetch('/api/scrape-url-enhanced', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url }) - }); + let scrapeData; - if (!scrapeResponse.ok) { - throw new Error('Failed to scrape website'); - } - - const scrapeData = await scrapeResponse.json(); - - if (!scrapeData.success) { - throw new Error(scrapeData.error || 'Failed to scrape website'); + // Check if we have pre-scraped markdown content from search results + const storedMarkdown = sessionStorage.getItem('siteMarkdown'); + if (storedMarkdown) { + // Use the pre-scraped content + scrapeData = { + success: true, + content: storedMarkdown, + title: new URL(url).hostname, + source: 'search-result' + }; + sessionStorage.removeItem('siteMarkdown'); // Clear after use + addChatMessage('Using cached content from search results...', 'system'); + } else { + // Perform fresh scraping + const scrapeResponse = await fetch('/api/scrape-url-enhanced', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }) + }); + + if (!scrapeResponse.ok) { + throw new Error('Failed to scrape website'); + } + + scrapeData = await scrapeResponse.json(); + + if (!scrapeData.success) { + throw new Error(scrapeData.error || 'Failed to scrape website'); + } } setUrlStatus(['Website scraped successfully!', 'Generating React app...']); @@ -3580,6 +3598,7 @@ Focus on the key sections and content, making it clean and modern.`; onChange={setAiChatInput} onSubmit={sendChatMessage} placeholder="Describe what you want to build..." + showSearchFeatures={false} /> diff --git a/app/page.tsx b/app/page.tsx index a9d6c4a..9c883b1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -28,11 +28,25 @@ import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/W import GithubIcon from "@/components/shared/header/Github/_svg/GithubIcon"; import ButtonUI from "@/components/ui/shadcn/button" +interface SearchResult { + url: string; + title: string; + description: string; + screenshot: string | null; + markdown: string; +} + export default function HomePage() { const [url, setUrl] = useState(""); const [selectedStyle, setSelectedStyle] = useState("1"); const [selectedModel, setSelectedModel] = useState(appConfig.ai.defaultModel); const [isValidUrl, setIsValidUrl] = useState(false); + const [showSearchTiles, setShowSearchTiles] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [hasSearched, setHasSearched] = useState(false); + const [isFadingOut, setIsFadingOut] = useState(false); + const [showSelectMessage, setShowSelectMessage] = useState(false); const router = useRouter(); // Simple URL validation @@ -43,6 +57,12 @@ export default function HomePage() { return urlPattern.test(urlString.toLowerCase()); }; + // Check if input is a URL (contains a dot) + const isURL = (str: string): boolean => { + const urlPattern = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/; + return urlPattern.test(str.trim()); + }; + const styles = [ { id: "1", name: "Glassmorphism", description: "Frosted glass effect" }, { id: "2", name: "Neumorphism", description: "Soft 3D shadows" }, @@ -59,20 +79,117 @@ export default function HomePage() { name: appConfig.ai.modelDisplayNames[model] || model, })); - const handleSubmit = () => { - if (!url.trim()) { - toast.error("Please enter a URL"); + const handleSubmit = async (selectedResult?: SearchResult) => { + const inputValue = url.trim(); + + if (!inputValue) { + toast.error("Please enter a URL or search term"); return; } - // Store the configuration in sessionStorage - sessionStorage.setItem('targetUrl', url); - sessionStorage.setItem('selectedStyle', selectedStyle); - sessionStorage.setItem('selectedModel', selectedModel); - sessionStorage.setItem('autoStart', 'true'); // Set flag to auto-start generation + // If it's a search result being selected, fade out and redirect + if (selectedResult) { + setIsFadingOut(true); + + // Wait for fade animation + setTimeout(() => { + sessionStorage.setItem('targetUrl', selectedResult.url); + sessionStorage.setItem('selectedStyle', selectedStyle); + sessionStorage.setItem('selectedModel', selectedModel); + sessionStorage.setItem('autoStart', 'true'); + if (selectedResult.markdown) { + sessionStorage.setItem('siteMarkdown', selectedResult.markdown); + } + router.push('/generation'); + }, 500); + return; + } - // Redirect to the generation interface - router.push('/generation'); + // If it's a URL, go straight to generation + if (isURL(inputValue)) { + sessionStorage.setItem('targetUrl', inputValue); + sessionStorage.setItem('selectedStyle', selectedStyle); + sessionStorage.setItem('selectedModel', selectedModel); + sessionStorage.setItem('autoStart', 'true'); + router.push('/generation'); + } else { + // It's a search term, fade out if results exist, then search + if (hasSearched && searchResults.length > 0) { + setIsFadingOut(true); + + setTimeout(async () => { + setSearchResults([]); + setIsFadingOut(false); + setShowSelectMessage(true); + + // Perform new search + await performSearch(inputValue); + setHasSearched(true); + setShowSearchTiles(true); + setShowSelectMessage(false); + + // Smooth scroll to carousel + setTimeout(() => { + const carouselSection = document.querySelector('.carousel-section'); + if (carouselSection) { + carouselSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 300); + }, 500); + } else { + // First search, no fade needed + setShowSelectMessage(true); + setIsSearching(true); + setHasSearched(true); + setShowSearchTiles(true); + + // Scroll to carousel area immediately + setTimeout(() => { + const carouselSection = document.querySelector('.carousel-section'); + if (carouselSection) { + carouselSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 100); + + await performSearch(inputValue); + setShowSelectMessage(false); + setIsSearching(false); + + // Smooth scroll to carousel + setTimeout(() => { + const carouselSection = document.querySelector('.carousel-section'); + if (carouselSection) { + carouselSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 300); + } + } + }; + + // Perform search when user types + const performSearch = async (searchQuery: string) => { + if (!searchQuery.trim() || isURL(searchQuery)) { + setSearchResults([]); + return; + } + + setIsSearching(true); + try { + const response = await fetch('/api/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: searchQuery }), + }); + + if (response.ok) { + const data = await response.json(); + setSearchResults(data.results || []); + } + } catch (error) { + console.error('Search error:', error); + } finally { + setIsSearching(false); + } }; return ( @@ -155,33 +272,83 @@ export default function HomePage() { >
- + {isURL(url) ? ( + // Scrape icon for URLs + + + + + ) : ( + // Search icon for search terms + + + + + )} { - setUrl(e.target.value); - setIsValidUrl(validateUrl(e.target.value)); + const value = e.target.value; + setUrl(value); + setIsValidUrl(validateUrl(value)); + // Reset search state when input changes + if (value.trim() === "") { + setShowSearchTiles(false); + setHasSearched(false); + setSearchResults([]); + } }} onKeyDown={(e) => { - if (e.key === "Enter") { + if (e.key === "Enter" && !isSearching) { e.preventDefault(); handleSubmit(); } }} + onFocus={() => { + if (url.trim() && !isURL(url)) { + setShowSearchTiles(true); + } + }} + onBlur={() => { + setTimeout(() => setShowSearchTiles(false), 200); + }} />
{ e.preventDefault(); - handleSubmit(); + if (!isSearching) { + handleSubmit(); + } }} + className={isSearching ? 'pointer-events-none' : ''} > - 0} /> + 0} + buttonText={isURL(url) ? 'Scrape Site' : 'Search'} + disabled={isSearching} + />
+ {/* Options Section - Only show when valid URL */}
+ {/* Full-width oval carousel section */} + {showSearchTiles && hasSearched && ( +
+
+ + {isSearching ? ( + // Loading state with animated skeletons +
+ {/* Edge fade overlays */} +
+
+ +
+ {[0, 1, 2, 3, 4].map((index) => ( +
+
+
+
+ + {/* Fake browser UI */} +
+
+
+
+
+
+
+
+ + {/* Content skeleton */} +
+
+
+
+
+
+
+ ))} +
+ + {/* Loading text */} +
+
+
+
+
+
+
+
+ Searching for sites... +
+
+
+
+ ) : searchResults.length > 0 ? ( + // Actual results +
+ {/* Edge fade overlays */} +
+
+ +
+ {/* Duplicate results for infinite scroll */} + {[...searchResults, ...searchResults].map((result, index) => ( + + ))} +
+
+ ) : ( + // No results state +
+
+
+ + + +
+

No results found

+

Try a different search term

+
+
+ )} +
+ )} +
+ + ); } \ No newline at end of file diff --git a/components/HeroInput.tsx b/components/HeroInput.tsx index 2d88944..ccf247c 100644 --- a/components/HeroInput.tsx +++ b/components/HeroInput.tsx @@ -8,6 +8,13 @@ interface HeroInputProps { onSubmit: () => void; placeholder?: string; className?: string; + showSearchFeatures?: boolean; +} + +function isURL(str: string): boolean { + // Check if string contains a dot and looks like a URL + const urlPattern = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/; + return urlPattern.test(str.trim()); } export default function HeroInput({ @@ -15,10 +22,13 @@ export default function HeroInput({ onChange, onSubmit, placeholder = "Describe what you want to build...", - className = "" + className = "", + showSearchFeatures = true }: HeroInputProps) { - // const [isFocused, setIsFocused] = useState(false); // Reserved for future focus effects + const [isFocused, setIsFocused] = useState(false); + const [showTiles, setShowTiles] = useState(false); const textareaRef = useRef(null); + const isURLInput = showSearchFeatures ? isURL(value) : false; // Reset textarea height when value changes (especially when cleared) useEffect(() => { @@ -26,7 +36,14 @@ export default function HeroInput({ textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'; } - }, [value]); + + // Show tiles animation for search terms (only if search features are enabled) + if (showSearchFeatures && value.trim() && !isURL(value) && isFocused) { + setShowTiles(true); + } else { + setShowTiles(false); + } + }, [value, isFocused]); const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { @@ -44,20 +61,52 @@ export default function HeroInput({