feat(ui): hover an applied tag chip → highlight its grounding crop (#133 step 4)
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:
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user