feat(ui): auto-tag accept/reject as an in-pill yes/no pair; confirm refocuses input
Fold the auto-tag accept into the chip: a provisional auto-tag now shows a compact green ✓ / red ✗ pair IN the pill (replacing the ✕), and the "auto" text label is dropped — the yes/no is signal enough (operator-asked). ✓ confirms (trains + shields from retraction), ✗ removes (records a negative). The name still ellipsis-truncates so the pair stays reachable. onConfirm now returns focus to the tag input like onRemove already does, so the input is the cursor's resting position after any chip action in both the modal and Explore views. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="fc-tag-chip" @mouseenter="onEnter" @mouseleave="onLeave">
|
<span class="fc-tag-chip" @mouseenter="onEnter" @mouseleave="onLeave">
|
||||||
<v-chip
|
<v-chip
|
||||||
size="small" closable
|
size="small" :closable="!unconfirmedAuto"
|
||||||
:color="store.colorFor(tag.kind)" variant="tonal"
|
:color="store.colorFor(tag.kind)" variant="tonal"
|
||||||
class="fc-tag-chip__nav"
|
class="fc-tag-chip__nav"
|
||||||
role="link"
|
role="link"
|
||||||
@@ -16,20 +16,26 @@
|
|||||||
>mdi-shield-outline</v-icon><span
|
>mdi-shield-outline</v-icon><span
|
||||||
v-if="tag.fandom_id" class="fc-tag-chip__fandom"
|
v-if="tag.fandom_id" class="fc-tag-chip__fandom"
|
||||||
:title="tag.fandom_name ? `Fandom: ${tag.fandom_name}` : 'Has a fandom'"
|
:title="tag.fandom_name ? `Fandom: ${tag.fandom_name}` : 'Has a fandom'"
|
||||||
>→<template v-if="fandomLabel"> {{ fandomLabel }}</template></span><span
|
>→<template v-if="fandomLabel"> {{ fandomLabel }}</template></span>
|
||||||
v-if="unconfirmedAuto" class="fc-tag-chip__auto"
|
<!-- Provisional auto-tag: an in-pill yes/no pair REPLACES the ✕ (the pair
|
||||||
title="Auto-applied — provisional: it won't train the model and can be retracted until you confirm it."
|
itself signals "auto" — no separate label, operator 2026-07-07). Yes
|
||||||
>auto</span>
|
confirms it (trains + shields from retraction); No removes it (records
|
||||||
|
a negative). Both hand focus back to the tag input via TagPanel. -->
|
||||||
|
<span v-if="unconfirmedAuto" class="fc-tag-chip__verdict">
|
||||||
|
<button
|
||||||
|
type="button" class="fc-tag-chip__yes"
|
||||||
|
:title="`Yes — keep “${tag.name}” (trains the model, won't be retracted)`"
|
||||||
|
:aria-label="`Confirm ${tag.name}`"
|
||||||
|
@click.stop="$emit('confirm', tag)"
|
||||||
|
><v-icon size="13">mdi-check</v-icon></button>
|
||||||
|
<button
|
||||||
|
type="button" class="fc-tag-chip__no"
|
||||||
|
:title="`No — remove “${tag.name}”`"
|
||||||
|
:aria-label="`Reject ${tag.name}`"
|
||||||
|
@click.stop="$emit('remove', tag.id)"
|
||||||
|
><v-icon size="13">mdi-close</v-icon></button>
|
||||||
|
</span>
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<!-- Keep/confirm an auto-applied tag: promotes it to a training positive and
|
|
||||||
shields it from the retraction sweep (milestone 139). Only shown for
|
|
||||||
provisional (unconfirmed) auto-tags. -->
|
|
||||||
<button
|
|
||||||
v-if="unconfirmedAuto" class="fc-tag-chip__confirm" type="button"
|
|
||||||
:title="`Keep “${tag.name}” — confirm this auto-tag so it trains the model and won't be retracted`"
|
|
||||||
:aria-label="`Confirm ${tag.name}`"
|
|
||||||
@click.stop="$emit('confirm', tag)"
|
|
||||||
><v-icon size="16">mdi-check</v-icon></button>
|
|
||||||
<!-- Modal-safe kebab is baked into KebabMenu (this chip lives in the
|
<!-- Modal-safe kebab is baked into KebabMenu (this chip lives in the
|
||||||
teleported image modal — #711). System tags hide it entirely: rename
|
teleported image modal — #711). System tags hide it entirely: rename
|
||||||
is refused server-side (the hygiene machinery keys on the row) and
|
is refused server-side (the hygiene machinery keys on the row) and
|
||||||
@@ -71,8 +77,8 @@ const store = useTagStore()
|
|||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
// An auto-applied tag the operator hasn't confirmed yet — provisional (milestone
|
// An auto-applied tag the operator hasn't confirmed yet — provisional (milestone
|
||||||
// 139): it doesn't train the model and the retraction sweep can drop it. Shows
|
// 139): it doesn't train the model and the retraction sweep can drop it. Shows an
|
||||||
// the "auto" badge + a Keep/confirm button. `source`/`confirmed` come from the
|
// in-pill yes/no pair in place of the ✕. `source`/`confirmed` come from the
|
||||||
// applied-tags payload (list_for_image / get_image_with_tags).
|
// applied-tags payload (list_for_image / get_image_with_tags).
|
||||||
const unconfirmedAuto = computed(() =>
|
const unconfirmedAuto = computed(() =>
|
||||||
AUTO_SOURCES.includes(props.tag.source) && !props.tag.confirmed
|
AUTO_SOURCES.includes(props.tag.source) && !props.tag.confirmed
|
||||||
@@ -130,10 +136,11 @@ function iconFor (k) { return KIND_ICONS[k] || 'mdi-tag' }
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* A chip must never exceed the rail — a long character+fandom(+AUTO) chip used
|
/* A chip must never exceed the rail — a long character+fandom chip used to
|
||||||
to overflow the right edge and clip its close ✕ (operator-flagged 2026-07-06).
|
overflow the right edge and clip its trailing control (operator-flagged
|
||||||
The name is the elastic part: it ellipsis-truncates so the ✕, fandom, and AUTO
|
2026-07-06). The name is the elastic part: it ellipsis-truncates so the ✕ (or
|
||||||
badge stay reachable. Full name is still available via the chip's hover title. */
|
the auto-tag yes/no pair) and fandom stay reachable. Full name is on the
|
||||||
|
chip's hover title. */
|
||||||
.fc-tag-chip { display: inline-flex; align-items: center; gap: 1px; max-width: 100%; min-width: 0; }
|
.fc-tag-chip { display: inline-flex; align-items: center; gap: 1px; max-width: 100%; min-width: 0; }
|
||||||
.fc-tag-chip__nav { max-width: 100%; min-width: 0; }
|
.fc-tag-chip__nav { max-width: 100%; min-width: 0; }
|
||||||
.fc-tag-chip__nav :deep(.v-chip__content) { min-width: 0; overflow: hidden; }
|
.fc-tag-chip__nav :deep(.v-chip__content) { min-width: 0; overflow: hidden; }
|
||||||
@@ -152,28 +159,23 @@ function iconFor (k) { return KIND_ICONS[k] || 'mdi-tag' }
|
|||||||
.fc-tag-chip__kebab { opacity: 0.7; }
|
.fc-tag-chip__kebab { opacity: 0.7; }
|
||||||
.fc-tag-chip:hover .fc-tag-chip__kebab { opacity: 1; }
|
.fc-tag-chip:hover .fc-tag-chip__kebab { opacity: 1; }
|
||||||
.fc-tag-chip__fandom { opacity: 0.7; font-size: 0.85em; }
|
.fc-tag-chip__fandom { opacity: 0.7; font-size: 0.85em; }
|
||||||
/* "auto" = provisional (auto-applied, unconfirmed). Quiet pill inside the chip. */
|
/* Provisional auto-tag: a compact green ✓ / red ✗ pair in place of the ✕. The
|
||||||
.fc-tag-chip__auto {
|
yes/no is obvious enough on its own, so there's no "auto" label (operator-asked
|
||||||
display: inline-block; vertical-align: middle; margin-left: 4px;
|
2026-07-07). flex:0 0 auto keeps it visible while the name ellipsis-truncates. */
|
||||||
font-size: 9px; font-weight: 700;
|
.fc-tag-chip__verdict {
|
||||||
text-transform: uppercase; letter-spacing: 0.04em;
|
flex: 0 0 auto; display: inline-flex; align-items: center; gap: 1px;
|
||||||
color: rgb(var(--v-theme-on-surface-variant));
|
margin-left: 3px;
|
||||||
background: rgb(var(--v-theme-surface-light));
|
|
||||||
border: 1px solid rgb(var(--v-theme-on-surface-variant), 0.3);
|
|
||||||
padding: 0 4px; border-radius: 999px;
|
|
||||||
}
|
}
|
||||||
/* Keep/confirm — the SAME filled green ✓ circle as the suggestion accept button
|
.fc-tag-chip__yes, .fc-tag-chip__no {
|
||||||
(.fc-act--yes in SuggestionItem), so "accept this tag" reads identically
|
|
||||||
wherever it appears (operator-asked 2026-07-06). */
|
|
||||||
.fc-tag-chip__confirm {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 26px; height: 26px; border-radius: 50%; border: none; cursor: pointer;
|
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
color: #fff; background: rgb(var(--v-theme-success));
|
width: 18px; height: 18px; padding: 0; border: none; border-radius: 50%;
|
||||||
opacity: 0.9; transition: transform 0.1s, opacity 0.1s;
|
background: transparent; cursor: pointer; transition: background 0.1s;
|
||||||
}
|
}
|
||||||
.fc-tag-chip__confirm:hover { opacity: 1; transform: scale(1.1); }
|
.fc-tag-chip__yes { color: rgb(var(--v-theme-success)); }
|
||||||
.fc-tag-chip__confirm:focus-visible {
|
.fc-tag-chip__no { color: rgb(var(--v-theme-error)); }
|
||||||
|
.fc-tag-chip__yes:hover { background: rgb(var(--v-theme-success), 0.16); }
|
||||||
|
.fc-tag-chip__no:hover { background: rgb(var(--v-theme-error), 0.16); }
|
||||||
|
.fc-tag-chip__yes:focus-visible, .fc-tag-chip__no:focus-visible {
|
||||||
outline: 2px solid rgb(var(--v-theme-accent)); outline-offset: 1px;
|
outline: 2px solid rgb(var(--v-theme-accent)); outline-offset: 1px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ async function onConfirm(tag) {
|
|||||||
try {
|
try {
|
||||||
await api.post(`/api/images/${host.currentImageId}/tags/${tag.id}/confirm`)
|
await api.post(`/api/images/${host.currentImageId}/tags/${tag.id}/confirm`)
|
||||||
await host.reloadTags()
|
await host.reloadTags()
|
||||||
|
// Return the cursor to the tag input — its resting position in these views,
|
||||||
|
// matching every other chip action (remove/accept) so the keyboard flow
|
||||||
|
// never leaves the field (operator-asked 2026-07-07).
|
||||||
|
focusTagInput()
|
||||||
}
|
}
|
||||||
catch (e) { errorMsg.value = e.message }
|
catch (e) { errorMsg.value = e.message }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user