1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { a as api } from "./index-w5lXls15.js"; 3import { a as DataTable, b as Drawer } from "./AdminShared-BP_vxG_A.js"; 4import { a as ai } from "./ai-B0Mlroaa.js"; 5import { r as extractJson } from "./utils-CEVb7nN1.js"; 6import { bI as Hash, ci as Database, f9 as Pen, dS as GitMerge, b5 as Trash2, bi as Sparkles, cf as Plus, b6 as LoaderCircle, cM as RefreshCw, bf as Search, cg as Activity, eC as Terminal, e1 as Languages, cd as Save, b1 as CircleAlert, bv as CircleCheckBig, eH as WandSparkles, aW as X, cB as ChevronUp, cC as ChevronDown } from "./vendor-D0P4XgNj.js"; 7import "./vendor-i18n-BC7ZMXWQ.js"; 8import "./vendor-ai-gQv1oOjO.js"; 9const TagFormDrawer = ({ tag, onClose, onRefresh }) => { 10 const [name, setName] = reactExports.useState(""); 11 const [slug, setSlug] = reactExports.useState(""); 12 const [translations, setTranslations] = reactExports.useState("{}"); 13 const [type, setType] = reactExports.useState("TAG"); 14 const [generating, setGenerating] = reactExports.useState(false); 15 const isEdit = !!(tag == null ? void 0 : tag.id); 16 reactExports.useEffect(() => { 17 if (tag) { 18 setName(tag.name || ""); 19 setSlug(tag.slug || ""); 20 setTranslations(JSON.stringify(tag.translations || {}, null, 2)); 21 setType(tag.type || "TAG"); 22 } else { 23 setName(""); 24 setSlug(""); 25 setTranslations("{}"); 26 setType("TAG"); 27 } 28 }, [tag]); 29 const handleSave = async () => { 30 if (!name.trim()) return; 31 let parsedTranslations = {}; 32 try { 33 parsedTranslations = JSON.parse(translations); 34 } catch (e) { 35 alert("Invalid JSON in translations"); 36 return; 37 } 38 if (isEdit) { 39 await api.updateTag(tag.id, name, slug, parsedTranslations, type); 40 } else { 41 await api.createTag(name, slug, parsedTranslations, type); 42 } 43 onRefresh(); 44 onClose(); 45 }; 46 const handleAutoGenerate = async () => { 47 if (!name) return; 48 setGenerating(true); 49 try { 50 const prompt = `Generate a JSON object for the tag "${name}" used in a music library. 51 1. "slug": lowercase kebab-case English key. 52 2. "translations": object with keys "en", "ja", "es", "fr", "zh", "de". 53 Return ONLY JSON.`; 54 const response = await ai.models.generateContent({ 55 model: "gemini-2.5-flash-latest", 56 contents: prompt 57 }); 58 const text = response.text || ""; 59 const jsonStr = extractJson(text); 60 const parsed = JSON.parse(jsonStr); 61 if (parsed.slug) setSlug(parsed.slug); 62 if (parsed.translations) setTranslations(JSON.stringify(parsed.translations, null, 2)); 63 } catch (e) { 64 alert("AI Generation failed. Check API Key."); 65 } finally { 66 setGenerating(false); 67 } 68 }; 69 if (tag === void 0) return null; 70 return /* @__PURE__ */ jsxRuntimeExports.jsx(Drawer, { isOpen: !!tag || tag === null, onClose, title: isEdit ? "Edit Tag" : "Create Tag", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ 71 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
72 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "Tag Name (Default)" }), 73 /* @__PURE__ */ jsxRuntimeExports.jsx( 74 "input", 75 { 76 value: name, 77 onChange: (e) => setName(e.target.value), 78 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded p-2 text-sm outline-none focus:border-indigo-500 text-zinc-900 dark:text-white", 79 placeholder: "e.g. Jazz" 80 } 81 ) 82 ] }), 83 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 84 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "Tag Type" }), 85 /* @__PURE__ */ jsxRuntimeExports.jsxs( 86 "select", 87 { 88 value: type, 89 onChange: (e) => setType(e.target.value), 90 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded p-2 text-sm outline-none focus:border-indigo-500 text-zinc-900 dark:text-white", 91 children: [
92 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "TAG", children: "Standard Tag" }), 93 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "GENRE", children: "Genre (Primary)" }), 94 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "INSTRUMENT", children: "Instrument" }), 95 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "SOURCE", children: "Source (Show/Game)" }), 96 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "MOOD", children: "Mood/Style" }) 97 ] 98 } 99 ) 100 ] }), 101 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-100 dark:border-indigo-800 flex justify-between items-center", children: [ 102 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 103 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "text-sm font-bold text-indigo-900 dark:text-white flex items-center gap-2", children: [ 104 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 14, className: "text-indigo-500" }), 105 " AI Auto-Fill" 106 ] }),
107 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-indigo-700 dark:text-indigo-300", children: "Generate canonical slug and multilingual translations." }) 108 ] }), 109 /* @__PURE__ */ jsxRuntimeExports.jsx( 110 "button", 111 { 112 onClick: handleAutoGenerate, 113 disabled: generating || !name, 114 className: "px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-md text-xs font-bold disabled:opacity-50 transition-all shadow-md", 115 children: generating ? "Generating..." : "Auto-Generate" 116 } 117 ) 118 ] }), 119 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 120 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "Canonical Slug (ID)" }), 121 /* @__PURE__ */ jsxRuntimeExports.jsx( 122 "input", 123 { 124 value: slug, 125 onChange: (e) => setSlug(e.target.value), 126 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded p-2 text-sm outline-none focus:border-indigo-500 text-zinc-900 dark:text-white font-mono", 127 placeholder: "e.g. jazz" 128 } 129 ),
130 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-zinc-400 mt-1", children: "This ID is used for filtering regardless of user language." }) 131 ] }), 132 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 133 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1 flex items-center gap-2", children: [ 134 /* @__PURE__ */ jsxRuntimeExports.jsx(Languages, { size: 12 }), 135 " Translations (JSON)" 136 ] }), 137 /* @__PURE__ */ jsxRuntimeExports.jsx( 138 "textarea", 139 { 140 value: translations, 141 onChange: (e) => setTranslations(e.target.value), 142 className: "w-full h-40 bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded p-3 text-xs outline-none focus:border-indigo-500 text-zinc-900 dark:text-white font-mono resize-none", 143 placeholder: '{ "en": "Jazz", "ja": "ã¸ã£ãº" }' 144 } 145 ) 146 ] }), 147 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 pt-4 border-t border-zinc-200 dark:border-zinc-800", children: [
148 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "flex-1 px-4 py-2 rounded-lg border border-zinc-300 dark:border-zinc-700 font-bold text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800", children: "Cancel" }), 149 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: handleSave, className: "flex-1 px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white font-bold flex items-center justify-center gap-2", children: [ 150 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 16 }), 151 " Save" 152 ] }) 153 ] }) 154 ] }) }); 155}; 156const MergeModal = ({ sourceTag, onClose, onRefresh }) => { 157 const [targetName, setTargetName] = reactExports.useState(""); 158 const [suggestions, setSuggestions] = reactExports.useState([]); 159 const [selectedTarget, setSelectedTarget] = reactExports.useState(null); 160 const handleSearch = async (val) => { 161 setTargetName(val); 162 if (val.length > 1) { 163 const res = await api.getAdminTags({ search: val, page: 1, pageSize: 5 }); 164 setSuggestions(res.data.filter((t) => t.id !== sourceTag.id)); 165 } else { 166 setSuggestions([]); 167 } 168 }; 169 const handleMerge = async () => { 170 if (!selectedTarget) return; 171 if (confirm(`Are you sure you want to merge "${sourceTag.name}" into "${selectedTarget.name}"? This will update all tracks and delete "${sourceTag.name}". This cannot be undone.`)) { 172 await api.mergeTags(sourceTag.id, selectedTarget.id); 173 onRefresh(); 174 onClose(); 175 } 176 }; 177 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4", children: [ 178 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: onClose }), 179 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-md bg-white dark:bg-zinc-900 rounded-xl shadow-2xl p-6 border border-zinc-200 dark:border-zinc-800 animate-in zoom-in-95", children: [
180 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-xl font-bold mb-4 text-zinc-900 dark:text-white flex items-center gap-2", children: [ 181 /* @__PURE__ */ jsxRuntimeExports.jsx(GitMerge, { className: "text-indigo-500" }), 182 " Merge Tags" 183 ] }), 184 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4 p-3 bg-amber-50 dark:bg-amber-900/10 border border-amber-100 dark:border-amber-900/30 rounded-lg text-sm text-amber-800 dark:text-amber-200 flex gap-2", children: [ 185 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "shrink-0 mt-0.5", size: 16 }), 186 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [ 187 "Merging will move all tracks from ", 188 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: sourceTag.name }), 189 " to the target tag, then delete ", 190 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: sourceTag.name }), 191 "." 192 ] }) 193 ] }), 194 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
195 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 196 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "Merge into (Target)" }), 197 /* @__PURE__ */ jsxRuntimeExports.jsx( 198 "input", 199 { 200 value: targetName, 201 onChange: (e) => handleSearch(e.target.value), 202 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded p-2 text-sm outline-none focus:border-indigo-500 text-zinc-900 dark:text-white", 203 placeholder: "Search existing tag..." 204 } 205 ), 206 suggestions.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-lg overflow-hidden", children: suggestions.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx( 207 "div", 208 { 209 onClick: () => { 210 setSelectedTarget(t); 211 setTargetName(t.name); 212 setSuggestions([]); 213 }, 214 className: "px-3 py-2 text-sm hover:bg-zinc-100 dark:hover:bg-zinc-700 cursor-pointer text-zinc-800 dark:text-zinc-200", 215 children: t.name 216 }, 217 t.id 218 )) }) 219 ] }), 220 selectedTarget && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm text-zinc-600 dark:text-zinc-400", children: [ 221 "Target selected: ",
222 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-indigo-500", children: selectedTarget.name }) 223 ] }) 224 ] }), 225 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 pt-6", children: [ 226 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "px-4 py-2 rounded-lg text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800 font-bold text-sm", children: "Cancel" }), 227 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleMerge, disabled: !selectedTarget, className: "px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-sm disabled:opacity-50", children: "Merge" }) 228 ] }) 229 ] }) 230 ] }); 231}; 232const CleanupModal = ({ onClose, onRefresh }) => { 233 const [loading, setLoading] = reactExports.useState(true); 234 const [preview, setPreview] = reactExports.useState(null); 235 const [executing, setExecuting] = reactExports.useState(false); 236 const [result, setResult] = reactExports.useState(null); 237 const [restoreEncoded, setRestoreEncoded] = reactExports.useState(true); 238 const [deleteJunk, setDeleteJunk] = reactExports.useState(true); 239 const [mergeDuplicates, setMergeDuplicates] = reactExports.useState(true); 240 const [showRestoreList, setShowRestoreList] = reactExports.useState(false); 241 const [showDeleteList, setShowDeleteList] = reactExports.useState(false); 242 const [showDupList, setShowDupList] = reactExports.useState(false); 243 reactExports.useEffect(() => { 244 const fetchPreview = async () => { 245 try { 246 const res = await api.previewTagCleanup(); 247 setPreview(res); 248 } catch (e) { 249 alert("Failed to fetch preview stats"); 250 } finally { 251 setLoading(false); 252 } 253 }; 254 fetchPreview(); 255 }, []); 256 const handleExecute = async () => { 257 setExecuting(true); 258 try { 259 const res = await api.executeTagCleanup({ 260 restoreEncoded, 261 deleteJunk, 262 mergeDuplicates 263 }); 264 setResult(res); 265 onRefresh(); 266 } catch (e) { 267 alert("Cleanup failed: " + e.message); 268 } finally { 269 setExecuting(false); 270 } 271 }; 272 if (loading) { 273 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4 animate-in fade-in-0 duration-200", children: [
274 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/60 backdrop-blur-sm", onClick: onClose }), 275 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-sm bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl p-8 border border-zinc-100 dark:border-zinc-850 flex flex-col items-center justify-center text-center", children: [ 276 /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "animate-spin text-indigo-500 mb-4", size: 40 }), 277 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-bold text-zinc-900 dark:text-white mb-1", children: "Analyzing Tag Dictionary..." }), 278 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Scanning 27k+ tags for HTML entities, duplicate casings, and scraper junk..." }) 279 ] }) 280 ] }); 281 } 282 if (result) { 283 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4 animate-in fade-in-0 duration-200", children: [
284 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/60 backdrop-blur-sm", onClick: onClose }), 285 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-md bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl p-8 border border-zinc-100 dark:border-zinc-850 animate-in zoom-in-95 duration-200", children: [ 286 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center text-center mb-6", children: [ 287 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 bg-emerald-100 dark:bg-emerald-950/30 rounded-full flex items-center justify-center text-emerald-600 dark:text-emerald-400 mb-4 shadow-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheckBig, { size: 36 }) }), 288 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-2xl font-bold text-zinc-900 dark:text-white mb-1", children: "Optimization Complete!" }), 289 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Your Tag Dictionary has been optimized and cataloged successfully." }) 290 ] }),
291 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3 bg-zinc-50 dark:bg-zinc-950/40 p-4 rounded-xl border border-zinc-100 dark:border-zinc-800/60 mb-6", children: [ 292 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-sm py-1 border-b border-zinc-100 dark:border-zinc-800/60", children: [ 293 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 dark:text-zinc-400", children: "Encoded Tags Decoded" }), 294 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono font-bold text-zinc-950 dark:text-white", children: [ 295 "+", 296 result.restoredCount 297 ] }) 298 ] }), 299 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-sm py-1 border-b border-zinc-100 dark:border-zinc-800/60", children: [ 300 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 dark:text-zinc-400", children: "Irrecoverable Junk Deleted" }),
301 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold text-red-500", children: result.deletedJunkCount }) 302 ] }), 303 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-sm py-1", children: [ 304 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 dark:text-zinc-400", children: "Duplicate Tags Merged" }), 305 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold text-indigo-500", children: result.mergedDuplicatesCount }) 306 ] }) 307 ] }), 308 /* @__PURE__ */ jsxRuntimeExports.jsx( 309 "button", 310 { 311 onClick: onClose, 312 className: "w-full px-4 py-2.5 rounded-lg bg-zinc-900 hover:bg-zinc-800 dark:bg-white dark:hover:bg-zinc-100 text-white dark:text-zinc-900 font-bold text-sm transition-all", 313 children: "Done" 314 } 315 ) 316 ] }) 317 ] }); 318 } 319 const restoreCount = (preview == null ? void 0 : preview.toRestore.length) || 0; 320 const deleteCount = (preview == null ? void 0 : preview.toDelete.length) || 0; 321 const dupCount = (preview == null ? void 0 : preview.duplicates.length) || 0; 322 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4 animate-in fade-in-0 duration-200", children: [ 323 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/60 backdrop-blur-sm", onClick: onClose }), 324 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-xl bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl p-6 border border-zinc-100 dark:border-zinc-850 flex flex-col max-h-[85vh] animate-in zoom-in-95 duration-200", children: [
325 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-start mb-4", children: [ 326 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 327 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-xl font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 328 /* @__PURE__ */ jsxRuntimeExports.jsx(WandSparkles, { className: "text-indigo-500" }), 329 " Auto-Clean Tag Dictionary" 330 ] }), 331 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-1", children: "Preserves unused autocomplete words. Fixes corrupt encodings and deletes useless spam/URLs." }) 332 ] }), 333 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg text-zinc-400 hover:text-zinc-600 transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 18 }) }) 334 ] }), 335 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto space-y-4 pr-1 mb-6", children: [
336 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950/40 rounded-xl border border-zinc-200/60 dark:border-zinc-800/60", children: [ 337 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-start justify-between", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-3", children: [ 338 /* @__PURE__ */ jsxRuntimeExports.jsx( 339 "input", 340 { 341 type: "checkbox", 342 id: "restoreEncoded", 343 checked: restoreEncoded, 344 onChange: (e) => setRestoreEncoded(e.target.checked), 345 className: "mt-1 h-4 w-4 rounded border-zinc-350 text-indigo-650 focus:ring-indigo-550 bg-white dark:bg-zinc-900" 346 } 347 ), 348 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "restoreEncoded", className: "cursor-pointer", children: [ 349 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "text-sm font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 350 "Decode & Restore Encoded Tags",
351 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "px-1.5 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-400 font-mono text-[10px] font-bold", children: [ 352 restoreCount, 353 " found" 354 ] }) 355 ] }), 356 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: [ 357 "Decodes raw HTML decimal entities (e.g. ", 358 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: "刘" }), 359 ") back to original characters like ", 360 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Hatsune Miku" }), 361 " or ", 362 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Andy Lau" }), 363 "." 364 ] }) 365 ] }) 366 ] }) }), 367 restoreCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 pl-7", children: [
368 /* @__PURE__ */ jsxRuntimeExports.jsxs( 369 "button", 370 { 371 onClick: () => setShowRestoreList(!showRestoreList), 372 className: "text-xs text-indigo-500 hover:text-indigo-600 font-bold flex items-center gap-1", 373 children: [ 374 showRestoreList ? "Hide preview" : "Preview conversions", 375 showRestoreList ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 12 }) 376 ] 377 } 378 ), 379 showRestoreList && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 bg-white dark:bg-zinc-950 border border-zinc-200/50 dark:border-zinc-800/80 rounded-lg max-h-40 overflow-y-auto p-2 text-xs divide-y divide-zinc-100 dark:divide-zinc-800/40 font-mono", children: preview == null ? void 0 : preview.toRestore.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-1.5 flex justify-between gap-2", children: [
380 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 truncate max-w-[200px]", children: item.original }), 381 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400", children: "â" }), 382 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-emerald-600 dark:text-emerald-400 truncate max-w-[200px] font-bold", children: item.decoded }) 383 ] }, item.id)) }) 384 ] }) 385 ] }), 386 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950/40 rounded-xl border border-zinc-200/60 dark:border-zinc-800/60", children: [ 387 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-start justify-between", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-3", children: [ 388 /* @__PURE__ */ jsxRuntimeExports.jsx( 389 "input", 390 { 391 type: "checkbox", 392 id: "mergeDuplicates", 393 checked: mergeDuplicates, 394 onChange: (e) => setMergeDuplicates(e.target.checked), 395 className: "mt-1 h-4 w-4 rounded border-zinc-350 text-indigo-650 focus:ring-indigo-550 bg-white dark:bg-zinc-900" 396 } 397 ),
398 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "mergeDuplicates", className: "cursor-pointer", children: [ 399 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "text-sm font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 400 "Merge Case & Spacing Duplicates", 401 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "px-1.5 py-0.5 rounded-full bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-400 font-mono text-[10px] font-bold", children: [ 402 dupCount, 403 " groups" 404 ] }) 405 ] }), 406 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: [ 407 "Merges duplicate tags with identical casing/spacing issues (e.g. ", 408 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: "Ray Parker Jr." }), 409 " into ", 410 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: "Ray Parker JR." }), 411 "). Relinks associated tracks safely." 412 ] }) 413 ] }) 414 ] }) }), 415 dupCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 pl-7", children: [
416 /* @__PURE__ */ jsxRuntimeExports.jsxs( 417 "button", 418 { 419 onClick: () => setShowDupList(!showDupList), 420 className: "text-xs text-indigo-500 hover:text-indigo-600 font-bold flex items-center gap-1", 421 children: [ 422 showDupList ? "Hide preview" : "Preview merges", 423 showDupList ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 12 }) 424 ] 425 } 426 ), 427 showDupList && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 bg-white dark:bg-zinc-950 border border-zinc-200/50 dark:border-zinc-800/80 rounded-lg max-h-40 overflow-y-auto p-2 text-xs divide-y divide-zinc-100 dark:divide-zinc-800/40 font-mono", children: preview == null ? void 0 : preview.duplicates.map((group, index) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-1.5 flex flex-col gap-1", children: [ 428 /* @__P
428URE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between font-bold text-zinc-700 dark:text-zinc-300", children: [ 429 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 430 "Keep: ", 431 group.primary.name 432 ] }), 433 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-400 font-normal", children: [ 434 "(", 435 group.primary.uses, 436 " uses)" 437 ] }) 438 ] }), 439 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pl-3 text-red-550 flex flex-col", children: group.others.map((o) => /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 440 "Merge & Delete: ", 441 o.name, 442 " (", 443 o.uses, 444 " uses)" 445 ] }, o.id)) }) 446 ] }, index)) }) 447 ] }) 448 ] }), 449 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950/40 rounded-xl border border-zinc-200/60 dark:border-zinc-800/60", children: [
450 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-start justify-between", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-3", children: [ 451 /* @__PURE__ */ jsxRuntimeExports.jsx( 452 "input", 453 { 454 type: "checkbox", 455 id: "deleteJunk", 456 checked: deleteJunk, 457 onChange: (e) => setDeleteJunk(e.target.checked), 458 className: "mt-1 h-4 w-4 rounded border-zinc-350 text-indigo-650 focus:ring-indigo-550 bg-white dark:bg-zinc-900" 459 } 460 ), 461 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "deleteJunk", className: "cursor-pointer", children: [ 462 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "text-sm font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 463 "Delete Irrecoverable Junk Tags", 464 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "px-1.5 py-0.5 rounded-full bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 font-mono text-[10px] font-bold", children: [ 465 deleteCount, 466 " found" 467 ] }) 468 ] }),
469 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: "Purges raw scraper URLs, Discord file attachments, website references, and essay spam URLs." }) 470 ] }) 471 ] }) }), 472 deleteCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 pl-7", children: [ 473 /* @__PURE__ */ jsxRuntimeExports.jsxs( 474 "button", 475 { 476 onClick: () => setShowDeleteList(!showDeleteList), 477 className: "text-xs text-indigo-500 hover:text-indigo-600 font-bold flex items-center gap-1", 478 children: [ 479 showDeleteList ? "Hide preview" : "Preview items", 480 showDeleteList ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 12 }) 481 ] 482 } 483 ), 484 showDeleteList && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 bg-white dark:bg-zinc-950 border border-zinc-200/50 dark:border-zinc-800/80 rounded-lg max-h-40 overflow-y-auto p-2 text-xs divide-y divide-zinc-100 dark:divide-zinc-800/40 font-mono", children: preview == null ? void 0 : preview.toDelete.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-1.5 flex justify-between gap-2", children: [
485 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-500 truncate max-w-[350px] font-bold", children: item.name }), 486 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 bg-zinc-100 dark:bg-zinc-800 px-1 py-0.5 rounded font-sans text-[10px]", children: item.reason }) 487 ] }, item.id)) }) 488 ] }) 489 ] }) 490 ] }), 491 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 pt-4 border-t border-zinc-200 dark:border-zinc-800", children: [ 492 /* @__PURE__ */ jsxRuntimeExports.jsx( 493 "button", 494 { 495 onClick: onClose, 496 disabled: executing, 497 className: "flex-1 px-4 py-2 rounded-lg border border-zinc-300 dark:border-zinc-700 font-bold text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-sm disabled:opacity-50", 498 children: "Cancel" 499 } 500 ), 501 /* @__PURE__ */ jsxRuntimeExports.jsx( 502 "button", 503 { 504 onClick: handleExecute, 505 disabled: executing || !restoreEncoded && !deleteJunk && !mergeDuplicates, 506 className: "flex-1 px-4 py-2 rounded-lg bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-700 hover:to-indigo-700 text-white font-bold text-sm shadow-md flex items-center justify-center gap-2 disabled:opacity-50 transition-all duration-200 hover:scale-[1.01]",
507 children: executing ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 508 /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "animate-spin", size: 16 }), 509 "Optimizing..." 510 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 511 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 16 }), 512 "Run Smart Optimization" 513 ] }) 514 } 515 ) 516 ] }) 517 ] }) 518 ] }); 519}; 520const MusicBrainzDashboard = () => { 521 const [status, setStatus] = reactExports.useState(null); 522 const [loadingStatus, setLoadingStatus] = reactExports.useState(true); 523 const [lookupTitle, setLookupTitle] = reactExports.useState(""); 524 const [lookupArtist, setLookupArtist] = reactExports.useState(""); 525 const [lookingUp, setLookingUp] = reactExports.useState(false); 526 const [lookupResults, setLookupResults] = reactExports.useState([]); 527 const [syncing, setSyncing] = reactExports.useState(false); 528 const [logs, setLogs] = reactExports.useState(""); 529 const [pollingLogs, setPollingLogs] = reactExports.useState(false); 530 const fetchStatus = async () => { 531 setLoadingStatus(true); 532 try { 533 const res = await api.getMusicBrainzStatus(); 534 setStatus(res); 535 } catch (e) { 536 console.error(e); 537 } finally { 538 setLoadingStatus(false); 539 } 540 }; 541 reactExports.useEffect(() => { 542 fetchStatus(); 543 }, []); 544 reactExports.useEffect(() => { 545 let timer; 546 if (pollingLogs) { 547 const fetchLogs = async () => { 548 try { 549 const res = await api.getMusicBrainzSyncLogs(); 550 if (res.success) { 551 setLogs(res.logs || ""); 552 if (res.logs && (res.logs.includes("=== MusicBrainz Setup Complete ===") || res.logs.includes("[!] Sync Failed"))) { 553 setPollingLogs(false); 554 fetchStatus(); 555 } 556 } 557 } catch (e) { 558 console.error(e); 559 } 560 }; 561 fetchLogs(); 562 timer = setInterval(fetchLogs, 4e3); 563 } else { 564 clearInterval(timer); 565 } 566 return () => clearInterval(timer); 567 }, [pollingLogs]); 568 const handleSync = async () => { 569 if (!confirm("Are you sure you want to trigger a manual sync of the MusicBrainz database? This will download the latest full dump, selectively extract and prune the database tables, and rebuild the trigram search indexes. This runs in the background and might take up to 10 minutes.")) { 570 return; 571 } 572 setSyncing(true); 573 setLogs("Triggering sync...\n"); 574 setPollingLogs(true); 575 try { 576 const res = await api.triggerMusicBrainzSync(); 577 if (res.success) { 578 } 579 } catch (e) { 580 alert("Failed to start sync: " + e.message); 581 setPollingLogs(false); 582 } finally { 583 setSyncing(false); 584 } 585 }; 586 const handleLookup = async (e) => { 587 e.preventDefault(); 588 if (!lookupTitle.trim()) return; 589 setLookingUp(true); 590 setLookupResults([]); 591 try { 592 const res = await api.musicbrainzTestLookup(lookupArtist, lookupTitle); 593 if (res.candidates) { 594 setLookupResults(res.candidates); 595 } 596 } catch (e2) { 597 alert("Lookup failed: " + e2.message); 598 } finally { 599 setLookingUp(false); 600 } 601 }; 602 const isOnline = (status == null ? void 0 : status.status) === "online"; 603 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6 animate-in fade-in-50 duration-200", children: [ 604 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-6", children: [
605 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 p-6 shadow-sm flex flex-col justify-between", children: [ 606 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-start", children: [ 607 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 608 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-xs font-bold text-zinc-400 uppercase tracking-wider mb-1", children: "Replica Service" }), 609 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-bold text-zinc-800 dark:text-white", children: "Local MusicBrainz" }) 610 ] }), 611 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-10 h-10 rounded-full flex items-center justify-center ${isOnline ? "bg-emerald-50 dark:bg-emerald-950/30 text-emerald-500" : "bg-red-50 dark:bg-red-950/30 text-red-500"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 20 }) }) 612 ] }),
613 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-6 flex items-center gap-3", children: [ 614 loadingStatus ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "animate-spin text-zinc-400", size: 16 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "relative flex h-3 w-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `relative inline-flex rounded-full h-3 w-3 ${isOnline ? "bg-emerald-500" : "bg-red-500"}` }) }), 615 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-bold text-zinc-700 dark:text-zinc-300", children: loadingStatus ? "Checking..." : isOnline ? "Online (Connected)" : "Offline (Disconnected)" }), 616 !loadingStatus && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: fetchStatus, className: "ml-auto text-zinc-400 hover:text-zinc-650 dark:hover:text-zinc-205", children: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 14 }) }) 617 ] }) 618 ] }), 619 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 p-6 shadow-sm col-span-2", children: [
620 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-xs font-bold text-zinc-400 uppercase tracking-wider mb-3", children: "Database Volume Stats" }), 621 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-4", children: [ 622 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950 p-3 rounded-xl border border-zinc-100 dark:border-zinc-850", children: [ 623 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-[10px] font-bold text-zinc-400 uppercase", children: "Recordings" }), 624 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xl font-mono font-bold text-zinc-800 dark:text-white", children: isOnline ? status.counts.recordings.toLocaleString() : "â" }) 625 ] }), 626 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950 p-3 rounded-xl border border-zinc-100 dark:border-zinc-850", children: [ 627 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-[10px] font-bold text-zinc-400 uppercase", children: "Compositions" }),
628 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xl font-mono font-bold text-zinc-800 dark:text-white", children: isOnline ? status.counts.works.toLocaleString() : "â" }) 629 ] }), 630 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950 p-3 rounded-xl border border-zinc-100 dark:border-zinc-850", children: [ 631 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-[10px] font-bold text-zinc-400 uppercase", children: "Artists" }), 632 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xl font-mono font-bold text-zinc-800 dark:text-white", children: isOnline ? status.counts.artists.toLocaleString() : "â" }) 633 ] }), 634 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950 p-3 rounded-xl border border-zinc-100 dark:border-zinc-850", children: [ 635 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-[10px] font-bold text-zinc-400 uppercase", children: "Folksonomy Tags" }),
636 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xl font-mono font-bold text-zinc-800 dark:text-white", children: isOnline ? status.counts.tags.toLocaleString() : "â" }) 637 ] }) 638 ] }) 639 ] }) 640 ] }), 641 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 xl:grid-cols-3 gap-6", children: [ 642 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "xl:col-span-2 bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 p-6 shadow-sm space-y-6", children: [ 643 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 644 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-lg font-bold text-zinc-800 dark:text-white flex items-center gap-2", children: [ 645 /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { className: "text-indigo-500", size: 18 }), 646 " Test Lookup Engine" 647 ] }), 648 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-450 mt-1", children: "Simulate the background worker's fuzzy matcher and verify exact entity mappings and folksonomy tag extractions." }) 649 ] }),
650 /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleLookup, className: "grid grid-cols-1 md:grid-cols-5 gap-3 items-end", children: [ 651 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "md:col-span-2", children: [ 652 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "Track Title" }), 653 /* @__PURE__ */ jsxRuntimeExports.jsx( 654 "input", 655 { 656 type: "text", 657 value: lookupTitle, 658 onChange: (e) => setLookupTitle(e.target.value), 659 placeholder: "e.g. Moonlight Sonata", 660 required: true, 661 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded p-2 text-sm outline-none focus:border-indigo-500 text-zinc-900 dark:text-white" 662 } 663 ) 664 ] }), 665 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "md:col-span-2", children: [
666 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "Artist / Composer (Optional)" }), 667 /* @__PURE__ */ jsxRuntimeExports.jsx( 668 "input", 669 { 670 type: "text", 671 value: lookupArtist, 672 onChange: (e) => setLookupArtist(e.target.value), 673 placeholder: "e.g. Beethoven", 674 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded p-2 text-sm outline-none focus:border-indigo-500 text-zinc-900 dark:text-white" 675 } 676 ) 677 ] }), 678 /* @__PURE__ */ jsxRuntimeExports.jsxs( 679 "button", 680 { 681 type: "submit", 682 disabled: lookingUp || !lookupTitle, 683 className: "w-full h-9 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded font-bold text-xs shadow transition-all flex items-center justify-center gap-2 animate-pulse", 684 children: [ 685 lookingUp ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 14 }), 686 "Test Fuzzy Match" 687 ] 688 } 689 ) 690 ] }), 691 lookupResults.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
692 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-xs font-bold text-zinc-400 uppercase tracking-wider", children: "Top Match Candidates" }), 693 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden bg-zinc-50/40 dark:bg-zinc-950/20", children: lookupResults.map((cand, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 flex flex-col gap-3 hover:bg-white dark:hover:bg-zinc-900 transition-colors", children: [ 694 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-start gap-4", children: [ 695 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 696 /* @__PURE__ */ jsxRuntimeExports.jsx("h5", { className: "font-bold text-zinc-900 dark:text-white text-sm", children: cand.recording_title }), 697 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-zinc-500", children: [ 698 "By ",
699 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "text-zinc-700 dark:text-zinc-300", children: cand.artist_name }) 700 ] }), 701 cand.work_title && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] text-indigo-500 mt-0.5", children: [ 702 "Composition (Work ID): ", 703 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: cand.work_title }) 704 ] }) 705 ] }), 706 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-right shrink-0", children: [ 707 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `inline-flex px-2 py-0.5 rounded text-xs font-bold ${cand.confidence >= 0.8 ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-400" : cand.confidence >= 0.6 ? "bg-amber-100 text-amber-700 dark:bg-amber-950/40 dark:text-amber-400" : "bg-zinc-100 text-zinc-600 dark:bg-zinc-850 dark:text-zinc-400"}`, children: [ 708 (cand.confidence * 100).toFixed(0), 709 "% Confidence" 710 ] }),
711 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-zinc-400 mt-1 font-mono", children: "Index Match" }) 712 ] }) 713 ] }), 714 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap gap-1.5 items-center", children: [ 715 cand.genres.map((g, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-2 py-0.5 rounded bg-emerald-50 dark:bg-emerald-950/30 text-emerald-600 dark:text-emerald-400 border border-emerald-100 dark:border-emerald-900/30 text-[10px] font-bold", children: g }, i)), 716 cand.tags.slice(0, 8).map((t, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-2 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 text-[10px]", children: t }, i)), 717 cand.genres.length === 0 && cand.tags.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 text-xs italic", children: "No folksonomy metadata found for this track." }) 718 ] }) 719 ] }, idx)) }) 720 ] }) : lookupResults.length === 0 && !lookingUp && lookupTitle ? /* @__P
720URE__ */ jsxRuntimeExports.jsx("div", { className: "p-8 text-center bg-zinc-50 dark:bg-zinc-950 rounded-xl border border-zinc-100 dark:border-zinc-850", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-400", children: "No matching track found on the replica database. Try loosening terms." }) }) : null 721 ] }), 722 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 p-6 shadow-sm flex flex-col h-[500px]", children: [ 723 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4", children: [ 724 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-sm font-bold text-zinc-800 dark:text-white flex items-center gap-2", children: [ 725 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { className: "text-indigo-500", size: 16 }), 726 " Sync Management" 727 ] }), 728 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-400 mt-1", children: "Fetch full MetaBrainz dump, prune tables, and rebuild indexes in background." }) 729 ] }),
730 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3 pb-4 border-b border-zinc-100 dark:border-zinc-800 mb-4", children: [ 731 /* @__PURE__ */ jsxRuntimeExports.jsxs( 732 "button", 733 { 734 onClick: handleSync, 735 disabled: pollingLogs || syncing, 736 className: "w-full py-2 bg-zinc-950 hover:bg-zinc-900 dark:bg-white dark:hover:bg-zinc-50 text-white dark:text-zinc-900 rounded font-bold text-xs shadow flex items-center justify-center gap-2 disabled:opacity-50 transition-all hover:scale-[1.01]", 737 children: [ 738 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 14, className: pollingLogs ? "animate-spin" : "" }), 739 "Rebuild Local Replica" 740 ] 741 } 742 ), 743 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center bg-zinc-50 dark:bg-zinc-950 p-2.5 rounded-lg border border-zinc-100 dark:border-zinc-850", children: [ 744 /* @__P
744URE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-500", children: "Weekly Cron Auto-Sync" }), 745 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-1.5 py-0.5 rounded bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 font-bold text-[10px]", children: "Enabled (Sunday 3AM)" }) 746 ] }) 747 ] }), 748 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col rounded-xl overflow-hidden border border-zinc-200 dark:border-zinc-800 bg-zinc-950 font-mono text-[11px] text-zinc-300", children: [ 749 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 px-3 py-2 border-b border-zinc-800 flex justify-between items-center shrink-0", children: [ 750 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-500 text-[10px] flex items-center gap-1.5 font-bold", children: [ 751 /* @__PURE__ */ jsxRuntimeExports.jsx(Terminal, { size: 12 }), 752 " replica_sync.log" 753 ] }),
754 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 755 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `h-2 w-2 rounded-full ${pollingLogs ? "bg-amber-500 animate-pulse" : "bg-zinc-600"}` }), 756 /* @__PURE__ */ jsxRuntimeExports.jsx( 757 "button", 758 { 759 onClick: () => setPollingLogs(!pollingLogs), 760 className: "text-[10px] text-zinc-400 hover:text-zinc-200", 761 children: pollingLogs ? "Stop Tail" : "Tail Logs" 762 } 763 ) 764 ] }) 765 ] }), 766 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 p-3 overflow-y-auto whitespace-pre-wrap select-text scrollbar-thin", children: logs ? logs : "No logs recorded yet. Press 'Rebuild Local Replica' or 'Tail Logs' to monitor status." }) 767 ] }) 768 ] }) 769 ] }) 770 ] }); 771}; 772const TagManager = () => { 773 const [activeTab, setActiveTab] = reactExports.useState("dictionary"); 774 const [isCleanupOpen, setIsCleanupOpen] = reactExports.useState(false); 775 const [localRefresh, setLocalRefresh] = reactExports.useState(0); 776 const [editingTag, setEditingTag] = reactExports.useState(void 0); 777 const [mergingTag, setMergingTag] = reactExports.useState(null); 778 const refresh = () => setLocalRefresh((p) => p + 1); 779 const fetcher = reactExports.useCallback((params) => api.getAdminTags(params), []); 780 const handleDelete = async (id) => { 781 if (confirm("Are you sure you want to delete this tag? It will be removed from all tracks.")) { 782 await api.deleteTag(id); 783 refresh(); 784 } 785 }; 786 const actions = /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 787 /* @__PURE__ */ jsxRuntimeExports.jsxs( 788 "button", 789 { 790 onClick: () => setIsCleanupOpen(true), 791 className: "flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gradient-to-r from-violet-600 to-indigo-600 text-white hover:from-violet-700 hover:to-indigo-700 text-xs font-bold shadow-md transition-all duration-200 hover:scale-[1.02]", 792 children: [
793 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 14 }), 794 " Auto-Clean" 795 ] 796 } 797 ), 798 /* @__PURE__ */ jsxRuntimeExports.jsxs( 799 "button", 800 { 801 onClick: () => setEditingTag(null), 802 className: "flex items-center gap-2 px-3 py-1.5 rounded-lg bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 hover:bg-zinc-200 dark:hover:bg-zinc-700 text-xs font-bold border border-zinc-200 dark:border-zinc-700 shadow-sm transition-all", 803 children: [ 804 /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { size: 14 }), 805 " Create Tag" 806 ] 807 } 808 ) 809 ] }); 810 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ 811 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex border-b border-zinc-200 dark:border-zinc-800 pb-px", children: [ 812 /* @__PURE__ */ jsxRuntimeExports.jsxs( 813 "button", 814 { 815 onClick: () => setActiveTab("dictionary"), 816 className: `px-4 py-2.5 text-sm font-bold border-b-2 transition-all flex items-center gap-2 ${activeTab === "dictionary" ? "border-indigo-500 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200"}`, 817 children: [
818 /* @__PURE__ */ jsxRuntimeExports.jsx(Hash, { size: 16 }), 819 "Tag Dictionary" 820 ] 821 } 822 ), 823 /* @__PURE__ */ jsxRuntimeExports.jsxs( 824 "button", 825 { 826 onClick: () => setActiveTab("musicbrainz"), 827 className: `px-4 py-2.5 text-sm font-bold border-b-2 transition-all flex items-center gap-2 ${activeTab === "musicbrainz" ? "border-indigo-500 text-indigo-600 dark:text-indigo-400" : "border-transparent text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200"}`, 828 children: [ 829 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 16 }), 830 "Knowledge Base (MusicBrainz)" 831 ] 832 } 833 ) 834 ] }), 835 activeTab === "dictionary" ? /* @__PURE__ */ jsxRuntimeExports.jsx( 836 DataTable, 837 { 838 title: "Tag Dictionary", 839 icon: Hash, 840 placeholder: "Search tags...", 841 filters: [], 842 onFetch: fetcher, 843 reloadTrigger: localRefresh, 844 actions, 845 columnHeaders: ["Name", "Type", "Slug", "Usage", "Actions"], 846 columns: (tag) => /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
847 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-900 dark:text-white", children: tag.name }) }), 848 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `px-2 py-0.5 rounded text-[10px] font-bold ${tag.type === "GENRE" ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400" : tag.type === "INSTRUMENT" ? "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" : tag.type === "SOURCE" ? "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400" : "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400"}`, children: tag.type || "TAG" }) }), 849 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs text-zinc-500", children: tag.slug }) }), 850 /* @__P
850URE__ */ jsxRuntimeExports.jsx("td", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 px-2 py-1 rounded text-xs font-mono", children: [ 851 tag.count, 852 " tracks" 853 ] }) }), 854 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 855 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setEditingTag(tag), className: "p-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded text-zinc-500 hover:text-indigo-500 transition-colors", title: "Edit", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Pen, { size: 14 }) }), 856 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setMergingTag(tag), className: "p-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded text-zinc-500 hover:text-amber-500 transition-colors", title: "Merge", children: /* @__PURE__ */ jsxRuntimeExports.jsx(GitMerge, { size: 14 }) }),
857 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => handleDelete(tag.id), className: "p-1.5 hover:bg-red-100 dark:hover:bg-red-900/30 rounded text-zinc-500 hover:text-red-500 transition-colors", title: "Delete", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 14 }) }) 858 ] }) }) 859 ] }) 860 } 861 ) : /* @__PURE__ */ jsxRuntimeExports.jsx(MusicBrainzDashboard, {}), 862 editingTag !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx( 863 TagFormDrawer, 864 { 865 tag: editingTag, 866 onClose: () => setEditingTag(void 0), 867 onRefresh: refresh 868 } 869 ), 870 mergingTag && /* @__PURE__ */ jsxRuntimeExports.jsx( 871 MergeModal, 872 { 873 sourceTag: mergingTag, 874 onClose: () => setMergingTag(null), 875 onRefresh: refresh 876 } 877 ), 878 isCleanupOpen && /* @__PURE__ */ jsxRuntimeExports.jsx( 879 CleanupModal, 880 { 881 onClose: () => setIsCleanupOpen(false), 882 onRefresh: refresh 883 } 884 ) 885 ] }); 886}; 887export { 888 TagManager as default 889};
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.