TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import { Map as MapIcon, Search } from "lucide-react";4import type { MapResult } from "@/lib/api";5import { runMap } from "@/actions/crawls";6import { Alert } from "@/components/ui/alert";7import { Badge } from "@/components/ui/badge";8import { Button } from "@/components/ui/button";9import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";10import { CopyButton } from "@/components/ui/copy-button";11import { Input } from "@/components/ui/input";12import { Field, Hint, Label } from "@/components/ui/label";13import { formatNumber } from "@/lib/format";1415/** Mini tool: discover the URLs of a site (sitemap + links) with POST /v1/map, synchronously. */16export function MapTool() {17 const [url, setUrl] = React.useState("");18 const [search, setSearch] = React.useState("");19 const [limit, setLimit] = React.useState(200);20 const [pending, startTransition] = React.useTransition();21 const [error, setError] = React.useState<string | null>(null);22 const [result, setResult] = React.useState<MapResult | null>(null);2324 function submit(e: React.FormEvent) {25 e.preventDefault();26 setError(null);27 startTransition(async () => {28 const res = await runMap({ url: url.trim(), search: search.trim() || undefined, limit: Math.min(10_000, Math.max(1, Math.round(limit || 1))) });29 if (!res.ok) {30 setResult(null);31 setError(res.error.message);32 return;33 }34 setResult(res.data);35 });36 }3738 return (39 <Card>40 <CardHeader>41 <CardTitle className="flex items-center gap-2">42 <MapIcon className="size-4 text-fg-subtle" aria-hidden /> Map a site43 </CardTitle>44 <CardDescription>45 List the URLs of a site from its sitemap and the links on the seed page, without fetching every page. Same as <code className="font-mono">POST /v1/map</code>; runs synchronously (up to 60 s).46 </CardDescription>47 </CardHeader>48 <CardContent className="grid gap-4">49 <form onSubmit={submit} className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_minmax(0,14rem)_6rem_auto] sm:items-end">50 <Field>51 <Label htmlFor="map-url">URL</Label>52 <Input id="map-url" type="url" inputMode="url" required value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://example.com" autoComplete="off" autoCapitalize="off" spellCheck={false} className="font-mono text-[13px]" />53 </Field>54 <Field>55 <Label htmlFor="map-search">Filter (optional)</Label>56 <Input id="map-search" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="blog, /docs/*, /regex/" maxLength={256} autoComplete="off" spellCheck={false} className="font-mono text-[12.5px]" />57 </Field>58 <Field>59 <Label htmlFor="map-limit">Limit</Label>60 <Input id="map-limit" type="number" inputMode="numeric" min={1} max={10_000} value={limit} onChange={(e) => setLimit(Number(e.target.value))} className="font-mono tabular" />61 </Field>62 <Button type="submit" variant="primary" loading={pending} className="sm:mb-px">63 {!pending ? <Search className="size-3.5" /> : null} Map64 </Button>65 </form>66 {error ? <Alert variant="danger">{error}</Alert> : null}67 {result ? (68 <div className="grid gap-2">69 <div className="flex flex-wrap items-center gap-2 text-[12.5px] text-fg-muted">70 <span className="font-mono tabular text-fg">{formatNumber(result.count)}</span> URL{result.count === 1 ? "" : "s"}71 <Badge variant="outline">sitemap {formatNumber(result.sources?.sitemap ?? 0)}</Badge>72 <Badge variant="outline">links {formatNumber(result.sources?.links ?? 0)}</Badge>73 {result.truncated ? <Badge variant="warning">truncated at {formatNumber(limit)}</Badge> : null}74 <span className="ml-auto">75 <CopyButton value={result.urls.join("\n")} label="Copy list" />76 </span>77 </div>78 {result.urls.length ? (79 <ol className="max-h-[360px] overflow-auto rounded-lg border border-border bg-bg-subtle p-3 font-mono text-[12px] leading-relaxed scrollbar-thin">80 {result.urls.map((u, i) => (81 <li key={`${u}-${i}`} className="flex gap-3">82 <span className="w-8 shrink-0 select-none text-right text-fg-subtle/70">{i + 1}</span>83 <a href={u} target="_blank" rel="noreferrer noopener nofollow" className="min-w-0 truncate text-fg hover:text-accent hover:underline" title={u}>84 {u}85 </a>86 </li>87 ))}88 </ol>89 ) : (90 <Hint>No URLs found. The site may have no sitemap and the seed page no links, or the filter matched nothing.</Hint>91 )}92 </div>93 ) : (94 <Hint>Use the result to pick include/exclude patterns before starting a crawl.</Hint>95 )}96 </CardContent>97 </Card>98 );99}100