feat(modal): ambiguous-candidate picker + accept payload rename
Accept suggestion requests now send 'name' (matches Task 8 backend) and preserve source/confidence for feedback logging. New 409 handler renders an inline picker with each candidate's display_name; user clicks one, which re-POSTs with explicit tag_id. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user