PageSourceSearch

https://www.hamienet.com/assets/WebScraper-YXZBgNTW.js

js hamienet.com collected 2026-09-24 19:01:09 UTC 23,224 bytes, 443 lines download raw bytes

1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js";
2import { bn as Globe, b1 as CircleAlert, b7 as Check, b9 as Settings, b0 as FileText, cc as Save, d6 as SquarePen, b5 as Trash2, cL as RefreshCw, bU as Play, ch as Database, bg as Music, cG as Tag, aW as X } from "./vendor-dVNaLJx_.js";
3import "./vendor-i18n-R-bMoYp_.js";
4const DEFAULT_SCRIPT = `async function scrape(url, fetch, cheerio) {
5  // Fetch the page HTML
6  const response = await fetch(url);
7  const html = await response.text();
8  
9  // Load into cheerio
10  const $ = cheerio.load(html);
11  
12  const results = [];
13  
14  // Customize selectors based on the target website structure
15  $('a').each((i, el) => {
16    const href = $(el).attr('href');
17    if (href && href.endsWith('.mid')) {
18      const absoluteUrl = new URL(href, url).toString();
19      
20      // Try to extract some tags and artist from surroundings
21      const text = $(el).text().trim();
22      
23      results.push({
24        sourceUrl: absoluteUrl,
25        title: text || 'Scraped MIDI',
26        artist: '',
27        tags: ['Scraped', 'Genre'],
28        midiUrl: absoluteUrl
29      });
30    }
31  });
32  
33  return results;
34}`;
35const WebScraper = () => {
36  const [activeTab, setActiveTab] = reactExports.useState("scrapers");
37  const [configs, setConfigs] = reactExports.useState([]);
38  const [results, setResults] = reactExports.useState([]);
39  const [loading, setLoading] = reactExports.useState(false);
40  const [runningScraperId, setRunningScraperId] = reactExports.useState(null);
41  const [actionLoadingId, setActionLoadingId] = reactExports.useState(null);
42  const [editingConfig, setEditingConfig] = reactExports.useState(null);
43  const [formName, setFormName] = reactExports.useState("");
44  const [formBaseUrl, setFormBaseUrl] = reactExports.useState("");
45  const [formScript, setFormScript] = reactExports.useState(DEFAULT_SCRIPT);
46  const [error, setError] = reactExports.useState(null);
47  const [success, setSuccess] = reactExports.useState(null);
48  reactExports.useEffect(() => {
49    fetchConfigs();
50    fetchResults();
51  }, []);
52  const fetchConfigs = async () => {
53    try {
54      const res = await fetch("/api/admin/scraper/configs");
55      if (res.ok) {
56        const data = await res.json();
57        setConfigs(data);
58      }
59    } catch (e) {
60      console.error(e);
61    }
62  };
63  const fetchResults = async () => {
64    try {
65      const res = await fetch("/api/admin/scraper/results?status=PENDING");
66      if (res.ok) {
67        const data = await res.json();
68        setResults(data);
69      }
70    } catch (e) {
71      console.error(e);
72    }
73  };
74  const handleSaveConfig = async (e) => {
75    e.preventDefault();
76    setLoading(true);
77    setError(null);
78    setSuccess(null);
79    try {
80      const payload = {
81        name: formName,
82        baseUrl: formBaseUrl,
83        script: formScript
84      };
85      let res;
86      if (editingConfig == null ? void 0 : editingConfig.id) {
87        res = await fetch(`/api/admin/scraper/configs/${editingConfig.id}`, {
88          method: "PUT",
89          headers: { "Content-Type": "application/json" },
90          body: JSON.stringify(payload)
91        });
92      } else {
93        res = await fetch("/api/admin/scraper/configs", {
94          method: "POST",
95          headers: { "Content-Type": "application/json" },
96          body: JSON.stringify(payload)
97        });
98      }
99      if (res.ok) {
100        setSuccess("Configuration saved successfully!");
101        setEditingConfig(null);
102        resetForm();
103        fetchConfigs();
104      } else {
105        const data = await res.json();
106        setError(data.error || "Failed to save configuration");
107      }
108    } catch (err) {
109      setError(err.message || "An error occurred");
110    } finally {
111      setLoading(false);
112    }
113  };
114  const handleEditConfig = (config) => {
115    setEditingConfig(config);
116    setFormName(config.name);
117    setFormBaseUrl(config.baseUrl);
118    setFormScript(config.script);
119    setError(null);
120    setSuccess(null);
121  };
122  const handleDeleteConfig = async (id) => {
123    if (!confirm("Are you sure you want to delete this scraper config?")) return;
124    try {
125      const res = await fetch(`/api/admin/scraper/configs/${id}`, { method: "DELETE" });
126      if (res.ok) {
127        fetchConfigs();
128        setSuccess("Config deleted successfully");
129      }
130    } catch (e) {
131      console.error(e);
132    }
133  };
134  const handleRunScraper = async (id) => {
135    setRunningScraperId(id);
136    setError(null);
137    setSuccess(null);
138    try {
139      const res = await fetch(`/api/admin/scraper/configs/${id}/run`, { method: "POST" });
140      const data = await res.json();
141      if (res.ok) {
142        setSuccess(`Scraper ran successfully! Found and staged ${data.count} new MIDIs for review.`);
143        fetchResults();
144      } else {
145        setError(data.error || "Scraper run failed");
146      }
147    } catch (err) {
148      setError(err.message || "An error occurred while running the scraper");
149    } finally {
150      setRunningScraperId(null);
151    }
152  };
153  const handleApprove = async (id) => {
154    setActionLoadingId(id);
155    setError(null);
156    setSuccess(null);
157    try {
158      const res = await fetch(`/api/admin/scraper/results/${id}/approve`, { method: "POST" });
159      if (res.ok) {
160        setSuccess("MIDI successfully imported and processed!");
161        fetchResults();
162      } else {
163        const data = await res.json();
164        setError(data.error || "Failed to import MIDI");
165      }
166    } catch (err) {
167      setError(err.message || "An error occurred");
168    } finally {
169      setActionLoadingId(null);
170    }
171  };
172  const handleReject = async (id) => {
173    setActionLoadingId(id);
174    try {
175      const res = await fetch(`/api/admin/scraper/results/${id}/reject`, { method: "POST" });
176      if (res.ok) {
177        fetchResults();
178      }
179    } catch (e) {
180      console.error(e);
181    } finally {
182      setActionLoadingId(null);
183    }
184  };
185  const resetForm = () => {
186    setEditingConfig(null);
187    setFormName("");
188    setFormBaseUrl("");
189    setFormScript(DEFAULT_SCRIPT);
190  };
191  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
192    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-indigo-50 dark:bg-indigo-950/40 p-4 rounded-xl border border-indigo-100 dark:border-indigo-900/60 flex items-start gap-3", children: [
193      /* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { className: "text-indigo-600 dark:text-indigo-400 mt-0.5", size: 20 }),
194      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
195        /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-indigo-900 dark:text-indigo-200 text-sm mb-1", children: "Web Scraper & MIDI Importer" }),
196        /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-indigo-700 dark:text-indigo-300", children: "Scan any third-party MIDI website using customizable, JavaScript-powered scraping logic. Stage the extracted tracks for manual review, and import them directly into the catalog as a Guest contributor (`userId: -1`)." })
197      ] })
198    ] }),
199    error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3.5 bg-red-50 dark:bg-red-950/20 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-900/40 rounded-lg text-xs flex items-center gap-2", children: [
200      /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 14, className: "shrink-0" }),
201      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: error })
202    ] }),
203    success && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3.5 bg-emerald-50 dark:bg-emerald-950/20 text-emerald-700 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-900/40 rounded-lg text-xs flex items-center gap-2", children: [
204      /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14, className: "shrink-0" }),
205      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: success })
206    ] }),
207    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex border-b border-zinc-200 dark:border-zinc-800", children: [
208      /* @__PURE__ */ jsxRuntimeExports.jsxs(
209        "button",
210        {
211          onClick: () => {
212            setActiveTab("scrapers");
213            setError(null);
214            setSuccess(null);
215          },
216          className: `px-5 py-2.5 font-semibold text-xs border-b-2 transition-colors flex items-center gap-2 ${activeTab === "scrapers" ? "border-indigo-500 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-700"}`,
217          children: [
218            /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 14 }),
219            " Scrapers Config"
220          ]
221        }
222      ),
223      /* @__PURE__ */ jsxRuntimeExports.jsxs(
224        "button",
225        {
226          onClick: () => {
227            setActiveTab("review");
228            setError(null);
229            setSuccess(null);
230          },
231          className: `px-5 py-2.5 font-semibold text-xs border-b-2 transition-colors flex items-center gap-2 ${activeTab === "review" ? "border-indigo-500 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-700"}`,
232          children: [
233            /* @__PURE__ */ jsxRuntimeExports.jsx(FileText, { size: 14 }),
234            " Review & Import",
235            results.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "bg-amber-500 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full", children: results.length })
236          ]
237        }
238      )
239    ] }),
240    activeTab === "scrapers" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-6", children: [
241      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lg:col-span-7 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl p-5 shadow-sm space-y-4", children: [
242        /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-800 dark:text-white text-xs uppercase tracking-wider", children: editingConfig ? "Edit Scraper Config" : "Create Scraper Config" }),
243        /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleSaveConfig, className: "space-y-4", children: [
244          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [
245            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
246              /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-semibold text-zinc-500 mb-1", children: "Name" }),
247              /* @__PURE__ */ jsxRuntimeExports.jsx(
248                "input",
249                {
250                  type: "text",
251                  required: true,
252                  value: formName,
253                  onChange: (e) => setFormName(e.target.value),
254                  placeholder: "e.g. Vintage MIDI Archive",
255                  className: "w-full text-xs p-2.5 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
256                }
257              )
258            ] }),
259            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
260              /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-semibold text-zinc-500 mb-1", children: "Base Target URL" }),
261              /* @__PURE__ */ jsxRuntimeExports.jsx(
262                "input",
263                {
264                  type: "url",
265                  required: true,
266                  value: formBaseUrl,
267                  onChange: (e) => setFormBaseUrl(e.target.value),
268                  placeholder: "https://example.com/midis",
269                  className: "w-full text-xs p-2.5 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
270                }
271              )
272            ] })
273          ] }),
274          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
275            /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "block text-xs font-semibold text-zinc-500 mb-1 flex items-center justify-between", children: [
276              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Parser Script (JavaScript with Cheerio)" }),
277              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-400", children: "Available: fetch, cheerio" })
278            ] }),
279            /* @__PURE__ */ jsxRuntimeExports.jsx(
280              "textarea",
281              {
282                value: formScript,
283                onChange: (e) => setFormScript(e.target.value),
284                rows: 15,
285                className: "w-full font-mono text-[11px] p-3 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 focus:ring-1 focus:ring-indigo-500 focus:outline-none whitespace-pre overflow-x-auto"
286              }
287            )
288          ] }),
289          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 justify-end", children: [
290            editingConfig && /* @__PURE__ */ jsxRuntimeExports.jsx(
291              "button",
292              {
293                type: "button",
294                onClick: resetForm,
295                className: "px-4 py-2 text-xs border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-lg transition-colors flex items-center gap-1.5",
296                children: "Cancel"
297              }
298            ),
299            /* @__PURE__ */ jsxRuntimeExports.jsxs(
300              "button",
301              {
302                type: "submit",
303                disabled: loading,
304                className: "px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-colors",
305                children: [
306                  /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 14 }),
307                  loading ? "Saving..." : "Save Configuration"
308                ]
309              }
310            )
311          ] })
312        ] })
313      ] }),
314      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lg:col-span-5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl p-5 shadow-sm space-y-4", children: [
315        /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-800 dark:text-white text-xs uppercase tracking-wider", children: "Active Configurations" }),
316        configs.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-center py-8 text-zinc-400 text-xs", children: "No configurations found. Create one on the left to get started!" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-3", children: configs.map((config) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3 bg-zinc-50 dark:bg-zinc-800/40 border border-zinc-100 dark:border-zinc-800 rounded-lg flex flex-col justify-between gap-3", children: [
317          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
318            /* @__PURE__ */ jsxRuntimeExports.jsxs("h5", { className: "font-bold text-xs text-zinc-800 dark:text-white flex items-center gap-1.5", children: [
319              /* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { size: 12, className: "text-indigo-500" }),
320              config.name
321            ] }),
322            /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-zinc-400 truncate mt-0.5", children: config.baseUrl })
323          ] }),
324          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1.5 justify-end", children: [
325            /* @__PURE__ */ jsxRuntimeExports.jsx(
326              "button",
327              {
328                onClick: () => handleEditConfig(config),
329                className: "p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-500 rounded transition-colors",
330                title: "Edit",
331                children: /* @__PURE__ */ jsxRuntimeExports.jsx(SquarePen, { size: 14 })
332              }
333            ),
334            /* @__PURE__ */ jsxRuntimeExports.jsx(
335              "button",
336              {
337                onClick: () => handleDeleteConfig(config.id),
338                className: "p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 text-red-500 rounded transition-colors",
339                title: "Delete",
340                children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 14 })
341              }
342            ),
343            /* @__PURE__ */ jsxRuntimeExports.jsxs(
344              "button",
345              {
346                onClick: () => handleRunScraper(config.id),
347                disabled: runningScraperId === config.id,
348                className: "px-2.5 py-1.5 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 rounded text-xs font-semibold flex items-center gap-1 transition-colors",
349                children: [
350                  runningScraperId === config.id ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 12, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 12 }),
351                  runningScraperId === config.id ? "Running" : "Scan"
352                ]
353              }
354            )
355          ] })
356        ] }, config.id)) })
357      ] })
358    ] }),
359    activeTab === "review" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-sm overflow-hidden", children: [
360      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center bg-zinc-50 dark:bg-zinc-800/10", children: [
361        /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "font-bold text-zinc-800 dark:text-white text-xs uppercase tracking-wider flex items-center gap-1.5", children: [
362          /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 14 }),
363          " Staged for Review (",
364          results.length,
365          " files)"
366        ] }),
367        /* @__PURE__ */ jsxRuntimeExports.jsx(
368          "button",
369          {
370            onClick: fetchResults,
371            className: "p-1.5 text-zinc-500 hover:text-indigo-500 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded transition-colors",
372            children: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 14 })
373          }
374        )
375      ] }),
376      results.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-center py-12 text-zinc-400 text-xs", children: "No staged MIDIs pending review. Run a scraper configuration above to discover tracks!" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-left text-xs border-collapse", children: [
377        /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "bg-zinc-50 dark:bg-zinc-950 text-zinc-500 font-semibold border-b border-zinc-200 dark:border-zinc-800", children: [
378          /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-3", children: "Track Info" }),
379          /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-3", children: "Artist" }),
380          /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-3", children: "Tags" }),
381          /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-3", children: "Source Config" }),
382          /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-3 text-right", children: "Actions" })
383        ] }) }),
384        /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-zinc-200 dark:divide-zinc-800", children: results.map((result) => {
385          var _a;
386          return /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "hover:bg-zinc-50/50 dark:hover:bg-zinc-900/30 transition-colors", children: [
387            /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "p-3 max-w-xs md:max-w-md", children: [
388              /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-bold text-zinc-800 dark:text-zinc-200 flex items-center gap-1.5", children: [
389                /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 12, className: "text-indigo-500" }),
390                result.title
391              ] }),
392              /* @__PURE__ */ jsxRuntimeExports.jsx(
393                "a",
394                {
395                  href: result.sourceUrl,
396                  target: "_blank",
397                  rel: "noreferrer",
398                  className: "text-[10px] text-zinc-400 hover:text-indigo-500 truncate block mt-0.5",
399                  children: result.sourceUrl
400                }
401              )
402            ] }),
403            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-3 text-zinc-600 dark:text-zinc-400", children: result.artist || "" }),
404            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-1 flex-wrap", children: Array.isArray(result.tags) && result.tags.map((tag, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-800 text-[9px] text-zinc-500 rounded flex items-center gap-0.5", children: [
405              /* @__PURE__ */ jsxRuntimeExports.jsx(Tag, { size: 8 }),
406              " ",
407              tag
408            ] }, i)) }) }),
409            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-3 text-zinc-500", children: ((_a = result.config) == null ? void 0 : _a.name) || "Unknown" }),
410            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-3 text-right", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-1.5", children: [
411              /* @__PURE__ */ jsxRuntimeExports.jsx(
412                "button",
413                {
414                  onClick: () => handleReject(result.id),
415                  disabled: actionLoadingId === result.id,
416                  className: "p-1 text-red-500 hover:bg-red-50 dark:hover:bg-red-950/20 rounded transition-colors",
417                  title: "Reject & Ignore",
418                  children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 16 })
419                }
420              ),
421              /* @__PURE__ */ jsxRuntimeExports.jsxs(
422                "button",
423                {
424                  onClick: () => handleApprove(result.id),
425                  disabled: actionLoadingId === result.id,
426                  className: "px-2 py-1 bg-indigo-600 hover:bg-indigo-700 text-white rounded font-bold text-[11px] flex items-center gap-1 transition-colors shadow-sm shadow-indigo-600/10",
427                  title: "Approve & Import",
428                  children: [
429                    actionLoadingId === result.id ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 12, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 12 }),
430                    "Import"
431                  ]
432                }
433              )
434            ] }) })
435          ] }, result.id);
436        }) })
437      ] }) })
438    ] })
439  ] });
440};
441export {
442  WebScraper as default
443};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.