import { useState, ChangeEvent, FormEvent, useEffect } from "react"; import { Card, CardHeader, CardTitle, CardContent, CardFooter, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; //! Hardcoded values (not recommended for production) //! Highly recommended to move all Firecrawl API calls to the backend (e.g. Next.js API route) const FIRECRAWL_API_URL = "https://api.firecrawl.dev"; // Replace with your actual API URL whether it is local or using Firecrawl Cloud const FIRECRAWL_API_KEY = "fc-YOUR_API_KEY"; // Replace with your actual API key interface FormData { url: string; crawlSubPages: boolean; limit: string; maxDepth: string; excludePaths: string; includePaths: string; extractMainContent: boolean; } interface CrawlerOptions { includes?: string[]; excludes?: string[]; maxDepth?: number; limit?: number; returnOnlyUrls: boolean; } interface PageOptions { onlyMainContent: boolean; } interface RequestBody { url: string; crawlerOptions?: CrawlerOptions; pageOptions: PageOptions; } interface ScrapeResultMetadata { title: string; description: string; language: string; sourceURL: string; pageStatusCode: number; pageError?: string; [key: string]: string | number | undefined; } interface ScrapeResultData { markdown: string; content: string; html: string; rawHtml: string; metadata: ScrapeResultMetadata; llm_extraction: Record; warning?: string; } interface ScrapeResult { success: boolean; data: ScrapeResultData; } export default function FirecrawlComponent() { const [formData, setFormData] = useState({ url: "", crawlSubPages: false, limit: "", maxDepth: "", excludePaths: "", includePaths: "", extractMainContent: false, }); const [loading, setLoading] = useState(false); const [scrapingSelectedLoading, setScrapingSelectedLoading] = useState(false); const [crawledUrls, setCrawledUrls] = useState([]); const [selectedUrls, setSelectedUrls] = useState([]); const [scrapeResults, setScrapeResults] = useState< Record >({}); const [isCollapsibleOpen, setIsCollapsibleOpen] = useState(true); const [crawlStatus, setCrawlStatus] = useState<{ current: number; total: number | null; }>({ current: 0, total: null }); const [elapsedTime, setElapsedTime] = useState(0); const [showCrawlStatus, setShowCrawlStatus] = useState(false); const [isScraping, setIsScraping] = useState(false); const [currentPage, setCurrentPage] = useState(1); const urlsPerPage = 10; useEffect(() => { let timer: NodeJS.Timeout; if (loading) { setShowCrawlStatus(true); timer = setInterval(() => { setElapsedTime((prevTime) => prevTime + 1); }, 1000); } return () => { if (timer) clearInterval(timer); }; }, [loading]); const handleChange = (e: ChangeEvent) => { const { name, value, type, checked } = e.target; setFormData((prevData) => ({ ...prevData, [name]: type === "checkbox" ? checked : value, })); }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setLoading(true); setIsCollapsibleOpen(false); setElapsedTime(0); setCrawlStatus({ current: 0, total: null }); setIsScraping(!formData.crawlSubPages); setCrawledUrls([]); setSelectedUrls([]); setScrapeResults({}); setScrapingSelectedLoading(false); setShowCrawlStatus(false); try { const endpoint = `${FIRECRAWL_API_URL}/v0/${ formData.crawlSubPages ? "crawl" : "scrape" }`; const requestBody: RequestBody = formData.crawlSubPages ? { url: formData.url, crawlerOptions: { includes: formData.includePaths ? formData.includePaths.split(",").map((p) => p.trim()) : undefined, excludes: formData.excludePaths ? formData.excludePaths.split(",").map((p) => p.trim()) : undefined, maxDepth: formData.maxDepth ? parseInt(formData.maxDepth) : undefined, limit: formData.limit ? parseInt(formData.limit) : undefined, returnOnlyUrls: true, }, pageOptions: { onlyMainContent: formData.extractMainContent, }, } : { url: formData.url, pageOptions: { onlyMainContent: formData.extractMainContent, }, }; const response = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${FIRECRAWL_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (formData.crawlSubPages) { const jobId = data.jobId; if (jobId) { const statusEndpoint = `${FIRECRAWL_API_URL}/v0/crawl/status/${jobId}`; let statusData: { status: string; data?: { url: string }[]; current?: number; total?: number; }; do { const statusResponse = await fetch(statusEndpoint, { headers: { Authorization: `Bearer ${FIRECRAWL_API_KEY}`, }, }); if (statusResponse.ok) { statusData = await statusResponse.json(); const urls = statusData.data ? statusData.data.map((urlObj) => urlObj.url) : []; setCrawledUrls(urls); setSelectedUrls(urls); setCrawlStatus({ current: urls.length || 0, total: urls.length || null, }); if (statusData.status !== "completed") { // Wait for 1 second before polling again await new Promise((resolve) => setTimeout(resolve, 1000)); console.log("Polling again..."); console.log(statusData); } else { console.log("Crawl completed with status:", statusData.status); console.log(statusData); } } else { console.error("Failed to fetch crawl status"); break; } } while (statusData.status !== "completed"); } else { console.error("No jobId received from crawl request"); } } else { setScrapeResults({ [formData.url]: data }); setCrawlStatus({ current: 1, total: 1 }); } } catch (error) { console.error("Error:", error); setScrapeResults({ error: { success: false, data: { metadata: { pageError: "Error occurred while fetching data", title: "", description: "", language: "", sourceURL: "", pageStatusCode: 0, }, markdown: "", content: "", html: "", rawHtml: "", llm_extraction: {}, }, }, }); } finally { setLoading(false); } }; const handleScrapeSelected = async () => { setLoading(true); setElapsedTime(0); setCrawlStatus({ current: 0, total: selectedUrls.length }); setIsScraping(true); setScrapingSelectedLoading(true); const newScrapeResults: Record = {}; for (const [index, url] of selectedUrls.entries()) { try { const response = await fetch(`${FIRECRAWL_API_URL}/v0/scrape`, { method: "POST", headers: { Authorization: `Bearer ${FIRECRAWL_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ url: url, pageOptions: { onlyMainContent: formData.extractMainContent, }, }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: ScrapeResult = await response.json(); newScrapeResults[url] = data; setCrawlStatus((prev) => ({ ...prev, current: index + 1 })); setScrapeResults({ ...scrapeResults, ...newScrapeResults }); } catch (error) { console.error(`Error scraping ${url}:`, error); newScrapeResults[url] = { success: false, data: { markdown: "", content: "", html: "", rawHtml: "", metadata: { title: "", description: "", language: "", sourceURL: url, pageStatusCode: 0, pageError: (error as Error).message, }, llm_extraction: {}, }, }; } } setLoading(false); setIsScraping(false); }; const handlePageChange = (newPage: number) => { setCurrentPage(newPage); }; const paginatedUrls = crawledUrls.slice( (currentPage - 1) * urlsPerPage, currentPage * urlsPerPage ); return (
Extract web content Powered by Firecrawl 🔥
Use this component to quickly give your users the ability to connect their AI apps to web data with Firecrawl. Learn more on the{" "} Firecrawl docs!
setFormData((prev) => ({ ...prev, crawlSubPages: checked, })) } />
setFormData((prev) => ({ ...prev, extractMainContent: checked, })) } />
{showCrawlStatus && (
{!isScraping && crawledUrls.length > 0 && !scrapingSelectedLoading && ( <> { if (checked) { setSelectedUrls([...crawledUrls]); } else { setSelectedUrls([]); } }} /> )}
{isScraping ? `Scraped ${crawlStatus.current} page(s) in ${elapsedTime}s` : `Crawled ${crawlStatus.current} pages in ${elapsedTime}s`}
)} {crawledUrls.length > 0 && !scrapingSelectedLoading && !isScraping && ( <>
    {paginatedUrls.map((url, index) => (
  • setSelectedUrls((prev) => prev.includes(url) ? prev.filter((u) => u !== url) : [...prev, url] ) } /> {url.length > 70 ? `${url.slice(0, 70)}...` : url}
  • ))}
Page {currentPage} of{" "} {Math.ceil(crawledUrls.length / urlsPerPage)}
)}
{crawledUrls.length > 0 && !scrapingSelectedLoading && ( )}
{Object.keys(scrapeResults).length > 0 && (

Scrape Results

You can do whatever you want with the scrape results. Here is a basic showcase of the markdown.

{Object.entries(scrapeResults).map(([url, result]) => ( {result.data.metadata.title} {url .replace(/^(https?:\/\/)?(www\.)?/, "") .replace(/\/$/, "")}
{result.success ? ( <>
                          {result.data.markdown.trim()}
                        
) : ( <>

Failed to scrape this URL

{result.toString()}

)}
))}
)}
); }