diff --git a/app/static/js/view-modal.js b/app/static/js/view-modal.js index ee16602..0de47c7 100644 --- a/app/static/js/view-modal.js +++ b/app/static/js/view-modal.js @@ -206,17 +206,23 @@ document.addEventListener('DOMContentLoaded', () => { if (chip.dataset.disabled === 'true') return; chip.dataset.disabled = 'true'; chip.style.pointerEvents = 'none'; + const name = chip.dataset.tagName || ''; + const category = chip.dataset.category || ''; + const source = chip.dataset.source || ''; + const confidence = Number(chip.dataset.confidence) || 0; try { const r = await fetch(`/image/${imageId}/suggestions/accept`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tag_name: chip.dataset.tagName, - source: chip.dataset.source, - category: chip.dataset.category, - confidence: Number(chip.dataset.confidence) || 0, - }), + body: JSON.stringify({ name, category, source, confidence }), }); + if (r.status === 409) { + const body = await r.json(); + if (body && body.error === 'ambiguous' && Array.isArray(body.candidates)) { + renderAmbiguousPicker(chip, body.candidates, { name, category, source, confidence, imageId }); + return; // chip stays disabled; picker handles next step + } + } const j = await r.json(); if (!j.ok) { chip.dataset.disabled = 'false'; @@ -227,7 +233,7 @@ document.addEventListener('DOMContentLoaded', () => { return; } if (typeof loadTags === 'function') await loadTags(imageId); - if (tagActionFeedback) tagActionFeedback.textContent = `Added ${j.tag.name}`; + if (tagActionFeedback) tagActionFeedback.textContent = `Added ${j.tag.display_name || j.tag.name}`; if (typeof window.refreshItemTags === 'function') window.refreshItemTags(imageId); animateChipOut(chip); } catch { @@ -236,6 +242,68 @@ document.addEventListener('DOMContentLoaded', () => { } } + function renderAmbiguousPicker(chip, candidates, ctx) { + const picker = document.createElement('div'); + picker.className = 'suggestion-ambiguous-picker'; + + const label = document.createElement('div'); + label.className = 'ambiguous-label'; + label.textContent = 'Which one?'; + picker.appendChild(label); + + for (const c of candidates) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'ambiguous-candidate'; + btn.dataset.tagId = String(c.id); + btn.textContent = c.display_name; + btn.addEventListener('click', async () => { + picker.querySelectorAll('button').forEach(b => { b.disabled = true; }); + try { + const r = await fetch(`/image/${ctx.imageId}/suggestions/accept`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: ctx.name, + category: ctx.category, + source: ctx.source, + confidence: ctx.confidence, + tag_id: c.id, + }), + }); + const j = await r.json(); + if (!j.ok) { + picker.querySelectorAll('button').forEach(b => { b.disabled = false; }); + if (tagActionFeedback) { + tagActionFeedback.textContent = `Couldn't accept: ${j.error || 'error'}`; + } + return; + } + if (typeof loadTags === 'function') await loadTags(ctx.imageId); + if (tagActionFeedback) tagActionFeedback.textContent = `Added ${j.tag.display_name || j.tag.name}`; + if (typeof window.refreshItemTags === 'function') window.refreshItemTags(ctx.imageId); + animateChipOut(chip); + } catch { + picker.querySelectorAll('button').forEach(b => { b.disabled = false; }); + } + }); + picker.appendChild(btn); + } + + const cancelBtn = document.createElement('button'); + cancelBtn.type = 'button'; + cancelBtn.className = 'ambiguous-cancel'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.addEventListener('click', () => { + picker.remove(); + chip.dataset.disabled = 'false'; + chip.style.pointerEvents = ''; + }); + picker.appendChild(cancelBtn); + + chip.appendChild(picker); + } + async function rejectSuggestion(chip, imageId) { chip.style.pointerEvents = 'none'; try { diff --git a/app/static/style.css b/app/static/style.css index 84c4227..e66eccf 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -3065,3 +3065,46 @@ body.select-mode .gallery-infinite-container { border-radius: 0.25rem; color: var(--fg, #eee); } + +/* Ambiguous-candidate picker (Task 24) */ +.suggestion-ambiguous-picker { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + padding: 0.5rem; + margin-top: 0.3rem; + background: rgba(255, 180, 0, 0.06); + border: 1px solid rgba(255, 180, 0, 0.25); + border-radius: 0.25rem; + font-size: 0.85rem; +} +.suggestion-ambiguous-picker .ambiguous-label { + width: 100%; + font-weight: 600; + color: #ffb400; + margin-bottom: 0.2rem; +} +.suggestion-ambiguous-picker .ambiguous-candidate { + padding: 0.25rem 0.5rem; + background: var(--button-bg, #222); + border: 1px solid var(--button-border, #444); + border-radius: 0.25rem; + color: var(--fg, #eee); + cursor: pointer; + font-size: 0.85rem; +} +.suggestion-ambiguous-picker .ambiguous-candidate:hover { + background: var(--button-hover-bg, #333); +} +.suggestion-ambiguous-picker .ambiguous-candidate:disabled { + opacity: 0.5; + cursor: wait; +} +.suggestion-ambiguous-picker .ambiguous-cancel { + padding: 0.25rem 0.5rem; + background: transparent; + border: none; + color: var(--muted, #888); + cursor: pointer; + margin-left: auto; +}