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.