feat(suggestions): tag-input dropdown searches the full prediction set
The typed dropdown sourced the threshold-filtered panel list (>= 0.70 general), so low-confidence actions/features the model DID predict never appeared — forcing hand-typed custom tags instead of accepting the model's canonical formatting. Add a threshold override: SuggestionService.for_image(threshold_override=) and GET /images/<id>/suggestions?min=<f> surface EVERY stored prediction (down to the 0.05 store floor), alias-resolved and normalized, still excluding applied/rejected and unsurfaced categories. The suggestions store gains allByCategory + loadAll (min=0); the dropdown searches that full set (cap 20), while the Suggestions panel stays curated at the configured threshold. Accept/dismiss drop from both lists. Operator-asked 2026-06-09. Test: a 0.30 general prediction is hidden by default but surfaced with threshold_override=0.0; unsurfaced categories still excluded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,9 +16,14 @@ export const CATEGORY_LABELS = {
|
||||
|
||||
export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
const api = useApi()
|
||||
const byCategory = ref({}) // { category: [suggestion, ...] }
|
||||
const byCategory = ref({}) // { category: [suggestion, ...] } — panel (≥ threshold)
|
||||
// The typed tag-input dropdown searches the model's FULL prediction set for
|
||||
// the image (min=0, down to the store floor) so low-confidence actions/
|
||||
// features can be picked in canonical formatting (operator-asked 2026-06-09).
|
||||
const allByCategory = ref({})
|
||||
const { loading, error, run } = useAsyncAction({ errorAs: 'message' })
|
||||
let currentImageId = null
|
||||
const inflightAll = useInflightToken()
|
||||
// Audit 2026-06-02: this store had no inflight guard — a late
|
||||
// /suggestions response from a prior image could overwrite
|
||||
// byCategory while currentImageId pointed at a new one, and
|
||||
@@ -42,10 +47,41 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function _drop(category, predicate) {
|
||||
const list = byCategory.value[category]
|
||||
if (!list) return
|
||||
byCategory.value[category] = list.filter(s => !predicate(s))
|
||||
// Load the full prediction set for the dropdown (separate inflight guard so a
|
||||
// late response from a prior image can't overwrite the current one's list).
|
||||
async function loadAll(imageId) {
|
||||
inflightAll.cancel()
|
||||
allByCategory.value = {}
|
||||
const t = inflightAll.claim()
|
||||
try {
|
||||
const body = await api.get(`/api/images/${imageId}/suggestions`, {
|
||||
params: { min: 0 },
|
||||
})
|
||||
if (!t.isCurrent()) return
|
||||
allByCategory.value = body.by_category || {}
|
||||
} catch {
|
||||
// Dropdown is best-effort — the panel surfaces load errors.
|
||||
}
|
||||
}
|
||||
|
||||
// A stable identity for a suggestion across the two lists (panel vs dropdown
|
||||
// are separate fetches, so object identity differs): tag id when known, else
|
||||
// the raw display key.
|
||||
function _keyOf(s) {
|
||||
return s.canonical_tag_id != null
|
||||
? `id:${s.canonical_tag_id}`
|
||||
: `raw:${s.category}:${(s.display_name || '').toLowerCase()}`
|
||||
}
|
||||
|
||||
// Drop an accepted/dismissed suggestion from BOTH the panel list and the
|
||||
// dropdown's full list so it can't reappear in either surface.
|
||||
function _dropEverywhere(suggestion) {
|
||||
const key = _keyOf(suggestion)
|
||||
const cat = suggestion.category
|
||||
for (const map of [byCategory.value, allByCategory.value]) {
|
||||
const list = map[cat]
|
||||
if (list) map[cat] = list.filter(s => _keyOf(s) !== key)
|
||||
}
|
||||
}
|
||||
|
||||
async function accept(suggestion) {
|
||||
@@ -70,7 +106,7 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
// Only drop from THIS image's category list — if the user navigated,
|
||||
// the new image has its own suggestions and this drop would corrupt them.
|
||||
if (currentImageId === imageId) {
|
||||
_drop(suggestion.category, s => s === suggestion)
|
||||
_dropEverywhere(suggestion)
|
||||
}
|
||||
toast({
|
||||
text: `Tagged: ${suggestion.display_name}`,
|
||||
@@ -89,7 +125,7 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
}
|
||||
})
|
||||
if (currentImageId === imageId) {
|
||||
_drop(suggestion.category, s => s === suggestion)
|
||||
_dropEverywhere(suggestion)
|
||||
}
|
||||
toast({
|
||||
text: `Aliased & tagged: ${suggestion.display_name}`,
|
||||
@@ -109,12 +145,12 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
})
|
||||
}
|
||||
if (currentImageId === imageId) {
|
||||
_drop(suggestion.category, s => s === suggestion)
|
||||
_dropEverywhere(suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
byCategory, loading, error,
|
||||
load, accept, aliasAccept, dismiss
|
||||
byCategory, allByCategory, loading, error,
|
||||
load, loadAll, accept, aliasAccept, dismiss
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user