"use client"; import { useMemo, useState } from "react"; import { Badge, Button, EmptyState, SearchInput, Tab, TabList, TabPanel, Tabs } from "@/components/ui"; import { cn } from "@/components/ui/cn"; import { DASH, fmtInt } from "@/lib/format"; import { adminFetch, type DevtoolFetchResult } from "@/lib/admin-api"; import { htmlToTextClient, srcDocFor } from "../document-inspect"; import { Checkbox, Code, countMatches, ErrorNote, ExtLink, Field, fmtBytes, fmtMs, Input, JsonView, KV, LevelBadge, Mono, Panel, Select, Spinner } from "../primitives"; import { useToast } from "../toast"; import { domainOf, slugify, type DevtoolShared } from "./state"; const SOURCE_CAP = 300_000; export function StepFetch({ s }: { s: DevtoolShared }) { const { draft, update, fetchResult: r, setFetchResult } = s; const f = draft.fetch; const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [search, setSearch] = useState(""); const [tab, setTab] = useState("rendered"); const toast = useToast(); const run = async () => { if (!f.url.trim()) return setError("Enter a URL"); setBusy(true); setError(null); const res = await adminFetch("devtool/fetch", { method: "POST", body: { url: f.url.trim(), level: f.level, maxLevel: Math.max(f.level, f.maxLevel), renderJs: f.renderJs || undefined, userAgent: f.userAgent, waitForSelector: f.waitForSelector.trim() || undefined }, timeoutMs: 120_000 }); setBusy(false); if (!res.ok) { setError(res.error); toast({ tone: "danger", title: "Fetch failed", description: res.error }); return; } setFetchResult(res.data); s.setPreviewResult(null); const d = domainOf(res.data.finalUrl || f.url); update((cur) => ({ ...cur, meta: { ...cur.meta, domain: cur.meta.domain || d, id: cur.meta.id || slugify(d), name: cur.meta.name || d.replace(/^www\./, ""), homepage: cur.meta.homepage || (d ? `https://${d}/` : "") } })); if (res.data.error) toast({ tone: "warn", title: `Fetched with error (HTML ${res.data.status})`, description: res.data.error.message ?? JSON.stringify(res.data.error).slice(0, 160) }); else toast({ tone: "ok", title: `Fetched ${res.data.status} via ${res.data.fetcher} (L${res.data.level})`, description: `${fmtBytes(res.data.bytes)} in ${fmtMs(res.data.durationMs)}${res.data.credits ? ` · ${res.data.credits} credits` : ""}` }); }; const html = r?.html ?? ""; const isHtml = /html|xml/i.test(r?.contentType ?? "") || / (isHtml && html ? srcDocFor(html, finalUrl) : ""), [html, isHtml, finalUrl]); const text = useMemo(() => (rText.length > 0 ? rText : html ? htmlToTextClient(html) : ""), [rText, html]); const cls = (r?.classification ?? null) as Record | null; return (
update({ fetch: { ...f, url: e.target.value } })} onKeyDown={(e) => e.key === "Enter" && void run()} placeholder="https://www.example.com/data-centers/…" mono autoFocus={!f.url} />
update({ fetch: { ...f, renderJs: e.target.checked } })} /> update({ fetch: { ...f, waitForSelector: e.target.value } })} placeholder=".facility-list" mono /> {f.maxLevel >= 3 && ( maxLevel ≥ 3 may spend Firecrawl / Scrapfly credits )}
{r && ( <>
= 400 && "text-danger")}>{r.status}} /> } /> 0 && "text-accent")}>{r.credits}} /> {r.contentType ?? DASH}} /> {cls && typeof cls.pageType === "string" ? cls.pageType : DASH}} />
{r.error && } {r.finalUrl !== r.url && (

Redirected to

)}
{isHtml && Rendered} HTML source Text JSON-LD Embedded JSON Classification Geo / address Links {(tab === "source" || tab === "text") && (
{search && {fmtInt(countMatches(tab === "source" ? html : text, search))} hits}
)}
{isHtml && (