refactor(tags): unify suggestion source — one canonical DB-tag dropdown, drop dead raw/alias machinery (#154)
Every tag suggestion is a canonical DB tag now (tagging-v2 #114: heads + CCIP score EXISTING concept tags). The pre-heads apparatus for model-predicted tags that didn't exist in the DB — creates_new_tag / raw_name / via_alias, the /suggestions/alias endpoint + add_alias_and_accept, AliasPickerDialog, and the store's aliasAccept/removeAlias — was dead and is removed. The type-to-add dropdown was TWO row sources (server autocomplete + the image's ML suggestions) merged with a dedup that dropped the %-bearing suggestion row when the debounced server hit landed — the operator's "confidence % flickers then vanishes". Now it's ONE list of DB-tag matches, each annotated with the model's confidence (join by canonical_tag_id) when the tag was scored for this image. No dedup, no flicker; picking a suggested tag still records acceptance via TagPanel.findPending. Single per-image fetch: score_image now reports above_threshold per row (computed vs the head's own suggest cut, separate from the inclusion floor), so the rail makes ONE min=0 request and derives the panel (above_threshold) and the dropdown (all, text-filtered) client-side — the two /suggestions calls collapse to one. Manual "Create 'X' as <kind>" (novel typed names) is unchanged; the alias table + tag-side alias admin + auto-apply alias matching are untouched. Tests: gate/serializer assertions updated (above_threshold; dropped dead-field + alias-endpoint checks); frontend spec seeds via the single load and covers the byCategory/aboveByCategory split. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgZP9v2otxVJymiYsnVuMy
This commit is contained in:
@@ -20,48 +20,39 @@
|
||||
first, so false positives are easy to spot and reject (reject-to-train
|
||||
for these heads). Small set — collapsible, open by default. -->
|
||||
<SuggestionsCategoryGroup
|
||||
v-if="store.byCategory.system && store.byCategory.system.length"
|
||||
label="System" :items="store.byCategory.system"
|
||||
v-if="store.aboveByCategory.system && store.aboveByCategory.system.length"
|
||||
label="System" :items="store.aboveByCategory.system"
|
||||
collapsible :default-open="true"
|
||||
@accept="onAccept" @alias="onAlias" @remove-alias="onRemoveAlias"
|
||||
@accept="onAccept"
|
||||
@dismiss="onDismiss" @undismiss="onUndismiss"
|
||||
@reject-all="onRejectAll('system')"
|
||||
/>
|
||||
<SuggestionsCategoryGroup
|
||||
v-for="cat in peopleCats" :key="cat"
|
||||
v-show="store.byCategory[cat] && store.byCategory[cat].length"
|
||||
:label="labelFor(cat)" :items="store.byCategory[cat] || []"
|
||||
@accept="onAccept" @alias="onAlias" @remove-alias="onRemoveAlias"
|
||||
v-show="store.aboveByCategory[cat] && store.aboveByCategory[cat].length"
|
||||
:label="labelFor(cat)" :items="store.aboveByCategory[cat] || []"
|
||||
@accept="onAccept"
|
||||
@dismiss="onDismiss" @undismiss="onUndismiss"
|
||||
@reject-all="onRejectAll(cat)"
|
||||
/>
|
||||
<SuggestionsCategoryGroup
|
||||
v-if="store.byCategory.general && store.byCategory.general.length"
|
||||
label="General" :items="store.byCategory.general"
|
||||
v-if="store.aboveByCategory.general && store.aboveByCategory.general.length"
|
||||
label="General" :items="store.aboveByCategory.general"
|
||||
collapsible :default-open="true"
|
||||
@accept="onAccept" @alias="onAlias" @remove-alias="onRemoveAlias"
|
||||
@accept="onAccept"
|
||||
@dismiss="onDismiss" @undismiss="onUndismiss"
|
||||
@reject-all="onRejectAll('general')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="aliasDialog" max-width="480">
|
||||
<AliasPickerDialog
|
||||
v-if="aliasTarget"
|
||||
:category="aliasTarget.category"
|
||||
@confirm="onAliasConfirm" @cancel="aliasDialog = false"
|
||||
/>
|
||||
</v-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { toast } from '../../utils/toast.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import { useSuggestionsStore, CATEGORY_LABELS } from '../../stores/suggestions.js'
|
||||
import { useModalStore } from '../../stores/modal.js'
|
||||
import SuggestionsCategoryGroup from './SuggestionsCategoryGroup.vue'
|
||||
import AliasPickerDialog from './AliasPickerDialog.vue'
|
||||
|
||||
const props = defineProps({
|
||||
imageId: { type: Number, required: true },
|
||||
@@ -100,13 +91,14 @@ const peopleCats = ['character']
|
||||
function labelFor(c) { return CATEGORY_LABELS[c] || c }
|
||||
|
||||
const isEmpty = computed(() =>
|
||||
Object.values(store.byCategory).every(list => !list || list.length === 0)
|
||||
Object.values(store.aboveByCategory).every(list => !list || list.length === 0)
|
||||
)
|
||||
|
||||
watch(() => props.imageId, (id) => {
|
||||
if (id == null) return
|
||||
store.load(id) // panel: curated, ≥ threshold
|
||||
store.loadAll(id) // dropdown: full prediction set (low-confidence included)
|
||||
// One fetch (min=0) backs both surfaces: the panel reads aboveByCategory, the
|
||||
// typed tag-input dropdown reads the full set. No second request.
|
||||
store.load(id)
|
||||
}, { immediate: true })
|
||||
|
||||
// After a successful accept/alias-accept, refresh the modal's current
|
||||
@@ -125,31 +117,6 @@ async function onAccept(s) {
|
||||
toast({ text: `Accept failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const aliasDialog = ref(false)
|
||||
const aliasTarget = ref(null)
|
||||
function onAlias(s) { aliasTarget.value = s; aliasDialog.value = true }
|
||||
async function onAliasConfirm(canonicalTagId) {
|
||||
try {
|
||||
await store.aliasAccept(aliasTarget.value, canonicalTagId)
|
||||
aliasDialog.value = false
|
||||
await host.reloadTags()
|
||||
emit('accepted')
|
||||
} catch (e) {
|
||||
toast({ text: `Alias failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
// Undo the model-key→tag mapping behind an aliased suggestion. The store
|
||||
// reloads suggestions so the prediction reverts to its raw form; the applied
|
||||
// canonical tag (if any) stays, so no tag-rail reload is needed.
|
||||
async function onRemoveAlias(s) {
|
||||
try {
|
||||
await store.removeAlias(s)
|
||||
} catch (e) {
|
||||
toast({ text: `Remove alias failed: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user