Merge pull request 'Auto-tag accept/reject as an in-pill yes/no pair; confirm refocuses the tag input' (#200) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m44s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m44s
This commit was merged in pull request #200.
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