From b85392977cb1b3a73a0330a787b7fcf5bf33e78c Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Tue, 2 Jun 2026 23:16:47 -0700 Subject: [PATCH] Artist Explorer: search-and-select instead of free text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Explorer prompt accepted any loose text and explored whatever you typed. Now it's a proper picker: type -> debounced search of the metadata source (reuses /api/discover/build-playlist/search-artists — Hydrabase if active, Spotify if configured, else the active metadata source) -> shows real artist results with images -> click one to explore that resolved artist. Enter picks the top match (never explores raw text); Escape/Cancel/backdrop close. Pure frontend: rebuilds _showArtistMapSearchPrompt() (same Promise contract, so the caller is unchanged), reusing the playlist-builder's search endpoint + picker styling. No backend change. --- webui/static/discover.js | 87 ++++++++++++++++++++++++++++++++-------- webui/static/style.css | 30 ++++++++++++++ 2 files changed, 101 insertions(+), 16 deletions(-) diff --git a/webui/static/discover.js b/webui/static/discover.js index 1872ea5d..bd378cc7 100644 --- a/webui/static/discover.js +++ b/webui/static/discover.js @@ -6431,14 +6431,22 @@ async function _openArtistMapExplorerWithName(name) { } function _showArtistMapSearchPrompt() { + // Search the metadata source and make the user PICK a real artist, rather + // than exploring whatever loose text they typed. Resolves with the chosen + // artist's resolved name (which the explorer hands to /artist-map/explore), + // or null if cancelled. return new Promise(resolve => { const existing = document.getElementById('artmap-search-prompt'); if (existing) existing.remove(); - const overlay = document.createElement('div'); + let done = false; + let overlay; + const finish = (val) => { if (done) return; done = true; if (overlay) overlay.remove(); resolve(val); }; + + overlay = document.createElement('div'); overlay.id = 'artmap-search-prompt'; overlay.className = 'modal-overlay'; - overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); resolve(null); } }; + overlay.onclick = (e) => { if (e.target === overlay) finish(null); }; overlay.innerHTML = `
@@ -6449,32 +6457,79 @@ function _showArtistMapSearchPrompt() {

Artist Explorer

-

Enter an artist to explore their connections

+

Search and pick an artist to explore

- +
+ + +
+
- - +
`; document.body.appendChild(overlay); const input = overlay.querySelector('#artmap-explore-input'); - const goBtn = overlay.querySelector('#artmap-explore-go'); + const results = overlay.querySelector('#artmap-explore-results'); + const spinner = overlay.querySelector('#artmap-explore-spinner'); + overlay.querySelector('#artmap-explore-cancel').onclick = () => finish(null); - const submit = () => { - const val = input.value.trim(); - overlay.remove(); - resolve(val || null); + const renderResults = (artists) => { + results.innerHTML = ''; + if (!artists.length) { + results.innerHTML = '
No artists found
'; + return; + } + artists.forEach(a => { + const img = a.image_url || '/static/placeholder-album.png'; + const row = document.createElement('div'); + row.className = 'artmap-explore-result'; + row.innerHTML = ` + + ${escapeHtml(a.name)} + Explore →`; + row.onclick = () => finish(a.name); // pick the resolved artist, not raw text + results.appendChild(row); + }); }; - goBtn.onclick = submit; - input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); }); + let timer = null; + let token = 0; + const doSearch = () => { + const q = input.value.trim(); + if (!q) { results.innerHTML = ''; spinner.style.display = 'none'; clearTimeout(timer); return; } + clearTimeout(timer); + timer = setTimeout(async () => { + const myToken = ++token; + spinner.style.display = 'flex'; + try { + const resp = await fetch(`/api/discover/build-playlist/search-artists?query=${encodeURIComponent(q)}`); + const data = await resp.json(); + if (myToken !== token) return; // a newer keystroke superseded this + renderResults((data && data.success && Array.isArray(data.artists)) ? data.artists : []); + } catch (e) { + if (myToken === token) results.innerHTML = '
Search failed — try again
'; + } finally { + if (myToken === token) spinner.style.display = 'none'; + } + }, 350); + }; + + input.addEventListener('input', doSearch); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const first = results.querySelector('.artmap-explore-result'); + if (first) first.click(); // Enter = pick top match, never raw text + } else if (e.key === 'Escape') { + finish(null); + } + }); setTimeout(() => input.focus(), 50); }); } diff --git a/webui/static/style.css b/webui/static/style.css index f5b13454..35afbdef 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -34358,6 +34358,36 @@ div.artist-hero-badge { .artmap-explore-input::placeholder { color: rgba(255,255,255,0.2); } .artmap-search-prompt-actions { display: flex; justify-content: flex-end; gap: 8px; } +/* Artist Explorer — search + select picker */ +.artmap-explore-search-wrap { position: relative; } +.artmap-explore-search-wrap .artmap-explore-input { margin-bottom: 12px; } +.artmap-explore-spinner { position: absolute; right: 12px; top: 11px; pointer-events: none; } +.artmap-explore-spinner .watch-all-loading-spinner { width: 18px; height: 18px; border-width: 2px; } +.artmap-explore-results { + max-height: 320px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; + margin-bottom: 18px; +} +.artmap-explore-results:empty { margin-bottom: 0; } +.artmap-explore-result { + display: flex; align-items: center; gap: 12px; padding: 8px 10px; border-radius: 10px; + cursor: pointer; transition: background 0.15s ease; +} +.artmap-explore-result:hover { background: rgba(138,43,226,0.14); } +.artmap-explore-result img { + width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; + background: rgba(255,255,255,0.06); +} +.artmap-explore-result-name { + flex: 1 1 auto; min-width: 0; color: #fff; font-size: 14px; font-weight: 600; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.artmap-explore-result-go { + flex: 0 0 auto; font-size: 11px; font-weight: 700; color: rgba(138,43,226,0.9); + opacity: 0; transition: opacity 0.15s ease; +} +.artmap-explore-result:hover .artmap-explore-result-go { opacity: 1; } +.artmap-explore-empty { padding: 18px; text-align: center; color: rgba(255,255,255,0.35); font-size: 13px; } + /* Genre picker modal */ .artmap-genre-picker-modal { background: linear-gradient(165deg, #1e1e32 0%, #181828 100%);