feat(ui): confirm/keep auto-applied tags (milestone 139)
Auto-applied tags are provisional (they don't train the model + can be retracted until confirmed), so surface and confirm them: - Backend: list_for_image + get_image_with_tags now include `source` + a `confirmed` flag on each applied tag (via serialize_tag, image-scoped; defaulted for autocomplete/directory callers). - Frontend: TagChip badges an unconfirmed auto-tag with an "auto" pill + a one-click Keep/confirm (✓) → POST /images/<id>/tags/<id>/confirm, which promotes it to a training positive and shields it from the retraction sweep; TagPanel reloads so the badge + button drop once confirmed. Contract test for the source/confirmed payload. 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:
@@ -16,8 +16,20 @@
|
||||
>mdi-shield-outline</v-icon><span
|
||||
v-if="tag.fandom_id" class="fc-tag-chip__fandom"
|
||||
:title="tag.fandom_name ? `Fandom: ${tag.fandom_name}` : 'Has a fandom'"
|
||||
>→<template v-if="fandomLabel"> {{ fandomLabel }}</template></span>
|
||||
>→<template v-if="fandomLabel"> {{ fandomLabel }}</template></span><span
|
||||
v-if="unconfirmedAuto" class="fc-tag-chip__auto"
|
||||
title="Auto-applied — provisional: it won't train the model and can be retracted until you confirm it."
|
||||
>auto</span>
|
||||
</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="14">mdi-check</v-icon></button>
|
||||
<!-- Modal-safe kebab is baked into KebabMenu (this chip lives in the
|
||||
teleported image modal — #711). System tags hide it entirely: rename
|
||||
is refused server-side (the hygiene machinery keys on the row) and
|
||||
@@ -43,6 +55,8 @@ import { useTagStore } from '../../stores/tags.js'
|
||||
import { useApi } from '../../composables/useApi.js'
|
||||
import KebabMenu from '../common/KebabMenu.vue'
|
||||
|
||||
const AUTO_SOURCES = ['head_auto', 'ccip_auto', 'ml_auto']
|
||||
|
||||
const props = defineProps({
|
||||
tag: { type: Object, required: true },
|
||||
// When set (the tagging panels), hovering the chip asks the backend which crop
|
||||
@@ -51,11 +65,19 @@ const props = defineProps({
|
||||
// the hover is inert (no injected target, or no image to ground against).
|
||||
imageId: { type: Number, default: null },
|
||||
})
|
||||
defineEmits(['remove', 'rename', 'set-fandom', 'navigate'])
|
||||
defineEmits(['remove', 'rename', 'set-fandom', 'navigate', 'confirm'])
|
||||
|
||||
const store = useTagStore()
|
||||
const api = useApi()
|
||||
|
||||
// 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
|
||||
// the "auto" badge + a Keep/confirm button. `source`/`confirmed` come from the
|
||||
// applied-tags payload (list_for_image / get_image_with_tags).
|
||||
const unconfirmedAuto = computed(() =>
|
||||
AUTO_SOURCES.includes(props.tag.source) && !props.tag.confirmed
|
||||
)
|
||||
|
||||
// #1206 Step 4: applied-tag grounding. `fcSuggestionHover` is provided by the
|
||||
// image viewer / Explore host (a no-op elsewhere). Applied tags aren't scored
|
||||
// live, so we fetch the winning region on demand and cache it per (image, tag).
|
||||
@@ -120,4 +142,26 @@ function iconFor (k) { return KIND_ICONS[k] || 'mdi-tag' }
|
||||
.fc-tag-chip__kebab { opacity: 0.7; }
|
||||
.fc-tag-chip:hover .fc-tag-chip__kebab { opacity: 1; }
|
||||
.fc-tag-chip__fandom { opacity: 0.7; font-size: 0.85em; }
|
||||
/* "auto" = provisional (auto-applied, unconfirmed). Quiet pill inside the chip. */
|
||||
.fc-tag-chip__auto {
|
||||
display: inline-block; vertical-align: middle; margin-left: 4px;
|
||||
font-size: 9px; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.04em;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
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 — a success-tinted check next to a provisional auto-tag. */
|
||||
.fc-tag-chip__confirm {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 20px; height: 20px; border-radius: 50%;
|
||||
border: none; background: transparent; cursor: pointer;
|
||||
color: rgb(var(--v-theme-success));
|
||||
}
|
||||
.fc-tag-chip__confirm:hover { background: rgb(var(--v-theme-success), 0.14); }
|
||||
.fc-tag-chip__confirm:focus-visible {
|
||||
outline: 2px solid rgb(var(--v-theme-success)); outline-offset: 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
v-for="tag in host.current?.tags || []"
|
||||
:key="tag.id" :tag="tag" :image-id="host.currentImageId"
|
||||
@remove="onRemove" @rename="openRename" @set-fandom="openSetFandom"
|
||||
@navigate="onNavigate"
|
||||
@navigate="onNavigate" @confirm="onConfirm"
|
||||
/>
|
||||
<span v-if="!host.current?.tags?.length" class="text-caption">No tags yet.</span>
|
||||
</div>
|
||||
@@ -65,6 +65,7 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useApi } from '../../composables/useApi.js'
|
||||
import { useModalStore } from '../../stores/modal.js'
|
||||
import { useSuggestionsStore } from '../../stores/suggestions.js'
|
||||
import TagChip from './TagChip.vue'
|
||||
@@ -83,6 +84,7 @@ const modalStore = useModalStore()
|
||||
const host = props.host || modalStore
|
||||
const suggestions = useSuggestionsStore()
|
||||
const router = useRouter()
|
||||
const api = useApi()
|
||||
const errorMsg = ref(null)
|
||||
const tagInputRef = ref(null)
|
||||
|
||||
@@ -106,6 +108,18 @@ defineExpose({ focusTagInput })
|
||||
// Every tag mutation hands focus back to the input so the operator can keep
|
||||
// typing the next tag without re-clicking — matches the accept-suggestion flow
|
||||
// (operator-asked 2026-06-26; the Explore workspace leans on this hard).
|
||||
// Confirm/keep an auto-applied tag (milestone 139): records the affirmation so
|
||||
// the tag becomes a training positive AND is shielded from the retraction sweep,
|
||||
// then reloads so the chip drops its "auto" badge + Keep button.
|
||||
async function onConfirm(tag) {
|
||||
errorMsg.value = null
|
||||
try {
|
||||
await api.post(`/api/images/${host.currentImageId}/tags/${tag.id}/confirm`)
|
||||
await host.reloadTags()
|
||||
}
|
||||
catch (e) { errorMsg.value = e.message }
|
||||
}
|
||||
|
||||
async function onRemove(tagId) {
|
||||
errorMsg.value = null
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user