feat(ui): hover an applied tag chip → highlight its grounding crop (#133 step 4)
CI / lint (push) Failing after 3s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m41s

Applied tags aren't scored live, so compute the grounding on demand: run the
tag's head over the image's max-over-bag (whole-image + concept crops), argmax
→ the region that best explains the tag on this image, mirroring what
score_image records for live suggestions.

- heads.py: extract _image_bag (now shared by score_image) + ground_applied_tag.
  Returns (grounding, has_head): has_head False = no head to localize with →
  no overlay; grounding None = the whole-image vector won → whole-image frame.
- tags.py: GET /api/images/<id>/tags/<id>/grounding → {grounding, has_head}.
- TagChip/TagPanel: applied chips inject fcSuggestionHover and fetch grounding
  on hover (cached per image+tag, race-guarded), reusing Step 3's overlay in
  both the modal and Explore. No new frontend overlay code.

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:
2026-07-06 13:19:41 -04:00
parent 524a26c618
commit 9bb4211722
6 changed files with 284 additions and 37 deletions
+46 -3
View File
@@ -1,5 +1,5 @@
<template>
<span class="fc-tag-chip">
<span class="fc-tag-chip" @mouseenter="onEnter" @mouseleave="onLeave">
<v-chip
size="small" closable
:color="store.colorFor(tag.kind)" variant="tonal"
@@ -38,14 +38,57 @@
</template>
<script setup>
import { computed } from 'vue'
import { computed, inject } from 'vue'
import { useTagStore } from '../../stores/tags.js'
import { useApi } from '../../composables/useApi.js'
import KebabMenu from '../common/KebabMenu.vue'
const props = defineProps({ tag: { type: Object, required: true } })
const props = defineProps({
tag: { type: Object, required: true },
// When set (the tagging panels), hovering the chip asks the backend which crop
// region best explains this applied tag and lights it up on the image — the
// same overlay the suggestion rail uses (#1206 Step 4). Omitted elsewhere →
// the hover is inert (no injected target, or no image to ground against).
imageId: { type: Number, default: null },
})
defineEmits(['remove', 'rename', 'set-fandom', 'navigate'])
const store = useTagStore()
const api = useApi()
// #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).
const hover = inject('fcSuggestionHover', null)
const _groundingCache = new Map()
// Bumped on every enter/leave so a slow fetch that resolves after the pointer
// has moved on can't draw a stale overlay.
let hoverSeq = 0
async function onEnter () {
if (!hover || props.imageId == null) return
const seq = ++hoverSeq
const key = `${props.imageId}:${props.tag.id}`
let res = _groundingCache.get(key)
if (res === undefined) {
try {
res = await api.get(
`/api/images/${props.imageId}/tags/${props.tag.id}/grounding`
)
_groundingCache.set(key, res)
} catch {
return // best-effort — leave the overlay untouched on error
}
}
if (seq !== hoverSeq) return // pointer already left / moved to another chip
// No head → nothing to localize; don't draw an overlay at all. With a head,
// null grounding still draws the whole-image frame ("the global vector won").
if (res.has_head) hover.value = { g: res.grounding ?? null }
}
function onLeave () {
hoverSeq++
if (hover) hover.value = null
}
// Show a character's fandom inline (truncated). Falls back to a bare arrow when
// only fandom_id is known but the name wasn't resolved (older payloads).
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="fc-tag-panel__chips">
<TagChip
v-for="tag in host.current?.tags || []"
:key="tag.id" :tag="tag"
:key="tag.id" :tag="tag" :image-id="host.currentImageId"
@remove="onRemove" @rename="openRename" @set-fandom="openSetFandom"
@navigate="onNavigate"
/>